Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89cadbebf7 |
+1
-1
@@ -50,7 +50,7 @@
|
||||
<div class="license-box" id="about-license"></div>
|
||||
<div class="about-footer">
|
||||
<span>© 2026 Machiel Heinen · 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>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* 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.
|
||||
|
||||
let tabs = [];
|
||||
@@ -243,9 +243,15 @@ function toggleBrowserFullscreen() {
|
||||
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() {
|
||||
const open = document.querySelector('.dlg-overlay.open, #about-overlay.open, #new-overlay.open');
|
||||
if (open) { open.classList.remove('open'); return true; }
|
||||
const open = document.querySelector(OVERLAYS);
|
||||
if (open) {
|
||||
const cancel = open.querySelector('[data-esc]');
|
||||
if (cancel) cancel.click(); else open.classList.remove('open');
|
||||
return true;
|
||||
}
|
||||
return Menubar.close() || UI.closePopovers();
|
||||
}
|
||||
|
||||
@@ -253,6 +259,12 @@ function wireHotkeys() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const inField = ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName);
|
||||
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) {
|
||||
const act = { s: () => editorCall('exportDesign'), n: openNewModal, o: () => editorCall('triggerImport'), '\\': togglePanels }[k];
|
||||
if (act && !e.altKey && !e.shiftKey) { e.preventDefault(); act(); }
|
||||
|
||||
+17
-10
@@ -9,11 +9,9 @@
|
||||
// zodat wire() alleen de eigen knoppen koppelt.
|
||||
|
||||
window.Inspector = (() => {
|
||||
const icon = (n) => `<i class="bi bi-${n}" aria-hidden="true"></i>`;
|
||||
const big = (label, iconName, attrs, cls) =>
|
||||
`<button type="button" class="ins-btn ins-big${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
|
||||
const small = (label, iconName, attrs, cls) =>
|
||||
`<button type="button" class="ins-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${icon(iconName)}<span>${label}</span></button>`;
|
||||
const { icon, esc } = UI;
|
||||
const big = UI.btnOf('ins-btn ins-big');
|
||||
const small = UI.btnOf('ins-btn');
|
||||
const kv = (label, value, unit) =>
|
||||
`<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;
|
||||
el._insKey = scrollKey;
|
||||
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>` : ''}
|
||||
<h2>${view.icon ? icon(view.icon) : ''}<span>${view.title}</span></h2>
|
||||
${view.sub ? `<div class="ins-sub">${view.sub}</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>${esc(view.title)}</span></h2>
|
||||
${view.sub ? `<div class="ins-sub">${esc(view.sub)}</div>` : ''}
|
||||
</div>
|
||||
${view.top ? `<div class="ins-top">${view.top}</div>` : ''}
|
||||
<div class="ins-sections"></div>`;
|
||||
@@ -45,16 +43,25 @@ window.Inspector = (() => {
|
||||
const isOpen = !collapsible || open.has(sec.key);
|
||||
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>` : '') +
|
||||
`<div class="ins-section-body">${groupsHtml(sec.groups ? sec.groups() : [], sec.label)}</div>`;
|
||||
'<div class="ins-section-body"></div>';
|
||||
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', () => {
|
||||
const now = !node.classList.contains('open');
|
||||
if (now) build();
|
||||
node.classList.toggle('open', now);
|
||||
node.querySelector('.ins-section-head').setAttribute('aria-expanded', now);
|
||||
if (now) open.add(sec.key); else open.delete(sec.key);
|
||||
saveOpen();
|
||||
});
|
||||
if (sec.wire) sec.wire(node);
|
||||
});
|
||||
el.scrollTop = keepScroll;
|
||||
}
|
||||
|
||||
+3
-2
@@ -23,13 +23,15 @@ window.Menubar = (() => {
|
||||
function close() {
|
||||
if (openIdx < 0) return false;
|
||||
openIdx = -1;
|
||||
UI.release(close);
|
||||
root.querySelectorAll('.mb-menu').forEach(m => m.classList.remove('open'));
|
||||
root.querySelectorAll('.mb-drop').forEach(d => { d.innerHTML = ''; });
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
const wrap = root.querySelectorAll('.mb-menu')[i];
|
||||
wrap.classList.add('open');
|
||||
@@ -54,7 +56,6 @@ window.Menubar = (() => {
|
||||
const top = e.target.closest('[data-menu]');
|
||||
if (top && openIdx >= 0 && Number(top.dataset.menu) !== openIdx) openMenu(Number(top.dataset.menu));
|
||||
});
|
||||
document.addEventListener('click', close);
|
||||
}
|
||||
|
||||
return { init, close };
|
||||
|
||||
+38
-13
@@ -80,8 +80,10 @@ window.UI = (() => {
|
||||
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>`;
|
||||
// Eén knopbouwer; base = de knopsoort (ui-btn, tb-btn, ins-btn …), cls = extra klassen.
|
||||
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 activeTabs = {};
|
||||
@@ -142,27 +144,50 @@ window.UI = (() => {
|
||||
el.querySelectorAll('[data-view]').forEach(b => b.classList.toggle('active', b.dataset.view === mode));
|
||||
}
|
||||
|
||||
const tb = (label, iconName, attrs, cls) =>
|
||||
`<button type="button" class="tb-btn${cls ? ' ' + cls : ''}" ${attrs || ''}>${iconName ? icon(iconName) : ''}${label ? `<span>${label}</span>` : ''}</button>`;
|
||||
const tb = btnOf('tb-btn');
|
||||
const tbSep = () => '<span class="tb-sep"></span>';
|
||||
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>`;
|
||||
|
||||
// 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;
|
||||
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) {
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
const was = panel.classList.contains('open');
|
||||
if (openPop) openPop();
|
||||
if (!was) { panel.classList.add('open'); btn.classList.add('open'); openPop = close; }
|
||||
if (panel.classList.contains('open')) { close(); return; }
|
||||
claim(close);
|
||||
panel.classList.add('open');
|
||||
btn.classList.add('open');
|
||||
});
|
||||
panel.addEventListener('click', (e) => e.stopPropagation());
|
||||
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.
|
||||
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));
|
||||
}
|
||||
|
||||
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,
|
||||
VIEWS, viewOf, setView, tb, tbSep, viewSeg, popover, closePopovers, masonry };
|
||||
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, editorBar, popover, claim, release, closePopovers, masonry };
|
||||
})();
|
||||
|
||||
+10
-3
@@ -20,7 +20,7 @@ window.ViewLayers = (() => {
|
||||
{ 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: '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' }
|
||||
];
|
||||
const KEYS = LIST.map(l => l.key);
|
||||
@@ -98,10 +98,17 @@ window.ViewLayers = (() => {
|
||||
emit();
|
||||
}
|
||||
});
|
||||
// Alles = ook de editor-eigen schakelaars weer aan.
|
||||
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();
|
||||
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 };
|
||||
|
||||
@@ -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="import">Importeren…</button>
|
||||
<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>
|
||||
</div>
|
||||
<input type="file" data-file="import" accept=".csv,.json,text/csv,application/json,text/plain" style="display:none">
|
||||
|
||||
@@ -146,8 +146,14 @@ window.ElektraModel = (() => {
|
||||
// rooms: leidingen lopen boven een vals plafond, niet tegen de onderkant van een hoog dak.
|
||||
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;
|
||||
// Vals plafond (mm boven vloer) van de ruimte op (x,z): undefined = geen ruimte (binnenwand, naad), null = geen vals plafond.
|
||||
const roomPlenum = (x, z) => { const r = rooms && rooms.roomAt ? rooms.roomAt(x, z) : null; return r ? (r.ceilingActive ? r.ceiling : null) : undefined; };
|
||||
// Vals plafond (mm boven vloer) van de ruimte op (x,z): undefined = in een wand of naad (geen gebied), null = geen
|
||||
// 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.
|
||||
const plenumAt = (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 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 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++) {
|
||||
const A = S[k - 1], B = S[k];
|
||||
if (!same(A.st, B.st)) {
|
||||
|
||||
+12
-17
@@ -159,7 +159,7 @@ const FLOOR_TEMPLATE = `
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -620,22 +620,21 @@ function mountFloorEditor(container, callbacks) {
|
||||
let lastResolvedOpenings = [];
|
||||
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();
|
||||
const skyEnv = Skybox.createEnvironment(scene);
|
||||
const applySky = () => {
|
||||
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.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() {
|
||||
if (floorGroup3D) scene.remove(floorGroup3D);
|
||||
@@ -682,10 +681,6 @@ function mountFloorEditor(container, callbacks) {
|
||||
}
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
|
||||
function setView(mode) {
|
||||
UI.setView(document.getElementById('floor-app'), mode);
|
||||
requestAnimationFrame(onWindowResize);
|
||||
}
|
||||
|
||||
function getPureState() {
|
||||
return {
|
||||
@@ -735,8 +730,8 @@ function mountFloorEditor(container, callbacks) {
|
||||
},
|
||||
exportDesign,
|
||||
triggerImport,
|
||||
setView,
|
||||
viewState: () => ({ view: UI.viewOf(document.getElementById('floor-app')) }),
|
||||
setView: bar.setView,
|
||||
viewState: bar.viewState,
|
||||
openProjectProperties,
|
||||
getStateJson: getPureState,
|
||||
loadStateJson: applyPureState
|
||||
|
||||
@@ -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 parts = HouseModel.computeParts(state, {
|
||||
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);
|
||||
return { parts, classify: classifier(M) };
|
||||
}
|
||||
|
||||
function generate(state) {
|
||||
const frame = build(state, ViewLayers.upTo(1));
|
||||
const sheet = build(state, ViewLayers.upTo(2));
|
||||
const frame = build(state, ViewLayers.upTo(1), true);
|
||||
const sheet = build(state, ViewLayers.upTo(2), false);
|
||||
const parts = frame.parts;
|
||||
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));
|
||||
|
||||
+13
-2
@@ -1700,7 +1700,7 @@ window.HouseModel = (() => {
|
||||
const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials(), lighting.indoor) : null;
|
||||
if (lightingGroup) lightingGroup.visible = shows('lampen');
|
||||
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');
|
||||
|
||||
const posts = PostModel.compute(state.settings.roof.posts, units, groundY);
|
||||
@@ -1722,7 +1722,18 @@ window.HouseModel = (() => {
|
||||
const fixturesGroup = build ? FixtureModel.build3D(fixtures) : null;
|
||||
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.
|
||||
|
||||
@@ -762,7 +762,8 @@ window.HousePdf = (() => {
|
||||
|
||||
function generate(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 sheets = sheetList(parts);
|
||||
|
||||
+70
-94
@@ -41,7 +41,7 @@ const HOUSE_TEMPLATE = `
|
||||
</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">
|
||||
<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>
|
||||
</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.
|
||||
const NAV_PREF = 'tectum.nav.closed';
|
||||
let navActiveKey = null;
|
||||
let navClosed;
|
||||
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) {
|
||||
@@ -262,8 +263,11 @@ function mountHouseEditor(container, callbacks) {
|
||||
}
|
||||
const tree = document.getElementById('house-nav-tree');
|
||||
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');
|
||||
if (act) act.scrollIntoView({ block: 'nearest' });
|
||||
if (act && key !== navActiveKey) act.scrollIntoView({ block: 'nearest' });
|
||||
navActiveKey = key;
|
||||
}
|
||||
|
||||
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'],
|
||||
['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]) =>
|
||||
UI.tb(label, ic, `data-ho-tool="${k}" title="${label} (${key}): ${hint}"`)).join('')}</div>
|
||||
${UI.tbSep()}${UI.tb('Unit', 'plus-square', 'data-tb="add-unit" title="Unit toevoegen aan de geselecteerde (of laatste) unit"')}
|
||||
<div class="tb-group tb-options" id="ho-tool-options"></div>
|
||||
<div class="tb-spacer"></div>
|
||||
<div class="tb-group tb-view" id="ho-view-group"></div>
|
||||
<div class="tb-group tb-section" id="ho-section-group">
|
||||
${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>
|
||||
</div>
|
||||
${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.tbSep()}
|
||||
${UI.tb('Rapport', 'clipboard-data', 'data-tb="report" title="Rapport in plaats van de plattegrond (4): transport, gewicht, kosten, hout en plaatwerk"')}
|
||||
${UI.viewSeg(UI.viewOf(document.getElementById('house-overview')))}`;
|
||||
const bar = UI.editorBar(toolbar, document.getElementById('house-overview'), {
|
||||
left: `<div class="tb-group tb-tools" role="group" aria-label="Gereedschap">${TOOLS.map(([k, label, ic, key, hint]) =>
|
||||
UI.tb(label, ic, `data-ho-tool="${k}" title="${label} (${key}): ${hint}"`)).join('')}</div>
|
||||
${UI.tbSep()}${UI.tb('Unit', 'plus-square', 'data-tb="add-unit" title="Unit toevoegen aan de geselecteerde (of laatste) unit"')}
|
||||
<div class="tb-group tb-options" id="ho-tool-options"></div>`,
|
||||
after: `<div class="tb-group tb-section" id="ho-section-group">
|
||||
${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>
|
||||
</div>
|
||||
${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.tbSep()}
|
||||
${UI.tb('Rapport', 'clipboard-data', 'data-tb="report" title="Rapport in plaats van de plattegrond (4): transport, gewicht, kosten, hout en plaatwerk"')}`,
|
||||
onResize,
|
||||
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-view]').forEach(b => b.addEventListener('click', () => setView(b.dataset.view)));
|
||||
toolbar.addEventListener('click', (e) => {
|
||||
const b = e.target.closest('[data-tb]');
|
||||
if (!b) return;
|
||||
@@ -446,9 +452,6 @@ function mountHouseEditor(container, callbacks) {
|
||||
else if (a === 'report') toggleReport();
|
||||
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() {
|
||||
const q = (a) => toolbar.querySelector(`[data-tb="${a}"]`);
|
||||
q('section').classList.toggle('active', view3D.section);
|
||||
@@ -464,10 +467,10 @@ function mountHouseEditor(container, callbacks) {
|
||||
function setLayers(l) {
|
||||
view3D.layers = ViewLayers.normalize(l);
|
||||
const L = view3D.layers;
|
||||
if (selection && ((!L.ruimtes && (selection.type === 'room' || selection.type === 'region')) ||
|
||||
(!L.inrichting && selection.type === 'fixture') || (!L.elektra && selection.type === 'elektra'))) selection = null;
|
||||
layerMenu.set(L);
|
||||
scheduleRefresh({ keepProps: true });
|
||||
const hidden = selection && ((!L.ruimtes && (selection.type === 'room' || selection.type === 'region')) ||
|
||||
(!L.inrichting && selection.type === 'fixture') || (!L.elektra && selection.type === 'elektra'));
|
||||
if (hidden) selection = null;
|
||||
scheduleRefresh({ keepProps: !hidden }); // vervalt de selectie, dan ook het paneel bijwerken
|
||||
}
|
||||
const sectionInput = document.getElementById('ho-section');
|
||||
function syncSectionRow() {
|
||||
@@ -667,7 +670,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
tool = t;
|
||||
drawStart = null;
|
||||
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;
|
||||
fxHover = null;
|
||||
renderToolOptions();
|
||||
@@ -675,6 +678,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
drawPlan();
|
||||
}
|
||||
|
||||
const toolCursor = () => tool === 'iw' || tool === 'fixture' ? 'crosshair' : tool === 'room' ? 'cell' : 'default';
|
||||
function updateHint() {
|
||||
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.')
|
||||
@@ -728,13 +732,13 @@ function mountHouseEditor(container, callbacks) {
|
||||
: ElektraModel.snapToWall(parts.elektra, w.x, w.z);
|
||||
drawPlan();
|
||||
});
|
||||
function onElektraDrop() {
|
||||
function onElektraDrop(e) {
|
||||
if (!elDrag) return;
|
||||
const d = elDrag;
|
||||
elDrag = null;
|
||||
canvas.style.cursor = tool === 'iw' ? 'crosshair' : tool === 'room' ? 'cell' : 'default';
|
||||
canvas.style.cursor = toolCursor();
|
||||
if (!d.moved || !d.snap) return;
|
||||
suppressClick = true;
|
||||
if (e.target === canvas) suppressClick = true;
|
||||
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 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);
|
||||
drawPlan();
|
||||
});
|
||||
function onFixtureDrop() {
|
||||
function onFixtureDrop(e) {
|
||||
if (!fxDrag) return;
|
||||
const d = fxDrag;
|
||||
fxDrag = null;
|
||||
canvas.style.cursor = 'default';
|
||||
canvas.style.cursor = toolCursor();
|
||||
if (!d.moved || !d.pose) return;
|
||||
suppressClick = true;
|
||||
if (e.target === canvas) suppressClick = true;
|
||||
const fs = fixtureState(d.id);
|
||||
if (fs) placeFixture(fs, d.pose.x, d.pose.z, d.pose.rot);
|
||||
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);
|
||||
drawPlan();
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
if (!chDrag) return;
|
||||
const d = chDrag;
|
||||
chDrag = null;
|
||||
canvas.style.cursor = 'default';
|
||||
canvas.style.cursor = toolCursor();
|
||||
if (!d.moved || !d.snap) return;
|
||||
suppressClick = true;
|
||||
if (e.target === canvas) suppressClick = true;
|
||||
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 });
|
||||
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);
|
||||
else if (selection.type === 'room') removeRoom(selection.id);
|
||||
else if (selection.type === 'elektra' && selection.id !== 'mk') removeSelectedElektra();
|
||||
else if (selection.type === 'iw') {
|
||||
state.interiorWalls = state.interiorWalls.filter(w => w.id !== selection.id);
|
||||
selection = null;
|
||||
refresh();
|
||||
}
|
||||
else if (selection.type === 'iw') removeInteriorWall(selection.id);
|
||||
}
|
||||
function removeInteriorWall(id) {
|
||||
state.interiorWalls = state.interiorWalls.filter(w => w.id !== id);
|
||||
selection = null;
|
||||
refresh();
|
||||
}
|
||||
function onKey(e) {
|
||||
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;
|
||||
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.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;
|
||||
}
|
||||
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 swatchOf = (hex) => `<div class="ins-swatch" style="background:${hex}"></div>`;
|
||||
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-ho-act]').forEach(b => b.addEventListener('click', () => {
|
||||
const a = b.dataset.hoAct;
|
||||
if (a === 'add-unit') addUnit();
|
||||
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(); }
|
||||
if (a === 'add-seams') { HouseModel.ensureSeamWalls(state, b.dataset.unit); refresh(); }
|
||||
else if (a === 'remove-unit') removeUnit(b.dataset.unit);
|
||||
else if (a === 'remove-iw') {
|
||||
state.interiorWalls = state.interiorWalls.filter(w => w.id !== Number(b.dataset.iw));
|
||||
selection = null;
|
||||
refresh();
|
||||
}
|
||||
else if (a === 'remove-iw') removeInteriorWall(Number(b.dataset.iw));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -2295,12 +2289,6 @@ function mountHouseEditor(container, callbacks) {
|
||||
syncToolbar();
|
||||
requestAnimationFrame(onResize);
|
||||
}
|
||||
function setView(mode) {
|
||||
setWalk(false);
|
||||
UI.setView(overviewEl, view3D.mode = mode);
|
||||
requestAnimationFrame(onResize);
|
||||
}
|
||||
|
||||
const esc = UI.esc;
|
||||
function stackedBars(title, iconName, series, rows, fmtVal, note) {
|
||||
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));
|
||||
if (parts.roofEdgeGroup) houseGroup.add(parts.roofEdgeGroup);
|
||||
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.lightingGroup && !section) houseGroup.add(parts.lightingGroup);
|
||||
if (parts.elektraGroup) houseGroup.add(parts.elektraGroup);
|
||||
if (parts.roomsGroup) {
|
||||
parts.roomsGroup.userData.ceilings.visible = !section;
|
||||
parts.roomsGroup.traverse(o => {
|
||||
if (!o.material || (o.material.clippingPlanes || []).length === clip.length) return;
|
||||
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);
|
||||
}
|
||||
clipGroup(parts.elektraGroup);
|
||||
clipGroup(parts.chimneysGroup); // laag Gevelbekleding, net als in de plattegrond
|
||||
if (parts.roomsGroup) parts.roomsGroup.userData.ceilings.visible = !section;
|
||||
clipGroup(parts.roomsGroup);
|
||||
clipGroup(parts.fixturesGroup);
|
||||
scene.add(houseGroup);
|
||||
|
||||
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,
|
||||
select,
|
||||
setTool,
|
||||
setView,
|
||||
setView: bar.setView,
|
||||
toggleReport: () => toggleReport(),
|
||||
toggleSection,
|
||||
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');
|
||||
|
||||
// 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.
|
||||
// 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) => {
|
||||
@@ -2873,13 +2857,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
const full = HouseModel.computeParts(state, {
|
||||
wallMaterials, floorMaterials: FloorModel.createMaterials(), roofMaterials, lightingMaterials: LightingModel.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.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),
|
||||
[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));
|
||||
@@ -2932,6 +2912,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
return (...args) => { if (activeEditor && typeof activeEditor[fn] === 'function') return activeEditor[fn](...args); };
|
||||
}
|
||||
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'];
|
||||
|
||||
function onPartKey(e) {
|
||||
@@ -2953,12 +2934,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
exportIfc,
|
||||
exportObj,
|
||||
triggerImport,
|
||||
mirrorHorizontal: forward('mirrorHorizontal'),
|
||||
setView: forward('setView'),
|
||||
toggleReport: forward('toggleReport'),
|
||||
toggleSection: forward('toggleSection'),
|
||||
fitView: forward('fitView'),
|
||||
toggleWalk: forward('toggleWalk'),
|
||||
...Object.fromEntries(FORWARDED.map(fn => [fn, forward(fn)])),
|
||||
viewState: () => forward('viewState')() || {},
|
||||
// Doorgestuurde acties bestaan alleen als de actieve onderdeel-editor ze kent (bijv. Rapport alleen in de woning).
|
||||
can: (fn) => !FORWARDED.includes(fn) || has(fn),
|
||||
|
||||
+16
-21
@@ -220,7 +220,7 @@ const WALL_TEMPLATE = `
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -335,11 +335,6 @@ function mountWallEditor(container, callbacks) {
|
||||
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',
|
||||
'inp-proj-tekenaar', 'inp-proj-titel', 'inp-proj-element', 'inp-proj-opmerking'];
|
||||
let projectPropsSnapshot = null;
|
||||
@@ -1299,25 +1294,25 @@ function mountWallEditor(container, callbacks) {
|
||||
|
||||
let spaanplaatVisible = 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();
|
||||
const skyEnv = Skybox.createEnvironment(scene);
|
||||
const applySky = () => {
|
||||
skyEnv.fitTo(wallGroup3D, 0);
|
||||
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;
|
||||
|
||||
@@ -1418,8 +1413,8 @@ function mountWallEditor(container, callbacks) {
|
||||
exportDesign,
|
||||
triggerImport,
|
||||
mirrorHorizontal,
|
||||
setView,
|
||||
viewState: () => ({ view: UI.viewOf(document.getElementById('app-container')) }),
|
||||
setView: bar.setView,
|
||||
viewState: bar.viewState,
|
||||
openProjectProperties,
|
||||
getStateJson: getPureState,
|
||||
loadStateJson: applyPureState
|
||||
|
||||
Reference in New Issue
Block a user