Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51921650e3 | ||
|
|
89cadbebf7 |
@@ -29,7 +29,8 @@ js/core/ gedeelde helpers: geometrie, weergavelagen (lagenmenu), menu
|
|||||||
js/wall/ WallModel (logica) + wand-PDF + wand-editor
|
js/wall/ WallModel (logica) + wand-PDF + wand-editor
|
||||||
js/floor/ FloorModel (logica) + vloer-PDF + vloer-editor
|
js/floor/ FloorModel (logica) + vloer-PDF + vloer-editor
|
||||||
js/roof/ RoofModel: dak (plat/lessenaar/zadel) + dakrand (boeidelen/daktrim)
|
js/roof/ RoofModel: dak (plat/lessenaar/zadel) + dakrand (boeidelen/daktrim)
|
||||||
PostModel: palen onder een overkapping/veranda (dak per unit te verlengen)
|
PostModel: palen onder een overkapping/veranda (dak per unit te verlengen), of een dichte
|
||||||
|
zijwand op een vrije hoek
|
||||||
js/house/chimney-model.js ChimneyModel: schoorsteen tegen een buitenwand (hol, sparing in de wand zoals een raam,
|
js/house/chimney-model.js ChimneyModel: schoorsteen tegen een buitenwand (hol, sparing in de wand zoals een raam,
|
||||||
ook over een naad, zonder hout erin; sparing in het dak met raveelwerk (stijlen, balken, kreupele
|
ook over een naad, zonder hout erin; sparing in het dak met raveelwerk (stijlen, balken, kreupele
|
||||||
balken), de beplating eindigt op het kader; onderbreekt de dakrand; een nokbalk stopt ervoor;
|
balken), de beplating eindigt op het kader; onderbreekt de dakrand; een nokbalk stopt ervoor;
|
||||||
@@ -85,7 +86,11 @@ assets/ logo, texturen, skybox
|
|||||||
en plafond naar de meterkast (strekkende meters per type, draad, buis,
|
en plafond naar de meterkast (strekkende meters per type, draad, buis,
|
||||||
steekverbinders op module-naden), materiaallijst en eigen PDF-blad.
|
steekverbinders op module-naden), materiaallijst en eigen PDF-blad.
|
||||||
Buizen minstens 50 mm uit elkaar (gebundeld alleen het laatste stuk naar
|
Buizen minstens 50 mm uit elkaar (gebundeld alleen het laatste stuk naar
|
||||||
de meterkast). Vloer-wcd's met leiding door de vloer. Elektra ook op de
|
de meterkast). Nooit door een raam of deur (`makeOpeningTest`): een
|
||||||
|
stijgleiding boven/onder een opening (wcd onder een raam) gaat eerst op
|
||||||
|
dezelfde hoogte opzij tot naast de opening, langs dezelfde wand met een
|
||||||
|
deur of raam ertussen via het plafond, gevellampen om de gevelhoeken heen,
|
||||||
|
en uit elkaar schuiven van buizen eindigt nooit in een opening. Vloer-wcd's met leiding door de vloer. Elektra ook op de
|
||||||
PDF-bladen per wand, vloer en dakvlak (plafond). Alles verplaatsbaar door
|
PDF-bladen per wand, vloer en dakvlak (plafond). Alles verplaatsbaar door
|
||||||
te slepen in de plattegrond. Kosten (groep Elektra): buis + draad per
|
te slepen in de plattegrond. Kosten (groep Elektra): buis + draad per
|
||||||
meter, boorgaten (geteld op stijlen, regels en balken), dozen,
|
meter, boorgaten (geteld op stijlen, regels en balken), dozen,
|
||||||
@@ -153,7 +158,15 @@ assets/ logo, texturen, skybox
|
|||||||
- **Overkapping:** bij een dak over de hele woning met `cover` loopt het
|
- **Overkapping:** bij een dak over de hele woning met `cover` loopt het
|
||||||
dak door tot de omhullende rechthoek waar geen unit staat (bijv. een
|
dak door tot de omhullende rechthoek waar geen unit staat (bijv. een
|
||||||
terras naast een kortere module), met een afgewerkte onderplaat in de
|
terras naast een kortere module), met een afgewerkte onderplaat in de
|
||||||
kleur van het boeideel (`HouseModel.coverExtensions`).
|
kleur van het boeideel (`HouseModel.coverExtensions`). Palen
|
||||||
|
(Woning › Dak › Palen overkapping) komen onder de vrije dakrand; met
|
||||||
|
"Zijwand" krijgt een vrije hoek geen paal maar een gestuukte wand van
|
||||||
|
200 mm van de dakhoek terug tot de gevel, langs de zijkant van de
|
||||||
|
overkapping (bij een hoek tussen twee vrije randen de kant met de kortste
|
||||||
|
weg naar de gevel); in 3D, plattegrond, PDF, DXF en kosten
|
||||||
|
(`PostModel`, `dak.zijwandM2`/`dak.zijwandMontage`). Ligt de buitenkant
|
||||||
|
in het vlak van een gevel, dan loopt die bekleding (felsplaten, rabat,
|
||||||
|
steen) erover door, gelijk met de gevel en tot onder de dakrand.
|
||||||
- **Dak over de hele woning:** eigenschappen Woning › Dak › "Over hele woning": één
|
- **Dak over de hele woning:** eigenschappen Woning › Dak › "Over hele woning": één
|
||||||
dakvorm (zelfde invoer als per unit) over de omhullende rechthoek van alle
|
dakvorm (zelfde invoer als per unit) over de omhullende rechthoek van alle
|
||||||
units; een wolfsdak alleen bij een rechthoek (bij een L-/T-vorm zouden
|
units; een wolfsdak alleen bij een rechthoek (bij een L-/T-vorm zouden
|
||||||
@@ -188,12 +201,20 @@ assets/ logo, texturen, skybox
|
|||||||
aangrenzende, dan krijgt het een eigen rand rondom (boven het lage dak
|
aangrenzende, dan krijgt het een eigen rand rondom (boven het lage dak
|
||||||
zonder spots). De rand van het lage dak loopt dan door tot de gevel van
|
zonder spots). De rand van het lage dak loopt dan door tot de gevel van
|
||||||
het hoge, of langs een zijgevel tot de volle overstek (onder de hoge
|
het hoge, of langs een zijgevel tot de volle overstek (onder de hoge
|
||||||
rand). Boeideel en daktrim steken 80 mm boven het dakleer uit.
|
rand). Boeideel en daktrim steken 80 mm boven het dakleer uit. Hebben
|
||||||
|
dak én gevel onder een rand felsplaten, dan komt er geen boeideel: de
|
||||||
|
dakplaat zet om over de rand, net vóór de gevelbekleding en zonder
|
||||||
|
opstand (`RoofModel.FELS_EDGE`, zetwerk zit in de prijs van de
|
||||||
|
felsplaten); een boeideel ernaast stopt gelijk met de buitenkant van zo'n
|
||||||
|
rand (ook naast daktrim).
|
||||||
- **Gevel:** gevelbekleding per woning, met override per wand en per
|
- **Gevel:** gevelbekleding per woning, met override per wand en per
|
||||||
wandstuk: rabat (vochtdoek + kruislat + rabat 12 mm, kleur per woning,
|
wandstuk: rabat (vochtdoek + kruislat + rabat 12 mm, kleur per woning,
|
||||||
getekend als plankentextuur die over naden doorloopt), steen (181
|
getekend als plankentextuur die over naden doorloopt), steen (181
|
||||||
texturen) of stucwerk (pleisterlaag 15 mm op cement-gebonden plaat, kleur
|
texturen), stucwerk (pleisterlaag 15 mm op cement-gebonden plaat, kleur
|
||||||
per woning, `WallModel.STUC`). Een zijde kan gesplitst worden (wand ›
|
per woning, `WallModel.STUC`) of felsplaten (staande naad, banen van
|
||||||
|
500 mm, zink/antraciet/zwart; opbouw als verticale rabat, kleuren gedeeld
|
||||||
|
met de felsplaten op het dak, `WallModel.FELS`; kosten `gevel.felsM2` en
|
||||||
|
`gevel.felsMontage`). Een zijde kan gesplitst worden (wand ›
|
||||||
Wandstukken › Splitsen op, gemeten vanaf de linker vloerhoek van buitenaf):
|
Wandstukken › Splitsen op, gemeten vanaf de linker vloerhoek van buitenaf):
|
||||||
elk stuk wordt een eigen wand-element en kan een eigen gevel krijgen
|
elk stuk wordt een eigen wand-element en kan een eigen gevel krijgen
|
||||||
(bijv. stuc links, hout rechts). Steen is overal een
|
(bijv. stuc links, hout rechts). Steen is overal een
|
||||||
@@ -395,7 +416,15 @@ hout.balk.38x235;Constructiehout vuren C24 38×235;€/m;4,91;Houthandel Y;;2026
|
|||||||
13 × 10 m, hoofdvolume met flauw zadeldak (twee lessenaars die op de naad
|
13 × 10 m, hoofdvolume met flauw zadeldak (twee lessenaars die op de naad
|
||||||
de nok vormen) en een vleugel met plat dak, steenstrips rondom met een
|
de nok vormen) en een vleugel met plat dak, steenstrips rondom met een
|
||||||
schoorsteen (800 mm, hart op de naad) in de oostgevel, kookeiland, logeerkamer en
|
schoorsteen (800 mm, hart op de naad) in de oostgevel, kookeiland, logeerkamer en
|
||||||
apart toilet in de vleugel. De standaardmodellen staan in
|
apart toilet in de vleugel. Model **Reest** (`HouseModel.createReest`,
|
||||||
|
naar `examples/ontwerp_reest.pdf`): 12,9 × 7,85 m, hoofdvolume
|
||||||
|
11,5 × 4,8 m onder één lessenaarsdak (hoog aan de noordgevel, twee units
|
||||||
|
van 3,4 en 1,4 m met lessenaars in hetzelfde vlak) dat aan de westkop
|
||||||
|
1,4 m doorloopt als overkapping bij de voordeur, dicht met een witte
|
||||||
|
zijwand; aanbouw met plat dak (toilet, berging, entree met open doorgang
|
||||||
|
naar de woonkamer, daktrim i.p.v. boeidelen); zuidgevel en dak in
|
||||||
|
antraciet felsplaten (zuidrand zonder boeideel, de felsplaten lopen over
|
||||||
|
de zijwand door), verder verticale rabat. De standaardmodellen staan in
|
||||||
`HouseModel.MODELS`; Bestand › Snel nieuw en het venster Nieuw worden
|
`HouseModel.MODELS`; Bestand › Snel nieuw en het venster Nieuw worden
|
||||||
daaruit opgebouwd (nieuw model = één regel plus een `create`-functie die
|
daaruit opgebouwd (nieuw model = één regel plus een `create`-functie die
|
||||||
`modelKit` gebruikt). De L-woning (8 × 3,6 m + 4,5 × 3,6 m) blijft
|
`modelKit` gebruikt). De L-woning (8 × 3,6 m + 4,5 × 3,6 m) blijft
|
||||||
|
|||||||
+1
-1
@@ -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>© 2026 Machiel Heinen · ontwikkeld in opdracht van Van Emous Mantelzorgwoningen</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.3';
|
||||||
// 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
@@ -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
@@ -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
@@ -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 };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+11
-4
@@ -13,14 +13,14 @@ window.ViewLayers = (() => {
|
|||||||
{ key: 'beplating', label: 'Beplating', icon: 'square', phase: 2, hint: 'Spaanplaat binnen, OSB vloer, plafond en dakbeschot, cementplaat buiten' },
|
{ key: 'beplating', label: 'Beplating', icon: 'square', phase: 2, hint: 'Spaanplaat binnen, OSB vloer, plafond en dakbeschot, cementplaat buiten' },
|
||||||
{ key: 'doek', label: 'Vochtdoek', icon: 'droplet', phase: 3, hint: 'Vochtdoek op de buitenzijde' },
|
{ key: 'doek', label: 'Vochtdoek', icon: 'droplet', phase: 3, hint: 'Vochtdoek op de buitenzijde' },
|
||||||
{ key: 'latwerk', label: 'Latwerk', icon: 'distribute-vertical', phase: 4, hint: 'Kruislat en trasraamplaat' },
|
{ key: 'latwerk', label: 'Latwerk', icon: 'distribute-vertical', phase: 4, hint: 'Kruislat en trasraamplaat' },
|
||||||
{ key: 'gevel', label: 'Gevelbekleding', icon: 'bricks', phase: 5, hint: 'Rabat, steenstrips, stuc, schoorsteen, sandwichpanelen' },
|
{ key: 'gevel', label: 'Gevelbekleding', icon: 'bricks', phase: 5, hint: 'Rabat, felsplaten, steenstrips, stuc, schoorsteen, sandwichpanelen' },
|
||||||
{ key: 'kozijnen', label: 'Kozijnen', icon: 'window', phase: 5, hint: 'Kozijnen, deuren en glas' },
|
{ key: 'kozijnen', label: 'Kozijnen', icon: 'window', phase: 5, hint: 'Kozijnen, deuren en glas' },
|
||||||
{ key: 'dakbedekking', label: 'Dakbedekking', icon: 'house-up', phase: 5, hint: 'Dakleer of riet' },
|
{ key: 'dakbedekking', label: 'Dakbedekking', icon: 'house-up', phase: 5, hint: 'Dakleer of riet' },
|
||||||
{ key: 'afwerking', label: 'Afwerking', icon: 'brush', phase: 6, hint: 'Vloerafwerking, verf, plafond, dakrand, dagkanten, logobord' },
|
{ key: 'afwerking', label: 'Afwerking', icon: 'brush', phase: 6, hint: 'Vloerafwerking, verf, plafond, dakrand, dagkanten, logobord' },
|
||||||
{ 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 };
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ window.COST_DEFAULTS = {
|
|||||||
steenM2: { label: 'Steenstrips (gevel + trasraam)', unit: '€/m²', value: 60 },
|
steenM2: { label: 'Steenstrips (gevel + trasraam)', unit: '€/m²', value: 60 },
|
||||||
steenMontage: { label: 'Montage steenstrips', unit: 's/m²', value: 1800 },
|
steenMontage: { label: 'Montage steenstrips', unit: 's/m²', value: 1800 },
|
||||||
stucM2: { label: 'Stucwerk gevel (wapening + sierpleister op cementgebonden plaat)', unit: '€/m²', value: 45 },
|
stucM2: { label: 'Stucwerk gevel (wapening + sierpleister op cementgebonden plaat)', unit: '€/m²', value: 45 },
|
||||||
stucMontage: { label: 'Stucwerk aanbrengen (wapenen, pleisteren)', unit: 's/m²', value: 1500 }
|
stucMontage: { label: 'Stucwerk aanbrengen (wapenen, pleisteren)', unit: 's/m²', value: 1500 },
|
||||||
|
felsM2: { label: 'Felsplaten gevel, staande naad (staal, klangen, ondergrond)', unit: '€/m²', value: 70 },
|
||||||
|
felsMontage: { label: 'Montage felsplaten gevel (ook dagkanten en hoeken)', unit: 's/m²', value: 2100 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -123,7 +125,9 @@ window.COST_DEFAULTS = {
|
|||||||
nokPersoneel: { label: 'Personeel bij plaatsen nokbalk (i.p.v. de bezetting)', unit: 'man', value: 3 },
|
nokPersoneel: { label: 'Personeel bij plaatsen nokbalk (i.p.v. de bezetting)', unit: 'man', value: 3 },
|
||||||
paalHoutM3: { label: 'Paalhout overkapping (eiken, geschaafd)', unit: '€/m³', value: 2400 },
|
paalHoutM3: { label: 'Paalhout overkapping (eiken, geschaafd)', unit: '€/m³', value: 2400 },
|
||||||
paalvoet: { label: 'Paalvoet verzinkt, verstelbaar (incl. ankers)', unit: '€/st.', value: 40 },
|
paalvoet: { label: 'Paalvoet verzinkt, verstelbaar (incl. ankers)', unit: '€/st.', value: 40 },
|
||||||
paalMontage: { label: 'Paal plaatsen: stellen, aftoppen, vastzetten aan het dak', unit: 's/st.', value: 2700 }
|
paalMontage: { label: 'Paal plaatsen: stellen, aftoppen, vastzetten aan het dak', unit: 's/st.', value: 2700 },
|
||||||
|
zijwandM2: { label: 'Zijwand overkapping (regelwerk, cementgebonden plaat en stuc aan beide kanten, fundering)', unit: '€/m²', value: 140 },
|
||||||
|
zijwandMontage: { label: 'Zijwand overkapping maken en plaatsen', unit: 's/m²', value: 5400 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ window.CostModel = (() => {
|
|||||||
add(el, 'Gevel', 'Gevel', `Vochtdoek (${fmtM(doekM)} m¹ wand)`, doek, 'm²', doek * G.vochtdoekM2, doekM * G.vochtdoekMontageM);
|
add(el, 'Gevel', 'Gevel', `Vochtdoek (${fmtM(doekM)} m¹ wand)`, doek, 'm²', doek * G.vochtdoekM2, doekM * G.vochtdoekMontageM);
|
||||||
const rabat = p.buitenMateriaal === 'vochtdoek' ? (st.rabatAreaM2 || 0) : 0;
|
const rabat = p.buitenMateriaal === 'vochtdoek' ? (st.rabatAreaM2 || 0) : 0;
|
||||||
add(el, 'Gevel', 'Gevel', 'Rabat', rabat, 'm²', rabat * G.rabatM2, rabat * G.rabatMontage);
|
add(el, 'Gevel', 'Gevel', 'Rabat', rabat, 'm²', rabat * G.rabatM2, rabat * G.rabatMontage);
|
||||||
|
const fels = p.buitenMateriaal === 'vochtdoek' ? (st.felsAreaM2 || 0) : 0;
|
||||||
|
add(el, 'Gevel', 'Gevel', 'Felsplaten (staande naad)', fels, 'm²', fels * (G.felsM2 || 0), fels * (G.felsMontage || 0));
|
||||||
const steen = (st.stoneAreaM2 || 0) + (st.trasraamStoneAreaM2 || 0);
|
const steen = (st.stoneAreaM2 || 0) + (st.trasraamStoneAreaM2 || 0);
|
||||||
add(el, 'Gevel', 'Gevel', 'Steenstrips', steen, 'm²', steen * G.steenM2, steen * G.steenMontage);
|
add(el, 'Gevel', 'Gevel', 'Steenstrips', steen, 'm²', steen * G.steenM2, steen * G.steenMontage);
|
||||||
const stuc = st.stucAreaM2 || 0;
|
const stuc = st.stucAreaM2 || 0;
|
||||||
@@ -242,6 +244,11 @@ window.CostModel = (() => {
|
|||||||
const pp = totals.posts;
|
const pp = totals.posts;
|
||||||
add(wo, 'Hout', 'Dak', `${pp.wood} palen ${pp.size}×${pp.size} mm (${pp.count} st.)`, pp.lengthM, 'm', pp.m3 * D.paalHoutM3, pp.count * D.paalMontage);
|
add(wo, 'Hout', 'Dak', `${pp.wood} palen ${pp.size}×${pp.size} mm (${pp.count} st.)`, pp.lengthM, 'm', pp.m3 * D.paalHoutM3, pp.count * D.paalMontage);
|
||||||
add(wo, 'Staal', 'Dak', 'Paalvoeten', pp.count, 'st.', pp.count * D.paalvoet, 0);
|
add(wo, 'Staal', 'Dak', 'Paalvoeten', pp.count, 'st.', pp.count * D.paalvoet, 0);
|
||||||
|
add(wo, 'Gevel', 'Dak', `Zijwand overkapping (${pp.walls || 0} st.)`, pp.wallM2 || 0, 'm²', (pp.wallM2 || 0) * (D.zijwandM2 || 0), (pp.wallM2 || 0) * (D.zijwandMontage || 0));
|
||||||
|
// Buitenkant zoals de gevel ernaast (stuc zit al in de zijwand).
|
||||||
|
const G = S.gevel, oo = pp.outsideM2 || {};
|
||||||
|
[['fels', 'felsplaten', G.felsM2, G.felsMontage], ['rabat', 'rabat', G.rabatM2, G.rabatMontage], ['steen', 'steenstrips', G.steenM2, G.steenMontage]].forEach(([k, label, m2, sec]) =>
|
||||||
|
add(wo, 'Gevel', 'Gevel', `Zijwand overkapping buitenzijde ${label}`, oo[k] || 0, 'm²', (oo[k] || 0) * (m2 || 0), (oo[k] || 0) * (sec || 0)));
|
||||||
}
|
}
|
||||||
if (parts.sandwich) {
|
if (parts.sandwich) {
|
||||||
const sw = parts.sandwich;
|
const sw = parts.sandwich;
|
||||||
|
|||||||
@@ -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…</button>
|
<button type="button" class="ui-btn" data-cs="import">Importeren…</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">
|
||||||
|
|||||||
+116
-20
@@ -143,11 +143,49 @@ window.ElektraModel = (() => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gaat het stuk p→q (wereld, mm) door een raam of deur? Een wand telt over zijn dikte plus 5 mm, de opening 1 mm
|
||||||
|
// kleiner: een buis naast de opening of erboven/eronder mag.
|
||||||
|
function makeOpeningTest(units, interior) {
|
||||||
|
const pieces = [];
|
||||||
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => { if (p.config.openings.length) pieces.push(p); })));
|
||||||
|
interior.forEach(p => { if (p.config.openings.length) pieces.push(p); });
|
||||||
|
const clip = (r, a, b, lo, hi) => {
|
||||||
|
const d = b - a;
|
||||||
|
if (Math.abs(d) < 1e-9) return a > lo && a < hi ? r : null;
|
||||||
|
let t0 = (lo - a) / d, t1 = (hi - a) / d;
|
||||||
|
if (t0 > t1) [t0, t1] = [t1, t0];
|
||||||
|
return Math.max(r[0], t0) < Math.min(r[1], t1) ? [Math.max(r[0], t0), Math.min(r[1], t1)] : null;
|
||||||
|
};
|
||||||
|
return (p, q) => pieces.some(pc => {
|
||||||
|
const pl = pc.place, c = Math.cos(pl.rotY), s = Math.sin(pl.rotY), half = pl.thickness / 2 + 5;
|
||||||
|
const loc = (v) => { const dx = v.x - pl.cx, dz = v.z - pl.cz; return { u: dx * c - dz * s + pl.width / 2, n: dx * s + dz * c, y: v.y - pl.floorTop }; };
|
||||||
|
const A = loc(p), B = loc(q);
|
||||||
|
const inWall = clip([0, 1], A.n, B.n, -half, half);
|
||||||
|
return !!inWall && pc.config.openings.some(op => {
|
||||||
|
let r = clip(inWall, A.u, B.u, op.x + 1, op.x + op.w - 1);
|
||||||
|
r = r && clip(r, A.y, B.y, op.y + 1, WallModel.openingTopMax(op) - 1);
|
||||||
|
if (!r) return false;
|
||||||
|
// Schuine bovenkant: alleen onder de daklijn van de opening.
|
||||||
|
return [0, 0.25, 0.5, 0.75, 1].some(k => {
|
||||||
|
const t = r[0] + (r[1] - r[0]) * k, u = A.u + (B.u - A.u) * t, y = A.y + (B.y - A.y) * t;
|
||||||
|
return y < WallModel.openingTopAt(op, u) - 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
// blocked(p, q): het stuk gaat door een wandopening (makeOpeningTest).
|
||||||
|
function makeRouter(units, rooms, blocked) {
|
||||||
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 +230,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)) {
|
||||||
@@ -250,28 +293,79 @@ window.ElektraModel = (() => {
|
|||||||
.filter(q => Math.hypot(q.x - a.x, q.z - a.z) > 0.5 && Math.hypot(q.x - b.x, q.z - b.z) > 0.5);
|
.filter(q => Math.hypot(q.x - a.x, q.z - a.z) > 0.5 && Math.hypot(q.x - b.x, q.z - b.z) > 0.5);
|
||||||
}
|
}
|
||||||
const strictlyInside = (x, z) => units.some(u => x > u.rect.x0 + 5 && x < u.rect.x1 - 5 && z > u.rect.z0 + 5 && z < u.rect.z1 - 5);
|
const strictlyInside = (x, z) => units.some(u => x > u.rect.x0 + 5 && x < u.rect.x1 - 5 && z > u.rect.z0 + 5 && z < u.rect.z1 - 5);
|
||||||
|
const clear = (pts) => !blocked || pts.slice(1).every((q, i) => !blocked(pts[i], q));
|
||||||
|
// Wandpunt dat recht omhoog/omlaag door een raam of deur zou gaan (wcd onder een raam, meterkast onder een
|
||||||
|
// bovenlicht): eerst op dezelfde hoogte opzij tot naast de opening (stijl + 60 mm), de dichtstbijzijnde kant
|
||||||
|
// die vrij is. yTo(punt) = hoogte waar de leiding heen gaat. null = recht kan, of nergens plek.
|
||||||
|
function sidestep(p, yTo) {
|
||||||
|
const f = p.face;
|
||||||
|
if (!f || !blocked || !blocked(p, { x: p.x, y: yTo(p), z: p.z })) return null;
|
||||||
|
const y = p.y - f.piece.place.floorTop, u0 = uAt(f, p), lo = Math.min(y, yTo(p) - f.piece.place.floorTop);
|
||||||
|
const cands = [];
|
||||||
|
f.piece.config.openings.forEach(op => {
|
||||||
|
if (WallModel.openingTopMax(op) > lo) cands.push(op.x - f.sw - 60, op.x + op.w + f.sw + 60);
|
||||||
|
});
|
||||||
|
const pick = cands.filter(u => u > f.u0 + 50 && u < f.u1 - 50).sort((m, n) => Math.abs(m - u0) - Math.abs(n - u0)).find(u => {
|
||||||
|
const q = Object.assign({}, p, facePoint(f, u, y, 0));
|
||||||
|
return !blocked(p, q) && !blocked(q, { x: q.x, y: yTo(q), z: q.z });
|
||||||
|
});
|
||||||
|
return pick === undefined ? null : Object.assign({}, p, facePoint(f, pick, y, 0));
|
||||||
|
}
|
||||||
|
// Gevellamp naar gevellamp: langs de gevel (om de hoeken heen), nooit dwars door de woning.
|
||||||
|
function outside(a, b) {
|
||||||
|
const gap = (p) => Math.min(...units.map(u => Math.max(u.rect.x0 - p.x, 0, p.x - u.rect.x1) + Math.max(u.rect.z0 - p.z, 0, p.z - u.rect.z1)));
|
||||||
|
const d = Math.max(60, gap(a), gap(b));
|
||||||
|
const cands = [[{ x: b.x, z: a.z }], [{ x: a.x, z: b.z }]];
|
||||||
|
units.forEach(u => {
|
||||||
|
[u.rect.x0 - d, u.rect.x1 + d].forEach(x => cands.push([{ x, z: a.z }, { x, z: b.z }]));
|
||||||
|
[u.rect.z0 - d, u.rect.z1 + d].forEach(z => cands.push([{ x: a.x, z }, { x: b.x, z }]));
|
||||||
|
});
|
||||||
|
const free = (p, q) => {
|
||||||
|
const n = Math.max(1, Math.ceil(Math.hypot(q.x - p.x, q.z - p.z) / 50));
|
||||||
|
for (let k = 1; k < n; k++) if (strictlyInside(p.x + (q.x - p.x) * k / n, p.z + (q.z - p.z) * k / n)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let best = null;
|
||||||
|
cands.forEach(c => {
|
||||||
|
const pts = [a].concat(c, [b]);
|
||||||
|
if (!pts.slice(1).every((q, i) => free(pts[i], q))) return;
|
||||||
|
const len = pts.slice(1).reduce((t, q, i) => t + Math.abs(q.x - pts[i].x) + Math.abs(q.z - pts[i].z), 0);
|
||||||
|
if (!best || len < best.len - 1) best = { len, c };
|
||||||
|
});
|
||||||
|
return best ? best.c : [strictlyInside(b.x, a.z) ? { x: a.x, z: b.z } : { x: b.x, z: a.z }];
|
||||||
|
}
|
||||||
function path(a, b) {
|
function path(a, b) {
|
||||||
const P = (p, wall, ceil, floor) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil, floor: !!floor, plenum: !!ceil && inPlenum(p.x, p.z, p.y) });
|
const P = (p, wall, ceil, floor) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil, floor: !!floor, plenum: !!ceil && inPlenum(p.x, p.z, p.y) });
|
||||||
if (a.mount === 'vloer' || b.mount === 'vloer') {
|
if (a.mount === 'vloer' || b.mount === 'vloer') {
|
||||||
const fa = floorAt(a.x, a.z) - FLOOR_DEPTH, fb = floorAt(b.x, b.z) - FLOOR_DEPTH;
|
const fa = floorAt(a.x, a.z) - FLOOR_DEPTH, fb = floorAt(b.x, b.z) - FLOOR_DEPTH;
|
||||||
return [P(a, a.face, false, a.mount === 'vloer'), P({ x: a.x, y: fa, z: a.z }, a.face, false, true)]
|
const A2 = a.mount === 'vloer' ? null : sidestep(a, () => fa), B2 = b.mount === 'vloer' ? null : sidestep(b, () => fb);
|
||||||
.concat(corners(a, b).map(c => P({ x: c.x, y: fa, z: c.z }, null, false, true)),
|
const a2 = A2 || a, b2 = B2 || b;
|
||||||
[P({ x: b.x, y: fb, z: b.z }, b.face, false, true), P(b, b.face, false, b.mount === 'vloer')]);
|
return [P(a, a.face, false, a.mount === 'vloer')].concat(A2 ? [P(A2, a.face)] : [], [P({ x: a2.x, y: fa, z: a2.z }, a.face, false, true)])
|
||||||
|
.concat(corners(a2, b2).map(c => P({ x: c.x, y: fa, z: c.z }, null, false, true)),
|
||||||
|
[P({ x: b2.x, y: fb, z: b2.z }, b.face, false, true)], B2 ? [P(B2, b.face)] : [], [P(b, b.face, false, b.mount === 'vloer')]);
|
||||||
}
|
}
|
||||||
if (a.face && a.face === b.face) return [P(a, a.face), P({ x: a.x, y: b.y, z: a.z }, a.face), P(b, b.face)];
|
// Langs dezelfde wand: eerst verticaal of eerst horizontaal; zit er een deur of raam tussen, dan via het plafond.
|
||||||
// Gevellamp naar gevellamp: langs de gevel, bij een binnenhoek niet dwars door de woning.
|
if (a.face && a.face === b.face) {
|
||||||
if (a.mount === 'buiten' && b.mount === 'buiten') {
|
const viaA = [P(a, a.face), P({ x: a.x, y: b.y, z: a.z }, a.face), P(b, b.face)];
|
||||||
const c = strictlyInside(b.x, a.z) ? { x: a.x, z: b.z } : { x: b.x, z: a.z };
|
const viaB = [P(a, a.face), P({ x: b.x, y: a.y, z: b.z }, a.face), P(b, b.face)];
|
||||||
return [P(a), P({ x: c.x, y: a.y, z: c.z }), P(b)];
|
if (clear(viaA)) return viaA;
|
||||||
|
if (clear(viaB)) return viaB;
|
||||||
}
|
}
|
||||||
|
if (a.mount === 'buiten' && b.mount === 'buiten') return [P(a)].concat(outside(a, b).map(c => P({ x: c.x, y: a.y, z: c.z })), [P(b)]);
|
||||||
// Wandpunt: de plafondhoogte iets de ruimte in meten (de wand zelf hoort bij geen ruimte).
|
// Wandpunt: de plafondhoogte iets de ruimte in meten (de wand zelf hoort bij geen ruimte).
|
||||||
const top = (p) => p.mount === 'plafond' ? p.y : p.face && p.face.n ? ceilAt(p.x + p.face.n.x * 50, p.z + p.face.n.z * 50) : ceilAt(p.x, p.z);
|
const top = (p) => p.mount === 'plafond' ? p.y : p.face && p.face.n ? ceilAt(p.x + p.face.n.x * 50, p.z + p.face.n.z * 50) : ceilAt(p.x, p.z);
|
||||||
const ya = top(a), yb = top(b), dropB = Math.abs(yb - b.y) > 1;
|
const A2 = a.mount === 'plafond' ? null : sidestep(a, top), B2 = b.mount === 'plafond' ? null : sidestep(b, top);
|
||||||
|
const a2 = A2 || a, b2 = B2 || b;
|
||||||
|
const ya = top(a2), yb = top(b2), dropB = Math.abs(yb - b.y) > 1;
|
||||||
const pts = [P(a, a.face, a.mount === 'plafond')];
|
const pts = [P(a, a.face, a.mount === 'plafond')];
|
||||||
if (Math.abs(ya - a.y) > 1) pts.push(P({ x: a.x, y: ya, z: a.z }, a.face, true));
|
if (A2) pts.push(P(A2, a.face));
|
||||||
corners(a, b, true).forEach(c => pts.push(P({ x: c.x, y: ceilAt(c.x, c.z), z: c.z }, null, true)));
|
if (Math.abs(ya - a.y) > 1) pts.push(P({ x: a2.x, y: ya, z: a2.z }, a.face, true));
|
||||||
pts.push(P({ x: b.x, y: yb, z: b.z }, dropB ? b.face : null, true));
|
corners(a2, b2, true).forEach(c => pts.push(P({ x: c.x, y: ceilAt(c.x, c.z), z: c.z }, null, true)));
|
||||||
if (dropB) pts.push(P(b, b.face, false));
|
pts.push(P({ x: b2.x, y: yb, z: b2.z }, dropB ? b.face : null, true));
|
||||||
|
if (dropB) {
|
||||||
|
if (B2) pts.push(P(B2, b.face));
|
||||||
|
pts.push(P(b, b.face, false));
|
||||||
|
}
|
||||||
return followCeiling(pts);
|
return followCeiling(pts);
|
||||||
}
|
}
|
||||||
const length = (pts) => pts.slice(1).reduce((s, q, i) => s + Math.hypot(q.x - pts[i].x, q.y - pts[i].y, q.z - pts[i].z), 0);
|
const length = (pts) => pts.slice(1).reduce((s, q, i) => s + Math.hypot(q.x - pts[i].x, q.y - pts[i].y, q.z - pts[i].z), 0);
|
||||||
@@ -301,7 +395,8 @@ window.ElektraModel = (() => {
|
|||||||
groups: [], runs: [], cables: [], wires: [], conduits: [], materials: [], warnings: [], items: [], faces: []
|
groups: [], runs: [], cables: [], wires: [], conduits: [], materials: [], warnings: [], items: [], faces: []
|
||||||
};
|
};
|
||||||
if (!s.enabled || !units.length) return out;
|
if (!s.enabled || !units.length) return out;
|
||||||
const R = makeRouter(units, rooms);
|
const blocked = makeOpeningTest(units, interior);
|
||||||
|
const R = makeRouter(units, rooms, blocked);
|
||||||
const faces = {};
|
const faces = {};
|
||||||
units.forEach(u => { faces[u.id] = facesOf(u, interior); });
|
units.forEach(u => { faces[u.id] = facesOf(u, interior); });
|
||||||
const faceOfPiece = (u, p) => faces[u.id].find(f => f.piece === p && f.side === -1);
|
const faceOfPiece = (u, p) => faces[u.id].find(f => f.piece === p && f.side === -1);
|
||||||
@@ -622,7 +717,7 @@ window.ElektraModel = (() => {
|
|||||||
});
|
});
|
||||||
if (mkLow) out.apps.forEach(a => addRun(APPLIANCES[a.app].cable, mkLow, Object.assign({}, a.pos, { mount: 'wand', face: a.face, name: a.code }), a.group));
|
if (mkLow) out.apps.forEach(a => addRun(APPLIANCES[a.app].cable, mkLow, Object.assign({}, a.pos, { mount: 'wand', face: a.face, name: a.code }), a.group));
|
||||||
|
|
||||||
separateRuns(out.runs);
|
separateRuns(out.runs, blocked);
|
||||||
const holesOf = makeHoleCounter(units, R);
|
const holesOf = makeHoleCounter(units, R);
|
||||||
out.runs.forEach(r => {
|
out.runs.forEach(r => {
|
||||||
r.rawMM = R.length(r.path);
|
r.rawMM = R.length(r.path);
|
||||||
@@ -711,7 +806,7 @@ window.ElektraModel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Uitzondering: het laatste stuk naar de meterkast; daar komen de buizen tegen elkaar aan de kast binnen.
|
// Uitzondering: het laatste stuk naar de meterkast; daar komen de buizen tegen elkaar aan de kast binnen.
|
||||||
function separateRuns(runs) {
|
function separateRuns(runs, blocked) {
|
||||||
const add = (p, d, k) => Object.assign({}, p, { x: p.x + d.x * k, y: p.y + d.y * k, z: p.z + d.z * k });
|
const add = (p, d, k) => Object.assign({}, p, { x: p.x + d.x * k, y: p.y + d.y * k, z: p.z + d.z * k });
|
||||||
const axisOf = (p, q) => {
|
const axisOf = (p, q) => {
|
||||||
const dx = Math.abs(q.x - p.x), dy = Math.abs(q.y - p.y), dz = Math.abs(q.z - p.z);
|
const dx = Math.abs(q.x - p.x), dy = Math.abs(q.y - p.y), dz = Math.abs(q.z - p.z);
|
||||||
@@ -782,6 +877,7 @@ window.ElektraModel = (() => {
|
|||||||
const kk = sign * k * step, p2 = add(p, dir, kk), q2 = add(q, dir, kk), s2 = seg(r, p2, q2, rad);
|
const kk = sign * k * step, p2 = add(p, dir, kk), q2 = add(q, dir, kk), s2 = seg(r, p2, q2, rad);
|
||||||
const j1 = seg(r, p, p2, rad), j2 = seg(r, q2, q, rad);
|
const j1 = seg(r, p, p2, rad), j2 = seg(r, q2, q, rad);
|
||||||
if (conflict(s2) || (!freeStart && conflict(j1)) || (!freeEnd && conflict(j2))) continue;
|
if (conflict(s2) || (!freeStart && conflict(j1)) || (!freeEnd && conflict(j2))) continue;
|
||||||
|
if (blocked && (blocked(p2, q2) || blocked(p, p2) || blocked(q2, q))) continue;
|
||||||
pts = pts.slice(0, i + 1).concat([p2, q2], pts.slice(i + 1));
|
pts = pts.slice(0, i + 1).concat([p2, q2], pts.slice(i + 1));
|
||||||
placed.push(s2, j1, j2);
|
placed.push(s2, j1, j2);
|
||||||
i += 2;
|
i += 2;
|
||||||
|
|||||||
+12
-17
@@ -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
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ window.HouseDxf = (() => {
|
|||||||
const s = pp.size / 2;
|
const s = pp.size / 2;
|
||||||
dxf.polyline([{ x: pp.x - s, z: pp.z - s }, { x: pp.x + s, z: pp.z - s }, { x: pp.x + s, z: pp.z + s }, { x: pp.x - s, z: pp.z + s }].map(T), true, 'PL-PALEN');
|
dxf.polyline([{ x: pp.x - s, z: pp.z - s }, { x: pp.x + s, z: pp.z - s }, { x: pp.x + s, z: pp.z + s }, { x: pp.x - s, z: pp.z + s }].map(T), true, 'PL-PALEN');
|
||||||
});
|
});
|
||||||
|
parts.posts.walls.forEach(w => dxf.polyline([{ x: w.x0, z: w.z0 }, { x: w.x1, z: w.z0 }, { x: w.x1, z: w.z1 }, { x: w.x0, z: w.z1 }].map(T), true, 'PL-PALEN'));
|
||||||
(parts.chimneys || []).forEach(ch => {
|
(parts.chimneys || []).forEach(ch => {
|
||||||
dxf.polyline(ch.footprint.map(T), true, 'PL-SCHOORSTEEN');
|
dxf.polyline(ch.footprint.map(T), true, 'PL-SCHOORSTEEN');
|
||||||
dxf.polyline(ch.flueFootprint.map(T), true, 'PL-SCHOORSTEEN');
|
dxf.polyline(ch.flueFootprint.map(T), true, 'PL-SCHOORSTEEN');
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
+162
-23
@@ -34,7 +34,7 @@ window.HouseModel = (() => {
|
|||||||
function createDefaultSettings() {
|
function createDefaultSettings() {
|
||||||
return {
|
return {
|
||||||
spacing: { exterior: { min: 240, max: 480 }, interior: { min: 300, max: 600 } },
|
spacing: { exterior: { min: 240, max: 480 }, interior: { min: 300, max: 600 } },
|
||||||
facade: { type: 'rabat', stone: 'K490', rabatColor: 'naturel', stucColor: 'wit' },
|
facade: { type: 'rabat', stone: 'K490', rabatColor: 'naturel', stucColor: 'wit', felsColor: 'antraciet' },
|
||||||
cladding: 'horizontaal',
|
cladding: 'horizontaal',
|
||||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM, finish: 'cementplaat' },
|
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM, finish: 'cementplaat' },
|
||||||
interiorPaint: true,
|
interiorPaint: true,
|
||||||
@@ -54,7 +54,7 @@ window.HouseModel = (() => {
|
|||||||
return { id, attach, unit, roof: RoofModel.createDefaultRoof(unit.floor), lighting: LightingModel.createDefaultUnitLighting(), facade: { walls: {} } };
|
return { id, attach, unit, roof: RoofModel.createDefaultRoof(unit.floor), lighting: LightingModel.createDefaultUnitLighting(), facade: { walls: {} } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const FACADE_TYPES = { rabat: 'Rabat', steen: 'Steen', stuc: 'Stucwerk' };
|
const FACADE_TYPES = { rabat: 'Rabat', steen: 'Steen', stuc: 'Stucwerk', fels: 'Felsplaten' };
|
||||||
function facadeFor(state, u, side, piece) {
|
function facadeFor(state, u, side, piece) {
|
||||||
const f = state.settings.facade;
|
const f = state.settings.facade;
|
||||||
const ow = (u.facade && u.facade.walls && u.facade.walls[side]) || null;
|
const ow = (u.facade && u.facade.walls && u.facade.walls[side]) || null;
|
||||||
@@ -62,15 +62,18 @@ window.HouseModel = (() => {
|
|||||||
const type = o && FACADE_TYPES[o.type] ? o.type : f.type;
|
const type = o && FACADE_TYPES[o.type] ? o.type : f.type;
|
||||||
const stone = o && o.type === 'steen' && WallModel.isStoneFinish(o.stone) ? o.stone : f.stone;
|
const stone = o && o.type === 'steen' && WallModel.isStoneFinish(o.stone) ? o.stone : f.stone;
|
||||||
const stuc = o && o.type === 'stuc' && WallModel.isStucColor(o.stuc) ? o.stuc : f.stucColor;
|
const stuc = o && o.type === 'stuc' && WallModel.isStucColor(o.stuc) ? o.stuc : f.stucColor;
|
||||||
return { type, stone, stuc, override: !!o };
|
const fels = o && o.type === 'fels' && WallModel.isFelsColor(o.fels) ? o.fels : f.felsColor;
|
||||||
|
return { type, stone, stuc, fels, override: !!o };
|
||||||
}
|
}
|
||||||
function facadeMaterials(state, fc) {
|
function facadeMaterials(state, fc) {
|
||||||
const tr = state.settings.trasraam;
|
const tr = state.settings.trasraam;
|
||||||
const trasraam = tr.enabled ? WallModel.trasraamFor(tr.height, tr.finish) : null;
|
const trasraam = tr.enabled ? WallModel.trasraamFor(tr.height, tr.finish) : null;
|
||||||
const cladding = state.settings.cladding;
|
const cladding = state.settings.cladding;
|
||||||
if (fc.type === 'steen') return { exterior: 'cementplaat', stone: fc.stone, stuc: null, cladding, trasraam, rabatColor: null };
|
if (fc.type === 'steen') return { exterior: 'cementplaat', stone: fc.stone, stuc: null, cladding, trasraam, rabatColor: null, fels: null };
|
||||||
if (fc.type === 'stuc') return { exterior: 'cementplaat', stone: null, stuc: fc.stuc, cladding, trasraam, rabatColor: null };
|
if (fc.type === 'stuc') return { exterior: 'cementplaat', stone: null, stuc: fc.stuc, cladding, trasraam, rabatColor: null, fels: null };
|
||||||
return { exterior: 'vochtdoek', stone: null, stuc: null, cladding, trasraam, rabatColor: state.settings.facade.rabatColor };
|
// Felsplaten: opbouw als verticale rabat (latten liggend), de banen staan altijd.
|
||||||
|
if (fc.type === 'fels') return { exterior: 'vochtdoek', stone: null, stuc: null, cladding: 'verticaal', trasraam, rabatColor: null, fels: fc.fels };
|
||||||
|
return { exterior: 'vochtdoek', stone: null, stuc: null, cladding, trasraam, rabatColor: state.settings.facade.rabatColor, fels: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function newModelState(naam) {
|
function newModelState(naam) {
|
||||||
@@ -660,6 +663,103 @@ window.HouseModel = (() => {
|
|||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model Reest (examples/ontwerp_reest.pdf): 12,9 × 7,85 m. Hoofdvolume 11,5 × 4,8 m onder één lessenaarsdak (hoog
|
||||||
|
// aan de noordgevel) dat aan de westkop 1,4 m doorloopt als overkapping bij de voordeur, aan de zuidkant dicht met
|
||||||
|
// een witte zijwand (PostModel sideWall, geen palen). 4,8 m is te diep voor één unit: twee units (3,4 + 1,4 m, de
|
||||||
|
// naad ten zuiden van het glas in de westgevel) met elk een lessenaar in hetzelfde vlak. Aan de noordwestkant een
|
||||||
|
// aanbouw van 3,89 × 3,05 m met plat dak (toilet, berging, entree), die met een open doorgang op de woonkamer
|
||||||
|
// uitkomt; de voordeur zit onder de overkapping. Zuidgevel en dak in felsplaten (daar geen boeideel: de dakplaat
|
||||||
|
// zet om, en de felsplaten lopen over de zijwand door), de rest verticale rabat; de aanbouw heeft daktrim. De
|
||||||
|
// aanbouw is de eerste unit, zodat alle maten buitenwerks vanaf zijn noordwesthoek gelden, zoals de tekening.
|
||||||
|
// De tekening heeft de zuidgevel van de aanbouw 240 mm zuidelijker (3290); in de configurator ligt hij op de naad.
|
||||||
|
function createReest() {
|
||||||
|
const st = newModelState('Reest');
|
||||||
|
const S = st.settings, HA = 2800, HN = 3500, HM = 2820, HS = 2540, DN = 3400, DS = 1400, V = 1400, FELS = 'antraciet';
|
||||||
|
Object.assign(S.facade, { type: 'rabat', rabatColor: 'naturel', felsColor: FELS });
|
||||||
|
S.cladding = 'verticaal';
|
||||||
|
S.trasraam.enabled = false;
|
||||||
|
Object.assign(S.roof, { fasciaColor: 'wit', overhang: 300, fasciaHeight: 300 });
|
||||||
|
S.roof.posts = PostModel.normalizeSettings({ enabled: true, sideWall: true, sideColor: 'wit', maxSpan: 6000 });
|
||||||
|
S.lighting.mode = 'gevel';
|
||||||
|
S.lighting.direction = 'both';
|
||||||
|
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
|
||||||
|
if (S.elektra) S.elektra.plateColor = 'antraciet';
|
||||||
|
|
||||||
|
const A = createUnitEntry('A', null, { width: 3890, depth: 3050, height: HA, windowOn: [] });
|
||||||
|
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: V }, { width: 11500, depth: DN, height: HM, windowOn: [] });
|
||||||
|
const C = createUnitEntry('C', { to: 'B', side: 'achter', align: 'start', offset: 0 }, { width: 11500, depth: DS, height: HS, windowOn: [] });
|
||||||
|
st.units.push(A, B, C);
|
||||||
|
UnitModel.applyRoofForm(B.unit, { type: 'lessenaar', dir: 'voor', low: HM, high: HN });
|
||||||
|
UnitModel.applyRoofForm(C.unit, { type: 'lessenaar', dir: 'voor', low: HS, high: HM });
|
||||||
|
[B, C].forEach(u => { u.roof.covering = 'fels_' + FELS; u.roof.extend = { f: 0, r: 0, b: 0, l: V }; });
|
||||||
|
A.roof.fascia = false;
|
||||||
|
C.facade.walls.achter = { type: 'fels', fels: FELS };
|
||||||
|
[A, B, C].forEach(u => ['voor', 'achter', 'links', 'rechts'].forEach(side => { u.lighting.walls[side] = 'geen'; }));
|
||||||
|
B.lighting.walls.links = 'openingen';
|
||||||
|
|
||||||
|
const { K, binnen, ops } = modelKit(st, HS, 90);
|
||||||
|
// Noord 'right' en zuid 'left' = vanaf de westhoek van de unit; west 'left' = vanaf het noordeind van de wand.
|
||||||
|
ops(A.unit.walls.voor, [['raam', 500, 700, 1400, 'right', 475, K('raam')], ['raam', 800, 1100, 1000, 'right', 2175, K('raam')]]);
|
||||||
|
ops(A.unit.walls.achter, [['voordeur', 1100, 2100, 0, 'left', 230, K('deur', { hinge: 'links', swing: 'buiten', glas: true })]]);
|
||||||
|
ops(B.unit.walls.voor, [
|
||||||
|
['raam', 600, 2100, 0, 'right', 3390, K('raam')],
|
||||||
|
['deur', 900, 2100, 0, 'right', 3990, K('deur', { leaf: 'rechts', hinge: 'links', swing: 'buiten', volglas: true })],
|
||||||
|
['deur', 900, 2100, 0, 'right', 4890, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })],
|
||||||
|
['raam', 600, 2100, 0, 'right', 5790, K('raam')],
|
||||||
|
['raam', 500, 1400, 700, 'right', 7525, K('raam')],
|
||||||
|
['raam', 500, 1400, 700, 'right', 9975, K('raam')]]);
|
||||||
|
ops(B.unit.walls.links, [['raam', 1800, 2600, 0, 'left', 1480, K('raam')]]);
|
||||||
|
ops(C.unit.walls.achter, [['raam', 1500, 500, 1600, 'left', 2050, K('raam')], ['raam', 1500, 500, 1600, 'left', 7440, K('raam')]]);
|
||||||
|
|
||||||
|
ensureSeamWalls(st, 'A');
|
||||||
|
const seam = st.interiorWalls.find(w => w.seam);
|
||||||
|
if (seam) {
|
||||||
|
// Open doorgang entree → woonkamer (x 1855–3045), gemeten vanaf de binnenkant van de westgevel van B.
|
||||||
|
const tB = WallModel.thicknessOf(WallModel.configFromState(B.unit.walls.links));
|
||||||
|
seam.wall.openings = [{ id: 1, type: 'deur', w: 1190, h: 2300, y: 0, anchor: 'left', offset: Math.round(1855 - V - tB),
|
||||||
|
fill: Object.assign(KozijnModel.defaultFill('deur'), { type: 'geen' }) }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DH = 2115;
|
||||||
|
// Aanbouw: wanden tot het platte dak. Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant.
|
||||||
|
const ann = modelKit(st, HA, 90);
|
||||||
|
ann.iw(1335, 190, 1335, 1690); // toilet | berging
|
||||||
|
ann.iw(190, 1735, 3700, 1735, [ // toilet, berging | entree
|
||||||
|
['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'buiten' })],
|
||||||
|
['deur', 930, DH, 'left', 1260, binnen({ hinge: 'links', swing: 'buiten' })]]);
|
||||||
|
// Hoofdvolume: wanden onder het laagste punt van het dak, slaapkamer en badkamer met een vlak plafond.
|
||||||
|
const main = modelKit(st, HS - 40, 90);
|
||||||
|
main.iw(8715, 3240, 8715, 7660, [['deur', 930, DH, 'left', 1890, binnen({ hinge: 'rechts', swing: 'buiten' })]]); // woonkamer | slaapkamer, badkamer
|
||||||
|
main.iw(8760, 6215, 12710, 6215, [['deur', 930, DH, 'left', 1815, // slaapkamer | badkamer
|
||||||
|
Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'binnen', dir: 'links' })]]);
|
||||||
|
|
||||||
|
const { room, fx } = main;
|
||||||
|
const plafond = { ceiling: HS - 40 };
|
||||||
|
room('A', 700, 1000, 'toilet');
|
||||||
|
room('A', 2500, 1000, 'berging', 'Berging');
|
||||||
|
room('A', 1800, 2400, 'hal', 'Entree');
|
||||||
|
room('B', 3000, 1500, 'woonkeuken', 'Woonkamer/keuken');
|
||||||
|
room('B', 9000, 1500, 'slaapkamer', 'Slaapkamer', plafond);
|
||||||
|
room('C', 9500, 600, 'badkamer', '', plafond);
|
||||||
|
|
||||||
|
const xB = (x) => x - V, zB = (z) => z - 3050, zC = (z) => z - 3050 - DN;
|
||||||
|
fx('toilet', 'A', 735, 640, 0, { w: 1100 });
|
||||||
|
fx('boiler', 'A', 3400, 1430, 90);
|
||||||
|
fx('keuken', 'C', xB(3390), zC(7360), 180, { w: 3600 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.29, kookplaat: true, kookplaatPlek: 0.71 });
|
||||||
|
fx('eettafel', 'B', xB(2490), zB(5160), 0, { w: 1000, d: 1000 }, { vorm: 'rond', stoelen: 4 });
|
||||||
|
fx('bank', 'B', xB(5200), zB(4360), 270, { w: 1600, d: 800 }, { chaise: 'geen' });
|
||||||
|
fx('salontafel', 'B', xB(6740), zB(4200), 0, { w: 800, d: 800 }, { vorm: 'rond' });
|
||||||
|
fx('fauteuil', 'B', xB(6940), zB(5600), 90);
|
||||||
|
fx('bed', 'B', xB(10390), zB(4320), 0, { w: 1600, d: 2100 }, { nachtkastjes: false });
|
||||||
|
fx('kledingkast', 'B', xB(12420), zB(4705), 90, { w: 2900, anchor: 'oost' });
|
||||||
|
fx('toilet', 'C', xB(9420), zC(7300), 180, { w: 1000 });
|
||||||
|
fx('wastafel', 'C', xB(10890), zC(7420), 180, { w: 800 });
|
||||||
|
fx('douche', 'C', xB(12260), zC(6960), 180, { w: 900, d: 1400 }, { glas: 'rechts', glasLengte: 800 });
|
||||||
|
|
||||||
|
computeParts(st, { skip3D: true });
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
// Standaardmodellen: Bestand › Snel nieuw en het venster Nieuw. De sleutel is de template.
|
// Standaardmodellen: Bestand › Snel nieuw en het venster Nieuw. De sleutel is de template.
|
||||||
const MODELS = {
|
const MODELS = {
|
||||||
amstel: { label: 'Woning Amstel', icon: 'house-heart', create: createAmstel },
|
amstel: { label: 'Woning Amstel', icon: 'house-heart', create: createAmstel },
|
||||||
@@ -667,7 +767,8 @@ window.HouseModel = (() => {
|
|||||||
ijssel: { label: 'Woning IJssel', icon: 'houses', create: createIJssel },
|
ijssel: { label: 'Woning IJssel', icon: 'houses', create: createIJssel },
|
||||||
waal: { label: 'Woning Waal', icon: 'house-add', create: createWaal },
|
waal: { label: 'Woning Waal', icon: 'house-add', create: createWaal },
|
||||||
maas: { label: 'Woning Maas', icon: 'house-check', create: createMaas },
|
maas: { label: 'Woning Maas', icon: 'house-check', create: createMaas },
|
||||||
rijn: { label: 'Woning Rijn', icon: 'house-gear', create: createRijn }
|
rijn: { label: 'Woning Rijn', icon: 'house-gear', create: createRijn },
|
||||||
|
reest: { label: 'Woning Reest', icon: 'house-up', create: createReest }
|
||||||
};
|
};
|
||||||
|
|
||||||
function createDefaultState(template) {
|
function createDefaultState(template) {
|
||||||
@@ -1259,10 +1360,10 @@ window.HouseModel = (() => {
|
|||||||
});
|
});
|
||||||
const ridgePairs = houseShape ? {} : doubleLessenaarPairs(state, rects, cuts);
|
const ridgePairs = houseShape ? {} : doubleLessenaarPairs(state, rects, cuts);
|
||||||
|
|
||||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop) {
|
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels) {
|
||||||
if (!build) return { group: null, stats: null, specs: null };
|
if (!build) return { group: null, stats: null, specs: null };
|
||||||
const built = WallModel.build3D(config, frame, {
|
const built = WallModel.build3D(config, frame, {
|
||||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop,
|
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels,
|
||||||
painted: state.settings.interiorPaint && shows('afwerking'), layers, rabatColor: state.settings.facade.rabatColor
|
painted: state.settings.interiorPaint && shows('afwerking'), layers, rabatColor: state.settings.facade.rabatColor
|
||||||
});
|
});
|
||||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
||||||
@@ -1282,11 +1383,12 @@ window.HouseModel = (() => {
|
|||||||
const cladding = m.cladding || 'horizontaal';
|
const cladding = m.cladding || 'horizontaal';
|
||||||
const stone = buitenMateriaal === 'cementplaat' ? (m.stone || null) : null;
|
const stone = buitenMateriaal === 'cementplaat' ? (m.stone || null) : null;
|
||||||
const stuc = buitenMateriaal === 'cementplaat' && !stone ? (m.stuc || null) : null;
|
const stuc = buitenMateriaal === 'cementplaat' && !stone ? (m.stuc || null) : null;
|
||||||
|
const fels = buitenMateriaal === 'vochtdoek' ? (m.fels || null) : null;
|
||||||
const trasraam = buitenMateriaal === 'vochtdoek' || stone || stuc ? (m.trasraam || null) : null;
|
const trasraam = buitenMateriaal === 'vochtdoek' || stone || stuc ? (m.trasraam || null) : null;
|
||||||
return Object.assign(p, {
|
return Object.assign(p, {
|
||||||
element: `${u.id}-W${wl.nr}${p.suffix}`,
|
element: `${u.id}-W${wl.nr}${p.suffix}`,
|
||||||
label: `Unit ${u.id} ${wl.label.toLowerCase()}` + (base ? ` opbouw (vanaf ${base} mm)` : p.suffix ? ` (${p.suffix})` : ''),
|
label: `Unit ${u.id} ${wl.label.toLowerCase()}` + (base ? ` opbouw (vanaf ${base} mm)` : p.suffix ? ` (${p.suffix})` : ''),
|
||||||
buitenMateriaal, cladding, trasraam: base ? null : trasraam, stone, stuc, facadeOwn: own,
|
buitenMateriaal, cladding, trasraam: base ? null : trasraam, stone, stuc, fels, facadeOwn: own,
|
||||||
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop + base, baseY: base }
|
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop + base, baseY: base }
|
||||||
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
|
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
|
||||||
});
|
});
|
||||||
@@ -1634,10 +1736,11 @@ window.HouseModel = (() => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const facadeDepth = (a, b, nrm) => {
|
// Gevelstuk onder een dakrand (grootste overlap langs de rand); dot < 0 = gevel van een hoger deel ertegenover.
|
||||||
|
const edgePiece = (a, b, nrm) => {
|
||||||
const horiz = Math.abs(nrm.z) > 0.5;
|
const horiz = Math.abs(nrm.z) > 0.5;
|
||||||
const line = horiz ? a.z : a.x, s0 = Math.min(horiz ? a.x : a.z, horiz ? b.x : b.z), s1 = Math.max(horiz ? a.x : a.z, horiz ? b.x : b.z);
|
const line = horiz ? a.z : a.x, s0 = Math.min(horiz ? a.x : a.z, horiz ? b.x : b.z), s1 = Math.max(horiz ? a.x : a.z, horiz ? b.x : b.z);
|
||||||
let best = 0, bestOv = 1;
|
let best = null, bestOv = 1;
|
||||||
extPieces.forEach(p => {
|
extPieces.forEach(p => {
|
||||||
const pn = { x: p.place.normal.x, z: p.place.normal.y };
|
const pn = { x: p.place.normal.x, z: p.place.normal.y };
|
||||||
const dot = pn.x * nrm.x + pn.z * nrm.z;
|
const dot = pn.x * nrm.x + pn.z * nrm.z;
|
||||||
@@ -1647,15 +1750,20 @@ window.HouseModel = (() => {
|
|||||||
const ov = Math.min(s1, Math.max(horiz ? q0.x : q0.z, horiz ? q1.x : q1.z)) - Math.max(s0, Math.min(horiz ? q0.x : q0.z, horiz ? q1.x : q1.z));
|
const ov = Math.min(s1, Math.max(horiz ? q0.x : q0.z, horiz ? q1.x : q1.z)) - Math.max(s0, Math.min(horiz ? q0.x : q0.z, horiz ? q1.x : q1.z));
|
||||||
if (ov <= bestOv) return;
|
if (ov <= bestOv) return;
|
||||||
bestOv = ov;
|
bestOv = ov;
|
||||||
best = Math.sign(dot) * WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc);
|
best = { p, dot };
|
||||||
});
|
});
|
||||||
return best;
|
return best;
|
||||||
};
|
};
|
||||||
|
const facadeDepth = (a, b, nrm) => {
|
||||||
|
const e = edgePiece(a, b, nrm);
|
||||||
|
return e ? Math.sign(e.dot) * WallModel.claddingOffset(e.p.buitenMateriaal, e.p.stone, e.p.stuc) : 0;
|
||||||
|
};
|
||||||
|
const facadeFels = (a, b, nrm) => { const e = edgePiece(a, b, nrm); return !!(e && e.dot > 0 && e.p.fels); };
|
||||||
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
|
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
|
||||||
units.map(u => ({
|
units.map(u => ({
|
||||||
unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false,
|
unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false,
|
||||||
topAt: u.roof.topAt, ridges: u.roof.ridges
|
covering: u.roof.covering, topAt: u.roof.topAt, ridges: u.roof.ridges
|
||||||
})), facadeDepth) : null;
|
})), facadeDepth, facadeFels) : null;
|
||||||
const pointInPoly = (q, poly) => {
|
const pointInPoly = (q, poly) => {
|
||||||
let inside = false;
|
let inside = false;
|
||||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||||
@@ -1671,13 +1779,13 @@ window.HouseModel = (() => {
|
|||||||
const sg = [10, 60, 150].map(d => ({ x: m.x + nrm.x * d, z: m.z + nrm.z * d }))
|
const sg = [10, 60, 150].map(d => ({ x: m.x + nrm.x * d, z: m.z + nrm.z * d }))
|
||||||
.map(q => roofEdge.segments.find(e => pointInPoly(q, [e.a, e.b, e.outerB, e.outerA]))).find(Boolean);
|
.map(q => roofEdge.segments.find(e => pointInPoly(q, [e.a, e.b, e.outerB, e.outerA]))).find(Boolean);
|
||||||
if (!sg) return;
|
if (!sg) return;
|
||||||
const h = sg.fascia ? roofEdge.fasciaHeight : roofEdge.trimHeight + roofEdge.edgeAbove;
|
const above = sg.fels ? RoofModel.FELS_EDGE.aboveMM : roofEdge.edgeAbove;
|
||||||
p.battenAbove = (p.skinAbove || 0) + roofEdge.edgeAbove - h;
|
p.battenAbove = (p.skinAbove || 0) + above - RoofModel.edgeHeight(roofEdge, sg);
|
||||||
});
|
});
|
||||||
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
|
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
|
||||||
const rooms = RoomModel.compute(state.rooms || [], units, extPieces.concat(interior));
|
const rooms = RoomModel.compute(state.rooms || [], units, extPieces.concat(interior));
|
||||||
extPieces.concat(interior).forEach(p => {
|
extPieces.concat(interior).forEach(p => {
|
||||||
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow, p.stone, p.skinAbove, p.trimTop, p.battenAbove, p.stuc, p.skinTop);
|
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow, p.stone, p.skinAbove, p.trimTop, p.battenAbove, p.stuc, p.skinTop, p.fels);
|
||||||
if (built.group && p.shiftZ) {
|
if (built.group && p.shiftZ) {
|
||||||
const outer = new THREE.Group();
|
const outer = new THREE.Group();
|
||||||
built.group.position.z = p.shiftZ * 0.001;
|
built.group.position.z = p.shiftZ * 0.001;
|
||||||
@@ -1700,10 +1808,24 @@ 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);
|
// Zijwand van een overkapping in het vlak van een gevel: die bekleding loopt erover door.
|
||||||
|
const postOutside = (w, out) => {
|
||||||
|
const line = out.x > 0 ? w.x1 : out.x < 0 ? w.x0 : out.z > 0 ? w.z1 : w.z0;
|
||||||
|
const p = extPieces.find(pc => {
|
||||||
|
const n = { x: pc.place.normal.x, z: pc.place.normal.y }, q = UnitModel.localToWorld(pc.place, 0, pc.place.thickness / 2);
|
||||||
|
return n.x * out.x + n.z * out.z > 0.5 && Math.abs((out.x ? q.x : q.z) - line) < 2 && !pc.place.baseY;
|
||||||
|
});
|
||||||
|
if (!p || (p.buitenMateriaal === 'cementplaat' && !p.stone && !p.stuc)) return null;
|
||||||
|
const kind = p.fels ? 'fels' : p.stone ? 'steen' : p.stuc ? 'stuc' : 'rabat';
|
||||||
|
return {
|
||||||
|
kind, depth: WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc),
|
||||||
|
finish: { fels: p.fels, stone: p.stone, stuc: p.stuc, rabatColor: kind === 'rabat' ? state.settings.facade.rabatColor : null, cladding: p.cladding }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const posts = PostModel.compute(state.settings.roof.posts, units, groundY, postOutside);
|
||||||
const postsGroup = build ? PostModel.build3D(posts, o.postMaterials) : null;
|
const postsGroup = build ? PostModel.build3D(posts, o.postMaterials) : null;
|
||||||
const chimneysGroup = build && chimneys.length ? ChimneyModel.build3D(chimneys, {
|
const chimneysGroup = build && chimneys.length ? ChimneyModel.build3D(chimneys, {
|
||||||
materials: o.chimneyMaterials, wallMaterial: o.wallMaterials && o.wallMaterials.spaanplaatMaterial
|
materials: o.chimneyMaterials, wallMaterial: o.wallMaterials && o.wallMaterials.spaanplaatMaterial
|
||||||
@@ -1722,7 +1844,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.
|
||||||
@@ -1946,6 +2079,10 @@ window.HouseModel = (() => {
|
|||||||
{
|
{
|
||||||
naam: `Rabat ${WallModel.CLADDING_THICK_MM} mm (gevelbekleding)`, m2: byMat('vochtdoek').reduce((a, p) => a + (p.stats.rabatAreaM2 || 0), 0),
|
naam: `Rabat ${WallModel.CLADDING_THICK_MM} mm (gevelbekleding)`, m2: byMat('vochtdoek').reduce((a, p) => a + (p.stats.rabatAreaM2 || 0), 0),
|
||||||
m3: null, kg: 0 // gewicht volgt
|
m3: null, kg: 0 // gewicht volgt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
naam: 'Felsplaten gevel (staande naad)', m2: byMat('vochtdoek').reduce((a, p) => a + (p.stats.felsAreaM2 || 0), 0),
|
||||||
|
m3: null, kg: byMat('vochtdoek').reduce((a, p) => a + (p.stats.felsAreaM2 || 0), 0) * RoofModel.FELS.kgM2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
weights,
|
weights,
|
||||||
@@ -1955,9 +2092,10 @@ window.HouseModel = (() => {
|
|||||||
return { unit: u.id, name: rb.profile.name, kgM: rb.profile.kgM, lengthM: rb.length / 1000, kg: rb.steelKg, fillM3: rb.fillM3, fillKg: rb.fillKg,
|
return { unit: u.id, name: rb.profile.name, kgM: rb.profile.kgM, lengthM: rb.length / 1000, kg: rb.steelKg, fillM3: rb.fillM3, fillKg: rb.fillKg,
|
||||||
util: rb.util, defl: rb.defl, deflMax: rb.deflMax, ok: rb.ok, below: rb.below, loadKNm: rb.loadKNm };
|
util: rb.util, defl: rb.defl, deflMax: rb.deflMax, ok: rb.ok, below: rb.below, loadKNm: rb.loadKNm };
|
||||||
}),
|
}),
|
||||||
posts: parts.posts.count ? {
|
posts: parts.posts.count || parts.posts.walls.length ? {
|
||||||
count: parts.posts.count, size: parts.posts.size, wood: (PostModel.WOODS[parts.posts.wood] || {}).label || '',
|
count: parts.posts.count, size: parts.posts.size, wood: (PostModel.WOODS[parts.posts.wood] || {}).label || '',
|
||||||
lengthM: parts.posts.lengthMM / 1000, m3: parts.posts.m3, kg: parts.posts.kg
|
lengthM: parts.posts.lengthMM / 1000, m3: parts.posts.m3, kg: parts.posts.kg,
|
||||||
|
walls: parts.posts.walls.length, wallM2: parts.posts.wallM2, outsideM2: parts.posts.outsideM2
|
||||||
} : null,
|
} : null,
|
||||||
roofEdge: parts.roofEdge ? {
|
roofEdge: parts.roofEdge ? {
|
||||||
fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000,
|
fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000,
|
||||||
@@ -2026,6 +2164,7 @@ window.HouseModel = (() => {
|
|||||||
if (!WallModel.isStoneFinish(state.settings.facade.stone)) state.settings.facade.stone = def.facade.stone;
|
if (!WallModel.isStoneFinish(state.settings.facade.stone)) state.settings.facade.stone = def.facade.stone;
|
||||||
if (!WallModel.RABAT_COLORS[state.settings.facade.rabatColor]) state.settings.facade.rabatColor = def.facade.rabatColor;
|
if (!WallModel.RABAT_COLORS[state.settings.facade.rabatColor]) state.settings.facade.rabatColor = def.facade.rabatColor;
|
||||||
if (!WallModel.isStucColor(state.settings.facade.stucColor)) state.settings.facade.stucColor = def.facade.stucColor;
|
if (!WallModel.isStucColor(state.settings.facade.stucColor)) state.settings.facade.stucColor = def.facade.stucColor;
|
||||||
|
if (!WallModel.isFelsColor(state.settings.facade.felsColor)) state.settings.facade.felsColor = def.facade.felsColor;
|
||||||
state.settings.trasraam = Object.assign(def.trasraam, state.settings.trasraam || {});
|
state.settings.trasraam = Object.assign(def.trasraam, state.settings.trasraam || {});
|
||||||
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
||||||
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
||||||
|
|||||||
+12
-7
@@ -139,6 +139,8 @@ window.HousePdf = (() => {
|
|||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
doc.setFillColor(160, 124, 84);
|
doc.setFillColor(160, 124, 84);
|
||||||
parts.posts.posts.forEach(pp => doc.rect(px(pp.x - pp.size / 2), py(pp.z - pp.size / 2), pp.size * scale, pp.size * scale, 'FD'));
|
parts.posts.posts.forEach(pp => doc.rect(px(pp.x - pp.size / 2), py(pp.z - pp.size / 2), pp.size * scale, pp.size * scale, 'FD'));
|
||||||
|
doc.setFillColor(235, 235, 232);
|
||||||
|
parts.posts.walls.forEach(w => doc.rect(px(w.x0), py(w.z0), (w.x1 - w.x0) * scale, (w.z1 - w.z0) * scale, 'FD'));
|
||||||
(parts.chimneys || []).forEach(ch => {
|
(parts.chimneys || []).forEach(ch => {
|
||||||
doc.setFillColor(150, 150, 150);
|
doc.setFillColor(150, 150, 150);
|
||||||
poly(ch.footprint, 'FD');
|
poly(ch.footprint, 'FD');
|
||||||
@@ -257,7 +259,8 @@ window.HousePdf = (() => {
|
|||||||
const addWall = (p, y0) => {
|
const addWall = (p, y0) => {
|
||||||
const n = { x: p.place.normal.x, z: p.place.normal.y };
|
const n = { x: p.place.normal.x, z: p.place.normal.y };
|
||||||
const zOut = p.place.thickness / 2 + WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc);
|
const zOut = p.place.thickness / 2 + WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc);
|
||||||
const color = p.stone ? (WallModel.STONE_COLORS[p.stone] || '#a0724f') : p.stuc ? WallModel.stucHex(p.stuc) : WallModel.rabatHex(state.settings.facade.rabatColor);
|
const color = p.stone ? (WallModel.STONE_COLORS[p.stone] || '#a0724f') : p.stuc ? WallModel.stucHex(p.stuc) : p.fels ? WallModel.felsHex(p.fels)
|
||||||
|
: WallModel.rabatHex(state.settings.facade.rabatColor);
|
||||||
faces.push({ kind: 'wand', pts: wallFace(p, y0).map(q => toWorld(p, q.x, q.y, zOut)), n, color, bias: 0 });
|
faces.push({ kind: 'wand', pts: wallFace(p, y0).map(q => toWorld(p, q.x, q.y, zOut)), n, color, bias: 0 });
|
||||||
const tr = p.trasraam;
|
const tr = p.trasraam;
|
||||||
if (tr && tr.top > y0) {
|
if (tr && tr.top > y0) {
|
||||||
@@ -312,10 +315,9 @@ window.HousePdf = (() => {
|
|||||||
|
|
||||||
const edge = parts.roofEdge;
|
const edge = parts.roofEdge;
|
||||||
if (edge) {
|
if (edge) {
|
||||||
const fc = (RoofModel.FASCIA_COLORS[edge.fasciaColor] || RoofModel.FASCIA_COLORS.antraciet).hex;
|
|
||||||
const lerp = (a, b, t) => ({ x: a.x + (b.x - a.x) * t, z: a.z + (b.z - a.z) * t });
|
const lerp = (a, b, t) => ({ x: a.x + (b.x - a.x) * t, z: a.z + (b.z - a.z) * t });
|
||||||
edge.segments.forEach(sg => {
|
edge.segments.forEach(sg => {
|
||||||
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
|
const h = RoofModel.edgeHeight(edge, sg);
|
||||||
const n = Math.max(1, Math.ceil(Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z) / 100));
|
const n = Math.max(1, Math.ceil(Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z) / 100));
|
||||||
const top = [], bottom = [];
|
const top = [], bottom = [];
|
||||||
for (let k = 0; k <= n; k++) {
|
for (let k = 0; k <= n; k++) {
|
||||||
@@ -324,7 +326,7 @@ window.HousePdf = (() => {
|
|||||||
top.push({ x: O.x, y: Math.max(tI, tO), z: O.z });
|
top.push({ x: O.x, y: Math.max(tI, tO), z: O.z });
|
||||||
bottom.push({ x: O.x, y: Math.min(tI, tO) - h, z: O.z });
|
bottom.push({ x: O.x, y: Math.min(tI, tO) - h, z: O.z });
|
||||||
}
|
}
|
||||||
faces.push({ kind: 'dakrand', pts: top.concat(bottom.reverse()), n: { x: sg.normal.x, z: sg.normal.z }, color: sg.fascia ? fc : 0xb8bcc0, bias: 3 });
|
faces.push({ kind: 'dakrand', pts: top.concat(bottom.reverse()), n: { x: sg.normal.x, z: sg.normal.z }, color: RoofModel.edgeColor(edge, sg), bias: 3 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
(parts.chimneys || []).forEach(ch => {
|
(parts.chimneys || []).forEach(ch => {
|
||||||
@@ -351,14 +353,16 @@ window.HousePdf = (() => {
|
|||||||
UnitModel.normalizeSplits(u.unit.walls[side].splits).filter(sp => sp.facade).forEach(sp => facades.push(HouseModel.facadeFor(state, u, side, sp.facade)));
|
UnitModel.normalizeSplits(u.unit.walls[side].splits).filter(sp => sp.facade).forEach(sp => facades.push(HouseModel.facadeFor(state, u, side, sp.facade)));
|
||||||
}));
|
}));
|
||||||
facades.forEach(fc => {
|
facades.forEach(fc => {
|
||||||
const key = fc.type === 'steen' ? 'steen:' + fc.stone : fc.type === 'stuc' ? 'stuc:' + fc.stuc : 'rabat';
|
const key = fc.type === 'steen' ? 'steen:' + fc.stone : fc.type === 'stuc' ? 'stuc:' + fc.stuc : fc.type === 'fels' ? 'fels:' + fc.fels : 'rabat';
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) return;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
items.push(fc.type === 'steen'
|
items.push(fc.type === 'steen'
|
||||||
? { label: 'Gevel', value: `Steen ${fc.stone}`, thumb: WallModel.stoneThumbUrl(fc.stone, 64), color: WallModel.STONE_COLORS[fc.stone] }
|
? { label: 'Gevel', value: `Steen ${fc.stone}`, thumb: WallModel.stoneThumbUrl(fc.stone, 64), color: WallModel.STONE_COLORS[fc.stone] }
|
||||||
: fc.type === 'stuc'
|
: fc.type === 'stuc'
|
||||||
? { label: 'Gevel', value: `Stucwerk ${((WallModel.STUC_COLORS[fc.stuc] || {}).label || '').toLowerCase()}`, color: WallModel.stucHex(fc.stuc) }
|
? { label: 'Gevel', value: `Stucwerk ${((WallModel.STUC_COLORS[fc.stuc] || {}).label || '').toLowerCase()}`, color: WallModel.stucHex(fc.stuc) }
|
||||||
: { label: 'Gevel', value: `Rabat ${((WallModel.RABAT_COLORS[st.facade.rabatColor] || {}).label || '').toLowerCase()} (${(WallModel.CLADDING_DIRECTIONS[st.cladding] || st.cladding).toLowerCase()})`, color: WallModel.rabatHex(st.facade.rabatColor) });
|
: fc.type === 'fels'
|
||||||
|
? { label: 'Gevel', value: `Felsplaten ${((WallModel.FELS.COLORS[fc.fels] || {}).label || '').toLowerCase()} (staande naad)`, color: WallModel.felsHex(fc.fels) }
|
||||||
|
: { label: 'Gevel', value: `Rabat ${((WallModel.RABAT_COLORS[st.facade.rabatColor] || {}).label || '').toLowerCase()} (${(WallModel.CLADDING_DIRECTIONS[st.cladding] || st.cladding).toLowerCase()})`, color: WallModel.rabatHex(st.facade.rabatColor) });
|
||||||
});
|
});
|
||||||
if (st.trasraam.enabled) {
|
if (st.trasraam.enabled) {
|
||||||
const tf = st.trasraam.finish;
|
const tf = st.trasraam.finish;
|
||||||
@@ -762,7 +766,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);
|
||||||
|
|||||||
+100
-103
@@ -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);
|
||||||
@@ -1182,6 +1188,11 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
const a = toPx(pp.x - pp.size / 2, pp.z - pp.size / 2), sz = pp.size * scale;
|
const a = toPx(pp.x - pp.size / 2, pp.z - pp.size / 2), sz = pp.size * scale;
|
||||||
ctx.fillRect(a.x, a.y, sz, sz); ctx.strokeRect(a.x, a.y, sz, sz);
|
ctx.fillRect(a.x, a.y, sz, sz); ctx.strokeRect(a.x, a.y, sz, sz);
|
||||||
});
|
});
|
||||||
|
if (L.dak) parts.posts.walls.forEach(w => {
|
||||||
|
const a = toPx(w.x0, w.z0);
|
||||||
|
ctx.fillStyle = WallModel.stucHex(w.color); ctx.strokeStyle = '#8a8a8a';
|
||||||
|
ctx.fillRect(a.x, a.y, (w.x1 - w.x0) * scale, (w.z1 - w.z0) * scale); ctx.strokeRect(a.x, a.y, (w.x1 - w.x0) * scale, (w.z1 - w.z0) * scale);
|
||||||
|
});
|
||||||
(L.gevel ? parts.chimneys || [] : []).forEach(ch => {
|
(L.gevel ? parts.chimneys || [] : []).forEach(ch => {
|
||||||
const path = (pts) => { ctx.beginPath(); pts.forEach((p, i) => { const q = toPx(p.x, p.z); if (i) ctx.lineTo(q.x, q.y); else ctx.moveTo(q.x, q.y); }); ctx.closePath(); };
|
const path = (pts) => { ctx.beginPath(); pts.forEach((p, i) => { const q = toPx(p.x, p.z); if (i) ctx.lineTo(q.x, q.y); else ctx.moveTo(q.x, q.y); }); ctx.closePath(); };
|
||||||
const sn = chDrag && chDrag.moved && chDrag.snap && chDrag.id === ch.id ? chDrag.snap : null;
|
const sn = chDrag && chDrag.moved && chDrag.snap && chDrag.id === ch.id ? chDrag.snap : null;
|
||||||
@@ -1337,26 +1348,15 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
const stoneOptions = Object.assign({}, ...WallModel.TRASRAAM_STONES.map(c => ({ [c]: `Steen ${c}` })));
|
const stoneOptions = Object.assign({}, ...WallModel.TRASRAAM_STONES.map(c => ({ [c]: `Steen ${c}` })));
|
||||||
const trasraamFinishes = Object.assign({ cementplaat: 'Cementplaat' }, stoneOptions);
|
const trasraamFinishes = Object.assign({ cementplaat: 'Cementplaat' }, stoneOptions);
|
||||||
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 felsColors = Object.fromEntries(Object.entries(WallModel.FELS.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();
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1368,16 +1368,21 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
const rabatUi = sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'Rabat op vochtdoek en kruislat' }) +
|
const rabatUi = sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'Rabat op vochtdoek en kruislat' }) +
|
||||||
pair(sel('hs-rabat-color', 'Kleur', rabatColors, st.facade.rabatColor, { hint: 'Ook voor rabat-overrides per wand' }),
|
pair(sel('hs-rabat-color', 'Kleur', rabatColors, st.facade.rabatColor, { hint: 'Ook voor rabat-overrides per wand' }),
|
||||||
`<div class="ins-swatch" style="background:${WallModel.rabatHex(st.facade.rabatColor)}"></div>`);
|
`<div class="ins-swatch" style="background:${WallModel.rabatHex(st.facade.rabatColor)}"></div>`);
|
||||||
const usesRabat = state.units.some(u => Object.keys(u.unit.walls).some(side => HouseModel.facadeFor(state, u, side).type === 'rabat' ||
|
const uses = (type) => state.units.some(u => Object.keys(u.unit.walls).some(side => HouseModel.facadeFor(state, u, side).type === type ||
|
||||||
UnitModel.normalizeSplits(u.unit.walls[side].splits).some(sp => sp.facade && sp.facade.type === 'rabat')));
|
UnitModel.normalizeSplits(u.unit.walls[side].splits).some(sp => sp.facade && sp.facade.type === type)));
|
||||||
|
const usesRabat = uses('rabat');
|
||||||
|
const felsUi = pair(sel('hs-fels-color', 'Kleur', felsColors, st.facade.felsColor,
|
||||||
|
{ hint: `Staande naad, banen van ${WallModel.FELS.SEAM_MM} mm, op latten zoals verticale rabat; ook voor fels-overrides per wand` }),
|
||||||
|
swatchOf(WallModel.felsHex(st.facade.felsColor)));
|
||||||
return [
|
return [
|
||||||
{ label: 'Bekleding', html: sel('hs-facade', 'Type', HouseModel.FACADE_TYPES, st.facade.type, { hint: 'Voor alle buitenwanden; per wand of wandstuk aan te passen (selecteer de wand)' }) +
|
{ label: 'Bekleding', html: sel('hs-facade', 'Type', HouseModel.FACADE_TYPES, st.facade.type, { hint: 'Voor alle buitenwanden; per wand of wandstuk aan te passen (selecteer de wand)' }) +
|
||||||
(st.facade.type === 'steen'
|
(st.facade.type === 'steen'
|
||||||
? pair(sel('hs-facade-stone', 'Steen', stoneOptions, st.facade.stone, { hint: `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik` }), thumb(WallModel.stoneThumbUrl(st.facade.stone, 64), st.facade.stone))
|
? pair(sel('hs-facade-stone', 'Steen', stoneOptions, st.facade.stone, { hint: `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik` }), thumb(WallModel.stoneThumbUrl(st.facade.stone, 64), st.facade.stone))
|
||||||
: st.facade.type === 'stuc'
|
: st.facade.type === 'stuc'
|
||||||
? pair(sel('hs-stuc-color', 'Kleur', stucColors, st.facade.stucColor, { hint: `Pleisterlaag ${WallModel.STUC.THICK_MM} mm op cement-gebonden plaat` }), swatchOf(WallModel.stucHex(st.facade.stucColor)))
|
? pair(sel('hs-stuc-color', 'Kleur', stucColors, st.facade.stucColor, { hint: `Pleisterlaag ${WallModel.STUC.THICK_MM} mm op cement-gebonden plaat` }), swatchOf(WallModel.stucHex(st.facade.stucColor)))
|
||||||
: rabatUi) },
|
: st.facade.type === 'fels' ? felsUi : rabatUi) },
|
||||||
st.facade.type !== 'rabat' && usesRabat ? { label: 'Rabat (overrides)', html: rabatUi } : null,
|
st.facade.type !== 'rabat' && usesRabat ? { label: 'Rabat (overrides)', html: rabatUi } : null,
|
||||||
|
st.facade.type !== 'fels' && uses('fels') ? { label: 'Felsplaten (overrides)', html: felsUi } : null,
|
||||||
{ label: 'Trasraam', html: toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Strook onderaan de gevel, onder rabat én steen' }) +
|
{ label: 'Trasraam', html: toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Strook onderaan de gevel, onder rabat én steen' }) +
|
||||||
(st.trasraam.enabled
|
(st.trasraam.enabled
|
||||||
? UI.range('hs-trasraam-h', 'Hoogte', st.trasraam.height, { min: 100, max: 1000, step: 10, hint: 'Vanaf bovenkant vloer' }) +
|
? UI.range('hs-trasraam-h', 'Hoogte', st.trasraam.height, { min: 100, max: 1000, step: 10, hint: 'Vanaf bovenkant vloer' }) +
|
||||||
@@ -1397,6 +1402,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
UI.onSel('hs-rabat-color', v => { st.facade.rabatColor = v; }, rerender);
|
UI.onSel('hs-rabat-color', v => { st.facade.rabatColor = v; }, rerender);
|
||||||
UI.onSel('hs-facade-stone', v => { st.facade.stone = v; }, rerender);
|
UI.onSel('hs-facade-stone', v => { st.facade.stone = v; }, rerender);
|
||||||
UI.onSel('hs-stuc-color', v => { st.facade.stucColor = v; }, rerender);
|
UI.onSel('hs-stuc-color', v => { st.facade.stucColor = v; }, rerender);
|
||||||
|
UI.onSel('hs-fels-color', v => { st.facade.felsColor = v; }, rerender);
|
||||||
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
|
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
|
||||||
UI.onRange('hs-trasraam-h', v => { if (v > 0) st.trasraam.height = v; }, after);
|
UI.onRange('hs-trasraam-h', v => { if (v > 0) st.trasraam.height = v; }, after);
|
||||||
UI.onSel('hs-trasraam-finish', v => { st.trasraam.finish = v; }, rerender);
|
UI.onSel('hs-trasraam-finish', v => { st.trasraam.finish = v; }, rerender);
|
||||||
@@ -1437,7 +1443,9 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
{ label: 'Daktrim', html: num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }) },
|
{ label: 'Daktrim', html: num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }) },
|
||||||
{ label: 'Palen overkapping', html:
|
{ label: 'Palen overkapping', html:
|
||||||
toggle('hs-posts', 'Palen', r.posts.enabled, { hint: `Onder een dak dat minstens de min. uitkraging buiten de vloer steekt (veranda); per unit: Overkapping. Nu ${parts ? parts.posts.count : 0} st.` }) +
|
toggle('hs-posts', 'Palen', r.posts.enabled, { hint: `Onder een dak dat minstens de min. uitkraging buiten de vloer steekt (veranda); per unit: Overkapping. Nu ${parts ? parts.posts.count : 0} st.` }) +
|
||||||
(r.posts.enabled ? sel('hs-posts-wood', 'Hout', Object.fromEntries(Object.entries(PostModel.WOODS).map(([k, w]) => [k, w.label])), r.posts.wood) +
|
(r.posts.enabled ? toggle('hs-posts-side', 'Zijwand', r.posts.sideWall, { hint: `Vrije hoek dicht met een gestuukte wand (${PostModel.SIDE_MM} mm) terug tot de gevel, in plaats van een hoekpaal` }) +
|
||||||
|
(r.posts.sideWall ? pair(sel('hs-posts-side-color', 'Kleur zijwand', stucColors, r.posts.sideColor), swatchOf(WallModel.stucHex(r.posts.sideColor))) : '') +
|
||||||
|
sel('hs-posts-wood', 'Hout', Object.fromEntries(Object.entries(PostModel.WOODS).map(([k, w]) => [k, w.label])), r.posts.wood) +
|
||||||
UI.range('hs-posts-size', 'Maat', r.posts.size, { min: 100, max: 300, step: 10, hint: 'Vierkant, mm' }) +
|
UI.range('hs-posts-size', 'Maat', r.posts.size, { min: 100, max: 300, step: 10, hint: 'Vierkant, mm' }) +
|
||||||
num('hs-posts-span', 'Max. h.o.h.', r.posts.maxSpan, { step: 100, hint: 'Grootste afstand tussen palen (of paal en gevel)' }) +
|
num('hs-posts-span', 'Max. h.o.h.', r.posts.maxSpan, { step: 100, hint: 'Grootste afstand tussen palen (of paal en gevel)' }) +
|
||||||
num('hs-posts-min', 'Min. uitkraging', r.posts.minExt, { step: 100, hint: 'Kleinere overstekken krijgen geen palen' }) : '') },
|
num('hs-posts-min', 'Min. uitkraging', r.posts.minExt, { step: 100, hint: 'Kleinere overstekken krijgen geen palen' }) : '') },
|
||||||
@@ -1455,6 +1463,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
UI.onSel('hs-fascia-color', v => { r.fasciaColor = v; }, rerender);
|
UI.onSel('hs-fascia-color', v => { r.fasciaColor = v; }, rerender);
|
||||||
UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos);
|
UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos);
|
||||||
UI.onToggle('hs-posts', v => { r.posts.enabled = v; }, rerender);
|
UI.onToggle('hs-posts', v => { r.posts.enabled = v; }, rerender);
|
||||||
|
UI.onToggle('hs-posts-side', v => { r.posts.sideWall = v; }, rerender);
|
||||||
|
UI.onSel('hs-posts-side-color', v => { r.posts.sideColor = v; }, rerender);
|
||||||
UI.onSel('hs-posts-wood', v => { r.posts.wood = v; }, rerender);
|
UI.onSel('hs-posts-wood', v => { r.posts.wood = v; }, rerender);
|
||||||
UI.onRange('hs-posts-size', v => { if (v > 0) r.posts.size = v; }, after);
|
UI.onRange('hs-posts-size', v => { if (v > 0) r.posts.size = v; }, after);
|
||||||
UI.onNum('hs-posts-span', v => { if (v >= 1000) r.posts.maxSpan = v; }, after, pos);
|
UI.onNum('hs-posts-span', v => { if (v >= 1000) r.posts.maxSpan = v; }, after, pos);
|
||||||
@@ -2142,7 +2152,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
{ label: 'Gevel (hele zijde)', html:
|
{ label: 'Gevel (hele zijde)', html:
|
||||||
sel('hw-facade', 'Type', Object.assign({ woning: woningLabel }, HouseModel.FACADE_TYPES), ov ? ov.type : 'woning') +
|
sel('hw-facade', 'Type', Object.assign({ woning: woningLabel }, HouseModel.FACADE_TYPES), ov ? ov.type : 'woning') +
|
||||||
(ov && ov.type === 'steen' ? pair(sel('hw-facade-stone', 'Steen', stoneOptions, fc.stone), thumb(WallModel.stoneThumbUrl(fc.stone, 64), fc.stone)) : '') +
|
(ov && ov.type === 'steen' ? pair(sel('hw-facade-stone', 'Steen', stoneOptions, fc.stone), thumb(WallModel.stoneThumbUrl(fc.stone, 64), fc.stone)) : '') +
|
||||||
(ov && ov.type === 'stuc' ? pair(sel('hw-facade-stuc', 'Kleur', stucColors, fc.stuc), swatchOf(WallModel.stucHex(fc.stuc))) : '') },
|
(ov && ov.type === 'stuc' ? pair(sel('hw-facade-stuc', 'Kleur', stucColors, fc.stuc), swatchOf(WallModel.stucHex(fc.stuc))) : '') +
|
||||||
|
(ov && ov.type === 'fels' ? pair(sel('hw-facade-fels', 'Kleur', felsColors, fc.fels), swatchOf(WallModel.felsHex(fc.fels))) : '') },
|
||||||
{ label: 'Wandstukken', html:
|
{ label: 'Wandstukken', html:
|
||||||
num('hw-split-at', 'Splitsen op', Math.round(e0 + (piece.u0 + piece.u1) / 2), { step: 10, hint: 'Afstand vanaf de linker vloerhoek, van buitenaf gezien. Elk stuk wordt een eigen wand-element (eigen regelwerk) en kan een eigen gevel krijgen.' }) +
|
num('hw-split-at', 'Splitsen op', Math.round(e0 + (piece.u0 + piece.u1) / 2), { step: 10, hint: 'Afstand vanaf de linker vloerhoek, van buitenaf gezien. Elk stuk wordt een eigen wand-element (eigen regelwerk) en kan een eigen gevel krijgen.' }) +
|
||||||
small('Splitsen', 'scissors', 'data-hw="split"') +
|
small('Splitsen', 'scissors', 'data-hw="split"') +
|
||||||
@@ -2150,6 +2161,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
(own ? sel('hw-piece-facade', 'Gevel dit stuk', Object.assign({ zijde: 'Zoals de zijde' }, HouseModel.FACADE_TYPES), own.facade ? own.facade.type : 'zijde', { hint: 'Gevel van dit wandstuk (rechts van de splitsing)' }) +
|
(own ? sel('hw-piece-facade', 'Gevel dit stuk', Object.assign({ zijde: 'Zoals de zijde' }, HouseModel.FACADE_TYPES), own.facade ? own.facade.type : 'zijde', { hint: 'Gevel van dit wandstuk (rechts van de splitsing)' }) +
|
||||||
(own.facade && own.facade.type === 'steen' ? sel('hw-piece-stone', 'Steen', stoneOptions, own.facade.stone || fc.stone) : '') +
|
(own.facade && own.facade.type === 'steen' ? sel('hw-piece-stone', 'Steen', stoneOptions, own.facade.stone || fc.stone) : '') +
|
||||||
(own.facade && own.facade.type === 'stuc' ? sel('hw-piece-stuc', 'Kleur', stucColors, own.facade.stuc || fc.stuc) : '') +
|
(own.facade && own.facade.type === 'stuc' ? sel('hw-piece-stuc', 'Kleur', stucColors, own.facade.stuc || fc.stuc) : '') +
|
||||||
|
(own.facade && own.facade.type === 'fels' ? sel('hw-piece-fels', 'Kleur', felsColors, own.facade.fels || fc.fels) : '') +
|
||||||
small('Splitsing weg', 'x-lg', 'data-hw="unsplit"') : '') },
|
small('Splitsing weg', 'x-lg', 'data-hw="unsplit"') : '') },
|
||||||
chimneyGroup(pu, wl, piece, e0),
|
chimneyGroup(pu, wl, piece, e0),
|
||||||
{ label: 'Gevellampen', html: lt.mode === 'gevel'
|
{ label: 'Gevellampen', html: lt.mode === 'gevel'
|
||||||
@@ -2168,16 +2180,18 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
wireChimneys(body, pu, wl);
|
wireChimneys(body, pu, wl);
|
||||||
UI.onSel('hw-facade', v => {
|
UI.onSel('hw-facade', v => {
|
||||||
if (v === 'woning') delete su.facade.walls[wl.side];
|
if (v === 'woning') delete su.facade.walls[wl.side];
|
||||||
else su.facade.walls[wl.side] = v === 'steen' ? { type: v, stone: fc.stone } : v === 'stuc' ? { type: v, stuc: fc.stuc } : { type: v };
|
else su.facade.walls[wl.side] = v === 'steen' ? { type: v, stone: fc.stone } : v === 'stuc' ? { type: v, stuc: fc.stuc } : v === 'fels' ? { type: v, fels: fc.fels } : { type: v };
|
||||||
}, rerender);
|
}, rerender);
|
||||||
UI.onSel('hw-facade-stone', v => { su.facade.walls[wl.side].stone = v; }, rerender);
|
UI.onSel('hw-facade-stone', v => { su.facade.walls[wl.side].stone = v; }, rerender);
|
||||||
UI.onSel('hw-facade-stuc', v => { su.facade.walls[wl.side].stuc = v; }, rerender);
|
UI.onSel('hw-facade-stuc', v => { su.facade.walls[wl.side].stuc = v; }, rerender);
|
||||||
|
UI.onSel('hw-facade-fels', v => { su.facade.walls[wl.side].fels = v; }, rerender);
|
||||||
UI.onSel('hw-piece-facade', v => {
|
UI.onSel('hw-piece-facade', v => {
|
||||||
if (!own) return;
|
if (!own) return;
|
||||||
own.facade = v === 'zijde' ? null : v === 'steen' ? { type: v, stone: fc.stone } : v === 'stuc' ? { type: v, stuc: fc.stuc } : { type: v };
|
own.facade = v === 'zijde' ? null : v === 'steen' ? { type: v, stone: fc.stone } : v === 'stuc' ? { type: v, stuc: fc.stuc } : v === 'fels' ? { type: v, fels: fc.fels } : { type: v };
|
||||||
}, rerender);
|
}, rerender);
|
||||||
UI.onSel('hw-piece-stone', v => { if (own && own.facade) own.facade.stone = v; }, rerender);
|
UI.onSel('hw-piece-stone', v => { if (own && own.facade) own.facade.stone = v; }, rerender);
|
||||||
UI.onSel('hw-piece-stuc', v => { if (own && own.facade) own.facade.stuc = v; }, rerender);
|
UI.onSel('hw-piece-stuc', v => { if (own && own.facade) own.facade.stuc = v; }, rerender);
|
||||||
|
UI.onSel('hw-piece-fels', v => { if (own && own.facade) own.facade.fels = v; }, rerender);
|
||||||
body.querySelectorAll('[data-hw]').forEach(b => b.addEventListener('click', () => {
|
body.querySelectorAll('[data-hw]').forEach(b => b.addEventListener('click', () => {
|
||||||
if (b.dataset.hw === 'split') {
|
if (b.dataset.hw === 'split') {
|
||||||
const v = Number(document.getElementById('hw-split-at').value);
|
const v = Number(document.getElementById('hw-split-at').value);
|
||||||
@@ -2295,12 +2309,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));
|
||||||
@@ -2366,9 +2374,10 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
if (totals.posts) {
|
if (totals.posts) {
|
||||||
const pp = totals.posts;
|
const pp = totals.posts;
|
||||||
cards.push(UI.specCard('Overkapping', 'columns', [
|
cards.push(UI.specCard('Overkapping', 'columns', (pp.count ? [
|
||||||
[`${pp.wood} palen ${pp.size}×${pp.size} mm`, pp.count, 'st.'],
|
[`${pp.wood} palen ${pp.size}×${pp.size} mm`, pp.count, 'st.'],
|
||||||
['Lengte totaal', f(pp.lengthM, 2), 'm'], ['Hout', f(pp.m3, 3), 'm³'], ['Gewicht', f(pp.kg), 'kg'], ['Paalvoeten', pp.count, 'st.']]));
|
['Lengte totaal', f(pp.lengthM, 2), 'm'], ['Hout', f(pp.m3, 3), 'm³'], ['Gewicht', f(pp.kg), 'kg'], ['Paalvoeten', pp.count, 'st.']] : [])
|
||||||
|
.concat(pp.walls ? [[`Zijwand (${pp.walls} st.)`, f(pp.wallM2, 2), 'm²']] : [])));
|
||||||
}
|
}
|
||||||
// TIJDELIJK (verwijderen voor productie): zaagmachine-overzicht.
|
// TIJDELIJK (verwijderen voor productie): zaagmachine-overzicht.
|
||||||
const sw = totals.saw, cs = cost.saw;
|
const sw = totals.saw, cs = cost.saw;
|
||||||
@@ -2442,28 +2451,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 +2782,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 +2868,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 +2878,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 +2933,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 +2955,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),
|
||||||
|
|||||||
+85
-7
@@ -7,6 +7,9 @@
|
|||||||
// Palen onder een overkapping: steekt een dak minstens minExt buiten de vloeren uit (veranda), dan komen er
|
// Palen onder een overkapping: steekt een dak minstens minExt buiten de vloeren uit (veranda), dan komen er
|
||||||
// palen onder de dakrand. Hoekpalen staan met hun buitenkant gelijk met de dakhoek; een eind dat op een gevel
|
// palen onder de dakrand. Hoekpalen staan met hun buitenkant gelijk met de dakhoek; een eind dat op een gevel
|
||||||
// uitkomt rust op die wand. Daartussen zo weinig palen dat de h.o.h.-afstand onder maxSpan blijft.
|
// uitkomt rust op die wand. Daartussen zo weinig palen dat de h.o.h.-afstand onder maxSpan blijft.
|
||||||
|
// sideWall: een vrije hoek krijgt geen paal maar een dichte zijwand (gestuukt) van de dakhoek terug tot de gevel,
|
||||||
|
// met de buitenkant gelijk met de zijkant van het dak. Ligt die buitenkant in het vlak van een gevel, dan loopt de
|
||||||
|
// bekleding van die gevel erover door (outsideOf, bijv. felsplaten) en staat de wand gelijk met de bekleding.
|
||||||
|
|
||||||
window.PostModel = (() => {
|
window.PostModel = (() => {
|
||||||
const EPS = 0.5;
|
const EPS = 0.5;
|
||||||
@@ -15,12 +18,16 @@ window.PostModel = (() => {
|
|||||||
eiken: { label: 'Eiken', hex: 0xa07c54, kgM3: 750 },
|
eiken: { label: 'Eiken', hex: 0xa07c54, kgM3: 750 },
|
||||||
douglas: { label: 'Douglas', hex: 0xb98a5e, kgM3: 530 }
|
douglas: { label: 'Douglas', hex: 0xb98a5e, kgM3: 530 }
|
||||||
};
|
};
|
||||||
const DEFAULT_SETTINGS = { enabled: true, size: 200, wood: 'eiken', maxSpan: 3600, minExt: 1000 };
|
const DEFAULT_SETTINGS = { enabled: true, size: 200, wood: 'eiken', maxSpan: 3600, minExt: 1000, sideWall: false, sideColor: 'wit' };
|
||||||
|
const SIDE_MM = 200; // dikte zijwand (regelwerk, plaat en stuc aan beide kanten)
|
||||||
|
const SIDE_TOP_BELOW_MM = 40; // doorlopende gevelbekleding: tot zo ver onder de bovenkant van het dak
|
||||||
|
|
||||||
function normalizeSettings(settings) {
|
function normalizeSettings(settings) {
|
||||||
const s = Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
const s = Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
||||||
s.enabled = s.enabled !== false;
|
s.enabled = s.enabled !== false;
|
||||||
|
s.sideWall = !!s.sideWall;
|
||||||
if (!WOODS[s.wood]) s.wood = DEFAULT_SETTINGS.wood;
|
if (!WOODS[s.wood]) s.wood = DEFAULT_SETTINGS.wood;
|
||||||
|
if (!WallModel.isStucColor(s.sideColor)) s.sideColor = DEFAULT_SETTINGS.sideColor;
|
||||||
const num = (v, def, min, max) => (isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : def);
|
const num = (v, def, min, max) => (isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : def);
|
||||||
s.size = num(s.size, DEFAULT_SETTINGS.size, 100, 400);
|
s.size = num(s.size, DEFAULT_SETTINGS.size, 100, 400);
|
||||||
s.maxSpan = num(s.maxSpan, DEFAULT_SETTINGS.maxSpan, 1000, 8000);
|
s.maxSpan = num(s.maxSpan, DEFAULT_SETTINGS.maxSpan, 1000, 8000);
|
||||||
@@ -29,9 +36,10 @@ window.PostModel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// units: [{ rect (vloer), roof: { rect, ceilingAt } }] in wereld-mm; groundY = maaiveld (absoluut).
|
// units: [{ rect (vloer), roof: { rect, ceilingAt } }] in wereld-mm; groundY = maaiveld (absoluut).
|
||||||
function compute(settings, units, groundY) {
|
// outsideOf(wall, out): { finish, kind, depth } van de gevel in het vlak van de buitenkant, of null.
|
||||||
|
function compute(settings, units, groundY, outsideOf) {
|
||||||
const s = normalizeSettings(settings);
|
const s = normalizeSettings(settings);
|
||||||
const out = { posts: [], count: 0, size: s.size, wood: s.wood, lengthMM: 0, m3: 0, kg: 0 };
|
const out = { posts: [], count: 0, size: s.size, wood: s.wood, lengthMM: 0, m3: 0, kg: 0, walls: [], wallM2: 0, sideColor: s.sideColor };
|
||||||
if (!s.enabled || !units.length) return out;
|
if (!s.enabled || !units.length) return out;
|
||||||
const floors = units.map(u => u.rect);
|
const floors = units.map(u => u.rect);
|
||||||
const onFloor = (x, z, m) => floors.some(f => x > f.x0 - m && x < f.x1 + m && z > f.z0 - m && z < f.z1 + m);
|
const onFloor = (x, z, m) => floors.some(f => x > f.x0 - m && x < f.x1 + m && z > f.z0 - m && z < f.z1 + m);
|
||||||
@@ -79,11 +87,56 @@ window.PostModel = (() => {
|
|||||||
|
|
||||||
const half = s.size / 2;
|
const half = s.size / 2;
|
||||||
const pts = [];
|
const pts = [];
|
||||||
|
const roofAt = (x, z) => units.map(u => u.roof).find(rf => x > rf.rect.x0 - EPS && x < rf.rect.x1 + EPS && z > rf.rect.z0 - EPS && z < rf.rect.z1 + EPS);
|
||||||
|
const endFreeOf = (r, t) => { const p = r.axis === 'z' ? { x: r.at, z: t } : { x: t, z: r.at }; return !onFloor(p.x, p.z, s.size); };
|
||||||
|
// Zijwand op het vrije eind t van run r (dir +1: wand van t de run in, -1: andersom), terug tot de gevel waar de
|
||||||
|
// run naar kijkt. Delen twee runs een vrije hoek (veranda in een binnenhoek), dan is de run met de kortste weg
|
||||||
|
// naar de gevel de voorkant en komt de wand langs de zijkant; het andere eind rust op die wand.
|
||||||
|
const sideCands = [];
|
||||||
|
if (s.sideWall) merged.forEach(r => [[r.a, 1], [r.b, -1]].forEach(([t, dir]) => {
|
||||||
|
if (!endFreeOf(r, t)) return;
|
||||||
|
const lo = Math.min(t, t + dir * SIDE_MM), hi = Math.max(t, t + dir * SIDE_MM);
|
||||||
|
let reach = Infinity;
|
||||||
|
floors.forEach(f => {
|
||||||
|
const [s0, s1, c0, c1] = r.axis === 'z' ? [f.z0, f.z1, f.x0, f.x1] : [f.x0, f.x1, f.z0, f.z1];
|
||||||
|
if (s0 > hi - EPS || s1 < lo + EPS) return;
|
||||||
|
const d = r.n > 0 ? r.at - c1 : c0 - r.at;
|
||||||
|
if (d > -EPS && d < reach) reach = d;
|
||||||
|
});
|
||||||
|
if (!isFinite(reach) || reach < s.size) return;
|
||||||
|
const a = Math.min(r.at, r.at - r.n * reach), b = Math.max(r.at, r.at - r.n * reach);
|
||||||
|
const corner = r.axis === 'z' ? { x: r.at, z: t } : { x: t, z: r.at };
|
||||||
|
const outward = r.axis === 'z' ? { x: 0, z: -dir } : { x: -dir, z: 0 };
|
||||||
|
sideCands.push({ r, t, corner, reach, out: outward, w: r.axis === 'z' ? { x0: a, x1: b, z0: lo, z1: hi } : { x0: lo, x1: hi, z0: a, z1: b } });
|
||||||
|
}));
|
||||||
|
const sameCorner = (p, q) => Math.abs(p.x - q.x) < EPS && Math.abs(p.z - q.z) < EPS;
|
||||||
|
const sideAt = (r, t) => { const c = sideCands.find(d => d.r === r && d.t === t); return !!c && sideCands.some(d => d.built && sameCorner(d.corner, c.corner)); };
|
||||||
|
sideCands.forEach(c => {
|
||||||
|
if (sideCands.some(d => d !== c && sameCorner(d.corner, c.corner) && (d.reach < c.reach - EPS || (Math.abs(d.reach - c.reach) < EPS && sideCands.indexOf(d) < sideCands.indexOf(c))))) return;
|
||||||
|
const w = c.w;
|
||||||
|
const tops = [[w.x0 + 1, w.z0 + 1], [w.x1 - 1, w.z0 + 1], [w.x0 + 1, w.z1 - 1], [w.x1 - 1, w.z1 - 1]]
|
||||||
|
.map(([x, z]) => { const rf = roofAt(x, z); return rf ? rf.ceilingAt(x, z) : null; }).filter(v => v !== null);
|
||||||
|
if (!tops.length || !(Math.max(...tops) > groundY)) return;
|
||||||
|
const wall = Object.assign({}, w, { y0: groundY, y1: Math.round(Math.max(...tops)), color: s.sideColor, out: c.out, outside: null });
|
||||||
|
const o = outsideOf ? outsideOf(wall, c.out) : null;
|
||||||
|
if (o) {
|
||||||
|
if (c.out.x > 0) wall.x1 += o.depth; else if (c.out.x < 0) wall.x0 -= o.depth;
|
||||||
|
if (c.out.z > 0) wall.z1 += o.depth; else if (c.out.z < 0) wall.z0 -= o.depth;
|
||||||
|
Object.assign(wall, { outside: o.finish, outsideKind: o.kind });
|
||||||
|
// Zoals de gevel: de bekleding loopt tot onder de dakrand door (over de kant van het dakpakket).
|
||||||
|
const roofTops = [[w.x0 + 1, w.z0 + 1], [w.x1 - 1, w.z0 + 1], [w.x0 + 1, w.z1 - 1], [w.x1 - 1, w.z1 - 1]]
|
||||||
|
.map(([x, z]) => { const rf = roofAt(x, z); return rf && rf.topAt ? rf.topAt(x, z) : null; }).filter(v => v !== null);
|
||||||
|
if (roofTops.length) wall.y1 = Math.max(wall.y1, Math.round(Math.min(...roofTops) - SIDE_TOP_BELOW_MM));
|
||||||
|
}
|
||||||
|
out.walls.push(wall);
|
||||||
|
c.built = true;
|
||||||
|
});
|
||||||
merged.forEach(r => {
|
merged.forEach(r => {
|
||||||
const pos = (t) => (r.axis === 'z' ? { x: r.at - r.n * half, z: t } : { x: t, z: r.at - r.n * half });
|
const pos = (t) => (r.axis === 'z' ? { x: r.at - r.n * half, z: t } : { x: t, z: r.at - r.n * half });
|
||||||
// Een eind dat op of tegen een vloer uitkomt rust op die gevel.
|
// Een eind dat op of tegen een vloer uitkomt rust op die gevel.
|
||||||
const endFree = (t) => { const p = r.axis === 'z' ? { x: r.at, z: t } : { x: t, z: r.at }; return !onFloor(p.x, p.z, s.size); };
|
const endFree = (t) => { const p = r.axis === 'z' ? { x: r.at, z: t } : { x: t, z: r.at }; return !onFloor(p.x, p.z, s.size); };
|
||||||
const fa = endFree(r.a), fb = endFree(r.b);
|
// Een dichte zijwand neemt de plaats van de hoekpaal in; het eind rust dan op die wand.
|
||||||
|
const fa = endFree(r.a) && !sideAt(r, r.a), fb = endFree(r.b) && !sideAt(r, r.b);
|
||||||
const pa = fa ? r.a + half : r.a, pb = fb ? r.b - half : r.b;
|
const pa = fa ? r.a + half : r.a, pb = fb ? r.b - half : r.b;
|
||||||
if (pb - pa < EPS) return;
|
if (pb - pa < EPS) return;
|
||||||
const n = Math.max(1, Math.ceil((pb - pa) / s.maxSpan - 1e-9));
|
const n = Math.max(1, Math.ceil((pb - pa) / s.maxSpan - 1e-9));
|
||||||
@@ -94,7 +147,8 @@ window.PostModel = (() => {
|
|||||||
});
|
});
|
||||||
pts.forEach(p => {
|
pts.forEach(p => {
|
||||||
if (out.posts.some(q => Math.abs(q.x - p.x) < s.size && Math.abs(q.z - p.z) < s.size)) return;
|
if (out.posts.some(q => Math.abs(q.x - p.x) < s.size && Math.abs(q.z - p.z) < s.size)) return;
|
||||||
const roof = units.map(u => u.roof).find(rf => p.x > rf.rect.x0 - EPS && p.x < rf.rect.x1 + EPS && p.z > rf.rect.z0 - EPS && p.z < rf.rect.z1 + EPS);
|
if (out.walls.some(w => p.x > w.x0 - half && p.x < w.x1 + half && p.z > w.z0 - half && p.z < w.z1 + half)) return;
|
||||||
|
const roof = roofAt(p.x, p.z);
|
||||||
if (!roof) return;
|
if (!roof) return;
|
||||||
// Hoogste hoek: bij een hellend dak verdwijnt de paal aan de lage kant in het dakpakket, zonder kier.
|
// Hoogste hoek: bij een hellend dak verdwijnt de paal aan de lage kant in het dakpakket, zonder kier.
|
||||||
const top = Math.max(...[[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => roof.ceilingAt(p.x + a * (half - 1), p.z + b * (half - 1))));
|
const top = Math.max(...[[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => roof.ceilingAt(p.x + a * (half - 1), p.z + b * (half - 1))));
|
||||||
@@ -105,6 +159,10 @@ window.PostModel = (() => {
|
|||||||
out.lengthMM = out.posts.reduce((t, p) => t + p.y1 - p.y0 - SHOE_MM, 0);
|
out.lengthMM = out.posts.reduce((t, p) => t + p.y1 - p.y0 - SHOE_MM, 0);
|
||||||
out.m3 = out.lengthMM * s.size * s.size / 1e9;
|
out.m3 = out.lengthMM * s.size * s.size / 1e9;
|
||||||
out.kg = out.m3 * WOODS[s.wood].kgM3;
|
out.kg = out.m3 * WOODS[s.wood].kgM3;
|
||||||
|
const wallArea = (w) => Math.max(w.x1 - w.x0, w.z1 - w.z0) * (w.y1 - w.y0) / 1e6;
|
||||||
|
out.wallM2 = out.walls.reduce((t, w) => t + wallArea(w), 0);
|
||||||
|
out.outsideM2 = {};
|
||||||
|
out.walls.filter(w => w.outsideKind).forEach(w => { out.outsideM2[w.outsideKind] = (out.outsideM2[w.outsideKind] || 0) + wallArea(w); });
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +175,7 @@ window.PostModel = (() => {
|
|||||||
|
|
||||||
function build3D(data, materials) {
|
function build3D(data, materials) {
|
||||||
const toM = 0.001, group = new THREE.Group();
|
const toM = 0.001, group = new THREE.Group();
|
||||||
if (!data || !data.posts.length) return group;
|
if (!data || !(data.posts.length || (data.walls || []).length)) return group;
|
||||||
const m = materials || createMaterials();
|
const m = materials || createMaterials();
|
||||||
const woodMat = m.wood[data.wood] || m.wood.eiken;
|
const woodMat = m.wood[data.wood] || m.wood.eiken;
|
||||||
data.posts.forEach(p => {
|
data.posts.forEach(p => {
|
||||||
@@ -132,8 +190,28 @@ window.PostModel = (() => {
|
|||||||
stem.position.set(p.x * toM, (p.y0 + 10 + (SHOE_MM - 10) / 2) * toM, p.z * toM);
|
stem.position.set(p.x * toM, (p.y0 + 10 + (SHOE_MM - 10) / 2) * toM, p.z * toM);
|
||||||
group.add(stem);
|
group.add(stem);
|
||||||
});
|
});
|
||||||
|
(data.walls || []).forEach(w => {
|
||||||
|
const c = [(w.x0 + w.x1) / 2 * toM, (w.y0 + w.y1) / 2 * toM, (w.z0 + w.z1) / 2 * toM];
|
||||||
|
const geo = new THREE.BoxGeometry((w.x1 - w.x0) * toM, (w.y1 - w.y0) * toM, (w.z1 - w.z0) * toM);
|
||||||
|
// UV in wereldmeters, zoals de gevels: texturen op dezelfde schaal.
|
||||||
|
const pos = geo.attributes.position, nrm = geo.attributes.normal, uv = geo.attributes.uv;
|
||||||
|
for (let k = 0; k < pos.count; k++) {
|
||||||
|
const x = pos.getX(k) + c[0], y = pos.getY(k) + c[1], z = pos.getZ(k) + c[2];
|
||||||
|
if (Math.abs(nrm.getX(k)) > 0.5) uv.setXY(k, z, y); else if (Math.abs(nrm.getY(k)) > 0.5) uv.setXY(k, x, z); else uv.setXY(k, x, y);
|
||||||
|
}
|
||||||
|
uv.needsUpdate = true;
|
||||||
|
// Volgorde van de vlakken in BoxGeometry: +x, -x, +y, -y, +z, -z.
|
||||||
|
const stuc = WallModel.stucMaterial(w.color), mats = [stuc, stuc, stuc, stuc, stuc, stuc];
|
||||||
|
const outMat = w.outside && w.out ? WallModel.claddingMaterial(w.outside) : null;
|
||||||
|
if (outMat) mats[w.out.x > 0 ? 0 : w.out.x < 0 ? 1 : w.out.z > 0 ? 4 : 5] = outMat;
|
||||||
|
const box = new THREE.Mesh(geo, mats);
|
||||||
|
box.position.set(...c);
|
||||||
|
box.castShadow = true;
|
||||||
|
box.receiveShadow = true;
|
||||||
|
group.add(box);
|
||||||
|
});
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { WOODS, SHOE_MM, DEFAULT_SETTINGS, normalizeSettings, compute, createMaterials, build3D };
|
return { WOODS, SHOE_MM, SIDE_MM, DEFAULT_SETTINGS, normalizeSettings, compute, createMaterials, build3D };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+49
-9
@@ -12,6 +12,9 @@ window.RoofModel = (() => {
|
|||||||
const DENSITY_DAKLEER_KGM2 = 1.4;
|
const DENSITY_DAKLEER_KGM2 = 1.4;
|
||||||
const TRIM_THICK_MM = 25;
|
const TRIM_THICK_MM = 25;
|
||||||
const EDGE_ABOVE_MM = 80;
|
const EDGE_ABOVE_MM = 80;
|
||||||
|
// Felsplaat op dak én gevel: geen boeideel; de dakplaat zet om over de rand (net vóór de gevelbekleding), zonder
|
||||||
|
// opstand (de waterkant van een lessenaar), tot over de bovenkant van de gevelplaten.
|
||||||
|
const FELS_EDGE = { lipMM: 2, aboveMM: 2, heightMM: 120 };
|
||||||
const SPOT_RATIO = 0.4;
|
const SPOT_RATIO = 0.4;
|
||||||
const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
|
const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
|
||||||
|
|
||||||
@@ -35,7 +38,9 @@ window.RoofModel = (() => {
|
|||||||
// Riet e.d. alleen bij een hellend dak; plat valt terug op dakleer.
|
// Riet e.d. alleen bij een hellend dak; plat valt terug op dakleer.
|
||||||
// Riet: één keuze per textuur assets/textures/dak/riet_<nr>.jpg (nummer hier toevoegen).
|
// Riet: één keuze per textuur assets/textures/dak/riet_<nr>.jpg (nummer hier toevoegen).
|
||||||
const RIET = { thickMM: 180, kgM2: 3.8, tileMM: 1000, nrs: ['001', '002', '003'], colors: { '001': '#947d69', '002': '#7c4c3f', '003': '#735e43' } };
|
const RIET = { thickMM: 180, kgM2: 3.8, tileMM: 1000, nrs: ['001', '002', '003'], colors: { '001': '#947d69', '002': '#7c4c3f', '003': '#735e43' } };
|
||||||
const FELS = { thickMM: 25, kgM2: 6.5, seamMM: 500, colors: { zink: ['Felsplaat zink', '#8e9497'], antraciet: ['Felsplaat antraciet', '#3d4146'], zwart: ['Felsplaat zwart', '#1f2022'] } };
|
// Kleuren en naadafstand gedeeld met felsplaten op de gevel (WallModel.FELS).
|
||||||
|
const FELS = { thickMM: 25, kgM2: 6.5, seamMM: WallModel.FELS.SEAM_MM,
|
||||||
|
colors: Object.fromEntries(Object.entries(WallModel.FELS.COLORS).map(([k, c]) => [k, ['Felsplaat ' + c.label.toLowerCase(), c.hex]])) };
|
||||||
const COVERINGS = Object.assign({
|
const COVERINGS = Object.assign({
|
||||||
dakleer: { label: 'Dakleer (EPDM)', kind: 'dakleer', color: '#2a2a2a', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
|
dakleer: { label: 'Dakleer (EPDM)', kind: 'dakleer', color: '#2a2a2a', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
|
||||||
}, ...RIET.nrs.map(nr => ({
|
}, ...RIET.nrs.map(nr => ({
|
||||||
@@ -923,7 +928,8 @@ window.RoofModel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// facadeDepth negatief = gevel van een hoger deel die naar dit dak kijkt: de rand stopt tegen die gevel.
|
// facadeDepth negatief = gevel van een hoger deel die naar dit dak kijkt: de rand stopt tegen die gevel.
|
||||||
function computeEdge(settings, roofs, facadeDepth) {
|
// facadeFels(a, b, normal): de gevel onder deze rand heeft felsplaten (met een felsdak: geen boeideel).
|
||||||
|
function computeEdge(settings, roofs, facadeDepth, facadeFels) {
|
||||||
const s = normalizeSettings(settings);
|
const s = normalizeSettings(settings);
|
||||||
const rects = roofs.map(r => r.rect);
|
const rects = roofs.map(r => r.rect);
|
||||||
if (!roofs.length) return null;
|
if (!roofs.length) return null;
|
||||||
@@ -967,15 +973,18 @@ window.RoofModel = (() => {
|
|||||||
const j = (i + 1) % n, b = base[j];
|
const j = (i + 1) % n, b = base[j];
|
||||||
const mid = { x: (a.x + b.x) / 2, z: (a.z + b.z) / 2 };
|
const mid = { x: (a.x + b.x) / 2, z: (a.z + b.z) / 2 };
|
||||||
const owner = members.find(r => onRectEdge(r.rect, mid)) || members[0];
|
const owner = members.find(r => onRectEdge(r.rect, mid)) || members[0];
|
||||||
const fascia = owner.fascia !== false;
|
const normal = rectilinearNormal(a, b);
|
||||||
|
const fels = coverKind(owner.covering) === 'fels' && !!facadeFels && !!facadeFels(a, b, normal);
|
||||||
|
const fascia = owner.fascia !== false && !fels;
|
||||||
const off = fascia ? offBoei : offTrim;
|
const off = fascia ? offBoei : offTrim;
|
||||||
// Tegen een hoger dak vervalt de ribbe (daar staat de gevel), boven een lager dak blijft hij zonder spots.
|
// Tegen een hoger dak vervalt de ribbe (daar staat de gevel), boven een lager dak blijft hij zonder spots.
|
||||||
const nb = others.find(r => onRectEdge(r.rect, mid));
|
const nb = others.find(r => onRectEdge(r.rect, mid));
|
||||||
const dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
|
const dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
|
||||||
|
const above = fels ? FELS_EDGE.aboveMM : EDGE_ABOVE_MM;
|
||||||
return {
|
return {
|
||||||
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
|
a, b, owner: owner.unitId, fascia, fels, covering: owner.covering, normal,
|
||||||
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
|
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
|
||||||
topAt: (p) => owner.topAt(p.x, p.z) + EDGE_ABOVE_MM, ridges: owner.ridges || [],
|
topAt: (p) => owner.topAt(p.x, p.z) + above, ridges: owner.ridges || [],
|
||||||
overRoof: !!nb && !dropped, dropped
|
overRoof: !!nb && !dropped, dropped
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -995,6 +1004,23 @@ window.RoofModel = (() => {
|
|||||||
});
|
});
|
||||||
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i] }));
|
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i] }));
|
||||||
|
|
||||||
|
// Rand zonder overstek (daktrim, felsrand): de felsrand ligt net vóór de gevelbekleding en loopt bij een hoek
|
||||||
|
// door tot de buitenkant van de buurrand; een boeideel ernaast stopt op de buitenkant van zo'n rand.
|
||||||
|
const edgeOff = (k) => segs[k].fels ? Math.max(TRIM_THICK_MM, depRaw[k] + FELS_EDGE.lipMM) : TRIM_THICK_MM;
|
||||||
|
const mv = (p, nrm, d) => ({ x: p.x + nrm.x * d, z: p.z + nrm.z * d });
|
||||||
|
segs.forEach((sg, i) => {
|
||||||
|
const ends = [['a', 'outerA', (i - 1 + n) % n], ['b', 'outerB', (i + 1) % n]];
|
||||||
|
if (sg.fels) ends.forEach(([k, ok, j]) => {
|
||||||
|
const nb = segs[j], q = mv(sg[k], sg.normal, edgeOff(i));
|
||||||
|
sg[ok] = perp(sg.normal, nb.normal) && !nb.fascia && !nb.dropped ? mv(q, nb.normal, edgeOff(j)) : q;
|
||||||
|
});
|
||||||
|
else if (sg.fascia) ends.forEach(([k, ok, j]) => {
|
||||||
|
const nb = segs[j];
|
||||||
|
if (nb.fascia || nb.dropped || !perp(sg.normal, nb.normal)) return;
|
||||||
|
sg[ok] = mv(mv(sg[k], sg.normal, s.overhang), nb.normal, edgeOff(j));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const insideOther = (p) => others.some(r => p.x > r.rect.x0 + 0.5 && p.x < r.rect.x1 - 0.5 && p.z > r.rect.z0 + 0.5 && p.z < r.rect.z1 - 0.5);
|
const insideOther = (p) => others.some(r => p.x > r.rect.x0 + 0.5 && p.x < r.rect.x1 - 0.5 && p.z > r.rect.z0 + 0.5 && p.z < r.rect.z1 - 0.5);
|
||||||
segs.forEach((sg, i) => {
|
segs.forEach((sg, i) => {
|
||||||
if (sg.dropped) return;
|
if (sg.dropped) return;
|
||||||
@@ -1024,7 +1050,9 @@ window.RoofModel = (() => {
|
|||||||
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaColor: s.fasciaColor,
|
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaColor: s.fasciaColor,
|
||||||
top, edgeAbove: EDGE_ABOVE_MM,
|
top, edgeAbove: EDGE_ABOVE_MM,
|
||||||
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
||||||
trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
// Felsrand: zetwerk van de dakplaten, zit in de prijs van de felsplaten.
|
||||||
|
trimLengthMM: segments.filter(sg => !sg.fascia && !sg.fels).reduce((t, sg) => t + segLen(sg), 0),
|
||||||
|
felsEdgeLengthMM: segments.filter(sg => sg.fels).reduce((t, sg) => t + segLen(sg), 0),
|
||||||
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
|
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
|
||||||
spots: []
|
spots: []
|
||||||
};
|
};
|
||||||
@@ -1056,6 +1084,17 @@ window.RoofModel = (() => {
|
|||||||
return geo;
|
return geo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hoogte van de rand vanaf sg.topAt naar beneden: boeideel, felsrand of daktrim (met opstand boven het dak).
|
||||||
|
function edgeHeight(edge, sg) {
|
||||||
|
return sg.fascia ? edge.fasciaHeight : sg.fels ? FELS_EDGE.heightMM : edge.trimHeight + edge.edgeAbove;
|
||||||
|
}
|
||||||
|
// Kleur van de rand in tekeningen (hex-getal).
|
||||||
|
function edgeColor(edge, sg) {
|
||||||
|
if (sg.fascia) return (FASCIA_COLORS[edge.fasciaColor] || FASCIA_COLORS.antraciet).hex;
|
||||||
|
const c = sg.fels && COVERINGS[sg.covering];
|
||||||
|
return c ? parseInt(c.color.slice(1), 16) : 0xb8bcc0;
|
||||||
|
}
|
||||||
|
|
||||||
function buildEdge3D(edge, materials, opts) {
|
function buildEdge3D(edge, materials, opts) {
|
||||||
const o = opts || {};
|
const o = opts || {};
|
||||||
const toM = 0.001;
|
const toM = 0.001;
|
||||||
@@ -1075,9 +1114,10 @@ window.RoofModel = (() => {
|
|||||||
const t = (p) => alongZ ? p.z : p.x;
|
const t = (p) => alongZ ? p.z : p.x;
|
||||||
polys = polys.flatMap(pl => [clipPoly(pl, p => g0 - t(p)), clipPoly(pl, p => t(p) - g1)]).filter(pl => pl.length >= 3);
|
polys = polys.flatMap(pl => [clipPoly(pl, p => g0 - t(p)), clipPoly(pl, p => t(p) - g1)]).filter(pl => pl.length >= 3);
|
||||||
});
|
});
|
||||||
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
|
const h = edgeHeight(edge, sg);
|
||||||
|
const mat = sg.fascia ? materials.fasciaMaterial : sg.fels ? coveringMaterial(sg.covering) || materials.trimMaterial : materials.trimMaterial;
|
||||||
polys.forEach(pl => {
|
polys.forEach(pl => {
|
||||||
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
|
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), mat);
|
||||||
mesh.visible = o.edgeVisible !== false;
|
mesh.visible = o.edgeVisible !== false;
|
||||||
group.add(mesh);
|
group.add(mesh);
|
||||||
});
|
});
|
||||||
@@ -1112,6 +1152,6 @@ window.RoofModel = (() => {
|
|||||||
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
|
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
|
||||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||||
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, sharedRidgeBeam, joinValue, buildUnit3D, capRegions,
|
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, sharedRidgeBeam, joinValue, buildUnit3D, capRegions,
|
||||||
computeEdge, buildEdge3D, normalizeSettings
|
FELS_EDGE, computeEdge, buildEdge3D, edgeHeight, edgeColor, normalizeSettings
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+59
-5
@@ -72,6 +72,50 @@ window.WallModel = (() => {
|
|||||||
return mat;
|
return mat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Felsplaten (staande naad) op de gevel: zelfde opbouw als verticale rabat (doek, latten, bekleding), alleen het
|
||||||
|
// materiaal verschilt. Kleuren ook voor het dak (RoofModel.FELS). In 3D de naden als textuur over het blok.
|
||||||
|
const FELS = {
|
||||||
|
SEAM_MM: 500, TILE_SEAMS: 6,
|
||||||
|
COLORS: {
|
||||||
|
zink: { label: 'Zink', hex: '#8e9497' },
|
||||||
|
antraciet: { label: 'Antraciet', hex: '#3d4146' },
|
||||||
|
zwart: { label: 'Zwart', hex: '#1f2022' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const isFelsColor = (key) => !!FELS.COLORS[key];
|
||||||
|
const felsHex = (key) => (FELS.COLORS[key] || FELS.COLORS.antraciet).hex;
|
||||||
|
const felsMaterials = {};
|
||||||
|
function felsMaterial(key) {
|
||||||
|
const k = isFelsColor(key) ? key : 'antraciet';
|
||||||
|
if (felsMaterials[k]) return felsMaterials[k];
|
||||||
|
let mat;
|
||||||
|
const canvas = typeof document !== 'undefined' && document.createElement('canvas');
|
||||||
|
const ctx = canvas && canvas.getContext && canvas.getContext('2d');
|
||||||
|
if (!ctx) mat = new THREE.MeshStandardMaterial({ color: felsHex(k), roughness: 0.4, metalness: 0.55 });
|
||||||
|
else {
|
||||||
|
const B = 100, N = FELS.TILE_SEAMS; // 5 mm/px
|
||||||
|
canvas.width = B * N;
|
||||||
|
canvas.height = 16;
|
||||||
|
const base = new THREE.Color(felsHex(k));
|
||||||
|
const shade = (f) => '#' + base.clone().multiplyScalar(f).getHexString();
|
||||||
|
const col = (a, b, color) => { ctx.fillStyle = color; ctx.fillRect(a, 0, b - a, canvas.height); };
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
const a = i * B, f = 0.97 + ((i * 29) % 7) / 100; // per baan net iets anders
|
||||||
|
col(a, a + B, shade(f));
|
||||||
|
col(a + 6, a + 14, shade(f * 1.04)); // lichte zetting naast de naad
|
||||||
|
col(a, a + 2, shade(f * 0.6)); // schaduw van de naad
|
||||||
|
col(a + 2, a + 5, shade(f * 1.35)); // staande naad (licht)
|
||||||
|
}
|
||||||
|
const tex = new THREE.CanvasTexture(canvas);
|
||||||
|
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||||
|
tex.repeat.set(1000 / (FELS.SEAM_MM * N), 1);
|
||||||
|
tex.anisotropy = 8;
|
||||||
|
mat = new THREE.MeshStandardMaterial({ color: 0xffffff, map: tex, roughness: 0.4, metalness: 0.55 });
|
||||||
|
}
|
||||||
|
felsMaterials[k] = mat;
|
||||||
|
return mat;
|
||||||
|
}
|
||||||
|
|
||||||
function claddingOffset(buitenMateriaal, stone, stuc) {
|
function claddingOffset(buitenMateriaal, stone, stuc) {
|
||||||
if (buitenMateriaal === 'vochtdoek') return 2 * BATTEN.thick + CLADDING_THICK_MM;
|
if (buitenMateriaal === 'vochtdoek') return 2 * BATTEN.thick + CLADDING_THICK_MM;
|
||||||
if (buitenMateriaal !== 'cementplaat') return 0;
|
if (buitenMateriaal !== 'cementplaat') return 0;
|
||||||
@@ -121,6 +165,14 @@ window.WallModel = (() => {
|
|||||||
return mat;
|
return mat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Materiaal van een gevelbekleding (UV in meters): felsplaten, steen, stuc of rabat; null = kale plaat.
|
||||||
|
function claddingMaterial(f) {
|
||||||
|
if (isFelsColor(f.fels)) return felsMaterial(f.fels);
|
||||||
|
if (isStoneFinish(f.stone)) return stoneMaterial(f.stone);
|
||||||
|
if (isStucColor(f.stuc)) return stucMaterial(f.stuc);
|
||||||
|
return f.rabatColor ? rabatMaterial(f.rabatColor, f.cladding) : null;
|
||||||
|
}
|
||||||
|
|
||||||
// Hoogte vanaf het loopvlak (= onderkant van de wand).
|
// Hoogte vanaf het loopvlak (= onderkant van de wand).
|
||||||
const TRASRAAM_DEFAULT_MM = 350;
|
const TRASRAAM_DEFAULT_MM = 350;
|
||||||
function trasraamFor(heightMM, finish) {
|
function trasraamFor(heightMM, finish) {
|
||||||
@@ -763,6 +815,7 @@ window.WallModel = (() => {
|
|||||||
const skinFrom = opts.skinFrom > 0 ? opts.skinFrom : 0;
|
const skinFrom = opts.skinFrom > 0 ? opts.skinFrom : 0;
|
||||||
const painted = !!opts.painted;
|
const painted = !!opts.painted;
|
||||||
const stone = buitenMateriaal === 'cementplaat' && isStoneFinish(opts.stone) ? opts.stone : null;
|
const stone = buitenMateriaal === 'cementplaat' && isStoneFinish(opts.stone) ? opts.stone : null;
|
||||||
|
const felsKey = buitenMateriaal === 'vochtdoek' && isFelsColor(opts.fels) ? opts.fels : null;
|
||||||
// spaanplaatBelow: naadwand geheel in één unit; onder het dak van de andere unit is het een binnenwand.
|
// spaanplaatBelow: naadwand geheel in één unit; onder het dak van de andere unit is het een binnenwand.
|
||||||
const spaanplaatBelow = !!opts.spaanplaatBelow && skinFrom > 0 && buitenMateriaal === 'vochtdoek';
|
const spaanplaatBelow = !!opts.spaanplaatBelow && skinFrom > 0 && buitenMateriaal === 'vochtdoek';
|
||||||
const rabatOn = buitenMateriaal === 'vochtdoek' && buitenVisible && show('gevel');
|
const rabatOn = buitenMateriaal === 'vochtdoek' && buitenVisible && show('gevel');
|
||||||
@@ -792,7 +845,7 @@ window.WallModel = (() => {
|
|||||||
let spaanplaatBuitenAreaM2 = 0;
|
let spaanplaatBuitenAreaM2 = 0;
|
||||||
let spaanplaatBuitenVolumeM3 = 0;
|
let spaanplaatBuitenVolumeM3 = 0;
|
||||||
let battenLengthM = 0, battenVolumeM3 = 0, battenCount = 0, battenPieces = 0;
|
let battenLengthM = 0, battenVolumeM3 = 0, battenCount = 0, battenPieces = 0;
|
||||||
let rabatAreaM2 = 0;
|
let rabatAreaM2 = 0, felsAreaM2 = 0;
|
||||||
let trasraamAreaM2 = 0, trasraamVolumeM3 = 0;
|
let trasraamAreaM2 = 0, trasraamVolumeM3 = 0;
|
||||||
let stoneAreaM2 = 0, stoneIsolatieM3 = 0;
|
let stoneAreaM2 = 0, stoneIsolatieM3 = 0;
|
||||||
let stucAreaM2 = 0;
|
let stucAreaM2 = 0;
|
||||||
@@ -1294,6 +1347,7 @@ window.WallModel = (() => {
|
|||||||
return shape;
|
return shape;
|
||||||
}) : [];
|
}) : [];
|
||||||
rabatAreaM2 = rabatShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
rabatAreaM2 = rabatShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
||||||
|
if (felsKey) { felsAreaM2 = rabatAreaM2; rabatAreaM2 = 0; }
|
||||||
if (rabatOn && rabatShapes.length) {
|
if (rabatOn && rabatShapes.length) {
|
||||||
const geo = new THREE.ExtrudeGeometry(rabatShapes, { steps: 1, depth: rabatDepthMM * toM, bevelEnabled: false });
|
const geo = new THREE.ExtrudeGeometry(rabatShapes, { steps: 1, depth: rabatDepthMM * toM, bevelEnabled: false });
|
||||||
const gridM = (facade && facade.gridY0 !== undefined ? facade.gridY0 : skinStartY(trasraam)) * toM;
|
const gridM = (facade && facade.gridY0 !== undefined ? facade.gridY0 : skinStartY(trasraam)) * toM;
|
||||||
@@ -1303,7 +1357,7 @@ window.WallModel = (() => {
|
|||||||
if (Math.abs(nrm.getX(k)) > 0.5) uv.setXY(k, z, y); else if (Math.abs(nrm.getY(k)) > 0.5) uv.setXY(k, x, z); else uv.setXY(k, x, y);
|
if (Math.abs(nrm.getX(k)) > 0.5) uv.setXY(k, z, y); else if (Math.abs(nrm.getY(k)) > 0.5) uv.setXY(k, x, z); else uv.setXY(k, x, y);
|
||||||
}
|
}
|
||||||
uv.needsUpdate = true;
|
uv.needsUpdate = true;
|
||||||
const mesh = new THREE.Mesh(geo, rabatMaterial(opts.rabatColor, cladding));
|
const mesh = new THREE.Mesh(geo, felsKey ? felsMaterial(felsKey) : rabatMaterial(opts.rabatColor, cladding));
|
||||||
mesh.position.set(0, 0, zDoek);
|
mesh.position.set(0, 0, zDoek);
|
||||||
mesh.castShadow = true;
|
mesh.castShadow = true;
|
||||||
mesh.receiveShadow = true;
|
mesh.receiveShadow = true;
|
||||||
@@ -1391,7 +1445,7 @@ window.WallModel = (() => {
|
|||||||
stats: {
|
stats: {
|
||||||
woodVolumeM3, spaanplaatVolumeM3, spaanplaatAreaM2, cementplaatVolumeM3, cementplaatAreaM2, doekAreaM2,
|
woodVolumeM3, spaanplaatVolumeM3, spaanplaatAreaM2, cementplaatVolumeM3, cementplaatAreaM2, doekAreaM2,
|
||||||
spaanplaatBuitenAreaM2, spaanplaatBuitenVolumeM3, horizBeamCount,
|
spaanplaatBuitenAreaM2, spaanplaatBuitenVolumeM3, horizBeamCount,
|
||||||
battenLengthM, battenVolumeM3, battenCount, battenPieces, rabatAreaM2, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3, stucAreaM2
|
battenLengthM, battenVolumeM3, battenCount, battenPieces, rabatAreaM2, felsAreaM2, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3, stucAreaM2
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1620,8 +1674,8 @@ window.WallModel = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, RABAT, RABAT_COLORS, rabatHex, STUC, STUC_COLORS, isStucColor, stucHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, RABAT, RABAT_COLORS, rabatHex, FELS, isFelsColor, felsHex, STUC, STUC_COLORS, isStucColor, stucHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||||
stoneMaterial, stucMaterial, sheathingHoles, skinSegmentsAtY, topLine, skinConfig, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
stoneMaterial, stucMaterial, felsMaterial, claddingMaterial, sheathingHoles, skinSegmentsAtY, topLine, skinConfig, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||||
createMaterials, build3D, computeSpecs
|
createMaterials, build3D, computeSpecs
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-23
@@ -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;
|
||||||
@@ -605,7 +600,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
function getPureState() {
|
function getPureState() {
|
||||||
return {
|
return {
|
||||||
wall: collectWallParams(),
|
wall: collectWallParams(),
|
||||||
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone, stuc, rabatColor },
|
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone, stuc, rabatColor, fels },
|
||||||
openings: openings.map(rawOpening)
|
openings: openings.map(rawOpening)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -634,6 +629,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
stone = (state.materials && state.materials.stone) || null;
|
stone = (state.materials && state.materials.stone) || null;
|
||||||
stuc = (state.materials && state.materials.stuc) || null;
|
stuc = (state.materials && state.materials.stuc) || null;
|
||||||
rabatColor = (state.materials && state.materials.rabatColor) || null;
|
rabatColor = (state.materials && state.materials.rabatColor) || null;
|
||||||
|
fels = (state.materials && state.materials.fels) || null;
|
||||||
syncMaterialButtons();
|
syncMaterialButtons();
|
||||||
|
|
||||||
openings = state.openings.map(rawOpening);
|
openings = state.openings.map(rawOpening);
|
||||||
@@ -1282,6 +1278,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
let stone = null;
|
let stone = null;
|
||||||
let stuc = null;
|
let stuc = null;
|
||||||
let rabatColor = null;
|
let rabatColor = null;
|
||||||
|
let fels = null;
|
||||||
let layers = ViewLayers.all();
|
let layers = ViewLayers.all();
|
||||||
function syncMaterialButtons() {
|
function syncMaterialButtons() {
|
||||||
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => b.classList.toggle('active', b.dataset.material === buitenMateriaal));
|
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => b.classList.toggle('active', b.dataset.material === buitenMateriaal));
|
||||||
@@ -1299,32 +1296,32 @@ 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;
|
||||||
|
|
||||||
function update3DScene() {
|
function update3DScene() {
|
||||||
if (wallGroup3D) scene.remove(wallGroup3D);
|
if (wallGroup3D) scene.remove(wallGroup3D);
|
||||||
const result = WallModel.build3D(CONFIG, calcData, {
|
const result = WallModel.build3D(CONFIG, calcData, {
|
||||||
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, stuc, rabatColor, spaanplaatVisible, buitenVisible, layers
|
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, stuc, rabatColor, fels, spaanplaatVisible, buitenVisible, layers
|
||||||
});
|
});
|
||||||
wallGroup3D = result.group;
|
wallGroup3D = result.group;
|
||||||
lastWallStats = result.stats;
|
lastWallStats = result.stats;
|
||||||
@@ -1418,8 +1415,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
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ gevel.vochtdoekMontageM;Montage vochtdoek per strekkende meter wand;s/m;600
|
|||||||
gevel.rabatMontage;Montage rabat;s/m²;600
|
gevel.rabatMontage;Montage rabat;s/m²;600
|
||||||
gevel.steenMontage;Montage steenstrips;s/m²;1800
|
gevel.steenMontage;Montage steenstrips;s/m²;1800
|
||||||
gevel.stucMontage;Stucwerk aanbrengen (wapenen, pleisteren);s/m²;1500
|
gevel.stucMontage;Stucwerk aanbrengen (wapenen, pleisteren);s/m²;1500
|
||||||
|
gevel.felsMontage;Montage felsplaten gevel (ook dagkanten en hoeken);s/m²;2100
|
||||||
dak.dakleerMontage;Montage dakleer;s/m²;600
|
dak.dakleerMontage;Montage dakleer;s/m²;600
|
||||||
dak.rietMontage;Montage riet;s/m²;3600
|
dak.rietMontage;Montage riet;s/m²;3600
|
||||||
dak.beschotMontage;Montage dakbeschot;s/m²;150
|
dak.beschotMontage;Montage dakbeschot;s/m²;150
|
||||||
@@ -41,6 +42,7 @@ dak.nokPlaatsen;Stalen nokbalk plaatsen, stellen en vastzetten;s;2700
|
|||||||
dak.nokVulhoutMontage;Vulhout in de holling van de nokbalk aanbrengen (beide zijden);s/m;240
|
dak.nokVulhoutMontage;Vulhout in de holling van de nokbalk aanbrengen (beide zijden);s/m;240
|
||||||
dak.nokPersoneel;Personeel bij plaatsen nokbalk (i.p.v. de bezetting);man;3
|
dak.nokPersoneel;Personeel bij plaatsen nokbalk (i.p.v. de bezetting);man;3
|
||||||
dak.paalMontage;Paal plaatsen: stellen, aftoppen, vastzetten aan het dak;s/st.;2700
|
dak.paalMontage;Paal plaatsen: stellen, aftoppen, vastzetten aan het dak;s/st.;2700
|
||||||
|
dak.zijwandMontage;Zijwand overkapping maken en plaatsen;s/m²;5400
|
||||||
vloer.afwerkingMontage;Montage vloerafwerking;s/m²;1200
|
vloer.afwerkingMontage;Montage vloerafwerking;s/m²;1200
|
||||||
vloer.sandwichMontage;Montage per sandwichpaneel;s;600
|
vloer.sandwichMontage;Montage per sandwichpaneel;s;600
|
||||||
afwerking.valsPlafondMontage;Montage vals plafond;s/m²;1800
|
afwerking.valsPlafondMontage;Montage vals plafond;s/m²;1800
|
||||||
|
|||||||
|
@@ -12,6 +12,7 @@ gevel.vochtdoekM2;Vochtdoek;€/m²;1,5;;;
|
|||||||
gevel.rabatM2;Rabat (gevelbekleding);€/m²;35;;;
|
gevel.rabatM2;Rabat (gevelbekleding);€/m²;35;;;
|
||||||
gevel.steenM2;Steenstrips (gevel + trasraam);€/m²;60;;;
|
gevel.steenM2;Steenstrips (gevel + trasraam);€/m²;60;;;
|
||||||
gevel.stucM2;Stucwerk gevel (wapening + sierpleister op cementgebonden plaat);€/m²;45;;;
|
gevel.stucM2;Stucwerk gevel (wapening + sierpleister op cementgebonden plaat);€/m²;45;;;
|
||||||
|
gevel.felsM2;Felsplaten gevel, staande naad (staal, klangen, ondergrond);€/m²;70;;;
|
||||||
dak.dakleerM2;Dakleer (EPDM);€/m²;18;;;
|
dak.dakleerM2;Dakleer (EPDM);€/m²;18;;;
|
||||||
dak.rietM2;Riet;€/m²;90;;;
|
dak.rietM2;Riet;€/m²;90;;;
|
||||||
dak.beschotM2;Dakbeschot OSB 18 mm (bovenop de sporen);€/m²;14;;;
|
dak.beschotM2;Dakbeschot OSB 18 mm (bovenop de sporen);€/m²;14;;;
|
||||||
@@ -23,6 +24,7 @@ dak.staalKg;Stalen nokbalk IPE S235 (op maat gezaagd, geboord, gegrond);€/kg;2
|
|||||||
dak.nokOplegging;Oplegging nokbalk (stalen schoen + bouten), 2 per balk;€/st.;45;;;
|
dak.nokOplegging;Oplegging nokbalk (stalen schoen + bouten), 2 per balk;€/st.;45;;;
|
||||||
dak.paalHoutM3;Paalhout overkapping (eiken, geschaafd);€/m³;2400;;;
|
dak.paalHoutM3;Paalhout overkapping (eiken, geschaafd);€/m³;2400;;;
|
||||||
dak.paalvoet;Paalvoet verzinkt, verstelbaar (incl. ankers);€/st.;40;;;
|
dak.paalvoet;Paalvoet verzinkt, verstelbaar (incl. ankers);€/st.;40;;;
|
||||||
|
dak.zijwandM2;Zijwand overkapping (regelwerk, cementgebonden plaat en stuc aan beide kanten, fundering);€/m²;140;;;
|
||||||
vloer.afwerkingM2;Vloerafwerking;€/m²;35;;;
|
vloer.afwerkingM2;Vloerafwerking;€/m²;35;;;
|
||||||
vloer.sandwichM2;Sandwichpaneel;€/m²;35;;;
|
vloer.sandwichM2;Sandwichpaneel;€/m²;35;;;
|
||||||
afwerking.valsPlafondM2;Vals plafond (regelwerk + gipsplaat, afgewerkt);€/m²;38;;;
|
afwerking.valsPlafondM2;Vals plafond (regelwerk + gipsplaat, afgewerkt);€/m²;38;;;
|
||||||
|
|||||||
|
Reference in New Issue
Block a user