diff --git a/index.html b/index.html
index 180d42e..0765718 100644
--- a/index.html
+++ b/index.html
@@ -50,7 +50,7 @@
diff --git a/js/app.js b/js/app.js
index 4fb0c58..62c8bea 100644
--- a/js/app.js
+++ b/js/app.js
@@ -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(); }
diff --git a/js/core/inspector.js b/js/core/inspector.js
index 755fa41..56312a4 100644
--- a/js/core/inspector.js
+++ b/js/core/inspector.js
@@ -9,11 +9,9 @@
// zodat wire() alleen de eigen knoppen koppelt.
window.Inspector = (() => {
- const icon = (n) => ``;
- const big = (label, iconName, attrs, cls) =>
- ``;
- const small = (label, iconName, attrs, cls) =>
- ``;
+ const { icon, esc } = UI;
+ const big = UI.btnOf('ins-btn ins-big');
+ const small = UI.btnOf('ins-btn');
const kv = (label, value, unit) =>
`${label}${value}${unit ? `${unit}` : ''}
`;
@@ -32,9 +30,9 @@ window.Inspector = (() => {
const keepScroll = el._insKey === scrollKey ? el.scrollTop : 0;
el._insKey = scrollKey;
el.innerHTML = `
- ${view.crumbs && view.crumbs.length ? `
${view.crumbs.map(c => ``).join(icon('chevron-right'))}${icon('chevron-right')}
` : ''}
-
${view.icon ? icon(view.icon) : ''}${view.title}
- ${view.sub ? `
${view.sub}
` : ''}
+ ${view.crumbs && view.crumbs.length ? `
${view.crumbs.map(c => ``).join(icon('chevron-right'))}${icon('chevron-right')}
` : ''}
+
${view.icon ? icon(view.icon) : ''}${esc(view.title)}
+ ${view.sub ? `
${esc(view.sub)}
` : ''}
${view.top ? `${view.top}
` : ''}
`;
@@ -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 ? `` : '') +
- `${groupsHtml(sec.groups ? sec.groups() : [], sec.label)}
`;
+ '';
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;
}
diff --git a/js/core/menubar.js b/js/core/menubar.js
index 29edde9..c637a78 100644
--- a/js/core/menubar.js
+++ b/js/core/menubar.js
@@ -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 };
diff --git a/js/core/ui-kit.js b/js/core/ui-kit.js
index 407b2f3..6a1e2fb 100644
--- a/js/core/ui-kit.js
+++ b/js/core/ui-kit.js
@@ -80,8 +80,10 @@ window.UI = (() => {
const info = (text) => `${icon('info-circle')}${text}
`;
const warn = (text) => `${icon('exclamation-triangle')}${text}
`;
const grid = (...cols) => `${cols.map(c => `
${c}
`).join('')}
`;
- const button = (label, iconName, attrs, cls) =>
- ``;
+ // Eén knopbouwer; base = de knopsoort (ui-btn, tb-btn, ins-btn …), cls = extra klassen.
+ const btnOf = (base) => (label, iconName, attrs, cls) =>
+ ``;
+ const button = btnOf('ui-btn');
const actions = (...btns) => `${btns.join('')}
`;
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) =>
- ``;
+ const tb = btnOf('tb-btn');
const tbSep = () => '';
const viewSeg = (mode) => `${VIEWS.map(v =>
tb('', v.icon, `data-view="${v.key}" title="${v.label} (${v.key1})"`, v.key === mode ? 'active' : '')).join('')}
`;
- // 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 || ''}${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 };
})();
diff --git a/js/core/view-layers.js b/js/core/view-layers.js
index a626c23..479eb60 100644
--- a/js/core/view-layers.js
+++ b/js/core/view-layers.js
@@ -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 };
diff --git a/js/cost/cost-settings.js b/js/cost/cost-settings.js
index 24e5fe8..d182d71 100644
--- a/js/cost/cost-settings.js
+++ b/js/cost/cost-settings.js
@@ -29,7 +29,7 @@ window.CostSettings = (() => {
-
+
diff --git a/js/elektra/elektra-model.js b/js/elektra/elektra-model.js
index 63c819f..aead6ce 100644
--- a/js/elektra/elektra-model.js
+++ b/js/elektra/elektra-model.js
@@ -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)) {
diff --git a/js/floor/floor.js b/js/floor/floor.js
index 7eed262..2d8352a 100644
--- a/js/floor/floor.js
+++ b/js/floor/floor.js
@@ -159,7 +159,7 @@ const FLOOR_TEMPLATE = `
@@ -620,22 +620,21 @@ function mountFloorEditor(container, callbacks) {
let lastResolvedOpenings = [];
let lastChassisData = { wheels: [], outOfBounds: false };
- const toolbar = document.getElementById('floor-toolbar');
- toolbar.innerHTML = `${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
diff --git a/js/house/house-ifc.js b/js/house/house-ifc.js
index ead4e42..3978e2e 100644
--- a/js/house/house-ifc.js
+++ b/js/house/house-ifc.js
@@ -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));
diff --git a/js/house/house-model.js b/js/house/house-model.js
index 8720e38..bd8ffbc 100644
--- a/js/house/house-model.js
+++ b/js/house/house-model.js
@@ -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.
diff --git a/js/house/house-pdf.js b/js/house/house-pdf.js
index 7a13143..893d0b5 100644
--- a/js/house/house-pdf.js
+++ b/js/house/house-pdf.js
@@ -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);
diff --git a/js/house/house.js b/js/house/house.js
index 693b537..b37a783 100644
--- a/js/house/house.js
+++ b/js/house/house.js
@@ -41,7 +41,7 @@ const HOUSE_TEMPLATE = `
Geldt voor elk blad van de woning-PDF. Titel en elementnummer worden per blad automatisch ingevuld.
@@ -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 = `${TOOLS.map(([k, label, ic, key, hint]) =>
- UI.tb(label, ic, `data-ho-tool="${k}" title="${label} (${key}): ${hint}"`)).join('')}
- ${UI.tbSep()}${UI.tb('Unit', 'plus-square', 'data-tb="add-unit" title="Unit toevoegen aan de geselecteerde (of laatste) unit"')}
-
-
-
-
- ${UI.tb('Doorsnede', 'scissors', 'data-tb="section" title="Horizontale doorsnede (X)"')}
-
-
- ${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: `${TOOLS.map(([k, label, ic, key, hint]) =>
+ UI.tb(label, ic, `data-ho-tool="${k}" title="${label} (${key}): ${hint}"`)).join('')}
+ ${UI.tbSep()}${UI.tb('Unit', 'plus-square', 'data-tb="add-unit" title="Unit toevoegen aan de geselecteerde (of laatste) unit"')}
+ `,
+ after: `
+ ${UI.tb('Doorsnede', 'scissors', 'data-tb="section" title="Horizontale doorsnede (X)"')}
+
+
+ ${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) => ``;
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),
diff --git a/js/wall/wall.js b/js/wall/wall.js
index 80f8139..1871d8c 100644
--- a/js/wall/wall.js
+++ b/js/wall/wall.js
@@ -220,7 +220,7 @@ const WALL_TEMPLATE = `
@@ -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 = `${UI.tb('Spiegelen', 'symmetry-vertical', 'data-wt="mirror" title="Horizontaal spiegelen (Shift+H)"')}
- ${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: `${UI.tb('Spiegelen', 'symmetry-vertical', 'data-wt="mirror" title="Horizontaal spiegelen (Shift+H)"')}
`,
+ 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