1 Commits
14 changed files with 217 additions and 175 deletions
+1 -1
View File
@@ -50,7 +50,7 @@
<div class="license-box" id="about-license"></div> <div class="license-box" id="about-license"></div>
<div class="about-footer"> <div class="about-footer">
<span>&copy; 2026 Machiel Heinen &middot; ontwikkeld in opdracht van Van Emous Mantelzorgwoningen</span> <span>&copy; 2026 Machiel Heinen &middot; ontwikkeld in opdracht van Van Emous Mantelzorgwoningen</span>
<button id="about-close" class="about-close">Sluiten</button> <button id="about-close" class="about-close" data-esc>Sluiten</button>
</div> </div>
</div> </div>
</div> </div>
+15 -3
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md. * Zie LICENTIE.md.
*/ */
const APP_VERSION = '0.7.1'; const APP_VERSION = '0.7.2';
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang. // 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
let tabs = []; let tabs = [];
@@ -243,9 +243,15 @@ function toggleBrowserFullscreen() {
else document.documentElement.requestFullscreen(); else document.documentElement.requestFullscreen();
} }
// Alle dialogen; Esc gebruikt hun Annuleren-knop (data-esc), zodat bijv. projectgegevens worden teruggezet.
const OVERLAYS = '.dlg-overlay.open, #about-overlay.open, #new-overlay.open, #house-project-overlay.open, #wall-project-overlay.open, #cost-settings-overlay.open';
function closeOverlays() { function closeOverlays() {
const open = document.querySelector('.dlg-overlay.open, #about-overlay.open, #new-overlay.open'); const open = document.querySelector(OVERLAYS);
if (open) { open.classList.remove('open'); return true; } if (open) {
const cancel = open.querySelector('[data-esc]');
if (cancel) cancel.click(); else open.classList.remove('open');
return true;
}
return Menubar.close() || UI.closePopovers(); return Menubar.close() || UI.closePopovers();
} }
@@ -253,6 +259,12 @@ function wireHotkeys() {
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
const inField = ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName); const inField = ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName);
const k = e.key.toLowerCase(); const k = e.key.toLowerCase();
// Dialoog open: alleen Esc (sluiten); geen sneltoetsen naar de editor erachter. Typen in de dialoog blijft werken.
if (document.querySelector(OVERLAYS)) {
if (e.key === 'Escape') closeOverlays();
e.stopImmediatePropagation();
return;
}
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
const act = { s: () => editorCall('exportDesign'), n: openNewModal, o: () => editorCall('triggerImport'), '\\': togglePanels }[k]; const act = { s: () => editorCall('exportDesign'), n: openNewModal, o: () => editorCall('triggerImport'), '\\': togglePanels }[k];
if (act && !e.altKey && !e.shiftKey) { e.preventDefault(); act(); } if (act && !e.altKey && !e.shiftKey) { e.preventDefault(); act(); }
+17 -10
View File
@@ -9,11 +9,9 @@
// zodat wire() alleen de eigen knoppen koppelt. // zodat wire() alleen de eigen knoppen koppelt.
window.Inspector = (() => { window.Inspector = (() => {
const icon = (n) => `<i class="bi bi-${n}" aria-hidden="true"></i>`; const { icon, esc } = UI;
const big = (label, iconName, attrs, cls) => const big = UI.btnOf('ins-btn ins-big');
`<button type="button" class="ins-btn ins-big${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`; const small = UI.btnOf('ins-btn');
const small = (label, iconName, attrs, cls) =>
`<button type="button" class="ins-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
const kv = (label, value, unit) => const kv = (label, value, unit) =>
`<div class="ins-kv"><span>${label}</span><b>${value}${unit ? `<small>${unit}</small>` : ''}</b></div>`; `<div class="ins-kv"><span>${label}</span><b>${value}${unit ? `<small>${unit}</small>` : ''}</b></div>`;
@@ -32,9 +30,9 @@ window.Inspector = (() => {
const keepScroll = el._insKey === scrollKey ? el.scrollTop : 0; const keepScroll = el._insKey === scrollKey ? el.scrollTop : 0;
el._insKey = scrollKey; el._insKey = scrollKey;
el.innerHTML = `<div class="ins-head"> el.innerHTML = `<div class="ins-head">
${view.crumbs && view.crumbs.length ? `<div class="ins-crumbs">${view.crumbs.map(c => `<button type="button" class="ins-crumb" ${c.attrs || ''}>${c.label}</button>`).join(icon('chevron-right'))}${icon('chevron-right')}</div>` : ''} ${view.crumbs && view.crumbs.length ? `<div class="ins-crumbs">${view.crumbs.map(c => `<button type="button" class="ins-crumb" ${c.attrs || ''}>${esc(c.label)}</button>`).join(icon('chevron-right'))}${icon('chevron-right')}</div>` : ''}
<h2>${view.icon ? icon(view.icon) : ''}<span>${view.title}</span></h2> <h2>${view.icon ? icon(view.icon) : ''}<span>${esc(view.title)}</span></h2>
${view.sub ? `<div class="ins-sub">${view.sub}</div>` : ''} ${view.sub ? `<div class="ins-sub">${esc(view.sub)}</div>` : ''}
</div> </div>
${view.top ? `<div class="ins-top">${view.top}</div>` : ''} ${view.top ? `<div class="ins-top">${view.top}</div>` : ''}
<div class="ins-sections"></div>`; <div class="ins-sections"></div>`;
@@ -45,16 +43,25 @@ window.Inspector = (() => {
const isOpen = !collapsible || open.has(sec.key); const isOpen = !collapsible || open.has(sec.key);
node.className = 'ins-section' + (collapsible ? ' ins-collapsible' : '') + (isOpen ? ' open' : ''); node.className = 'ins-section' + (collapsible ? ' ins-collapsible' : '') + (isOpen ? ' open' : '');
node.innerHTML = (collapsible ? `<button type="button" class="ins-section-head" aria-expanded="${isOpen}">${sec.icon ? icon(sec.icon) : ''}<span>${sec.label}</span>${icon('chevron-down')}</button>` : '') + node.innerHTML = (collapsible ? `<button type="button" class="ins-section-head" aria-expanded="${isOpen}">${sec.icon ? icon(sec.icon) : ''}<span>${sec.label}</span>${icon('chevron-down')}</button>` : '') +
`<div class="ins-section-body">${groupsHtml(sec.groups ? sec.groups() : [], sec.label)}</div>`; '<div class="ins-section-body"></div>';
host.appendChild(node); host.appendChild(node);
// Een dichte sectie wordt pas opgebouwd (en gekoppeld) als hij opengaat.
let built = false;
const build = () => {
if (built) return;
built = true;
node.querySelector('.ins-section-body').innerHTML = groupsHtml(sec.groups ? sec.groups() : [], sec.label);
if (sec.wire) sec.wire(node);
};
if (isOpen) build();
if (collapsible) node.querySelector('.ins-section-head').addEventListener('click', () => { if (collapsible) node.querySelector('.ins-section-head').addEventListener('click', () => {
const now = !node.classList.contains('open'); const now = !node.classList.contains('open');
if (now) build();
node.classList.toggle('open', now); node.classList.toggle('open', now);
node.querySelector('.ins-section-head').setAttribute('aria-expanded', now); node.querySelector('.ins-section-head').setAttribute('aria-expanded', now);
if (now) open.add(sec.key); else open.delete(sec.key); if (now) open.add(sec.key); else open.delete(sec.key);
saveOpen(); saveOpen();
}); });
if (sec.wire) sec.wire(node);
}); });
el.scrollTop = keepScroll; el.scrollTop = keepScroll;
} }
+3 -2
View File
@@ -23,13 +23,15 @@ window.Menubar = (() => {
function close() { function close() {
if (openIdx < 0) return false; if (openIdx < 0) return false;
openIdx = -1; openIdx = -1;
UI.release(close);
root.querySelectorAll('.mb-menu').forEach(m => m.classList.remove('open')); root.querySelectorAll('.mb-menu').forEach(m => m.classList.remove('open'));
root.querySelectorAll('.mb-drop').forEach(d => { d.innerHTML = ''; }); root.querySelectorAll('.mb-drop').forEach(d => { d.innerHTML = ''; });
return true; return true;
} }
function openMenu(i) { function openMenu(i) {
close(); if (openIdx < 0) UI.claim(close);
else { root.querySelectorAll('.mb-menu').forEach(m => m.classList.remove('open')); root.querySelectorAll('.mb-drop').forEach(d => { d.innerHTML = ''; }); }
openIdx = i; openIdx = i;
const wrap = root.querySelectorAll('.mb-menu')[i]; const wrap = root.querySelectorAll('.mb-menu')[i];
wrap.classList.add('open'); wrap.classList.add('open');
@@ -54,7 +56,6 @@ window.Menubar = (() => {
const top = e.target.closest('[data-menu]'); const top = e.target.closest('[data-menu]');
if (top && openIdx >= 0 && Number(top.dataset.menu) !== openIdx) openMenu(Number(top.dataset.menu)); if (top && openIdx >= 0 && Number(top.dataset.menu) !== openIdx) openMenu(Number(top.dataset.menu));
}); });
document.addEventListener('click', close);
} }
return { init, close }; return { init, close };
+38 -13
View File
@@ -80,8 +80,10 @@ window.UI = (() => {
const info = (text) => `<div class="ui-info">${icon('info-circle')}<span>${text}</span></div>`; 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 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 grid = (...cols) => `<div class="ui-grid">${cols.map(c => `<div>${c}</div>`).join('')}</div>`;
const button = (label, iconName, attrs, cls) => // Eén knopbouwer; base = de knopsoort (ui-btn, tb-btn, ins-btn …), cls = extra klassen.
`<button type="button" class="ui-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}<span>${label}</span></button>`; const btnOf = (base) => (label, iconName, attrs, cls) =>
`<button type="button" class="${base}${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}${label ? `<span>${label}</span>` : ''}</button>`;
const button = btnOf('ui-btn');
const actions = (...btns) => `<div class="ui-actions">${btns.join('')}</div>`; const actions = (...btns) => `<div class="ui-actions">${btns.join('')}</div>`;
const activeTabs = {}; const activeTabs = {};
@@ -142,27 +144,50 @@ window.UI = (() => {
el.querySelectorAll('[data-view]').forEach(b => b.classList.toggle('active', b.dataset.view === mode)); el.querySelectorAll('[data-view]').forEach(b => b.classList.toggle('active', b.dataset.view === mode));
} }
const tb = (label, iconName, attrs, cls) => const tb = btnOf('tb-btn');
`<button type="button" class="tb-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}${label ? `<span>${label}</span>` : ''}</button>`;
const tbSep = () => '<span class="tb-sep"></span>'; const tbSep = () => '<span class="tb-sep"></span>';
const viewSeg = (mode) => `<div class="tb-seg" role="group" aria-label="Weergave">${VIEWS.map(v => const viewSeg = (mode) => `<div class="tb-seg" role="group" aria-label="Weergave">${VIEWS.map(v =>
tb('', v.icon, `data-view="${v.key}" title="${v.label} (${v.key1})"`, v.key === mode ? 'active' : '')).join('')}</div>`; tb('', v.icon, `data-view="${v.key}" title="${v.label} (${v.key1})"`, v.key === mode ? 'active' : '')).join('')}</div>`;
// Uitklappaneel onder een knop; sluit bij klik buiten, Esc of een andere popover. // Werkbalk van een editor: [left] … [Lagen, Omgeving] [after] | [2D + 3D, 2D, 3D]. Gedeeld door woning, wand en vloer.
// o: left/after (html), onResize, layers (opties voor ViewLayers.createMenu), onSky, beforeView(mode).
function editorBar(toolbar, root, o) {
toolbar.innerHTML = `${o.left || ''}<div class="tb-spacer"></div><div class="tb-group tb-view"></div>${o.after || ''}${tbSep()}${viewSeg(viewOf(root))}`;
const group = toolbar.querySelector('.tb-view');
if (o.layers) ViewLayers.createMenu(group, o.layers);
if (o.onSky) Skybox.createToggle(group, o.onSky);
const bar = {
setView(mode) {
if (o.beforeView) o.beforeView(mode);
setView(root, mode);
requestAnimationFrame(o.onResize);
},
viewState: () => ({ view: viewOf(root) })
};
toolbar.querySelectorAll('[data-view]').forEach(b => b.addEventListener('click', () => bar.setView(b.dataset.view)));
return bar;
}
// Eén uitklapper tegelijk (popover of menu): wie opent, sluit de vorige. claim bij openen, release bij sluiten.
let openPop = null; let openPop = null;
function claim(close) { if (openPop && openPop !== close) openPop(); openPop = close; }
function release(close) { if (openPop === close) openPop = null; }
const closePopovers = () => { if (openPop) { openPop(); return true; } return false; };
document.addEventListener('click', () => closePopovers());
// Uitklappaneel onder een knop; sluit bij klik buiten, Esc of een andere uitklapper.
function popover(btn, panel) { function popover(btn, panel) {
const close = () => { panel.classList.remove('open'); btn.classList.remove('open'); if (openPop === close) openPop = null; }; const close = () => { panel.classList.remove('open'); btn.classList.remove('open'); release(close); };
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const was = panel.classList.contains('open'); if (panel.classList.contains('open')) { close(); return; }
if (openPop) openPop(); claim(close);
if (!was) { panel.classList.add('open'); btn.classList.add('open'); openPop = close; } panel.classList.add('open');
btn.classList.add('open');
}); });
panel.addEventListener('click', (e) => e.stopPropagation()); panel.addEventListener('click', (e) => e.stopPropagation());
return close; return close;
} }
const closePopovers = () => { if (openPop) { openPop(); return true; } return false; };
document.addEventListener('click', () => closePopovers());
// Kaarten verdelen over kolommen van min. minW breed; telkens in de kortste kolom. Opnieuw aanroepen bij een andere breedte. // Kaarten verdelen over kolommen van min. minW breed; telkens in de kortste kolom. Opnieuw aanroepen bij een andere breedte.
function masonry(el, minW, gap) { function masonry(el, minW, gap) {
@@ -178,6 +203,6 @@ window.UI = (() => {
cards.forEach(card => cols.reduce((a, b) => b.offsetHeight < a.offsetHeight ? b : a).appendChild(card)); cards.forEach(card => cols.reduce((a, b) => b.offsetHeight < a.offsetHeight ? b : a).appendChild(card));
} }
return { esc, icon, hint, num, range, onRange, setRange, sel, toggle, color, onColor, text, onText, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, return { esc, icon, hint, num, range, onRange, setRange, sel, toggle, color, onColor, text, onText, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, btnOf,
VIEWS, viewOf, setView, tb, tbSep, viewSeg, popover, closePopovers, masonry }; VIEWS, viewOf, setView, tb, tbSep, viewSeg, editorBar, popover, claim, release, closePopovers, masonry };
})(); })();
+10 -3
View File
@@ -20,7 +20,7 @@ window.ViewLayers = (() => {
{ key: 'lampen', label: 'Verlichting', icon: 'lightbulb', phase: 6, hint: 'Gevellampen, spots en binnenverlichting' }, { key: 'lampen', label: 'Verlichting', icon: 'lightbulb', phase: 6, hint: 'Gevellampen, spots en binnenverlichting' },
{ key: 'inrichting', label: 'Inrichting', icon: 'lamp', phase: 6, hint: 'Sanitair, keuken, kasten, bed, zithoek' }, { key: 'inrichting', label: 'Inrichting', icon: 'lamp', phase: 6, hint: 'Sanitair, keuken, kasten, bed, zithoek' },
{ key: 'ruimtes', label: 'Ruimtes', icon: 'bounding-box', phase: 6, hint: 'Ruimtes met functie, vloer en wandpanelen' }, { key: 'ruimtes', label: 'Ruimtes', icon: 'bounding-box', phase: 6, hint: 'Ruimtes met functie, vloer en wandpanelen' },
{ key: 'dak', label: 'Dak', icon: 'house', hint: 'Het hele dak met dakrand, overkapping, palen en schoorsteen; uit om van boven in de woning te kijken' }, { key: 'dak', label: 'Dak', icon: 'house', hint: 'Het hele dak met dakrand, overkapping en palen; uit om van boven in de woning te kijken' },
{ key: 'maten', label: 'Maten', icon: 'rulers', hint: 'Maatvoering in de plattegrond' } { key: 'maten', label: 'Maten', icon: 'rulers', hint: 'Maatvoering in de plattegrond' }
]; ];
const KEYS = LIST.map(l => l.key); const KEYS = LIST.map(l => l.key);
@@ -98,10 +98,17 @@ window.ViewLayers = (() => {
emit(); emit();
} }
}); });
// Alles = ook de editor-eigen schakelaars weer aan.
const all = panel.querySelector('[data-all]'); const all = panel.querySelector('[data-all]');
if (all) all.addEventListener('click', () => { cur = ViewLayers.all(); emit(); }); if (all) all.addEventListener('click', () => {
const extraOff = extras.some(x => !extra[x.key]);
extras.forEach(x => { extra[x.key] = true; });
cur = ViewLayers.all();
emit();
if (extraOff && o.onExtra) o.onExtra(Object.assign({}, extra));
});
sync(); sync();
return { el: wrap, set: (l) => { cur = normalize(l); sync(); }, toggle: (key) => { cur = Object.assign({}, cur, { [key]: !cur[key] }); emit(); } }; return { el: wrap, set: (l) => { cur = normalize(l); sync(); } };
} }
return { LIST, KEYS, PHASES, GROUPS, all, normalize, shows, upTo, phaseOf, hideByMaterial, createMenu }; return { LIST, KEYS, PHASES, GROUPS, all, normalize, shows, upTo, phaseOf, hideByMaterial, createMenu };
+1 -1
View File
@@ -29,7 +29,7 @@ window.CostSettings = (() => {
<button type="button" class="ui-btn" data-cs="export-changes" title="Alleen de velden die afwijken van intern.csv">Wijzigingen downloaden</button> <button type="button" class="ui-btn" data-cs="export-changes" title="Alleen de velden die afwijken van intern.csv">Wijzigingen downloaden</button>
<button type="button" class="ui-btn" data-cs="import">Importeren&hellip;</button> <button type="button" class="ui-btn" data-cs="import">Importeren&hellip;</button>
<span style="flex:1"></span> <span style="flex:1"></span>
<button type="button" class="ui-btn" data-cs="cancel">Annuleren</button> <button type="button" class="ui-btn" data-cs="cancel" data-esc>Annuleren</button>
<button type="button" class="ui-btn ui-btn-primary" data-cs="save">Toepassen</button> <button type="button" class="ui-btn ui-btn-primary" data-cs="save">Toepassen</button>
</div> </div>
<input type="file" data-file="import" accept=".csv,.json,text/csv,application/json,text/plain" style="display:none"> <input type="file" data-file="import" accept=".csv,.json,text/csv,application/json,text/plain" style="display:none">
+14 -3
View File
@@ -146,8 +146,14 @@ window.ElektraModel = (() => {
// rooms: leidingen lopen boven een vals plafond, niet tegen de onderkant van een hoog dak. // rooms: leidingen lopen boven een vals plafond, niet tegen de onderkant van een hoog dak.
function makeRouter(units, rooms) { function makeRouter(units, rooms) {
const unitAt = (x, z) => units.find(u => x >= u.rect.x0 - 1 && x <= u.rect.x1 + 1 && z >= u.rect.z0 - 1 && z <= u.rect.z1 + 1) || null; const unitAt = (x, z) => units.find(u => x >= u.rect.x0 - 1 && x <= u.rect.x1 + 1 && z >= u.rect.z0 - 1 && z <= u.rect.z1 + 1) || null;
// Vals plafond (mm boven vloer) van de ruimte op (x,z): undefined = geen ruimte (binnenwand, naad), null = geen vals plafond. // Vals plafond (mm boven vloer) van de ruimte op (x,z): undefined = in een wand of naad (geen gebied), null = geen
const roomPlenum = (x, z) => { const r = rooms && rooms.roomAt ? rooms.roomAt(x, z) : null; return r ? (r.ceilingActive ? r.ceiling : null) : undefined; }; // vals plafond (ook een gebied zonder functie: niet het plafond van de buurruimte door de wand heen overnemen).
const roomPlenum = (x, z) => {
if (!rooms || !rooms.roomAt) return undefined;
const r = rooms.roomAt(x, z);
if (r) return r.ceilingActive ? r.ceiling : null;
return rooms.regionAt && rooms.regionAt(x, z) ? null : undefined;
};
// Absoluut; in een binnenwand het laagste valse plafond ernaast: de leiding gaat daar door de wand, niet omhoog. // Absoluut; in een binnenwand het laagste valse plafond ernaast: de leiding gaat daar door de wand, niet omhoog.
const plenumAt = (x, z) => { const plenumAt = (x, z) => {
const u = unitAt(x, z); const u = unitAt(x, z);
@@ -192,7 +198,12 @@ window.ElektraModel = (() => {
const hOf = (pt, st) => st !== null ? st : roofCeilAt(pt.x, pt.z); const hOf = (pt, st) => st !== null ? st : roofCeilAt(pt.x, pt.z);
const mk = (pt, y) => Object.assign({}, p, pt, { y, wall: null, ceil: true, floor: false, plenum: inPlenum(pt.x, pt.z, y) }); const mk = (pt, y) => Object.assign({}, p, pt, { y, wall: null, ceil: true, floor: false, plenum: inPlenum(pt.x, pt.z, y) });
const n = Math.max(2, Math.ceil(len / 100)); const n = Math.max(2, Math.ceil(len / 100));
const S = Array.from({ length: n + 1 }, (_, k) => { const pt = at(k / n), st = plenumOn(pt.x, pt.z); return { t: k / n, pt, st, y: hOf(pt, st) }; }); // Begin/eind op een wandvlak 50 mm de ruimte in meten, net als top(): anders telt de buurruimte achter de wand mee.
const probe = (pt, e) => e.wall && e.wall.n ? { x: pt.x + e.wall.n.x * 50, z: pt.z + e.wall.n.z * 50 } : pt;
const S = Array.from({ length: n + 1 }, (_, k) => {
const pt = at(k / n), pr = k === 0 ? probe(pt, p) : k === n ? probe(pt, q) : pt, st = plenumOn(pr.x, pr.z);
return { t: k / n, pt, st, y: hOf(pt, st) };
});
for (let k = 1; k <= n; k++) { for (let k = 1; k <= n; k++) {
const A = S[k - 1], B = S[k]; const A = S[k - 1], B = S[k];
if (!same(A.st, B.st)) { if (!same(A.st, B.st)) {
+12 -17
View File
@@ -159,7 +159,7 @@ const FLOOR_TEMPLATE = `
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="wall-project-cancel" class="ui-btn">Annuleren</button> <button type="button" id="wall-project-cancel" class="ui-btn" data-esc>Annuleren</button>
<button type="button" id="wall-project-save" class="ui-btn ui-btn-primary">Opslaan</button> <button type="button" id="wall-project-save" class="ui-btn ui-btn-primary">Opslaan</button>
</div> </div>
</div> </div>
@@ -620,22 +620,21 @@ function mountFloorEditor(container, callbacks) {
let lastResolvedOpenings = []; let lastResolvedOpenings = [];
let lastChassisData = { wheels: [], outOfBounds: false }; let lastChassisData = { wheels: [], outOfBounds: false };
const toolbar = document.getElementById('floor-toolbar');
toolbar.innerHTML = `<div class="tb-spacer"></div><div class="tb-group tb-view"></div>${UI.tbSep()}${UI.viewSeg('split')}`;
toolbar.querySelectorAll('[data-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
const viewGroup = toolbar.querySelector('.tb-view');
ViewLayers.createMenu(viewGroup, {
layers: {}, keys: [],
extras: [{ key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' }],
onExtra: (v) => { osbVisible = v.beplating; update3DScene(); }
});
let skyMode = Skybox.getMode(); let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene); const skyEnv = Skybox.createEnvironment(scene);
const applySky = () => { const applySky = () => {
skyEnv.fitTo(floorGroup3D, 0); skyEnv.fitTo(floorGroup3D, 0);
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient: floorAmbient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: floorGroup3D }); Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient: floorAmbient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: floorGroup3D });
}; };
Skybox.createToggle(viewGroup, (m) => { skyMode = m; applySky(); }); const bar = UI.editorBar(document.getElementById('floor-toolbar'), document.getElementById('floor-app'), {
onResize: () => onWindowResize(),
layers: {
layers: {}, keys: [],
extras: [{ key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' }],
onExtra: (v) => { osbVisible = v.beplating; update3DScene(); }
},
onSky: (m) => { skyMode = m; applySky(); }
});
function update3DScene() { function update3DScene() {
if (floorGroup3D) scene.remove(floorGroup3D); if (floorGroup3D) scene.remove(floorGroup3D);
@@ -682,10 +681,6 @@ function mountFloorEditor(container, callbacks) {
} }
window.addEventListener('resize', onWindowResize); window.addEventListener('resize', onWindowResize);
function setView(mode) {
UI.setView(document.getElementById('floor-app'), mode);
requestAnimationFrame(onWindowResize);
}
function getPureState() { function getPureState() {
return { return {
@@ -735,8 +730,8 @@ function mountFloorEditor(container, callbacks) {
}, },
exportDesign, exportDesign,
triggerImport, triggerImport,
setView, setView: bar.setView,
viewState: () => ({ view: UI.viewOf(document.getElementById('floor-app')) }), viewState: bar.viewState,
openProjectProperties, openProjectProperties,
getStateJson: getPureState, getStateJson: getPureState,
loadStateJson: applyPureState loadStateJson: applyPureState
+5 -4
View File
@@ -49,19 +49,20 @@ window.HouseIfc = (() => {
}; };
} }
function build(state, layers) { // rough: buizen en dozen bouwen; de plaatfase verzamelt alleen platen en heeft ze niet nodig.
function build(state, layers, rough) {
const M = materialsSet(); const M = materialsSet();
const parts = HouseModel.computeParts(state, { const parts = HouseModel.computeParts(state, {
wallMaterials: M.wall, floorMaterials: M.floor, roofMaterials: M.roof, elektraMaterials: M.elektra, postMaterials: M.post, wallMaterials: M.wall, floorMaterials: M.floor, roofMaterials: M.roof, elektraMaterials: M.elektra, postMaterials: M.post,
layers: Object.assign(layers, { lampen: false }) layers, elektraRough: rough
}); });
HouseModel.placeGroups(parts); HouseModel.placeGroups(parts);
return { parts, classify: classifier(M) }; return { parts, classify: classifier(M) };
} }
function generate(state) { function generate(state) {
const frame = build(state, ViewLayers.upTo(1)); const frame = build(state, ViewLayers.upTo(1), true);
const sheet = build(state, ViewLayers.upTo(2)); const sheet = build(state, ViewLayers.upTo(2), false);
const parts = frame.parts; const parts = frame.parts;
if (!parts.units.length) throw new Error('geen units in het ontwerp'); if (!parts.units.length) throw new Error('geen units in het ontwerp');
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bz1 = Math.max(...parts.rects.map(r => r.z1)); const bx0 = Math.min(...parts.rects.map(r => r.x0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
+13 -2
View File
@@ -1700,7 +1700,7 @@ window.HouseModel = (() => {
const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials(), lighting.indoor) : null; const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials(), lighting.indoor) : null;
if (lightingGroup) lightingGroup.visible = shows('lampen'); if (lightingGroup) lightingGroup.visible = shows('lampen');
const elektra = ElektraModel.compute(state.settings.elektra, units, interior, lighting, roofEdge, rooms); const elektra = ElektraModel.compute(state.settings.elektra, units, interior, lighting, roofEdge, rooms);
const elektraGroup = build ? ElektraModel.build3D(elektra, o.elektraMaterials, layers, o.elektraRough) : null; const elektraGroup = build ? ElektraModel.build3D(elektra, o.elektraMaterials, layers, o.finishedOnly ? false : o.elektraRough) : null;
if (elektraGroup) elektraGroup.visible = shows('elektra'); if (elektraGroup) elektraGroup.visible = shows('elektra');
const posts = PostModel.compute(state.settings.roof.posts, units, groundY); const posts = PostModel.compute(state.settings.roof.posts, units, groundY);
@@ -1722,7 +1722,18 @@ window.HouseModel = (() => {
const fixturesGroup = build ? FixtureModel.build3D(fixtures) : null; const fixturesGroup = build ? FixtureModel.build3D(fixtures) : null;
if (fixturesGroup) fixturesGroup.visible = shows('inrichting'); if (fixturesGroup) fixturesGroup.visible = shows('inrichting');
return { rects, seams, overlaps: overlappingPairs(rects), units, interior, roofEdge, roofEdgeGroup, lighting, lightingGroup, elektra, elektraGroup, sandwich, sandwichGroup, posts, postsGroup, chimneys, chimneysGroup, rooms, roomsGroup, fixtures, fixturesGroup }; const parts = { rects, seams, overlaps: overlappingPairs(rects), units, interior, roofEdge, roofEdgeGroup, lighting, lightingGroup, elektra, elektraGroup, sandwich, sandwichGroup, posts, postsGroup, chimneys, chimneysGroup, rooms, roomsGroup, fixtures, fixturesGroup };
if (build && o.finishedOnly) keepFinished(parts, o.wallMaterials);
return parts;
}
// finishedOnly (render-export): alleen wat in de afgewerkte woning te zien is. Naast de buitenste vloerbalken en geen
// elektraleidingen (hierboven): geen isolatie, latten en vochtdoek achter de gevel, geen lichtkegels, valse plafonds altijd.
function keepFinished(parts, wallMaterials) {
if (wallMaterials) allWallPieces(parts).forEach(p => ViewLayers.hideByMaterial(p.group,
[wallMaterials.isolatieMaterial, wallMaterials.battenMaterial, wallMaterials.doekMaterial]));
if (parts.lightingGroup) parts.lightingGroup.traverse(x => { if (x.userData.beam) x.visible = false; });
if (parts.roomsGroup && parts.roomsGroup.userData.ceilings) parts.roomsGroup.userData.ceilings.visible = true;
} }
// 3D-groepen op hun plek in de woning (m); elektra, palen en dakrand staan al in woningcoördinaten. // 3D-groepen op hun plek in de woning (m); elektra, palen en dakrand staan al in woningcoördinaten.
+2 -1
View File
@@ -762,7 +762,8 @@ window.HousePdf = (() => {
function generate(state) { function generate(state) {
const parts = HouseModel.computeParts(state, { const parts = HouseModel.computeParts(state, {
wallMaterials: WallModel.createMaterials(), floorMaterials: FloorModel.createMaterials(), roofMaterials: RoofModel.createMaterials() wallMaterials: WallModel.createMaterials(), floorMaterials: FloorModel.createMaterials(), roofMaterials: RoofModel.createMaterials(),
elektraRough: false
}); });
const totals = HouseModel.computeTotals(parts); const totals = HouseModel.computeTotals(parts);
const sheets = sheetList(parts); const sheets = sheetList(parts);
+70 -94
View File
@@ -41,7 +41,7 @@ const HOUSE_TEMPLATE = `
</div> </div>
<div class="material-info">Geldt voor elk blad van de woning-PDF. Titel en elementnummer worden per blad automatisch ingevuld.</div> <div class="material-info">Geldt voor elk blad van de woning-PDF. Titel en elementnummer worden per blad automatisch ingevuld.</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="house-project-cancel" class="ui-btn">Annuleren</button> <button type="button" id="house-project-cancel" class="ui-btn" data-esc>Annuleren</button>
<button type="button" id="house-project-save" class="ui-btn ui-btn-primary">Opslaan</button> <button type="button" id="house-project-save" class="ui-btn ui-btn-primary">Opslaan</button>
</div> </div>
</div> </div>
@@ -198,6 +198,7 @@ function mountHouseEditor(container, callbacks) {
// Structuur: groepen in-/uitklapbaar (onthouden); een groep met het actieve onderdeel of de selectie klapt vanzelf open. // Structuur: groepen in-/uitklapbaar (onthouden); een groep met het actieve onderdeel of de selectie klapt vanzelf open.
const NAV_PREF = 'tectum.nav.closed'; const NAV_PREF = 'tectum.nav.closed';
let navActiveKey = null;
let navClosed; let navClosed;
try { navClosed = new Set(JSON.parse(localStorage.getItem(NAV_PREF) || 'null') || ['iw', 'rooms', 'fixtures']); } catch (e) { navClosed = new Set(['iw', 'rooms', 'fixtures']); } try { navClosed = new Set(JSON.parse(localStorage.getItem(NAV_PREF) || 'null') || ['iw', 'rooms', 'fixtures']); } catch (e) { navClosed = new Set(['iw', 'rooms', 'fixtures']); }
function renderNav(parts) { function renderNav(parts) {
@@ -262,8 +263,11 @@ function mountHouseEditor(container, callbacks) {
} }
const tree = document.getElementById('house-nav-tree'); const tree = document.getElementById('house-nav-tree');
tree.innerHTML = html; tree.innerHTML = html;
// Alleen in beeld schuiven als de selectie of het onderdeel verandert, niet bij elke herberekening.
const key = activePart + '|' + JSON.stringify(selection);
const act = tree.querySelector('.hn-item.active'); const act = tree.querySelector('.hn-item.active');
if (act) act.scrollIntoView({ block: 'nearest' }); if (act && key !== navActiveKey) act.scrollIntoView({ block: 'nearest' });
navActiveKey = key;
} }
document.getElementById('house-nav-tree').addEventListener('click', (e) => { document.getElementById('house-nav-tree').addEventListener('click', (e) => {
@@ -418,23 +422,25 @@ function mountHouseEditor(container, callbacks) {
['room', 'Ruimte', 'paint-bucket', 'R', 'Functie kiezen en in een ruimte klikken; de ruimte loopt tot de wanden'], ['room', 'Ruimte', 'paint-bucket', 'R', 'Functie kiezen en in een ruimte klikken; de ruimte loopt tot de wanden'],
['fixture', 'Inrichting', 'lamp', 'I', 'Stuk kiezen en in de plattegrond klikken (D = draaien)'] ['fixture', 'Inrichting', 'lamp', 'I', 'Stuk kiezen en in de plattegrond klikken (D = draaien)']
]; ];
toolbar.innerHTML = `<div class="tb-group tb-tools" role="group" aria-label="Gereedschap">${TOOLS.map(([k, label, ic, key, hint]) => const bar = UI.editorBar(toolbar, document.getElementById('house-overview'), {
UI.tb(label, ic, `data-ho-tool="${k}" title="${label} (${key}): ${hint}"`)).join('')}</div> left: `<div class="tb-group tb-tools" role="group" aria-label="Gereedschap">${TOOLS.map(([k, label, ic, key, hint]) =>
${UI.tbSep()}${UI.tb('Unit', 'plus-square', 'data-tb="add-unit" title="Unit toevoegen aan de geselecteerde (of laatste) unit"')} UI.tb(label, ic, `data-ho-tool="${k}" title="${label} (${key}): ${hint}"`)).join('')}</div>
<div class="tb-group tb-options" id="ho-tool-options"></div> ${UI.tbSep()}${UI.tb('Unit', 'plus-square', 'data-tb="add-unit" title="Unit toevoegen aan de geselecteerde (of laatste) unit"')}
<div class="tb-spacer"></div> <div class="tb-group tb-options" id="ho-tool-options"></div>`,
<div class="tb-group tb-view" id="ho-view-group"></div> after: `<div class="tb-group tb-section" id="ho-section-group">
<div class="tb-group tb-section" id="ho-section-group"> ${UI.tb('Doorsnede', 'scissors', 'data-tb="section" title="Horizontale doorsnede (X)"')}
${UI.tb('Doorsnede', 'scissors', 'data-tb="section" title="Horizontale doorsnede (X)"')} <span class="tb-range"><input type="range" id="ho-section" min="100" max="4000" step="50" value="${view3D.sectionMM}" aria-label="Hoogte doorsnede"><span id="ho-section-val"></span></span>
<span class="tb-range"><input type="range" id="ho-section" min="100" max="4000" step="50" value="${view3D.sectionMM}" aria-label="Hoogte doorsnede"><span id="ho-section-val"></span></span> </div>
</div> ${UI.tb('', 'camera', 'data-tb="render" title="Render: elke lamp en spot als echte lichtbron"')}
${UI.tb('', 'camera', 'data-tb="render" title="Render: elke lamp en spot als echte lichtbron"')} ${UI.tb('', 'person-walking', 'data-tb="walk" title="Lopen (L): W/S vooruit/achteruit, A/D opzij, slepen = rondkijken, E = deur/schakelaar"')}
${UI.tb('', 'person-walking', 'data-tb="walk" title="Lopen (L): W/S vooruit/achteruit, A/D opzij, slepen = rondkijken, E = deur/schakelaar"')} ${UI.tbSep()}
${UI.tbSep()} ${UI.tb('Rapport', 'clipboard-data', 'data-tb="report" title="Rapport in plaats van de plattegrond (4): transport, gewicht, kosten, hout en plaatwerk"')}`,
${UI.tb('Rapport', 'clipboard-data', 'data-tb="report" title="Rapport in plaats van de plattegrond (4): transport, gewicht, kosten, hout en plaatwerk"')} onResize,
${UI.viewSeg(UI.viewOf(document.getElementById('house-overview')))}`; layers: { layers: view3D.layers, onChange: setLayers },
onSky: (m) => { skyMode = m; applyRenderMode(); },
beforeView: (mode) => { setWalk(false); if (mode === '3d') toggleReport(false); view3D.mode = mode; }
});
toolbar.querySelectorAll('[data-ho-tool]').forEach(b => b.addEventListener('click', () => setTool(b.dataset.hoTool))); toolbar.querySelectorAll('[data-ho-tool]').forEach(b => b.addEventListener('click', () => setTool(b.dataset.hoTool)));
toolbar.querySelectorAll('[data-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
toolbar.addEventListener('click', (e) => { toolbar.addEventListener('click', (e) => {
const b = e.target.closest('[data-tb]'); const b = e.target.closest('[data-tb]');
if (!b) return; if (!b) return;
@@ -446,9 +452,6 @@ function mountHouseEditor(container, callbacks) {
else if (a === 'report') toggleReport(); else if (a === 'report') toggleReport();
b.blur(); b.blur();
}); });
const viewGroup = document.getElementById('ho-view-group');
const layerMenu = ViewLayers.createMenu(viewGroup, { layers: view3D.layers, onChange: setLayers });
Skybox.createToggle(viewGroup, (m) => { skyMode = m; applyRenderMode(); });
function syncToolbar() { function syncToolbar() {
const q = (a) => toolbar.querySelector(`[data-tb="${a}"]`); const q = (a) => toolbar.querySelector(`[data-tb="${a}"]`);
q('section').classList.toggle('active', view3D.section); q('section').classList.toggle('active', view3D.section);
@@ -464,10 +467,10 @@ function mountHouseEditor(container, callbacks) {
function setLayers(l) { function setLayers(l) {
view3D.layers = ViewLayers.normalize(l); view3D.layers = ViewLayers.normalize(l);
const L = view3D.layers; const L = view3D.layers;
if (selection && ((!L.ruimtes && (selection.type === 'room' || selection.type === 'region')) || const hidden = selection && ((!L.ruimtes && (selection.type === 'room' || selection.type === 'region')) ||
(!L.inrichting && selection.type === 'fixture') || (!L.elektra && selection.type === 'elektra'))) selection = null; (!L.inrichting && selection.type === 'fixture') || (!L.elektra && selection.type === 'elektra'));
layerMenu.set(L); if (hidden) selection = null;
scheduleRefresh({ keepProps: true }); scheduleRefresh({ keepProps: !hidden }); // vervalt de selectie, dan ook het paneel bijwerken
} }
const sectionInput = document.getElementById('ho-section'); const sectionInput = document.getElementById('ho-section');
function syncSectionRow() { function syncSectionRow() {
@@ -667,7 +670,7 @@ function mountHouseEditor(container, callbacks) {
tool = t; tool = t;
drawStart = null; drawStart = null;
document.querySelectorAll('[data-ho-tool]').forEach(b => b.classList.toggle('active', b.dataset.hoTool === t)); document.querySelectorAll('[data-ho-tool]').forEach(b => b.classList.toggle('active', b.dataset.hoTool === t));
canvas.style.cursor = t === 'iw' || t === 'fixture' ? 'crosshair' : t === 'room' ? 'cell' : 'default'; canvas.style.cursor = toolCursor();
roomHover = null; roomHover = null;
fxHover = null; fxHover = null;
renderToolOptions(); renderToolOptions();
@@ -675,6 +678,7 @@ function mountHouseEditor(container, callbacks) {
drawPlan(); drawPlan();
} }
const toolCursor = () => tool === 'iw' || tool === 'fixture' ? 'crosshair' : tool === 'room' ? 'cell' : 'default';
function updateHint() { function updateHint() {
document.getElementById('ho-hint').textContent = tool === 'iw' document.getElementById('ho-hint').textContent = tool === 'iw'
? (drawStart ? 'Klik het eindpunt (Esc = annuleren). Snapt op 50 mm, haaks, en op wandvlakken.' : 'Klik het beginpunt van de binnenwand.') ? (drawStart ? 'Klik het eindpunt (Esc = annuleren). Snapt op 50 mm, haaks, en op wandvlakken.' : 'Klik het beginpunt van de binnenwand.')
@@ -728,13 +732,13 @@ function mountHouseEditor(container, callbacks) {
: ElektraModel.snapToWall(parts.elektra, w.x, w.z); : ElektraModel.snapToWall(parts.elektra, w.x, w.z);
drawPlan(); drawPlan();
}); });
function onElektraDrop() { function onElektraDrop(e) {
if (!elDrag) return; if (!elDrag) return;
const d = elDrag; const d = elDrag;
elDrag = null; elDrag = null;
canvas.style.cursor = tool === 'iw' ? 'crosshair' : tool === 'room' ? 'cell' : 'default'; canvas.style.cursor = toolCursor();
if (!d.moved || !d.snap) return; if (!d.moved || !d.snap) return;
suppressClick = true; if (e.target === canvas) suppressClick = true;
if (d.snap.floor) { if (d.snap.floor) {
const rc = parts.rects.find(q => d.snap.x > q.x0 && d.snap.x < q.x1 && d.snap.z > q.z0 && d.snap.z < q.z1); const rc = parts.rects.find(q => d.snap.x > q.x0 && d.snap.x < q.x1 && d.snap.z > q.z0 && d.snap.z < q.z1);
const vw = state.settings.elektra.vloerWcd.find(o => 'vwcd:' + o.id === d.key); const vw = state.settings.elektra.vloerWcd.find(o => 'vwcd:' + o.id === d.key);
@@ -769,13 +773,13 @@ function mountHouseEditor(container, callbacks) {
if (it) fxDrag.pose = fxPose(it, w.x + fxDrag.dx, w.z + fxDrag.dz, e.altKey); if (it) fxDrag.pose = fxPose(it, w.x + fxDrag.dx, w.z + fxDrag.dz, e.altKey);
drawPlan(); drawPlan();
}); });
function onFixtureDrop() { function onFixtureDrop(e) {
if (!fxDrag) return; if (!fxDrag) return;
const d = fxDrag; const d = fxDrag;
fxDrag = null; fxDrag = null;
canvas.style.cursor = 'default'; canvas.style.cursor = toolCursor();
if (!d.moved || !d.pose) return; if (!d.moved || !d.pose) return;
suppressClick = true; if (e.target === canvas) suppressClick = true;
const fs = fixtureState(d.id); const fs = fixtureState(d.id);
if (fs) placeFixture(fs, d.pose.x, d.pose.z, d.pose.rot); if (fs) placeFixture(fs, d.pose.x, d.pose.z, d.pose.rot);
selection = { type: 'fixture', id: d.id }; selection = { type: 'fixture', id: d.id };
@@ -801,13 +805,13 @@ function mountHouseEditor(container, callbacks) {
if (ch) chDrag.snap = ChimneyModel.snapToFacade(parts.rects, w.x, w.z, ch.w); if (ch) chDrag.snap = ChimneyModel.snapToFacade(parts.rects, w.x, w.z, ch.w);
drawPlan(); drawPlan();
}); });
window.addEventListener('mouseup', () => { window.addEventListener('mouseup', (e) => {
if (!chDrag) return; if (!chDrag) return;
const d = chDrag; const d = chDrag;
chDrag = null; chDrag = null;
canvas.style.cursor = 'default'; canvas.style.cursor = toolCursor();
if (!d.moved || !d.snap) return; if (!d.moved || !d.snap) return;
suppressClick = true; if (e.target === canvas) suppressClick = true;
const cs = (state.chimneys || []).find(c => c.id === d.id); const cs = (state.chimneys || []).find(c => c.id === d.id);
if (cs) Object.assign(cs, { unitId: d.snap.unitId, side: d.snap.side, u: d.snap.u }); if (cs) Object.assign(cs, { unitId: d.snap.unitId, side: d.snap.side, u: d.snap.u });
const u = parts.units.find(q => q.id === d.snap.unitId), wl = u && u.walls.find(q => q.side === d.snap.side), pc = wl && wl.pieces.find(p => !p.baseY); const u = parts.units.find(q => q.id === d.snap.unitId), wl = u && u.walls.find(q => q.side === d.snap.side), pc = wl && wl.pieces.find(p => !p.baseY);
@@ -887,11 +891,12 @@ function mountHouseEditor(container, callbacks) {
if (selection.type === 'fixture') removeFixture(selection.id); if (selection.type === 'fixture') removeFixture(selection.id);
else if (selection.type === 'room') removeRoom(selection.id); else if (selection.type === 'room') removeRoom(selection.id);
else if (selection.type === 'elektra' && selection.id !== 'mk') removeSelectedElektra(); else if (selection.type === 'elektra' && selection.id !== 'mk') removeSelectedElektra();
else if (selection.type === 'iw') { else if (selection.type === 'iw') removeInteriorWall(selection.id);
state.interiorWalls = state.interiorWalls.filter(w => w.id !== selection.id); }
selection = null; function removeInteriorWall(id) {
refresh(); state.interiorWalls = state.interiorWalls.filter(w => w.id !== id);
} selection = null;
refresh();
} }
function onKey(e) { function onKey(e) {
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName)) return; if (['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
@@ -962,9 +967,10 @@ function mountHouseEditor(container, callbacks) {
plan2D.cz = (canvas.height / 2 - (pan.oy + e.clientY - pan.sy)) / view.scale; plan2D.cz = (canvas.height / 2 - (pan.oy + e.clientY - pan.sy)) / view.scale;
drawPlan(); drawPlan();
} }
function onPanEnd() { // Na een sleep de klik op de plattegrond overslaan, maar alleen als die klik er ook komt (loslaten op de plattegrond).
function onPanEnd(e) {
if (!pan) return; if (!pan) return;
if (pan.moved) { canvas.style.cursor = 'default'; if (!pan.mid) suppressClick = true; } if (pan.moved) { canvas.style.cursor = toolCursor(); if (!pan.mid && e.target === canvas) suppressClick = true; }
pan = null; pan = null;
} }
window.addEventListener('mousemove', onPanMove); window.addEventListener('mousemove', onPanMove);
@@ -1339,24 +1345,12 @@ function mountHouseEditor(container, callbacks) {
const stucColors = Object.fromEntries(Object.entries(WallModel.STUC_COLORS).map(([k, c]) => [k, c.label])); const stucColors = Object.fromEntries(Object.entries(WallModel.STUC_COLORS).map(([k, c]) => [k, c.label]));
const swatchOf = (hex) => `<div class="ins-swatch" style="background:${hex}"></div>`; const swatchOf = (hex) => `<div class="ins-swatch" style="background:${hex}"></div>`;
function wireButtons(body) { function wireButtons(body) {
body.querySelectorAll('[data-ho-tool]').forEach(b => b.addEventListener('click', () => setTool(b.dataset.hoTool)));
body.querySelectorAll('[data-go]').forEach(b => b.addEventListener('click', () => showPart(b.dataset.go))); body.querySelectorAll('[data-go]').forEach(b => b.addEventListener('click', () => showPart(b.dataset.go)));
body.querySelectorAll('[data-ho-act]').forEach(b => b.addEventListener('click', () => { body.querySelectorAll('[data-ho-act]').forEach(b => b.addEventListener('click', () => {
const a = b.dataset.hoAct; const a = b.dataset.hoAct;
if (a === 'add-unit') addUnit(); if (a === 'add-seams') { HouseModel.ensureSeamWalls(state, b.dataset.unit); refresh(); }
else if (a === 'project') openProjectProperties();
else if (a === 'save') exportDesign();
else if (a === 'dxf') exportDxf();
else if (a === 'ifc') exportIfc();
else if (a === 'obj') exportObj();
else if (a === 'costs') CostSettings.open();
else if (a === 'add-seams') { HouseModel.ensureSeamWalls(state, b.dataset.unit); refresh(); }
else if (a === 'remove-unit') removeUnit(b.dataset.unit); else if (a === 'remove-unit') removeUnit(b.dataset.unit);
else if (a === 'remove-iw') { else if (a === 'remove-iw') removeInteriorWall(Number(b.dataset.iw));
state.interiorWalls = state.interiorWalls.filter(w => w.id !== Number(b.dataset.iw));
selection = null;
refresh();
}
})); }));
} }
@@ -2295,12 +2289,6 @@ function mountHouseEditor(container, callbacks) {
syncToolbar(); syncToolbar();
requestAnimationFrame(onResize); requestAnimationFrame(onResize);
} }
function setView(mode) {
setWalk(false);
UI.setView(overviewEl, view3D.mode = mode);
requestAnimationFrame(onResize);
}
const esc = UI.esc; const esc = UI.esc;
function stackedBars(title, iconName, series, rows, fmtVal, note) { function stackedBars(title, iconName, series, rows, fmtVal, note) {
const max = Math.max(1, ...rows.map(r => r.total)); const max = Math.max(1, ...rows.map(r => r.total));
@@ -2442,28 +2430,24 @@ function mountHouseEditor(container, callbacks) {
parts.units.forEach(u => houseGroup.add(u.roof.group)); parts.units.forEach(u => houseGroup.add(u.roof.group));
if (parts.roofEdgeGroup) houseGroup.add(parts.roofEdgeGroup); if (parts.roofEdgeGroup) houseGroup.add(parts.roofEdgeGroup);
if (parts.postsGroup) houseGroup.add(parts.postsGroup); if (parts.postsGroup) houseGroup.add(parts.postsGroup);
if (parts.chimneysGroup) houseGroup.add(parts.chimneysGroup);
} }
// Alles wat binnen de wanden of onder het plafond zit, snijdt mee met de doorsnede (anders zweeft het boven de snede).
const clipGroup = (g) => {
if (!g) return;
g.traverse(o => {
if (!o.material || (o.material.clippingPlanes || []).length === clip.length) return;
o.material.clippingPlanes = clip;
o.material.needsUpdate = true;
});
houseGroup.add(g);
};
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup); if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
if (parts.lightingGroup && !section) houseGroup.add(parts.lightingGroup); if (parts.lightingGroup && !section) houseGroup.add(parts.lightingGroup);
if (parts.elektraGroup) houseGroup.add(parts.elektraGroup); clipGroup(parts.elektraGroup);
if (parts.roomsGroup) { clipGroup(parts.chimneysGroup); // laag Gevelbekleding, net als in de plattegrond
parts.roomsGroup.userData.ceilings.visible = !section; if (parts.roomsGroup) parts.roomsGroup.userData.ceilings.visible = !section;
parts.roomsGroup.traverse(o => { clipGroup(parts.roomsGroup);
if (!o.material || (o.material.clippingPlanes || []).length === clip.length) return; clipGroup(parts.fixturesGroup);
o.material.clippingPlanes = clip;
o.material.needsUpdate = true;
});
houseGroup.add(parts.roomsGroup);
}
if (parts.fixturesGroup) {
parts.fixturesGroup.traverse(o => {
if (!o.material || (o.material.clippingPlanes || []).length === clip.length) return;
o.material.clippingPlanes = clip;
o.material.needsUpdate = true;
});
houseGroup.add(parts.fixturesGroup);
}
scene.add(houseGroup); scene.add(houseGroup);
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1)); const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1));
@@ -2777,7 +2761,7 @@ function mountHouseEditor(container, callbacks) {
refresh, refresh,
select, select,
setTool, setTool,
setView, setView: bar.setView,
toggleReport: () => toggleReport(), toggleReport: () => toggleReport(),
toggleSection, toggleSection,
fitView: () => fitPlan(), fitView: () => fitPlan(),
@@ -2863,7 +2847,7 @@ function mountHouseEditor(container, callbacks) {
const exportIfc = () => exportAs('ifc', 'BIM-export (IFC)', (f) => HouseIfc.generate(state).save(f), 'IFC4: balken, platen en elektra'); const exportIfc = () => exportAs('ifc', 'BIM-export (IFC)', (f) => HouseIfc.generate(state).save(f), 'IFC4: balken, platen en elektra');
// Render-export (sfeerimpressie): alleen wat in de afgewerkte woning te zien is, los van de gekozen lagen, doorsnede // Render-export (sfeerimpressie): alleen wat in de afgewerkte woning te zien is, los van de gekozen lagen, doorsnede
// en weergave-opties; geen buizen, dozen, binnenste vloerbalken, latten en isolatie achter de gevel, lichtkegels. // en weergave-opties (HouseModel.computeParts met finishedOnly).
// Lampen als echte lichtbronnen via een Blender-script in de ZIP. // Lampen als echte lichtbronnen via een Blender-script in de ZIP.
// Eigen materiaalsets (steen, riet, rabat en tegels komen uit gedeelde caches); maaiveld onder het midden = oorsprong. // Eigen materiaalsets (steen, riet, rabat en tegels komen uit gedeelde caches); maaiveld onder het midden = oorsprong.
const exportObj = () => exportAs('zip', 'render-export (OBJ + texturen + lampen, ZIP)', (filename) => { const exportObj = () => exportAs('zip', 'render-export (OBJ + texturen + lampen, ZIP)', (filename) => {
@@ -2873,13 +2857,9 @@ function mountHouseEditor(container, callbacks) {
const full = HouseModel.computeParts(state, { const full = HouseModel.computeParts(state, {
wallMaterials, floorMaterials: FloorModel.createMaterials(), roofMaterials, lightingMaterials: LightingModel.createMaterials(), wallMaterials, floorMaterials: FloorModel.createMaterials(), roofMaterials, lightingMaterials: LightingModel.createMaterials(),
sandwichMaterials: SandwichModel.createMaterials(), postMaterials: PostModel.createMaterials(), chimneyMaterials: ChimneyModel.createMaterials(), sandwichMaterials: SandwichModel.createMaterials(), postMaterials: PostModel.createMaterials(), chimneyMaterials: ChimneyModel.createMaterials(),
elektraMaterials: ElektraModel.createMaterials(), layers: ViewLayers.all(), elektraRough: false, finishedOnly: true elektraMaterials: ElektraModel.createMaterials(), layers: ViewLayers.all(), finishedOnly: true
}); });
HouseModel.placeGroups(full); HouseModel.placeGroups(full);
HouseModel.allWallPieces(full).forEach(p => ViewLayers.hideByMaterial(p.group,
[wallMaterials.isolatieMaterial, wallMaterials.battenMaterial, wallMaterials.doekMaterial]));
if (full.lightingGroup) full.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = false; });
if (full.roomsGroup && full.roomsGroup.userData.ceilings) full.roomsGroup.userData.ceilings.visible = true;
const roots = full.units.map(u => u.floorGroup).concat(HouseModel.allWallPieces(full).map(p => p.group), full.units.map(u => u.roof.group), const roots = full.units.map(u => u.floorGroup).concat(HouseModel.allWallPieces(full).map(p => p.group), full.units.map(u => u.roof.group),
[full.roofEdgeGroup, full.postsGroup, full.chimneysGroup, full.sandwichGroup, full.lightingGroup, full.elektraGroup, full.roomsGroup, full.fixturesGroup]).filter(Boolean); [full.roofEdgeGroup, full.postsGroup, full.chimneysGroup, full.sandwichGroup, full.lightingGroup, full.elektraGroup, full.roomsGroup, full.fixturesGroup]).filter(Boolean);
const bx0 = Math.min(...full.rects.map(r => r.x0)), bx1 = Math.max(...full.rects.map(r => r.x1)); const bx0 = Math.min(...full.rects.map(r => r.x0)), bx1 = Math.max(...full.rects.map(r => r.x1));
@@ -2932,6 +2912,7 @@ function mountHouseEditor(container, callbacks) {
return (...args) => { if (activeEditor && typeof activeEditor[fn] === 'function') return activeEditor[fn](...args); }; return (...args) => { if (activeEditor && typeof activeEditor[fn] === 'function') return activeEditor[fn](...args); };
} }
const has = (fn) => !!(activeEditor && typeof activeEditor[fn] === 'function'); const has = (fn) => !!(activeEditor && typeof activeEditor[fn] === 'function');
// Acties die naar de actieve onderdeel-editor gaan; de API hieronder wordt uit deze lijst opgebouwd.
const FORWARDED = ['mirrorHorizontal', 'setView', 'toggleReport', 'toggleSection', 'fitView', 'toggleWalk']; const FORWARDED = ['mirrorHorizontal', 'setView', 'toggleReport', 'toggleSection', 'fitView', 'toggleWalk'];
function onPartKey(e) { function onPartKey(e) {
@@ -2953,12 +2934,7 @@ function mountHouseEditor(container, callbacks) {
exportIfc, exportIfc,
exportObj, exportObj,
triggerImport, triggerImport,
mirrorHorizontal: forward('mirrorHorizontal'), ...Object.fromEntries(FORWARDED.map(fn => [fn, forward(fn)])),
setView: forward('setView'),
toggleReport: forward('toggleReport'),
toggleSection: forward('toggleSection'),
fitView: forward('fitView'),
toggleWalk: forward('toggleWalk'),
viewState: () => forward('viewState')() || {}, viewState: () => forward('viewState')() || {},
// Doorgestuurde acties bestaan alleen als de actieve onderdeel-editor ze kent (bijv. Rapport alleen in de woning). // Doorgestuurde acties bestaan alleen als de actieve onderdeel-editor ze kent (bijv. Rapport alleen in de woning).
can: (fn) => !FORWARDED.includes(fn) || has(fn), can: (fn) => !FORWARDED.includes(fn) || has(fn),
+16 -21
View File
@@ -220,7 +220,7 @@ const WALL_TEMPLATE = `
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="wall-project-cancel" class="ui-btn">Annuleren</button> <button type="button" id="wall-project-cancel" class="ui-btn" data-esc>Annuleren</button>
<button type="button" id="wall-project-save" class="ui-btn ui-btn-primary">Opslaan</button> <button type="button" id="wall-project-save" class="ui-btn ui-btn-primary">Opslaan</button>
</div> </div>
</div> </div>
@@ -335,11 +335,6 @@ function mountWallEditor(container, callbacks) {
updateAll(); updateAll();
} }
function setView(mode) {
UI.setView(document.getElementById('app-container'), mode);
requestAnimationFrame(onWindowResize);
}
const PROJECT_PROP_IDS = ['inp-proj-naam', 'inp-proj-plaats', 'inp-proj-straat', 'inp-proj-nummer', const PROJECT_PROP_IDS = ['inp-proj-naam', 'inp-proj-plaats', 'inp-proj-straat', 'inp-proj-nummer',
'inp-proj-tekenaar', 'inp-proj-titel', 'inp-proj-element', 'inp-proj-opmerking']; 'inp-proj-tekenaar', 'inp-proj-titel', 'inp-proj-element', 'inp-proj-opmerking'];
let projectPropsSnapshot = null; let projectPropsSnapshot = null;
@@ -1299,25 +1294,25 @@ function mountWallEditor(container, callbacks) {
let spaanplaatVisible = true; let spaanplaatVisible = true;
let buitenVisible = true; let buitenVisible = true;
const toolbar = document.getElementById('wall-toolbar');
toolbar.innerHTML = `<div class="tb-group tb-tools">${UI.tb('Spiegelen', 'symmetry-vertical', 'data-wt="mirror" title="Horizontaal spiegelen (Shift+H)"')}</div>
<div class="tb-spacer"></div><div class="tb-group tb-view"></div>${UI.tbSep()}${UI.viewSeg('split')}`;
toolbar.querySelector('[data-wt="mirror"]').addEventListener('click', () => mirrorHorizontal());
toolbar.querySelectorAll('[data-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
const viewGroup = toolbar.querySelector('.tb-view');
ViewLayers.createMenu(viewGroup, {
layers, keys: ['regelwerk', 'beplating', 'doek', 'latwerk', 'gevel', 'kozijnen', 'afwerking'],
onChange: (l) => { layers = l; update3DScene(); },
extras: [{ key: 'binnen', label: 'Beplating binnenzijde' }, { key: 'buiten', label: 'Beplating buitenzijde' }],
onExtra: (v) => { spaanplaatVisible = v.binnen; buitenVisible = v.buiten; update3DScene(); }
});
let skyMode = Skybox.getMode(); let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene); const skyEnv = Skybox.createEnvironment(scene);
const applySky = () => { const applySky = () => {
skyEnv.fitTo(wallGroup3D, 0); skyEnv.fitTo(wallGroup3D, 0);
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: wallGroup3D }); Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: wallGroup3D });
}; };
Skybox.createToggle(viewGroup, (m) => { skyMode = m; applySky(); }); const toolbar = document.getElementById('wall-toolbar');
const bar = UI.editorBar(toolbar, document.getElementById('app-container'), {
left: `<div class="tb-group tb-tools">${UI.tb('Spiegelen', 'symmetry-vertical', 'data-wt="mirror" title="Horizontaal spiegelen (Shift+H)"')}</div>`,
onResize: onWindowResize,
layers: {
layers, keys: ['regelwerk', 'beplating', 'doek', 'latwerk', 'gevel', 'kozijnen', 'afwerking'],
onChange: (l) => { layers = l; update3DScene(); },
extras: [{ key: 'binnen', label: 'Beplating binnenzijde' }, { key: 'buiten', label: 'Beplating buitenzijde' }],
onExtra: (v) => { spaanplaatVisible = v.binnen; buitenVisible = v.buiten; update3DScene(); }
},
onSky: (m) => { skyMode = m; applySky(); }
});
toolbar.querySelector('[data-wt="mirror"]').addEventListener('click', () => mirrorHorizontal());
let lastWallStats = null; let lastWallStats = null;
@@ -1418,8 +1413,8 @@ function mountWallEditor(container, callbacks) {
exportDesign, exportDesign,
triggerImport, triggerImport,
mirrorHorizontal, mirrorHorizontal,
setView, setView: bar.setView,
viewState: () => ({ view: UI.viewOf(document.getElementById('app-container')) }), viewState: bar.viewState,
openProjectProperties, openProjectProperties,
getStateJson: getPureState, getStateJson: getPureState,
loadStateJson: applyPureState loadStateJson: applyPureState