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
+8
View File
@@ -151,6 +151,14 @@ assets/ logo (preloader) + bestemmingsmap voor toekomstige
t.o.v. het bovenaanzicht (boven = noord): voor = noord, achter = zuid,
links = west, rechts = oost (intern blijven de oude sleutels). Oude unit-JSON's kunnen in
een woning worden geïmporteerd. Units kunnen (nog) niet gedraaid worden.
- **UI-opschoning (v0.4.1).** Gedeelde UI-kit (js/core/ui-kit.js, window.UI):
velden, schakelaars, secties, tabbladen, specificatie-kaarten en iconen
(Bootstrap Icons 1.11.3, MIT, lokaal in css/vendor met ingebed lettertype,
werkt via file://). Woning-instellingen staan in tabbladen (Algemeen, Gevel,
Regelwerk, Dakrand, Verlichting); unit-eigenschappen ook (Afmetingen,
Koppeling, Dak). Labels: zelfstandig naamwoord, geen dubbele punt, uitleg
in een hint (i-icoon) i.p.v. tussen haakjes. Sneltoets T wisselt in drie
stappen: standaard -> 3D groot -> alleen de render -> standaard.
- **Mijlpaal v0.4 — gevelopbouw.** Samenvatting van de v0.3.11–0.3.19-iteraties:
kruislat-latwerk op vochtdoek (richting gevelbekleding globaal), lampen op de
afgewerkte gevel (+52 mm), logo-watermerk op het doek, trasraam (cement-
+92
View File
@@ -544,3 +544,95 @@ html, body { height: 100%; }
.vf-menu.open { display: block; }
.vf-item { display: flex; align-items: center; gap: 8px; color: #eee; font-size: 13px; padding: 4px 0; cursor: pointer; user-select: none; }
.vf-item input { accent-color: var(--primary-on-dark); width: 15px; height: 15px; cursor: pointer; }
/* ============================================================
UI-KIT (core/ui-kit.js): velden, schakelaars, secties, tabbladen,
specificatie-kaarten. Iconen: Bootstrap Icons (css/vendor).
============================================================ */
.bi { line-height: 1; vertical-align: -0.125em; }
.ui-section { margin-bottom: 14px; }
.ui-section h2, .ui-spec-card h2 {
display: flex; align-items: center; gap: 7px; margin: 0 0 8px; font-size: 12px; font-weight: 600;
color: var(--primary); text-transform: uppercase; letter-spacing: .6px;
}
.ui-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 28px; }
.ui-field { display: flex; justify-content: space-between; align-items: center; gap: 10px; min-height: 28px; }
.ui-field label { font-size: 12.5px; color: var(--text); display: flex; align-items: center; gap: 5px; }
.ui-field.is-disabled label { color: var(--text-faint); }
.ui-control { display: flex; align-items: center; flex: none; }
.ui-control input[type=number], .ui-control select {
width: 118px; background: var(--surface); border: 1px solid var(--border); color: var(--text);
padding: 4px 6px; border-radius: 4px; font-size: 12.5px; font-family: inherit;
}
.ui-control input[type=number] { text-align: right; }
.ui-control input:focus, .ui-control select:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 2px var(--primary-soft); }
.ui-control input:disabled { opacity: .55; background: var(--border-soft); cursor: not-allowed; }
.ui-unit { width: 26px; margin-left: 5px; font-size: 11px; color: var(--text-faint); }
.ui-hint { color: var(--text-faint); font-size: 11px; cursor: help; }
.ui-hint:hover { color: var(--primary); }
.ui-switch { position: relative; display: inline-block; width: 36px; height: 20px; margin-right: 82px; }
.ui-switch input { opacity: 0; width: 0; height: 0; }
.ui-switch span { position: absolute; inset: 0; background: #c9d1db; border-radius: 20px; cursor: pointer; transition: background .15s; }
.ui-switch span::before { content: ''; position: absolute; width: 14px; height: 14px; left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: transform .15s; }
.ui-switch input:checked + span { background: var(--primary); }
.ui-switch input:checked + span::before { transform: translateX(16px); }
.ui-switch input:focus-visible + span { box-shadow: 0 0 0 2px var(--primary-soft); }
.ui-info, .ui-warn { display: flex; gap: 8px; align-items: flex-start; font-size: 12px; line-height: 1.5; border-radius: 4px; padding: 7px 10px; margin: 6px 0 10px; }
.ui-info { color: var(--text-muted); background: var(--surface); border: 1px solid var(--border-soft); }
.ui-warn { color: var(--danger); background: var(--danger-soft); border: 1px solid var(--danger); }
.ui-info .bi, .ui-warn .bi { margin-top: 2px; }
.ui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.ui-btn {
display: inline-flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--border);
color: var(--text); padding: 5px 11px; border-radius: 4px; font-size: 12px; cursor: pointer; font-family: inherit;
}
.ui-btn:hover { border-color: var(--primary); color: var(--primary); }
.ui-btn.ui-btn-danger { color: var(--danger); border-color: #e3b4b0; }
.ui-btn.ui-btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }
.ui-btn.ui-btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
.ui-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.ui-panel-head h1 { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; }
.ui-panel-head .ui-sub { font-size: 12px; color: var(--text-muted); }
.ui-tab-bar { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin: 0 0 12px; }
.ui-tab {
display: inline-flex; align-items: center; gap: 6px; background: none; border: none; border-bottom: 2px solid transparent;
margin-bottom: -1px; padding: 7px 12px; font-size: 12.5px; color: var(--text-muted); cursor: pointer; font-family: inherit;
}
.ui-tab:hover { color: var(--text); }
.ui-tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
.ui-tab-page { display: none; }
.ui-tab-page.active { display: block; }
.ui-spec-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
.ui-spec-card { background: var(--surface); border: 1px solid var(--border-soft); border-radius: 6px; padding: 10px 12px; }
.ui-spec-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; padding: 3px 0; border-bottom: 1px dashed var(--border-soft); }
.ui-spec-row:last-child { border-bottom: none; }
.ui-spec-val { font-weight: 600; color: var(--text); text-align: right; font-variant-numeric: tabular-nums; }
.ui-spec-val small { font-weight: normal; color: var(--text-faint); margin-left: 3px; }
.ui-spec-total { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 2px; }
.ui-spec-total .ui-spec-val { color: var(--primary); }
.btn-3d-toggle { display: inline-flex; align-items: center; gap: 6px; }
.hn-item .bi { margin-right: 6px; color: var(--text-muted); }
.hn-item.active .bi, .hn-item:hover .bi { color: var(--primary); }
.hn-label { display: inline-flex; align-items: center; }
/* Weergave-cyclus (T): render-only = ook het info-deel onder de 3D weg. */
.render-only #ho-specs, .render-only #specs-panel-3d, .render-only #floor-specs-panel-3d { display: none; }
/* Bestaande editor-panelen (wand/vloer) in dezelfde stijl als de UI-kit. */
.control-group { min-height: 28px; margin-bottom: 3px; gap: 10px; }
.control-group label { font-size: 12.5px; color: var(--text); }
.control-group input, .control-group select { width: 118px; font-size: 12.5px; padding: 4px 6px; font-family: inherit; }
h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; letter-spacing: .6px; }
.tab-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; text-transform: none; font-size: 12.5px; letter-spacing: 0; }
.result-row { padding: 3px 0; border-bottom: 1px dashed var(--border-soft); }
.result-row:last-child { border-bottom: none; }
.result-row .highlight { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
#specs-panel-3d .spec-section, #floor-specs-panel-3d .spec-section {
background: var(--surface); border: 1px solid var(--border-soft); border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;
}
.add-opening-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; text-transform: none; font-size: 12.5px; }
.hn-actions .ui-btn { flex: 1; justify-content: center; }
.hn-title { display: flex; align-items: center; gap: 6px; }
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019-2024 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -10,6 +10,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tectum — Van Emous Mantelzorgwoningen</title>
<link rel="stylesheet" href="css/vendor/bootstrap-icons.min.css">
<link rel="stylesheet" href="css/app.css">
<script src="js/vendor/three.min.js"></script>
<script src="js/vendor/OrbitControls.js"></script>
@@ -52,8 +53,8 @@
<button id="menu-beeld-btn" class="menu-btn">Beeld</button>
<div id="menu-beeld-dropdown" class="menu-dropdown">
<div class="menu-item" id="menu-fullscreen"><span>Volledig scherm</span><span class="menu-hotkey">F</span></div>
<div class="menu-item" id="menu-3d-fullscreen"><span>3D volledig scherm</span><span class="menu-hotkey">T</span></div>
<div class="menu-item" id="menu-2d-fullscreen"><span>2D volledig scherm</span><span class="menu-hotkey">R</span></div>
<div class="menu-item" id="menu-3d-fullscreen"><span>3D groot / alleen render</span><span class="menu-hotkey">T</span></div>
<div class="menu-item" id="menu-2d-fullscreen"><span>2D groot</span><span class="menu-hotkey">R</span></div>
</div>
</div>
@@ -116,6 +117,7 @@
<script src="js/core/canvas-labels.js"></script>
<script src="js/core/camera-memory.js"></script>
<script src="js/core/view-filter.js"></script>
<script src="js/core/ui-kit.js"></script>
<script src="js/vendor/jspdf.umd.min.js"></script>
<script src="js/core/pdf-sheet.js"></script>
<script src="js/wall/wall-model.js"></script>
+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);
}
+203 -268
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);
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);
}
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';
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 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 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(); }
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();
}
const lt = state.settings.lighting;
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);
}
// 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 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);
}
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; });
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; });
// 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>