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
+1 -1
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
const APP_VERSION = '0.4.0';
const APP_VERSION = '0.4.1';
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
let tabs = [];
+121
View File
@@ -0,0 +1,121 @@
/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// core/ui-kit.js — gedeelde UI-bouwstenen (window.UI): iconen (Bootstrap
// Icons, css/vendor), invoervelden, schakelaars, secties, tabbladen,
// specificatie-kaarten en de 3-staps weergave-cyclus (sneltoets T).
// Eén plek voor opmaak en woordgebruik, zodat alle panelen gelijk ogen.
//
// Woordgebruik labels: zelfstandig naamwoord met hoofdletter, geen dubbele
// punt, geen uitleg tussen haakjes (die gaat in een hint), eenheid rechts
// van het veld. Bijvoorbeeld "Afstand min." i.p.v. "Lampen min. h.o.h.".
// Plain script (geen ES-module).
// ============================================================
window.UI = (() => {
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const icon = (name, cls) => `<i class="bi bi-${name}${cls ? ' ' + cls : ''}" aria-hidden="true"></i>`;
const hint = (text) => text ? `<span class="ui-hint" title="${esc(text)}">${icon('info-circle')}</span>` : '';
function field(id, label, control, o) {
return `<div class="ui-field${o && o.disabled ? ' is-disabled' : ''}"><label for="${id}">${label}${hint(o && o.hint)}</label><div class="ui-control">${control}</div></div>`;
}
// Getal. o: { step, unit ('mm' standaard, '' = geen), hint, min, disabled }
function num(id, label, value, o) {
o = o || {};
const unit = o.unit === undefined ? 'mm' : o.unit;
return field(id, label,
`<input type="number" id="${id}" value="${value}" step="${o.step || 50}"${o.min !== undefined ? ` min="${o.min}"` : ''}${o.disabled ? ' disabled' : ''}>` +
`<span class="ui-unit">${unit}</span>`, o);
}
// Keuzelijst. options: { waarde: 'Label' }.
function sel(id, label, options, value, o) {
const opts = Object.entries(options).map(([k, v]) => `<option value="${k}"${k === value ? ' selected' : ''}>${v}</option>`).join('');
return field(id, label, `<select id="${id}">${opts}</select><span class="ui-unit"></span>`, o);
}
// Aan/uit-schakelaar.
function toggle(id, label, checked, o) {
return field(id, label,
`<label class="ui-switch"><input type="checkbox" id="${id}"${checked ? ' checked' : ''}><span></span></label><span class="ui-unit"></span>`, o);
}
const section = (title, iconName, body) =>
`<section class="ui-section"><h2>${iconName ? icon(iconName) : ''}${title}</h2>${body}</section>`;
const info = (text) => `<div class="ui-info">${icon('info-circle')}<span>${text}</span></div>`;
const warn = (text) => `<div class="ui-warn">${icon('exclamation-triangle')}<span>${text}</span></div>`;
const grid = (...cols) => `<div class="ui-grid">${cols.map(c => `<div>${c}</div>`).join('')}</div>`;
const button = (label, iconName, attrs, cls) =>
`<button type="button" class="ui-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}<span>${label}</span></button>`;
const actions = (...btns) => `<div class="ui-actions">${btns.join('')}</div>`;
// Tabbladen. group = sleutel waaronder het actieve tabblad onthouden wordt
// (bijv. 'woning', 'unit'); tabs = [{ key, label, icon, html }].
const activeTabs = {};
function tabs(group, list) {
const active = list.some(t => t.key === activeTabs[group]) ? activeTabs[group] : list[0].key;
return `<div class="ui-tabs" data-tab-group="${group}">
<div class="ui-tab-bar">${list.map(t => `<button type="button" class="ui-tab${t.key === active ? ' active' : ''}" data-tab="${t.key}">${t.icon ? icon(t.icon) : ''}<span>${t.label}</span></button>`).join('')}</div>
${list.map(t => `<div class="ui-tab-page${t.key === active ? ' active' : ''}" data-tab-page="${t.key}">${t.html}</div>`).join('')}
</div>`;
}
// Eenmalig per container: klik op een tab wisselt de pagina.
function wireTabs(container) {
if (container._uiTabsWired) return;
container._uiTabsWired = true;
container.addEventListener('click', (e) => {
const btn = e.target.closest('.ui-tab');
if (!btn) return;
const wrap = btn.closest('.ui-tabs');
activeTabs[wrap.dataset.tabGroup] = btn.dataset.tab;
wrap.querySelectorAll('.ui-tab').forEach(b => b.classList.toggle('active', b === btn));
wrap.querySelectorAll('.ui-tab-page').forEach(p => p.classList.toggle('active', p.dataset.tabPage === btn.dataset.tab));
});
}
// Specificaties: kaart met regels [label, waarde, eenheid].
function specCard(title, iconName, rows, total) {
const row = ([label, value, unit]) =>
`<div class="ui-spec-row"><span>${label}</span><span class="ui-spec-val">${value}${unit ? `<small>${unit}</small>` : ''}</span></div>`;
return `<div class="ui-spec-card"><h2>${icon(iconName)}${title}</h2>${rows.map(row).join('')}${total ? `<div class="ui-spec-total">${row(total)}</div>` : ''}</div>`;
}
// Invoer koppelen: onNum/onSel/onToggle(root, id, fn, after).
function onNum(id, fn, after, o) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('input', (e) => {
const v = parseFloat(e.target.value);
if (isFinite(v) && (!(o && o.positive) || v > 0)) { fn(v); after && after(); }
});
}
function onSel(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('input', (e) => { fn(e.target.value); after && after(); });
}
function onToggle(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('change', (e) => { fn(e.target.checked); after && after(); });
}
// Weergave-cyclus 3D (sneltoets T): standaard -> 3D groot -> alleen de
// render (ook het info-/specificatiedeel eronder weg) -> standaard.
function cycle3D(el) {
el.classList.remove('fullscreen-2d');
if (!el.classList.contains('fullscreen-3d')) el.classList.add('fullscreen-3d');
else if (!el.classList.contains('render-only')) el.classList.add('render-only');
else el.classList.remove('fullscreen-3d', 'render-only');
}
function toggle2D(el) {
el.classList.remove('fullscreen-3d', 'render-only');
el.classList.toggle('fullscreen-2d');
}
return { esc, icon, hint, num, sel, toggle, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
})();
+1 -1
View File
@@ -21,7 +21,7 @@ function createViewFilter(toolbar, items, onChange) {
const wrap = document.createElement('div');
wrap.className = 'view-filter';
wrap.innerHTML = `
<button type="button" class="btn-3d-toggle vf-btn">Weergave &#9662;</button>
<button type="button" class="btn-3d-toggle vf-btn"><i class="bi bi-eye"></i>Weergave<i class="bi bi-chevron-down"></i></button>
<div class="vf-menu">
${items.map(it => `<label class="vf-item"><input type="checkbox" data-key="${it.key}" ${state[it.key] ? 'checked' : ''}> ${it.label}</label>`).join('')}
</div>`;
+67 -73
View File
@@ -32,11 +32,11 @@ const FLOOR_TEMPLATE = `
<div id="floor-left-column">
<div id="floor-pane-2d-container">
<div class="pane-2d-sub" id="floor-pane-top">
<div class="pane-label">2D BOVENAANZICHT</div>
<div class="pane-label">BOVENAANZICHT</div>
<canvas id="floor-canvas-top"></canvas>
</div>
<div class="pane-2d-sub" id="floor-pane-section">
<div class="pane-label">DOORSNEDE (BALKPROFIEL)</div>
<div class="pane-label">DOORSNEDE</div>
<canvas id="floor-canvas-section"></canvas>
</div>
</div>
@@ -46,59 +46,59 @@ const FLOOR_TEMPLATE = `
<span id="f-io-status" class="io-status"></span>
</div>
<div class="tab-bar">
<button class="tab-btn active" data-tab="floor-tab-main">1. Vloer Opbouw</button>
<button class="tab-btn" data-tab="floor-tab-openings">2. Doorvoeren</button>
<button class="tab-btn" data-tab="floor-tab-chassis">3. Chassis</button>
<button class="tab-btn active" data-tab="floor-tab-main"><i class="bi bi-rulers"></i> Opbouw</button>
<button class="tab-btn" data-tab="floor-tab-openings"><i class="bi bi-bullseye"></i> Doorvoeren</button>
<button class="tab-btn" data-tab="floor-tab-chassis"><i class="bi bi-truck"></i> Chassis</button>
</div>
<div id="floor-tab-main" class="tab-content active">
<div class="controls-grid">
<div>
<h1>Vloer Dimensies (mm)</h1>
<div class="control-group"><label>Breedte:</label>
<h1><i class="bi bi-rulers"></i> Afmetingen</h1>
<div class="control-group"><label>Breedte</label>
<div><input type="number" id="f-inp-width" value="2000" step="50"><span class="unit">mm</span></div></div>
<div class="control-group"><label>Diepte:</label>
<div class="control-group"><label>Diepte</label>
<div><input type="number" id="f-inp-depth" value="2000" step="50"><span class="unit">mm</span></div></div>
<div class="divider"></div>
<h1>Vloerbalken</h1>
<div class="control-group"><label>Balk Breedte:</label>
<h1><i class="bi bi-columns-gap"></i> Vloerbalken</h1>
<div class="control-group"><label>Breedte</label>
<div><input type="number" id="f-inp-joist-w" value="70" step="5"><span class="unit">mm</span></div></div>
<div class="control-group"><label>Balk Hoogte:</label>
<div class="control-group"><label>Hoogte</label>
<div><input type="number" id="f-inp-joist-h" value="220" step="5"><span class="unit">mm</span></div></div>
<div class="divider"></div>
<h1>Onderlaag (OSB-plaat)</h1>
<div class="control-group"><label>Dikte:</label>
<h1><i class="bi bi-layers"></i> Onderlaag OSB</h1>
<div class="control-group"><label>Dikte</label>
<div><input type="number" id="f-inp-underlay-t" value="22" step="1" min="3" max="40"><span class="unit">mm</span></div></div>
</div>
<div>
<h1>Regelwerk Afstand (h.o.h.)</h1>
<div class="control-group"><label>Min. h.o.h.:</label>
<h1><i class="bi bi-arrows"></i> Regelwerk</h1>
<div class="control-group"><label>Afstand min.</label>
<div><input type="number" id="f-inp-min-sp" value="300" step="10"><span class="unit">mm</span></div></div>
<div class="control-group"><label>Max. h.o.h.:</label>
<div class="control-group"><label>Afstand max.</label>
<div><input type="number" id="f-inp-max-sp" value="600" step="10"><span class="unit">mm</span></div></div>
<div class="divider"></div>
<div class="result-row"><span>Totaal vloerbalken:</span><span data-spec="joist-count" class="highlight">-</span></div>
<div class="result-row"><span>Vloeroppervlak:</span><span data-spec="floor-area" class="highlight">-</span></div>
<div class="result-row"><span>Volume balkhout:</span><span data-spec="joist-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht balkhout:</span><span data-spec="joist-kg" class="highlight">-</span></div>
<div class="result-row"><span>Vloerbalken</span><span data-spec="joist-count" class="highlight">-</span></div>
<div class="result-row"><span>Vloeroppervlak</span><span data-spec="floor-area" class="highlight">-</span></div>
<div class="result-row"><span>Volume balkhout</span><span data-spec="joist-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht balkhout</span><span data-spec="joist-kg" class="highlight">-</span></div>
<div class="divider"></div>
<div class="result-row"><span>OSB-plaat oppervlak:</span><span data-spec="osb-m2" class="highlight">-</span></div>
<div class="result-row"><span>OSB-plaat volume:</span><span data-spec="osb-m3" class="highlight">-</span></div>
<div class="result-row"><span>OSB-plaat gewicht:</span><span data-spec="osb-kg" class="highlight">-</span></div>
<div class="result-row"><span>OSB oppervlak</span><span data-spec="osb-m2" class="highlight">-</span></div>
<div class="result-row"><span>OSB volume</span><span data-spec="osb-m3" class="highlight">-</span></div>
<div class="result-row"><span>OSB gewicht</span><span data-spec="osb-kg" class="highlight">-</span></div>
<div class="divider"></div>
<div class="result-row"><span>Isolatie-inhoud (leegte):</span><span data-spec="void-m3" class="highlight">-</span></div>
<div class="result-row"><span>Totaal gewicht vloer:</span><span data-spec="total-kg" class="highlight">-</span></div>
<div class="result-row"><span>Isolatie</span><span data-spec="void-m3" class="highlight">-</span></div>
<div class="result-row"><span>Totaal gewicht</span><span data-spec="total-kg" class="highlight">-</span></div>
</div>
</div>
</div>
<div id="floor-tab-openings" class="tab-content">
<button id="f-btn-add-opening" class="add-opening-btn">+ Doorvoer Toevoegen</button>
<button id="f-btn-add-opening" class="add-opening-btn"><i class="bi bi-plus-lg"></i> Doorvoer toevoegen</button>
<div id="f-openings-global-warn" class="global-warning" style="display:none;"></div>
<div id="f-openings-list"></div>
</div>
@@ -109,13 +109,13 @@ const FLOOR_TEMPLATE = `
</div>
<div class="controls-grid">
<div>
<div class="control-group"><label>Wieldiameter:</label>
<div class="control-group"><label>Wieldiameter</label>
<div><input type="number" id="f-inp-wheel-d" value="400" step="10" min="50"><span class="unit">mm</span></div></div>
</div>
<div>
<div class="control-group"><label>Spoorbreedte (X):</label>
<div class="control-group"><label>Spoorbreedte</label>
<div><input type="number" id="f-inp-track" value="1200" step="50" min="100"><span class="unit">mm</span></div></div>
<div class="control-group"><label>Wielbasis (Y):</label>
<div class="control-group"><label>Wielbasis</label>
<div><input type="number" id="f-inp-wheelbase" value="1200" step="50" min="100"><span class="unit">mm</span></div></div>
</div>
</div>
@@ -126,7 +126,7 @@ const FLOOR_TEMPLATE = `
<div id="floor-right-column">
<div id="floor-pane-3d">
<div class="pane-label">3D PERSPECTIEF</div>
<div class="pane-label">3D</div>
<div class="toolbar-3d" id="floor-toolbar-3d"></div>
</div>
@@ -134,27 +134,27 @@ const FLOOR_TEMPLATE = `
<div class="controls-grid">
<div>
<div class="spec-section">
<h1>Balken</h1>
<div class="result-row"><span>Vloerbalken:</span><span data-spec="joist-count" class="highlight">-</span></div>
<div class="result-row"><span>Vloeroppervlak:</span><span data-spec="floor-area" class="highlight">-</span></div>
<h1><i class="bi bi-columns-gap"></i> Balken</h1>
<div class="result-row"><span>Vloerbalken</span><span data-spec="joist-count" class="highlight">-</span></div>
<div class="result-row"><span>Vloeroppervlak</span><span data-spec="floor-area" class="highlight">-</span></div>
</div>
<div class="spec-section">
<h1>Houtwerk</h1>
<div class="result-row"><span>Volume balkhout:</span><span data-spec="joist-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht balkhout:</span><span data-spec="joist-kg" class="highlight">-</span></div>
<h1><i class="bi bi-tree"></i> Houtwerk</h1>
<div class="result-row"><span>Volume balkhout</span><span data-spec="joist-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht balkhout</span><span data-spec="joist-kg" class="highlight">-</span></div>
</div>
</div>
<div>
<div class="spec-section">
<h1>OSB-plaat (onderlaag, 22mm)</h1>
<div class="result-row"><span>Oppervlak:</span><span data-spec="osb-m2" class="highlight">-</span></div>
<div class="result-row"><span>Volume:</span><span data-spec="osb-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht:</span><span data-spec="osb-kg" class="highlight">-</span></div>
<h1><i class="bi bi-layers"></i> OSB onderlaag</h1>
<div class="result-row"><span>Oppervlak</span><span data-spec="osb-m2" class="highlight">-</span></div>
<div class="result-row"><span>Volume</span><span data-spec="osb-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht</span><span data-spec="osb-kg" class="highlight">-</span></div>
</div>
<div class="spec-section">
<h1>Isolatie &amp; totaal</h1>
<div class="result-row"><span>Inhoud leegtes:</span><span data-spec="void-m3" class="highlight">-</span></div>
<div class="result-row"><span>Totaal gewicht vloer:</span><span data-spec="total-kg" class="highlight">-</span></div>
<h1><i class="bi bi-speedometer2"></i> Isolatie en gewicht</h1>
<div class="result-row"><span>Isolatie</span><span data-spec="void-m3" class="highlight">-</span></div>
<div class="result-row"><span>Totaal gewicht</span><span data-spec="total-kg" class="highlight">-</span></div>
</div>
</div>
</div>
@@ -170,23 +170,23 @@ const FLOOR_TEMPLATE = `
</div>
<div class="controls-grid">
<div>
<div class="control-group"><label for="inp-proj-naam">Projectnaam:</label>
<div class="control-group"><label for="inp-proj-naam">Projectnaam</label>
<div><input type="text" id="inp-proj-naam" value=""></div></div>
<div class="control-group"><label for="inp-proj-plaats">Plaats:</label>
<div class="control-group"><label for="inp-proj-plaats">Plaats</label>
<div><input type="text" id="inp-proj-plaats" value=""></div></div>
<div class="control-group"><label for="inp-proj-straat">Straat:</label>
<div class="control-group"><label for="inp-proj-straat">Straat</label>
<div><input type="text" id="inp-proj-straat" value=""></div></div>
<div class="control-group"><label for="inp-proj-nummer">Projectnummer:</label>
<div class="control-group"><label for="inp-proj-nummer">Projectnummer</label>
<div><input type="text" id="inp-proj-nummer" value=""></div></div>
</div>
<div>
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar:</label>
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar</label>
<div><input type="text" id="inp-proj-tekenaar" value=""></div></div>
<div class="control-group"><label for="inp-proj-titel">Titel:</label>
<div class="control-group"><label for="inp-proj-titel">Titel</label>
<div><input type="text" id="inp-proj-titel" value="Vloer"></div></div>
<div class="control-group"><label for="inp-proj-element">Element nummer:</label>
<div class="control-group"><label for="inp-proj-element">Elementnummer</label>
<div><input type="text" id="inp-proj-element" value=""></div></div>
<div class="control-group"><label for="inp-proj-opmerking">Opmerking:</label>
<div class="control-group"><label for="inp-proj-opmerking">Opmerking</label>
<div><input type="text" id="inp-proj-opmerking" value=""></div></div>
</div>
</div>
@@ -203,18 +203,16 @@ const FLOOR_TEMPLATE = `
// (RoofModel). Zelfde editor, andere teksten + gespiegelde doorsnede/3D.
function floorTemplateForRoof() {
const r = [
['1. Vloer Opbouw', '1. Dak Opbouw'],
['Vloer Dimensies (mm)', 'Dak Dimensies (mm)'],
['<h1>Vloerbalken</h1>', '<h1>Dakbalken</h1>'],
['Onderlaag (OSB-plaat)', 'Plafondplaat (OSB, onderzijde)'],
['Totaal vloerbalken:', 'Totaal dakbalken:'],
['OSB-plaat (onderlaag, 22mm)', 'OSB-plaat (plafond)'],
['<button class="tab-btn" data-tab="floor-tab-chassis">3. Chassis</button>', ''],
['<h1>Isolatie &amp; totaal</h1>', '<h1>Dakleer, isolatie &amp; totaal</h1>\n <div class="result-row"><span>Dakleer oppervlak:</span><span data-spec="dakleer-m2" class="highlight">-</span></div>\n <div class="result-row"><span>Dakleer gewicht:</span><span data-spec="dakleer-kg" class="highlight">-</span></div>']
[' Vloerbalken</h1>', ' Dakbalken</h1>'],
['<span>Vloerbalken</span>', '<span>Dakbalken</span>'],
[' Onderlaag OSB</h1>', ' Plafondplaat OSB</h1>'],
[' OSB onderlaag</h1>', ' OSB plafond</h1>'],
['<button class="tab-btn" data-tab="floor-tab-chassis"><i class="bi bi-truck"></i> Chassis</button>', ''],
[' Isolatie en gewicht</h1>', ' Dakleer, isolatie en gewicht</h1>\n <div class="result-row"><span>Dakleer oppervlak</span><span data-spec="dakleer-m2" class="highlight">-</span></div>\n <div class="result-row"><span>Dakleer gewicht</span><span data-spec="dakleer-kg" class="highlight">-</span></div>']
];
let t = FLOOR_TEMPLATE;
r.forEach(([a, b]) => { t = t.split(a).join(b); });
return t.split('Vloeroppervlak:').join('Dakoppervlak:').split('Totaal gewicht vloer:').join('Totaal gewicht dak:');
return t.split('Vloeroppervlak').join('Dakoppervlak');
}
function mountFloorEditor(container, callbacks) {
@@ -262,9 +260,9 @@ function mountFloorEditor(container, callbacks) {
});
function anchorLabel(a) {
if (a === 'right') return 'Afstand vanaf oostzijde (X)';
if (a === 'middle') return 'Verschuiving t.o.v. midden (X)';
return 'Afstand vanaf westzijde (X)';
if (a === 'right') return 'Afstand tot oostzijde';
if (a === 'middle') return 'Verschuiving t.o.v. midden';
return 'Afstand tot westzijde';
}
function renderOpeningsList() {
@@ -277,13 +275,13 @@ function mountFloorEditor(container, callbacks) {
</div>
<div class="controls-grid">
<div>
<div class="control-group"><label>Diameter:</label>
<div class="control-group"><label>Diameter</label>
<div><input type="number" data-field="diameter" data-id="${op.id}" value="${op.diameter}" step="5" min="10"><span class="unit">mm</span></div></div>
<div class="control-group"><label>Afstand vanaf noordzijde (Y):</label>
<div class="control-group"><label>Afstand tot noordzijde</label>
<div><input type="number" data-field="offsetY" data-id="${op.id}" value="${op.offsetY}" step="50"><span class="unit">mm</span></div></div>
</div>
<div>
<div class="control-group"><label>Anker (X):</label>
<div class="control-group"><label>Anker</label>
<div>
<select data-field="anchorX" data-id="${op.id}">
<option value="left" ${op.anchorX === 'left' ? 'selected' : ''}>West</option>
@@ -292,7 +290,7 @@ function mountFloorEditor(container, callbacks) {
</select>
</div>
</div>
<div class="control-group"><label>${anchorLabel(op.anchorX)}:</label>
<div class="control-group"><label>${anchorLabel(op.anchorX)}</label>
<div><input type="number" data-field="offsetX" data-id="${op.id}" value="${op.offsetX}" step="50"><span class="unit">mm</span></div></div>
</div>
</div>
@@ -778,15 +776,11 @@ function mountFloorEditor(container, callbacks) {
window.addEventListener('resize', onWindowResize);
function toggleFullscreen3D() {
const el = document.getElementById('floor-app');
el.classList.remove('fullscreen-2d');
el.classList.toggle('fullscreen-3d');
UI.cycle3D(document.getElementById('floor-app')); // T: standaard -> 3D groot -> alleen render
requestAnimationFrame(onWindowResize);
}
function toggleFullscreen2D() {
const el = document.getElementById('floor-app');
el.classList.remove('fullscreen-3d');
el.classList.toggle('fullscreen-2d');
UI.toggle2D(document.getElementById('floor-app'));
requestAnimationFrame(onWindowResize);
}
+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)
};
}
+68 -75
View File
@@ -25,16 +25,16 @@ const WALL_TEMPLATE = `
<div id="left-column">
<div id="pane-2d-container">
<div class="pane-2d-sub pane-2d-primary" id="pane-2d-front">
<div class="pane-label">2D BUITENAANZICHT</div>
<div class="pane-label">BUITENAANZICHT</div>
<canvas id="canvas-2d-front"></canvas>
</div>
<div class="pane-2d-sub pane-2d-row" id="pane-2d-bottom-row">
<div class="pane-2d-half" id="pane-2d-top">
<div class="pane-label">2D BOVENAANZICHT</div>
<div class="pane-label">BOVENAANZICHT</div>
<canvas id="canvas-2d-top"></canvas>
</div>
<div class="pane-2d-half" id="pane-2d-side">
<div class="pane-label">2D ZIJAANZICHT</div>
<div class="pane-label">ZIJAANZICHT</div>
<canvas id="canvas-2d-side"></canvas>
</div>
</div>
@@ -46,100 +46,97 @@ const WALL_TEMPLATE = `
<span id="io-status" class="io-status"></span>
</div>
<div class="tab-bar">
<button class="tab-btn active" data-tab="tab-wall">1. Wand Opbouw</button>
<button class="tab-btn" data-tab="tab-opening">2. Openingen</button>
<button class="tab-btn" data-tab="tab-materials">3. Materiaalkeuze</button>
<button class="tab-btn active" data-tab="tab-wall"><i class="bi bi-rulers"></i> Opbouw</button>
<button class="tab-btn" data-tab="tab-opening"><i class="bi bi-door-open"></i> Openingen</button>
<button class="tab-btn" data-tab="tab-materials"><i class="bi bi-layers"></i> Materiaal</button>
</div>
<!-- TAB 1: WAND -->
<div id="tab-wall" class="tab-content active">
<div class="controls-grid">
<div>
<h1>Wand Dimensies (mm)</h1>
<h1><i class="bi bi-rulers"></i> Afmetingen</h1>
<div class="control-group">
<label for="inp-wall-w">Wand Breedte:</label>
<label for="inp-wall-w">Breedte</label>
<div><input type="number" id="inp-wall-w" value="6000" step="50" min="800" max="10000"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-wall-h-all">Totale Hoogte:</label>
<label for="inp-wall-h-all">Hoogte</label>
<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 ($Y_1$):</label>
<label for="inp-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 ($Y_2$):</label>
<label for="inp-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="divider"></div>
<h1>Verticale Balken & Dorpels</h1>
<h1><i class="bi bi-columns-gap"></i> Stijlen</h1>
<div class="control-group">
<label for="inp-stud-w">Verticale Balk Dikte/Breedte:</label>
<label for="inp-stud-w">Dikte</label>
<div><input type="number" id="inp-stud-w" value="38" step="5" min="20" max="300"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-stud-d">Verticale Balk Diepte:</label>
<label for="inp-stud-d">Diepte</label>
<div><input type="number" id="inp-stud-d" value="235" step="5" min="20" max="400"><span class="unit">mm</span></div>
</div>
</div>
<div>
<h1>Muur Liggers (Top & Onder)</h1>
<h1><i class="bi bi-distribute-vertical"></i> Regels (boven en onder)</h1>
<div class="control-group">
<label for="inp-plate-w">Ligger Dikte:</label>
<label for="inp-plate-w">Dikte</label>
<div><input type="number" id="inp-plate-w" value="38" step="5" min="20" max="200"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-plate-d">Ligger Diepte:</label>
<label for="inp-plate-d">Diepte</label>
<div><input type="number" id="inp-plate-d" value="235" step="5" min="20" max="400"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-sheathing-t">Beplating Dikte:</label>
<label for="inp-sheathing-t">Dikte beplating</label>
<div><input type="number" id="inp-sheathing-t" value="10" step="1" min="3" max="30"><span class="unit">mm</span></div>
</div>
<div class="divider"></div>
<h1>Regelwerk Afstand (h.o.h.)</h1>
<h1><i class="bi bi-arrows"></i> Regelwerk</h1>
<div class="control-group">
<label for="inp-min-sp">Min. h.o.h.:</label>
<label for="inp-min-sp">Afstand min.</label>
<div><input type="number" id="inp-min-sp" value="240" step="10" min="100" max="1000"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-max-sp">Max. h.o.h.:</label>
<label for="inp-max-sp">Afstand max.</label>
<div><input type="number" id="inp-max-sp" value="480" step="10" min="150" max="1500"><span class="unit">mm</span></div>
</div>
<div class="divider"></div>
<div class="result-row"><span>Totaal verticale balken:</span><span data-spec="vert-beams" class="highlight">-</span></div>
<div class="result-row"><span>Totaal horizontale balken:</span><span data-spec="horiz-beams" class="highlight">-</span></div>
<div class="result-row"><span>Dakhelling:</span><span data-spec="roof-angle" class="highlight">-</span></div>
<div class="result-row"><span>Verticale balken</span><span data-spec="vert-beams" class="highlight">-</span></div>
<div class="result-row"><span>Horizontale balken</span><span data-spec="horiz-beams" class="highlight">-</span></div>
<div class="result-row"><span>Dakhelling</span><span data-spec="roof-angle" class="highlight">-</span></div>
</div>
</div>
</div>
<!-- TAB 2: OPENINGEN (meerdere, ankerbaar) -->
<div id="tab-opening" class="tab-content">
<button id="btn-add-opening" class="add-opening-btn">+ Opening Toevoegen</button>
<button id="btn-add-opening" class="add-opening-btn"><i class="bi bi-plus-lg"></i> Opening toevoegen</button>
<div id="openings-global-warning" class="global-warning" style="display:none;"></div>
<div id="openings-list"></div>
</div>
<!-- TAB 3: MATERIAALKEUZE -->
<div id="tab-materials" class="tab-content">
<h1>Binnenzijde</h1>
<div class="divider"></div>
<h1>Buitenzijde</h1>
<h1><i class="bi bi-layers"></i> Buitenzijde</h1>
<div class="material-toggle-group">
<button type="button" id="btn-mat-vochtdoek" class="material-toggle-btn active" data-material="vochtdoek">Vochtdoek</button>
<button type="button" id="btn-mat-cementplaat" class="material-toggle-btn" data-material="cementplaat">Cement-gebonden platen</button>
<button type="button" id="btn-mat-cementplaat" class="material-toggle-btn" data-material="cementplaat">Cementplaat</button>
<button type="button" id="btn-mat-spaanplaat" class="material-toggle-btn" data-material="spaanplaat">Spaanplaat</button>
</div>
<div id="cladding-section">
<div class="divider"></div>
<h1>Gevelbekleding (WEO/Novalit) &mdash; richting</h1>
<h1><i class="bi bi-grip-horizontal"></i> Richting gevelbekleding</h1>
<div class="material-toggle-group">
<button type="button" class="material-toggle-btn active" data-cladding="horizontaal">Horizontaal</button>
<button type="button" class="material-toggle-btn" data-cladding="verticaal">Verticaal</button>
@@ -153,7 +150,7 @@ const WALL_TEMPLATE = `
<div id="right-column">
<div id="pane-3d">
<div class="pane-label">3D PERSPECTIEF</div>
<div class="pane-label">3D</div>
<div class="toolbar-3d" id="wall-toolbar-3d"></div>
</div>
@@ -161,37 +158,37 @@ const WALL_TEMPLATE = `
<div class="controls-grid">
<div>
<div class="spec-section">
<h1>Balken &amp; Dak</h1>
<div class="result-row"><span>Verticale balken:</span><span data-spec="vert-beams" class="highlight">-</span></div>
<div class="result-row"><span>Horizontale balken:</span><span data-spec="horiz-beams" class="highlight">-</span></div>
<div class="result-row"><span>Dakhelling:</span><span data-spec="roof-angle" class="highlight">-</span></div>
<h1><i class="bi bi-columns-gap"></i> Balken</h1>
<div class="result-row"><span>Verticale balken</span><span data-spec="vert-beams" class="highlight">-</span></div>
<div class="result-row"><span>Horizontale balken</span><span data-spec="horiz-beams" class="highlight">-</span></div>
<div class="result-row"><span>Dakhelling</span><span data-spec="roof-angle" class="highlight">-</span></div>
</div>
<div class="spec-section">
<h1>Houtwerk</h1>
<div class="result-row"><span>Volume hout:</span><span data-spec="wood-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht hout:</span><span data-spec="wood-kg" class="highlight">-</span></div>
<div class="result-row"><span>Inkoop balken:</span><span data-spec="stock-list" class="highlight">-</span></div>
<div class="result-row"><span>Snijverlies:</span><span data-spec="stock-waste" class="highlight">-</span></div>
<h1><i class="bi bi-tree"></i> Houtwerk</h1>
<div class="result-row"><span>Volume hout</span><span data-spec="wood-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht hout</span><span data-spec="wood-kg" class="highlight">-</span></div>
<div class="result-row"><span>Inkoop balken</span><span data-spec="stock-list" class="highlight">-</span></div>
<div class="result-row"><span>Snijverlies</span><span data-spec="stock-waste" class="highlight">-</span></div>
</div>
<div class="spec-section">
<h1>Spaanplaat (binnenzijde)</h1>
<div class="result-row"><span>Oppervlak:</span><span data-spec="spaanplaat-m2" class="highlight">-</span></div>
<div class="result-row"><span>Volume:</span><span data-spec="spaanplaat-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht:</span><span data-spec="spaanplaat-kg" class="highlight">-</span></div>
<h1><i class="bi bi-layers"></i> Binnenzijde: spaanplaat</h1>
<div class="result-row"><span>Oppervlak</span><span data-spec="spaanplaat-m2" class="highlight">-</span></div>
<div class="result-row"><span>Volume</span><span data-spec="spaanplaat-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht</span><span data-spec="spaanplaat-kg" class="highlight">-</span></div>
</div>
</div>
<div>
<div class="spec-section">
<h1>Buitenzijde (<span data-spec="buiten-materiaalnaam">Vochtdoek</span>)</h1>
<div class="result-row"><span>Oppervlak:</span><span data-spec="buiten-m2" class="highlight">-</span></div>
<div class="result-row"><span>Volume:</span><span data-spec="buiten-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht:</span><span data-spec="buiten-kg" class="highlight">-</span></div>
<div class="result-row"><span>Latwerk 20x30 (kruislat):</span><span data-spec="latten" class="highlight">-</span></div>
<h1><i class="bi bi-layers-half"></i> Buitenzijde: <span data-spec="buiten-materiaalnaam">Vochtdoek</span></h1>
<div class="result-row"><span>Oppervlak</span><span data-spec="buiten-m2" class="highlight">-</span></div>
<div class="result-row"><span>Volume</span><span data-spec="buiten-m3" class="highlight">-</span></div>
<div class="result-row"><span>Gewicht</span><span data-spec="buiten-kg" class="highlight">-</span></div>
<div class="result-row"><span>Latwerk 20×30</span><span data-spec="latten" class="highlight">-</span></div>
</div>
<div class="spec-section">
<h1>Isolatie &amp; totaal</h1>
<div class="result-row"><span>Inhoud leegtes:</span><span data-spec="void-m3" class="highlight">-</span></div>
<div class="result-row"><span>Totaal gewicht wand:</span><span data-spec="total-kg" class="highlight">-</span></div>
<h1><i class="bi bi-speedometer2"></i> Isolatie en gewicht</h1>
<div class="result-row"><span>Isolatie</span><span data-spec="void-m3" class="highlight">-</span></div>
<div class="result-row"><span>Totaal gewicht</span><span data-spec="total-kg" class="highlight">-</span></div>
</div>
</div>
</div>
@@ -207,23 +204,23 @@ const WALL_TEMPLATE = `
</div>
<div class="controls-grid">
<div>
<div class="control-group"><label for="inp-proj-naam">Projectnaam:</label>
<div class="control-group"><label for="inp-proj-naam">Projectnaam</label>
<div><input type="text" id="inp-proj-naam" value=""></div></div>
<div class="control-group"><label for="inp-proj-plaats">Plaats:</label>
<div class="control-group"><label for="inp-proj-plaats">Plaats</label>
<div><input type="text" id="inp-proj-plaats" value=""></div></div>
<div class="control-group"><label for="inp-proj-straat">Straat:</label>
<div class="control-group"><label for="inp-proj-straat">Straat</label>
<div><input type="text" id="inp-proj-straat" value=""></div></div>
<div class="control-group"><label for="inp-proj-nummer">Projectnummer:</label>
<div class="control-group"><label for="inp-proj-nummer">Projectnummer</label>
<div><input type="text" id="inp-proj-nummer" value=""></div></div>
</div>
<div>
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar:</label>
<div class="control-group"><label for="inp-proj-tekenaar">Tekenaar</label>
<div><input type="text" id="inp-proj-tekenaar" value=""></div></div>
<div class="control-group"><label for="inp-proj-titel">Titel:</label>
<div class="control-group"><label for="inp-proj-titel">Titel</label>
<div><input type="text" id="inp-proj-titel" value="BuitenWand"></div></div>
<div class="control-group"><label for="inp-proj-element">Element nummer:</label>
<div class="control-group"><label for="inp-proj-element">Elementnummer</label>
<div><input type="text" id="inp-proj-element" value=""></div></div>
<div class="control-group"><label for="inp-proj-opmerking">Opmerking:</label>
<div class="control-group"><label for="inp-proj-opmerking">Opmerking</label>
<div><input type="text" id="inp-proj-opmerking" value=""></div></div>
</div>
</div>
@@ -338,9 +335,9 @@ function mountWallEditor(container, callbacks) {
// OPENINGEN: UI (lijst met kaarten, ankerkeuze, toevoegen/verwijderen)
// ============================================================
function anchorOffsetLabel(anchor) {
if (anchor === 'right') return 'Afstand vanaf rechts:';
if (anchor === 'middle') return 'Verschuiving t.o.v. midden:';
return 'Afstand vanaf links:';
if (anchor === 'right') return 'Afstand tot rechts';
if (anchor === 'middle') return 'Verschuiving t.o.v. midden';
return 'Afstand tot links';
}
function addOpening() {
@@ -360,16 +357,12 @@ function mountWallEditor(container, callbacks) {
}
function toggleFullscreen3D() {
const el = document.getElementById('app-container');
el.classList.remove('fullscreen-2d');
el.classList.toggle('fullscreen-3d');
UI.cycle3D(document.getElementById('app-container')); // T: standaard -> 3D groot -> alleen render
requestAnimationFrame(onWindowResize);
}
function toggleFullscreen2D() {
const el = document.getElementById('app-container');
el.classList.remove('fullscreen-3d');
el.classList.toggle('fullscreen-2d');
UI.toggle2D(document.getElementById('app-container'));
requestAnimationFrame(onWindowResize);
}
@@ -448,7 +441,7 @@ function mountWallEditor(container, callbacks) {
<div class="controls-grid">
<div>
<div class="control-group">
<label>Type:</label>
<label>Type</label>
<div>
<select data-field="type" data-id="${op.id}">
${Object.entries(WallModel.OPENING_TYPES).map(([k, t]) => `<option value="${k}" ${(op.type || 'raam') === k ? 'selected' : ''}>${t.label}</option>`).join('')}
@@ -456,21 +449,21 @@ function mountWallEditor(container, callbacks) {
</div>
</div>
<div class="control-group">
<label>Breedte:</label>
<label>Breedte</label>
<div><input type="number" data-field="w" data-id="${op.id}" value="${op.w}" step="50" min="400" max="2850"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label>Hoogte:</label>
<label>Hoogte</label>
<div><input type="number" data-field="h" data-id="${op.id}" value="${op.h}" step="50" min="400"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label>Hoogte vloer (Y):</label>
<label>Afstand tot vloer</label>
<div><input type="number" data-field="y" data-id="${op.id}" value="${op.y}" step="50" min="0"><span class="unit">mm</span></div>
</div>
</div>
<div>
<div class="control-group">
<label>Ankerpunt:</label>
<label>Ankerpunt</label>
<div>
<select data-field="anchor" data-id="${op.id}">
<option value="left" ${op.anchor === 'left' ? 'selected' : ''}>Links</option>