v0.4.1: UI opgeschoond — tabbladen, iconen, uniforme labels, T-cyclus

- core/ui-kit.js: gedeelde velden/schakelaars/tabs/specificatiekaarten
- Bootstrap Icons lokaal (css/vendor, font ingebed)
- Woning- en unit-instellingen in tabbladen, labels gestandaardiseerd
- Specificaties onder 3D als kaarten
- T: standaard -> 3D groot -> alleen render -> standaard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 11:51:18 +02:00
co-authored by Claude Opus 5.5
parent c690e72d13
commit 3591fad220
11 changed files with 597 additions and 426 deletions
+209 -274
View File
@@ -23,11 +23,11 @@
const HOUSE_TEMPLATE = `
<div id="house-app">
<aside id="house-nav">
<div class="hn-title">Woning</div>
<div class="hn-title"><i class="bi bi-diagram-3"></i> Woning</div>
<div id="house-nav-tree"></div>
<div class="hn-actions">
<button type="button" id="hn-add-unit" class="io-btn">+ Unit</button>
<button type="button" id="hn-add-iw" class="io-btn">+ Binnenwand</button>
<button type="button" id="hn-add-unit" class="ui-btn"><i class="bi bi-plus-lg"></i><span>Unit</span></button>
<button type="button" id="hn-add-iw" class="ui-btn"><i class="bi bi-plus-lg"></i><span>Binnenwand</span></button>
</div>
<div class="hn-soon">
<div class="hn-item disabled" title="Nog niet beschikbaar">Schuine daken</div>
@@ -47,14 +47,14 @@ const HOUSE_TEMPLATE = `
</div>
<div class="controls-grid">
<div>
<div class="control-group"><label for="h-proj-naam">Projectnaam:</label><div><input type="text" id="h-proj-naam"></div></div>
<div class="control-group"><label for="h-proj-plaats">Plaats:</label><div><input type="text" id="h-proj-plaats"></div></div>
<div class="control-group"><label for="h-proj-straat">Straat:</label><div><input type="text" id="h-proj-straat"></div></div>
<div class="control-group"><label for="h-proj-naam">Projectnaam</label><div><input type="text" id="h-proj-naam"></div></div>
<div class="control-group"><label for="h-proj-plaats">Plaats</label><div><input type="text" id="h-proj-plaats"></div></div>
<div class="control-group"><label for="h-proj-straat">Straat</label><div><input type="text" id="h-proj-straat"></div></div>
</div>
<div>
<div class="control-group"><label for="h-proj-nummer">Projectnummer:</label><div><input type="text" id="h-proj-nummer"></div></div>
<div class="control-group"><label for="h-proj-tekenaar">Tekenaar:</label><div><input type="text" id="h-proj-tekenaar"></div></div>
<div class="control-group"><label for="h-proj-opmerking">Opmerking:</label><div><input type="text" id="h-proj-opmerking"></div></div>
<div class="control-group"><label for="h-proj-nummer">Projectnummer</label><div><input type="text" id="h-proj-nummer"></div></div>
<div class="control-group"><label for="h-proj-tekenaar">Tekenaar</label><div><input type="text" id="h-proj-tekenaar"></div></div>
<div class="control-group"><label for="h-proj-opmerking">Opmerking</label><div><input type="text" id="h-proj-opmerking"></div></div>
</div>
</div>
<div class="material-info">Geldt voor elk blad van de woning-PDF. Titel en elementnummer worden per blad automatisch ingevuld.</div>
@@ -70,10 +70,10 @@ const HOUSE_OVERVIEW_TEMPLATE = `
<div id="house-overview">
<div class="ho-col ho-left">
<div id="ho-pane-plan" class="ho-pane">
<div class="pane-label">2D PLATTEGROND</div>
<div class="pane-label">PLATTEGROND</div>
<div class="toolbar-3d">
<button type="button" id="ho-tool-select" class="btn-3d-toggle active">Selecteren</button>
<button type="button" id="ho-tool-iw" class="btn-3d-toggle">Binnenwand tekenen</button>
<button type="button" id="ho-tool-select" class="btn-3d-toggle active" title="Selecteren"><i class="bi bi-cursor"></i>Selecteren</button>
<button type="button" id="ho-tool-iw" class="btn-3d-toggle" title="Binnenwand tekenen"><i class="bi bi-pencil"></i>Binnenwand</button>
</div>
<canvas id="ho-canvas"></canvas>
<div id="ho-hint" class="ho-hint"></div>
@@ -82,9 +82,9 @@ const HOUSE_OVERVIEW_TEMPLATE = `
</div>
<div class="ho-col ho-right">
<div id="ho-pane-3d" class="ho-pane">
<div class="pane-label">3D PERSPECTIEF</div>
<div class="pane-label">3D</div>
<div class="toolbar-3d" id="ho-toolbar-3d">
<button type="button" id="ho-btn-render" class="btn-3d-toggle" title="Avondscène: elke lamp en spot is een echte lichtbron">Volle render</button>
<button type="button" id="ho-btn-render" class="btn-3d-toggle" title="Avondscène: elke lamp en spot is een echte lichtbron"><i class="bi bi-moon-stars"></i>Render</button>
</div>
</div>
<div id="ho-specs"></div>
@@ -187,25 +187,25 @@ function mountHouseEditor(container, callbacks) {
// ------------------------------------------------------------
function renderNav() {
const parts = HouseModel.computeParts(state, { skip3D: true });
const item = (part, label, meta, cls) =>
const item = (part, label, meta, cls, iconName) =>
`<div class="hn-item ${cls || ''} ${activePart === part ? 'active' : ''}" data-part="${part}">
<span>${label}</span>${meta ? `<span class="hn-meta">${meta}</span>` : ''}</div>`;
let html = item('overzicht', 'Overzicht', '', 'hn-top');
<span class="hn-label">${iconName ? UI.icon(iconName) : ''}${label}</span>${meta ? `<span class="hn-meta">${meta}</span>` : ''}</div>`;
let html = item('overzicht', 'Overzicht', '', 'hn-top', 'grid-1x2');
parts.units.forEach(u => {
const sel = selection && selection.type === 'unit' && selection.id === u.id && activePart === 'overzicht';
html += `<div class="hn-group">`;
html += `<div class="hn-item hn-unit ${sel ? 'active' : ''}" data-select-unit="${u.id}"><span>Unit ${u.id}</span><span class="hn-meta">${u.rect.W} &times; ${u.rect.D}</span></div>`;
html += item(`floor:${u.id}`, 'Vloer', u.floorElement, 'hn-sub');
html += item(`roof:${u.id}`, 'Dak (plat)', u.roof.element, 'hn-sub');
html += `<div class="hn-item hn-unit ${sel ? 'active' : ''}" data-select-unit="${u.id}"><span class="hn-label">${UI.icon('box')}Unit ${u.id}</span><span class="hn-meta">${u.rect.W} &times; ${u.rect.D}</span></div>`;
html += item(`floor:${u.id}`, 'Vloer', u.floorElement, 'hn-sub', 'grid-3x3');
html += item(`roof:${u.id}`, 'Dak', u.roof.element, 'hn-sub', 'house');
u.walls.forEach(w => {
const meta = w.removed ? 'vervalt (naad)' : w.cutsU.length ? `deels naad &middot; ${w.pieces.map(p => p.element).join(', ')}` : w.pieces[0].element;
html += item(`wall:${u.id}:${w.side}`, w.label, meta, 'hn-sub' + (w.removed ? ' hn-removed' : ''));
const meta = w.removed ? 'vervalt' : w.pieces.map(p => p.element).join(', ');
html += item(`wall:${u.id}:${w.side}`, w.label, meta, 'hn-sub' + (w.removed ? ' hn-removed' : ''), 'bricks');
});
html += `</div>`;
});
if (parts.interior.length) {
html += `<div class="hn-group"><div class="hn-item hn-unit hn-static"><span>Binnenwanden</span></div>`;
parts.interior.forEach(p => { html += item(`iw:${p.id}`, p.element, `${p.place.width} mm`, 'hn-sub'); });
html += `<div class="hn-group"><div class="hn-item hn-unit hn-static"><span class="hn-label">${UI.icon('distribute-horizontal')}Binnenwanden</span></div>`;
parts.interior.forEach(p => { html += item(`iw:${p.id}`, p.element, `${p.place.width} mm`, 'hn-sub', 'distribute-horizontal'); });
html += `</div>`;
}
document.getElementById('house-nav-tree').innerHTML = html;
@@ -617,227 +617,172 @@ function mountHouseEditor(container, callbacks) {
}
}
// --- Eigenschappenpaneel (contextafhankelijk) ---
const num = (id, label, value, step) =>
`<div class="control-group"><label for="${id}">${label}:</label><div><input type="number" id="${id}" value="${value}" step="${step || 50}"><span class="unit">mm</span></div></div>`;
const sel = (id, label, options, value) =>
`<div class="control-group"><label for="${id}">${label}:</label><div><select id="${id}">${Object.entries(options).map(([k, v]) => `<option value="${k}" ${k === value ? 'selected' : ''}>${v}</option>`).join('')}</select></div></div>`;
// --- Eigenschappenpaneel (contextafhankelijk, opgebouwd met UI-kit) ---
const { num, sel, toggle, section, info, button, actions } = UI;
const cols = UI.grid; // (grid = de 3D-rasterhulp)
UI.wireTabs(propsEl);
const head = (iconName, title, sub) =>
`<div class="ui-panel-head"><h1>${UI.icon(iconName)}${title}</h1>${sub ? `<span class="ui-sub">${sub}</span>` : ''}</div>`;
const after = () => refresh({ keepProps: true }); // getal: panelen blijven staan
const rerender = () => refresh(); // keuze: velden kunnen wijzigen
function renderProps() {
const warn = parts.overlaps.length
? `<div class="global-warning">Let op: units overlappen elkaar (${parts.overlaps.map(p => p.join(' + ')).join(', ')}).</div>` : '';
const errs = parts.rects.filter(r => r.error).map(r => `<div class="global-warning">Unit ${r.id}: ${r.error}</div>`).join('');
const warn = (parts.overlaps.length ? UI.warn(`Units overlappen elkaar: ${parts.overlaps.map(p => p.join(' + ')).join(', ')}.`) : '') +
parts.rects.filter(r => r.error).map(r => UI.warn(`Unit ${r.id}: ${r.error}`)).join('');
if (selection && selection.type === 'unit' && unitById(selection.id)) {
const u = unitById(selection.id);
const isRoot = state.units[0].id === u.id;
const a = u.attach;
const axis = a && (a.side === 'voor' || a.side === 'achter') ? 'x' : 'z';
propsEl.innerHTML = `${warn}${errs}
<div class="controls-grid">
<div>
<h1>Unit ${u.id}</h1>
${num('hp-width', 'Breedte (west-oost)', u.unit.floor.floor.width)}
${num('hp-depth', 'Diepte (noord-zuid)', u.unit.floor.floor.depth)}
${sel('hp-corners', 'Hoeken', UnitModel.CORNER_MODES, u.unit.layout.corners)}
</div>
<div>
<h1>Koppeling</h1>
${isRoot ? '<div class="material-info">Basis-unit: noordwest-hoek op (0, 0). Andere units koppelen hieraan.</div>' : `
${sel('hp-att-to', 'Gekoppeld aan', Object.fromEntries(state.units.filter(o => o.id !== u.id).map(o => [o.id, 'Unit ' + o.id])), a.to)}
${sel('hp-att-side', 'Aan zijde', HouseModel.SIDE_LABELS, a.side)}
${sel('hp-att-align', 'Uitlijning', HouseModel.ALIGN_LABELS[axis], a.align)}
${num('hp-att-offset', 'Verschuiving', a.offset || 0)}`}
</div>
</div>
<div class="divider"></div>
<div class="controls-grid">
<div>
<h1>Dak (plat)</h1>
${sel('hp-roof-follow', 'Dak volgt vloer', { ja: 'Ja (standaard)', nee: 'Nee, eigen maat' }, u.roof.followFloor ? 'ja' : 'nee')}
${u.roof.followFloor ? '' : `
${num('hp-roof-w', 'Dakbreedte (west-oost)', u.roof.deck.floor.width)}
${num('hp-roof-d', 'Dakdiepte (noord-zuid)', u.roof.deck.floor.depth)}`}
</div>
<div>
${u.roof.followFloor ? '<div class="material-info">Het dak heeft dezelfde maat als de vloer. Kies "Nee, eigen maat" om het dak langer/breder te maken of te verschuiven.</div>' : `
<h1>Positie dak</h1>
${num('hp-roof-ox', 'Verschuiving oost (+)', u.roof.offsetX || 0)}
${num('hp-roof-oz', 'Verschuiving zuid (+)', u.roof.offsetZ || 0)}`}
</div>
</div>
<div class="divider"></div>
<div class="controls-grid">
<div>
<h1>Dakrand</h1>
${sel('hp-fascia', 'Boeidelen', { aan: 'Aan', uit: 'Uit (daktrim 80 mm)' }, u.roof.fascia !== false ? 'aan' : 'uit')}
</div>
<div>
<div class="material-info">Buitenverlichting (type, richting, afstanden) is woning-breed: Overzicht, niets geselecteerd.</div>
</div>
</div>
<div class="ho-prop-actions">
<button type="button" class="io-btn" data-go="floor:${u.id}">Vloer bewerken</button>
<button type="button" class="io-btn" data-go="roof:${u.id}">Dak bewerken</button>
${isRoot ? '' : `<button type="button" class="io-btn io-btn-danger" data-remove-unit="${u.id}">Unit verwijderen</button>`}
</div>`;
const onNum = (id, fn) => document.getElementById(id) && document.getElementById(id).addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
if (isFinite(v)) { fn(v); refresh({ keepProps: true }); }
});
onNum('hp-width', v => { if (v >= 1000) u.unit.floor.floor.width = v; });
onNum('hp-depth', v => { if (v >= 1000) u.unit.floor.floor.depth = v; });
onNum('hp-att-offset', v => { u.attach.offset = v; });
const onSel = (id, fn) => document.getElementById(id) && document.getElementById(id).addEventListener('input', (e) => { fn(e.target.value); refresh(); });
onSel('hp-corners', v => { u.unit.layout.corners = v; });
onSel('hp-att-to', v => { u.attach.to = v; });
onSel('hp-att-side', v => { u.attach.side = v; });
onSel('hp-att-align', v => { u.attach.align = v; });
onSel('hp-roof-follow', v => { u.roof.followFloor = v === 'ja'; });
onSel('hp-fascia', v => { u.roof.fascia = v === 'aan'; });
onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; });
onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; });
onNum('hp-roof-ox', v => { u.roof.offsetX = v; });
onNum('hp-roof-oz', v => { u.roof.offsetZ = v; });
} else if (selection && selection.type === 'wall') {
const pu = parts.units.find(x => x.id === selection.unit);
const wl = pu && pu.walls.find(x => x.side === selection.side);
const piece = wl && wl.pieces.find(p => p.element === selection.element);
if (!piece) { selection = null; return renderProps(); }
propsEl.innerHTML = `${warn}
<h1>${piece.element} &mdash; ${piece.label}</h1>
<div class="result-row"><span>Lengte volle zijde:</span><span class="highlight">${wl.width} mm</span></div>
<div class="result-row"><span>Dit wandstuk:</span><span class="highlight">${piece.place.width} mm (${Math.round(piece.u0)}&ndash;${Math.round(piece.u1)} vanaf links, van buitenaf gezien)</span></div>
<div class="result-row"><span>Naad met andere unit:</span><span class="highlight">${wl.cutsU.length ? wl.cutsU.map(([a, b]) => `${Math.round(a)}&ndash;${Math.round(b)}`).join(', ') + ' mm vervalt' : 'nee'}</span></div>
<div class="result-row"><span>Openingen in dit stuk:</span><span class="highlight">${piece.config.openings.length}</span></div>
${state.settings.lighting.mode === 'gevel' ? `
<div class="divider"></div>
<h1>Gevellampen op deze wand</h1>
${sel('hw-light', 'Lampen', LightingModel.WALL_OVERRIDES, unitById(pu.id).lighting.walls[wl.side] || 'auto')}
<div class="result-row"><span>Lampen op dit wandstuk:</span><span class="highlight">${piece.lamps.length}</span></div>
<div class="material-info">Automatisch: vanaf ${state.settings.lighting.endOffset} mm van hoek/uiteinde, alleen op vrije stukken gevel &ge; ${state.settings.lighting.minWallWidth} mm op ${state.settings.lighting.lampHeight} mm hoogte (smallere stukken krijgen geen lampen). Override: ${state.settings.lighting.openingOffset} mm naast elke opening.</div>`
: '<div class="material-info">Gevellampen: kies in de woning-instellingen "Buitenverlichting: Gevellampen".</div>'}
${wl.dropped.length ? `<div class="global-warning">${wl.dropped.length} opening(en) van deze zijde vallen in een naad en vervallen.</div>` : ''}
<div class="ho-prop-actions"><button type="button" class="io-btn" data-go="wall:${pu.id}:${wl.side}">Wand bewerken</button></div>`;
const hw = document.getElementById('hw-light');
if (hw) hw.addEventListener('input', (e) => { unitById(pu.id).lighting.walls[wl.side] = e.target.value; refresh(); });
} else if (selection && selection.type === 'iw' && iwById(selection.id)) {
const iw = iwById(selection.id);
const p = parts.interior.find(x => x.id === iw.id);
propsEl.innerHTML = `${warn}
<div class="controls-grid">
<div>
<h1>${p.element} &mdash; binnenwand</h1>
${num('hi-x1', 'Begin X', iw.x1)}${num('hi-z1', 'Begin Y', iw.z1)}
${num('hi-x2', 'Eind X', iw.x2)}${num('hi-z2', 'Eind Y', iw.z2)}
</div>
<div>
<h1>Opbouw</h1>
${num('hi-thick', 'Dikte', HouseModel.thicknessOf(iw.wall), 5)}
${num('hi-height', 'Hoogte', iw.wall.wall.heightAll)}
<div class="result-row"><span>Lengte:</span><span class="highlight">${p.place.width} mm</span></div>
</div>
</div>
<div class="ho-prop-actions">
<button type="button" class="io-btn" data-go="iw:${iw.id}">Wand bewerken (openingen)</button>
<button type="button" class="io-btn io-btn-danger" data-remove-iw="${iw.id}">Verwijderen</button>
</div>`;
const onNum = (id, fn) => document.getElementById(id).addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
if (isFinite(v)) { fn(v); refresh({ keepProps: true }); }
});
['x1', 'z1', 'x2', 'z2'].forEach(f => onNum('hi-' + f, v => { iw[f] = v; }));
onNum('hi-thick', v => { if (v >= 40) HouseModel.setThickness(iw.wall, v); });
onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; });
} else {
propsEl.innerHTML = `${warn}${errs}
<h1>Woning</h1>
<div class="material-info">${state.units.length} unit(s), ${state.interiorWalls.length} binnenwand(en).
Klik in de plattegrond op een unit, wand of binnenwand om die te selecteren; dubbelklik om te bewerken.
Waar units tegen elkaar staan (oranje gestippeld) vervalt de wand.</div>
<div class="ho-prop-actions">
<button type="button" class="io-btn" data-action="add-unit">+ Unit toevoegen</button>
<button type="button" class="io-btn" data-action="draw-iw">Binnenwand tekenen</button>
<button type="button" class="io-btn" data-action="project">Project eigenschappen</button>
</div>
<div class="divider"></div>
${renderSettingsHtml()}`;
wireSettings();
}
if (selection && selection.type === 'unit' && unitById(selection.id)) renderUnitProps(unitById(selection.id), warn);
else if (selection && selection.type === 'wall') renderWallProps(warn);
else if (selection && selection.type === 'iw' && iwById(selection.id)) renderIwProps(iwById(selection.id), warn);
else renderHouseProps(warn);
}
// Woning-instellingen: h.o.h. voor ALLE buiten-/binnenwanden + dakrand.
function renderSettingsHtml() {
const sp = state.settings.spacing, r = state.settings.roof, lt = state.settings.lighting;
return `
<div class="controls-grid">
<div>
<h1>Gevelbekleding (test, alle buitenwanden)</h1>
${sel('hs-cladding', 'Richting WEO/Novalit', WallModel.CLADDING_DIRECTIONS, state.settings.cladding)}
${sel('hs-trasraam', 'Trasraam (cement-gebonden plaat)', { aan: 'Aan', uit: 'Uit' }, state.settings.trasraam.enabled ? 'aan' : 'uit')}
${state.settings.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte trasraam (vanaf bovenkant vloer)', state.settings.trasraam.height, 10) : ''}
${sel('hs-sandwich', `Sandwichpanelen rondom (${SandwichModel.DEPTH_MM} mm de grond in)`, { aan: 'Aan', uit: 'Uit' }, state.settings.sandwich ? 'aan' : 'uit')}
<div class="divider"></div>
<h1>Regelwerk h.o.h. (alle wanden)</h1>
${num('hs-ext-min', 'Buitenwanden min.', sp.exterior.min, 10)}
${num('hs-ext-max', 'Buitenwanden max.', sp.exterior.max, 10)}
${num('hs-int-min', 'Binnenwanden min.', sp.interior.min, 10)}
${num('hs-int-max', 'Binnenwanden max.', sp.interior.max, 10)}
</div>
<div>
<h1>Dakrand (maten, hele woning)</h1>
${num('hs-overhang', `Overstek boeideel (${RoofModel.OVERHANG_MIN}–${RoofModel.OVERHANG_MAX})`, r.overhang, 10)}
${num('hs-fascia-h', 'Hoogte boeideel', r.fasciaHeight, 10)}
${num('hs-trim-h', 'Hoogte daktrim', r.trimHeight, 5)}
${num('hs-spot-min', 'Spots min. h.o.h.', r.spotMin, 50)}
${num('hs-spot-max', 'Spots max. h.o.h.', r.spotMax, 50)}
</div>
</div>
<div class="divider"></div>
<div class="controls-grid">
<div>
<h1>Buitenverlichting (hele woning)</h1>
${sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode)}
${lt.mode === 'gevel' ? sel('hs-light-dir', 'Lichtrichting', LightingModel.DIRECTIONS, lt.direction) : ''}
${lt.mode === 'spots' ? '<div class="material-info">Boeideel-spots komen alleen bij units met boeidelen.</div>' : ''}
${num('hs-lamp-h', 'Lamphoogte', lt.lampHeight, 50)}
${num('hs-lamp-min', 'Lampen min. h.o.h.', lt.lampMin, 50)}
${num('hs-lamp-max', 'Lampen max. h.o.h.', lt.lampMax, 50)}
</div>
<div>
<h1>&nbsp;</h1>
${num('hs-lamp-w', 'Min. vrije gevelbreedte', lt.minWallWidth, 50)}
${num('hs-lamp-end', 'Vanaf hoek/uiteinde', lt.endOffset, 50)}
${num('hs-lamp-open', 'Override: naast opening', lt.openingOffset, 50)}
</div>
</div>`;
function renderUnitProps(u, warn) {
const isRoot = state.units[0].id === u.id;
const a = u.attach;
const axis = a && (a.side === 'voor' || a.side === 'achter') ? 'x' : 'z';
const f = u.unit.floor.floor;
const koppeling = isRoot
? info('Basis-unit: de noordwesthoek ligt op (0, 0). Andere units koppelen hieraan.')
: sel('hp-att-to', 'Gekoppeld aan', Object.fromEntries(state.units.filter(o => o.id !== u.id).map(o => [o.id, 'Unit ' + o.id])), a.to) +
sel('hp-att-side', 'Zijde', HouseModel.SIDE_LABELS, a.side) +
sel('hp-att-align', 'Uitlijning', HouseModel.ALIGN_LABELS[axis], a.align) +
num('hp-att-offset', 'Verschuiving', a.offset || 0);
const dak = toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) +
(u.roof.followFloor ? '' :
num('hp-roof-w', 'Breedte', u.roof.deck.floor.width, { hint: 'West–oost' }) +
num('hp-roof-d', 'Diepte', u.roof.deck.floor.depth, { hint: 'Noord–zuid' }) +
num('hp-roof-ox', 'Verschuiving oost', u.roof.offsetX || 0) +
num('hp-roof-oz', 'Verschuiving zuid', u.roof.offsetZ || 0)) +
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' });
propsEl.innerHTML = warn + head('box', `Unit ${u.id}`, `${f.width} &times; ${f.depth} mm`) +
UI.tabs('unit', [
{ key: 'maat', label: 'Afmetingen', icon: 'rulers', html: cols(
num('hp-width', 'Breedte', f.width, { hint: 'West–oost' }) + num('hp-depth', 'Diepte', f.depth, { hint: 'Noord–zuid' }),
sel('hp-corners', 'Hoekaansluiting', UnitModel.CORNER_MODES, u.unit.layout.corners)) },
{ key: 'koppeling', label: 'Koppeling', icon: 'link-45deg', html: cols(koppeling, '') },
{ key: 'dak', label: 'Dak', icon: 'house', html: cols(dak, u.roof.followFloor ? info('Het dak heeft de maat van de vloer. Zet "Maat volgt vloer" uit voor een eigen maat of verschuiving.') : '') }
]) +
actions(
button('Vloer bewerken', 'grid-3x3', `data-go="floor:${u.id}"`),
button('Dak bewerken', 'house', `data-go="roof:${u.id}"`),
isRoot ? '' : button('Unit verwijderen', 'trash', `data-remove-unit="${u.id}"`, 'ui-btn-danger'));
UI.onNum('hp-width', v => { if (v >= 1000) f.width = v; }, after);
UI.onNum('hp-depth', v => { if (v >= 1000) f.depth = v; }, after);
UI.onNum('hp-att-offset', v => { u.attach.offset = v; }, after);
UI.onSel('hp-corners', v => { u.unit.layout.corners = v; }, rerender);
UI.onSel('hp-att-to', v => { u.attach.to = v; }, rerender);
UI.onSel('hp-att-side', v => { u.attach.side = v; }, rerender);
UI.onSel('hp-att-align', v => { u.attach.align = v; }, rerender);
UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender);
UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender);
UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, after);
UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, after);
UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, after);
UI.onNum('hp-roof-oz', v => { u.roof.offsetZ = v; }, after);
}
function wireSettings() {
const sp = state.settings.spacing, r = state.settings.roof;
const onNum = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('input', (e) => { const v = parseFloat(e.target.value); if (isFinite(v) && v > 0) { fn(v); refresh({ keepProps: true }); } }); };
const onSel = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('input', (e) => { fn(e.target.value); refresh(); }); };
onSel('hs-cladding', v => { state.settings.cladding = v; });
onSel('hs-trasraam', v => { state.settings.trasraam.enabled = v === 'aan'; });
onSel('hs-sandwich', v => { state.settings.sandwich = v === 'aan'; });
onNum('hs-trasraam-h', v => { state.settings.trasraam.height = v; });
onNum('hs-ext-min', v => { sp.exterior.min = v; });
onNum('hs-ext-max', v => { sp.exterior.max = v; });
onNum('hs-int-min', v => { sp.interior.min = v; });
onNum('hs-int-max', v => { sp.interior.max = v; });
function renderWallProps(warn) {
const pu = parts.units.find(x => x.id === selection.unit);
const wl = pu && pu.walls.find(x => x.side === selection.side);
const piece = wl && wl.pieces.find(p => p.element === selection.element);
if (!piece) { selection = null; return renderProps(); }
const lt = state.settings.lighting;
onNum('hs-overhang', v => { r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); });
onNum('hs-fascia-h', v => { r.fasciaHeight = v; });
onNum('hs-spot-min', v => { r.spotMin = v; });
onSel('hs-light-mode', v => { lt.mode = v; });
onSel('hs-light-dir', v => { lt.direction = v; });
onNum('hs-lamp-h', v => { lt.lampHeight = v; });
onNum('hs-lamp-min', v => { lt.lampMin = v; });
onNum('hs-lamp-max', v => { lt.lampMax = v; });
onNum('hs-lamp-w', v => { lt.minWallWidth = v; });
onNum('hs-lamp-end', v => { lt.endOffset = v; });
onNum('hs-lamp-open', v => { lt.openingOffset = v; });
onNum('hs-spot-max', v => { r.spotMax = v; });
onNum('hs-trim-h', v => { r.trimHeight = v; });
const rows = [
['Lengte zijde', wl.width, 'mm'],
['Lengte wandstuk', piece.place.width, 'mm'],
['Positie', `${Math.round(piece.u0)} – ${Math.round(piece.u1)}`, 'mm'],
['Naad', wl.cutsU.length ? wl.cutsU.map(([a, b]) => `${Math.round(a)} – ${Math.round(b)}`).join(', ') : 'geen', wl.cutsU.length ? 'mm' : ''],
['Openingen', piece.config.openings.length, '']
];
const lampen = lt.mode === 'gevel'
? sel('hw-light', 'Lampen', LightingModel.WALL_OVERRIDES, unitById(pu.id).lighting.walls[wl.side] || 'auto',
{ hint: `Automatisch: ${lt.endOffset} mm uit de hoek, alleen op vrije gevel van min. ${lt.minWallWidth} mm. Naast openingen: ${lt.openingOffset} mm naast elke opening.` }) +
UI.specCard('Op dit wandstuk', 'lightbulb', [['Gevellampen', piece.lamps.length, 'st.']])
: info('Gevellampen staan uit. Kies bij Woning › Verlichting het type "Gevellampen".');
propsEl.innerHTML = warn + head('bricks', piece.element, piece.label) +
cols(UI.specCard('Wandstuk', 'rulers', rows), section('Gevellampen', 'lightbulb', lampen)) +
(wl.dropped.length ? UI.warn(`${wl.dropped.length} opening(en) van deze zijde vallen in een naad en vervallen.`) : '') +
actions(button('Wand bewerken', 'pencil-square', `data-go="wall:${pu.id}:${wl.side}"`));
UI.onSel('hw-light', v => { unitById(pu.id).lighting.walls[wl.side] = v; }, rerender);
}
function renderIwProps(iw, warn) {
const p = parts.interior.find(x => x.id === iw.id);
propsEl.innerHTML = warn + head('distribute-horizontal', p.element, `Binnenwand &middot; ${p.place.width} mm`) +
cols(
section('Positie', 'crosshair', num('hi-x1', 'Begin X', iw.x1) + num('hi-z1', 'Begin Y', iw.z1) + num('hi-x2', 'Eind X', iw.x2) + num('hi-z2', 'Eind Y', iw.z2)),
section('Opbouw', 'layers', num('hi-thick', 'Dikte', HouseModel.thicknessOf(iw.wall), { step: 5 }) + num('hi-height', 'Hoogte', iw.wall.wall.heightAll))) +
actions(
button('Openingen bewerken', 'pencil-square', `data-go="iw:${iw.id}"`),
button('Verwijderen', 'trash', `data-remove-iw="${iw.id}"`, 'ui-btn-danger'));
['x1', 'z1', 'x2', 'z2'].forEach(k => UI.onNum('hi-' + k, v => { iw[k] = v; }, after));
UI.onNum('hi-thick', v => { if (v >= 40) HouseModel.setThickness(iw.wall, v); }, after);
UI.onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; }, after);
}
// Woning-instellingen (niets geselecteerd): alles wat voor de hele woning
// geldt, in tabbladen.
function renderHouseProps(warn) {
const st = state.settings, sp = st.spacing, r = st.roof, lt = st.lighting;
const pos = { positive: true };
const verlichting = lt.mode === 'gevel'
? cols(
section('Gevellampen', 'lightbulb', sel('hs-light-dir', 'Lichtrichting', LightingModel.DIRECTIONS, lt.direction) +
num('hs-lamp-h', 'Hoogte', lt.lampHeight, { hint: 'Vanaf bovenkant vloer' }) +
num('hs-lamp-min', 'Afstand min.', lt.lampMin, { hint: 'Hart-op-hart' }) + num('hs-lamp-max', 'Afstand max.', lt.lampMax, { hint: 'Hart-op-hart' })),
section('Plaatsing', 'distribute-horizontal', num('hs-lamp-w', 'Min. gevelbreedte', lt.minWallWidth, { hint: 'Smallere vrije stukken gevel krijgen geen lamp' }) +
num('hs-lamp-end', 'Afstand tot hoek', lt.endOffset) +
num('hs-lamp-open', 'Afstand tot opening', lt.openingOffset, { hint: 'Voor wanden met de instelling "Naast openingen"' })))
: lt.mode === 'spots'
? cols(section('Boeideel-spots', 'lightbulb', num('hs-spot-min', 'Afstand min.', r.spotMin, { hint: 'Hart-op-hart' }) + num('hs-spot-max', 'Afstand max.', r.spotMax, { hint: 'Hart-op-hart' })),
info('Spots komen alleen onder units met boeidelen.'))
: '';
propsEl.innerHTML = warn + head('house-door', 'Woning', `${state.units.length} unit(s) &middot; ${state.interiorWalls.length} binnenwand(en)`) +
UI.tabs('woning', [
{ key: 'algemeen', label: 'Algemeen', icon: 'house-door', html:
info('Klik in de plattegrond op een unit, wand of binnenwand om te selecteren; dubbelklik om te bewerken. Waar units tegen elkaar staan (oranje gestippeld) vervalt de wand.') +
actions(button('Unit toevoegen', 'plus-lg', 'data-action="add-unit"'), button('Binnenwand tekenen', 'pencil', 'data-action="draw-iw"'),
button('Projectgegevens', 'card-text', 'data-action="project"')) },
{ key: 'gevel', label: 'Gevel', icon: 'bricks', html: cols(
section('Gevelbekleding', 'grip-horizontal', sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'WEO/Novalit, voorlopig voor alle buitenwanden' })) +
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Cement-gebonden plaat onderaan de gevel' }) +
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) : '')),
section('Sandwichpanelen', 'bounding-box', toggle('hs-sandwich', 'Rondom woning', st.sandwich,
{ hint: `${SandwichModel.PANEL_WIDTH_MM} × ${SandwichModel.PANEL_LENGTH_MM} × ${SandwichModel.THICK_MM} mm, vanaf bovenkant vloer ${SandwichModel.DEPTH_MM} mm de grond in` }))) },
{ key: 'constructie', label: 'Regelwerk', icon: 'columns-gap', html: cols(
section('Buitenwanden', 'bricks', num('hs-ext-min', 'Afstand min.', sp.exterior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-ext-max', 'Afstand max.', sp.exterior.max, { step: 10, hint: 'Hart-op-hart stijlen' })),
section('Binnenwanden', 'distribute-horizontal', num('hs-int-min', 'Afstand min.', sp.interior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-int-max', 'Afstand max.', sp.interior.max, { step: 10, hint: 'Hart-op-hart stijlen' }))) },
{ key: 'dak', label: 'Dakrand', icon: 'house', html: cols(
section('Boeideel', 'border-outer', num('hs-overhang', 'Overstek', r.overhang, { step: 10, hint: `${RoofModel.OVERHANG_MIN} – ${RoofModel.OVERHANG_MAX} mm` }) + num('hs-fascia-h', 'Hoogte', r.fasciaHeight, { step: 10 })),
section('Daktrim', 'border-top', num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }))) },
{ key: 'verlichting', label: 'Verlichting', icon: 'lightbulb', html:
cols(sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode), '') + verlichting }
]);
UI.onSel('hs-cladding', v => { st.cladding = v; }, rerender);
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
UI.onNum('hs-trasraam-h', v => { st.trasraam.height = v; }, after, pos);
UI.onNum('hs-ext-min', v => { sp.exterior.min = v; }, after, pos);
UI.onNum('hs-ext-max', v => { sp.exterior.max = v; }, after, pos);
UI.onNum('hs-int-min', v => { sp.interior.min = v; }, after, pos);
UI.onNum('hs-int-max', v => { sp.interior.max = v; }, after, pos);
UI.onNum('hs-overhang', v => { r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); }, after, pos);
UI.onNum('hs-fascia-h', v => { r.fasciaHeight = v; }, after, pos);
UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos);
UI.onNum('hs-spot-min', v => { r.spotMin = v; }, after, pos);
UI.onNum('hs-spot-max', v => { r.spotMax = v; }, after, pos);
UI.onSel('hs-light-mode', v => { lt.mode = v; }, rerender);
UI.onSel('hs-light-dir', v => { lt.direction = v; }, rerender);
UI.onNum('hs-lamp-h', v => { lt.lampHeight = v; }, after, pos);
UI.onNum('hs-lamp-min', v => { lt.lampMin = v; }, after, pos);
UI.onNum('hs-lamp-max', v => { lt.lampMax = v; }, after, pos);
UI.onNum('hs-lamp-w', v => { lt.minWallWidth = v; }, after, pos);
UI.onNum('hs-lamp-end', v => { lt.endOffset = v; }, after, pos);
UI.onNum('hs-lamp-open', v => { lt.openingOffset = v; }, after, pos);
}
propsEl.addEventListener('click', (e) => {
@@ -859,37 +804,29 @@ function mountHouseEditor(container, callbacks) {
else if (act.dataset.action === 'project') openProjectProperties();
});
// --- Specificaties/totalen ---
// --- Specificaties/totalen (onder de 3D) ---
function renderSpecs(totals) {
const row = (label, value) => `<div class="result-row"><span>${label}:</span><span class="highlight">${value}</span></div>`;
document.getElementById('ho-specs').innerHTML = `
<div class="controls-grid">
<div>
<div class="spec-section"><h1>Gewicht</h1>
${totals.weights.map(r => row(r.label, houseFmt(r.kg) + ' kg')).join('')}
<div class="divider"></div>${row('Totaal woning', houseFmt(totals.totalKg) + ' kg')}
</div>
</div>
<div>
<div class="spec-section"><h1>Houtwerk &amp; isolatie</h1>
${row('Aantal balken', totals.beamCount + ' stuks')}
${row('Volume hout', houseFmt(totals.woodM3, 3) + ' m3')}
${row('Latwerk 20x30 (kruislat)', totals.battens.count + ' st. &agrave; ' + WallModel.BATTEN.length + ' mm')}
${row('Inkoop wandbalken', WallModel.purchaseText(totals.purchase))}
${row('Snijverlies', houseFmt(totals.purchaseWasteMM / 1000, 2) + ' m (' + houseFmt(totals.purchaseStockMM ? totals.purchaseWasteMM / totals.purchaseStockMM * 100 : 0) + '%)')}
${row('Glaswol (isolatie)', houseFmt(totals.isolatieM3, 3) + ' m3')}
</div>
<div class="spec-section"><h1>Plaatwerk</h1>
${totals.plaatwerk.filter(p => p.m2 > 0).map(p => row(p.naam, houseFmt(p.m2, 2) + ' m2')).join('')}
</div>
${totals.roofEdge ? `<div class="spec-section"><h1>Dakrand &amp; buitenverlichting</h1>
${totals.roofEdge.fasciaLengthM > 0 ? row('Boeidelen', houseFmt(totals.roofEdge.fasciaLengthM, 2) + ' m &middot; ' + totals.roofEdge.fasciaHeight + ' mm hoog') + row('Onderplaat boeideel', houseFmt(totals.roofEdge.soffitM2, 2) + ' m2') : ''}
${totals.roofEdge.trimLengthM > 0 ? row('Daktrim', houseFmt(totals.roofEdge.trimLengthM, 2) + ' m &middot; ' + totals.roofEdge.trimHeight + ' mm hoog') : ''}
${row('Boeideel-spots', totals.roofEdge.spots + ' stuks')}
${row('Gevellampen', totals.lamps.total + ' stuks')}
</div>` : ''}
</div>
</div>`;
const f = houseFmt;
const cards = [
UI.specCard('Gewicht', 'speedometer2', totals.weights.map(r => [r.label, f(r.kg), 'kg']), ['Totaal woning', f(totals.totalKg), 'kg']),
UI.specCard('Hout & isolatie', 'tree', [
['Balken', totals.beamCount, 'st.'],
['Volume hout', f(totals.woodM3, 3), 'm³'],
['Inkoop wandbalken', WallModel.purchaseText(totals.purchase), ''],
['Snijverlies', `${f(totals.purchaseWasteMM / 1000, 2)} m · ${f(totals.purchaseStockMM ? totals.purchaseWasteMM / totals.purchaseStockMM * 100 : 0)}`, '%'],
[`Latwerk ${WallModel.BATTEN.thick}×${WallModel.BATTEN.width}`, `${totals.battens.count} × ${WallModel.BATTEN.length}`, 'mm'],
['Glaswol', f(totals.isolatieM3, 3), 'm³']
]),
UI.specCard('Plaatwerk', 'layers', totals.plaatwerk.filter(p => p.m2 > 0).map(p => [p.naam, f(p.m2, 2), 'm²']))
];
if (totals.roofEdge) {
const e = totals.roofEdge, rows = [];
if (e.fasciaLengthM > 0) rows.push([`Boeidelen · ${e.fasciaHeight} mm`, f(e.fasciaLengthM, 2), 'm'], ['Onderplaat boeideel', f(e.soffitM2, 2), 'm²']);
if (e.trimLengthM > 0) rows.push([`Daktrim · ${e.trimHeight} mm`, f(e.trimLengthM, 2), 'm']);
rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.']);
cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows));
}
document.getElementById('ho-specs').innerHTML = `<div class="ui-spec-grid">${cards.join('')}</div>`;
}
// --- Alles opnieuw doorrekenen ---
@@ -952,10 +889,8 @@ function mountHouseEditor(container, callbacks) {
}
window.addEventListener('resize', onResize);
function toggleFullscreen(cls, other) {
const el = document.getElementById('house-overview');
el.classList.remove(other);
el.classList.toggle(cls);
function toggleView(fn) {
fn(document.getElementById('house-overview'));
requestAnimationFrame(onResize);
}
@@ -974,8 +909,8 @@ function mountHouseEditor(container, callbacks) {
},
refresh,
setTool,
toggleFullscreen3D: () => toggleFullscreen('fullscreen-3d', 'fullscreen-2d'),
toggleFullscreen2D: () => toggleFullscreen('fullscreen-2d', 'fullscreen-3d')
toggleFullscreen3D: () => toggleView(UI.cycle3D),
toggleFullscreen2D: () => toggleView(UI.toggle2D)
};
}