3 Commits
Author SHA1 Message Date
MachielandClaude Opus 5.5 86fcd57b96 v0.6.3: modellen Waal (12,85 × 7 m, flauw zadeldak dat aan de oostkop 1,5 m doorloopt als veranda), Maas (9,2 × 9,2 m, woningdak met inpandige veranda in de noordwesthoek) en Rijn (L-vorm 13 × 10 m, zadeldak als twee lessenaars op de naad, vleugel met plat dak, steenstrips met schoorsteenstrook, kookeiland, logeerkamer, spots in de boeidelen); overkapping per unit (dak verlengen voorbij de gevel) met palen (PostModel: hout, maat, h.o.h., op de plattegrond/PDF en in de kosten); kleur schakelaars/wcd's (wit, antraciet, zwart); gevellampen naast openingen en per vrij wandstuk gelijk verdeeld, nooit in een opening; logobord niet meer onder een unit bij een voordeur in een binnenhoek
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:26:06 +02:00
MachielandClaude Opus 5.5 b137a458d4 v0.6.2: dakaansluiting L/T (vleugel met nokdak loopt het hoofddak in met kilkepers, of de kop wordt een schild met hoekkeper; eigen dakdeel en PDF-blad per unit, ook in de kosten), model IJssel (L-vorm met zadeldaken in riet), unit max 4,7 m breed; mansarde vervallen (oude ontwerpen worden een zadeldak met dezelfde nok); gevelbekleding volgt voorbij het wandeinde de daklijn (geen blok meer boven een steil dakvlak) en bij een bolle daklijn per punt de bovenkant van het dak; gevellampen standaard min. 500 mm h.o.h. (geen samengevoegde lamp meer boven een smal raam)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:19:43 +02:00
MachielandClaude Opus 5.5 bd084e4a96 v0.6.1: standaardmodellen uit één lijst (HouseModel.MODELS) voor Snel nieuw en het venster Nieuw, gedeelde bouwstenen (modelKit) voor Amstel en nieuw model Wetering (10,5 × 4,8 m, twee units zonder naadwand, hoekkeuken, badkamer, slaapkamer met kast); keukenblok met kookplaat (plek instelbaar) en spoelbak aan/uit, kookplaat ook in de kosten van een keukenblok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:53:24 +02:00
20 changed files with 1326 additions and 263 deletions
+45 -9
View File
@@ -26,6 +26,7 @@ js/core/ gedeelde helpers: geometrie, bouwfasen, ribbon, UI-kit, PDF-
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)
js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots
js/elektra/ ElektraModel + ElektraPdf: meterkast, schakelaars (letters), wcd, groepen, leidingen js/elektra/ ElektraModel + ElektraPdf: meterkast, schakelaars (letters), wcd, groepen, leidingen
js/sandwich/ SandwichModel: sandwichpanelen rondom js/sandwich/ SandwichModel: sandwichpanelen rondom
@@ -97,26 +98,39 @@ assets/ logo, texturen, skybox
geschilderd in de plafondkleur (Binnen › Wanden › Plafond), zolang geschilderd in de plafondkleur (Binnen › Wanden › Plafond), zolang
"Geschilderd" aan staat; anders kale OSB. "Geschilderd" aan staat; anders kale OSB.
- **Dakvorm/wandhoogte:** per unit een dakvorm (Unit › Dakvorm: plat, - **Dakvorm/wandhoogte:** per unit een dakvorm (Unit › Dakvorm: plat,
lessenaar, zadel, mansarde of wolfsdak, met schuiven en hellingshoek). lessenaar, zadel of wolfsdak, met schuiven en hellingshoek).
Daaruit volgen de wanden: de kopgevels krijgen de daklijn (bij mansarde Daaruit volgen de wanden: de kopgevels krijgen de daklijn (bij een
met twee knikken, bij een wolfsdak afgeknot op het wolfseind, met wolfsdak afgeknot op het wolfseind, met
daarboven driehoekige wolfseinden met de dakhelling tot de nok, met een daarboven driehoekige wolfseinden met de dakhelling tot de nok, met een
rechte nok ertussen), de haakse wanden staan vlak op de goothoogte. Het rechte nok ertussen), de haakse wanden staan vlak op de goothoogte. Het
bovenste deel van een wolfsdak is gewoon houtwerk: sporen, hoekkepers, bovenste deel van een wolfsdak is gewoon houtwerk: sporen, hoekkepers,
nokbalk en kopbalken (ook in zaaglijst, specificaties en kosten). nokbalk en kopbalken (ook in zaaglijst, specificaties en kosten).
Goot en nok gelden over de volle vloermaat (goot boven de buitenkant van Goot en nok gelden over de volle vloermaat (goot boven de buitenkant van
de gevel); staat de kopgevel tussen de haakse wanden, dan loopt zijn de gevel); staat de kopgevel tussen de haakse wanden, dan loopt zijn
daklijn daar precies op aan. Knik en wolfseind schalen mee met goot en daklijn daar precies op aan. Het wolfseind schaalt mee met goot en
nok (verhouding). Bij een knik (mansarde) sluiten de dakvlakken in nok (verhouding). De PDF heeft voor een wolfsdak een eigen blad met het
verstek aan. De PDF heeft voor een wolfsdak een eigen blad met het
bovenste deel (bovenaanzicht houtwerk + stuklijst). bovenste deel (bovenaanzicht houtwerk + stuklijst).
Grenzen (`UnitModel.LIMITS`): nok max 5 m, goot / plat dak max 3 m (vanaf Grenzen (`UnitModel.LIMITS`): nok max 5 m, goot / plat dak max 3 m (vanaf
bovenkant vloer tot onderkant dak), unit max 4,3 x 14 m. Platte daken van verschillende hoogte krijgen elk hun eigen bovenkant vloer tot onderkant dak), unit max 4,7 x 14 m. Platte daken van verschillende hoogte krijgen elk hun eigen
boeideel (versprongen, het lage loopt door tot onder het hoge). Wijzig je een boeideel (versprongen, het lage loopt door tot onder het hoge). Wijzig je een
wandhoogte in de wand-editor, dan geldt dezelfde regel vanuit die wand. wandhoogte in de wand-editor, dan geldt dezelfde regel vanuit die wand.
De daklijn van een wand hoort bij de wand zonder naden: loopt een wand De daklijn van een wand hoort bij de wand zonder naden: loopt een wand
door een naad langer door (bijv. de kopgevel naast een gekoppelde unit), door een naad langer door (bijv. de kopgevel naast een gekoppelde unit),
dan blijven nok en knikken op hun plaats (`UnitModel.topLineOn`). dan blijven nok en knikken op hun plaats (`UnitModel.topLineOn`).
- **Dakaansluiting (L/T):** staat een unit met nokdak (zadel of
wolfsdak) met zijn nok haaks op de langszijde van een buur met nokdak
(vleugel tegen hoofdvolume), dan loopt de vleugel het hoofddak in tot
zijn nok, met kilkepers in de binnenhoeken (bij gelijke goot, nok en
overspanning onder 45°); het
wolfseind van de vleugel aan de naad vervalt. Ligt de vleugel gelijk met
een kop van het hoofdvolume, dan wordt die kop een schild in de vlakken
van de vleugel (hoekkeper, de goot loopt door; de kopgevel staat dan op
goothoogte, een wolfseind daar vervalt). Een vleugel die in een wolfseind
van het hoofdvolume zou steken sluit niet aan. Elke unit houdt zijn eigen
dakdeel (sporen, kreupele sporen, kepers, knik- en nokbalken; stalen
nokbalk alleen over de eigen nok) met een eigen PDF-blad
(`HouseModel.roofJoins`, `RoofModel.computeJoinedRoof`). Niet bij dak
over de hele woning; dakramen in zo'n dakdeel doen nog niet mee.
- **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
@@ -271,8 +285,30 @@ hout.balk.38x235;Constructiehout vuren C24 38×235;€/m;4,91;Houthandel Y;;2026
stucwerk wit met verticaal hout aan de slaapkamerkant, binnenwanden, stucwerk wit met verticaal hout aan de slaapkamerkant, binnenwanden,
dubbele schuifdeur tussen woonkamer en hal, en complete inrichting. dubbele schuifdeur tussen woonkamer en hal, en complete inrichting.
Langer maken: beide units breder (alles is vanaf de westgevel gemaat). Langer maken: beide units breder (alles is vanaf de westgevel gemaat).
Ook via Bestand › Woning Amstel. De L-woning (8 × 3,6 m + 4,5 × 3,6 m) Model **Wetering** (`HouseModel.createWetering`, naar
blijft beschikbaar als "Woning L-vorm". `examples/ontwerp_wetering.pdf`): 10,5 × 4,8 m, twee units van 2,6 en
2,2 m zonder naadwand (open woonkamer over de volle diepte), verticaal
rabat grijs, plat dak met zwart boeideel, hoekkeuken met kookplaat,
badkamer, slaapkamer met kledingkast. Model **IJssel**
(`HouseModel.createIJssel`, naar `examples/ontwerp_ijssel.pdf`): L-vorm
14 × 4,6 m met een vleugel van 4,6 × 3 m, twee units met zadeldak en
riet die met een kil en een hoekkeper op elkaar aansluiten, stucwerk wit,
openslaande deuren in de westgevel. Model **Waal**
(`HouseModel.createWaal`, naar `examples/ontwerp_waal.pdf`): 12,85 × 7 m,
flauw zadeldak dat aan de oostkop 1,5 m doorloopt als veranda op eiken
palen (Overkapping per unit). Model **Maas** (`HouseModel.createMaas`,
naar `examples/ontwerp_maas.pdf`): vierkant 9,2 × 9,2 m, flauw zadeldak
over de hele woning; de overkapping maakt van de noordwesthoek een
inpandige veranda (2,5 × 4,6 m) op eiken palen. Model **Rijn**
(`HouseModel.createRijn`, naar `examples/ontwerp_rijn.pdf`): L-vorm
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
donkere schoorsteenstrook in de oostgevel, kookeiland, logeerkamer en
apart toilet in de vleugel. De standaardmodellen staan in
`HouseModel.MODELS`; Bestand › Snel nieuw en het venster Nieuw worden
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
beschikbaar als "Woning L-vorm".
- Netto m² per ruimte telt de hele vloer, ook onder een laag schuin dak - Netto m² per ruimte telt de hele vloer, ook onder een laag schuin dak
(geen NEN 2580-correctie voor hoogte < 1,5 m). (geen NEN 2580-correctie voor hoogte < 1,5 m).
- Units kunnen niet gedraaid worden. - Units kunnen niet gedraaid worden.
+1
View File
@@ -428,6 +428,7 @@ body { display: flex; flex-direction: column; }
color: var(--nav-link-color); cursor: pointer; width: 110px; color: var(--nav-link-color); cursor: pointer; width: 110px;
} }
.new-option svg { width: 40px; height: 40px; } .new-option svg { width: 40px; height: 40px; }
.new-option .bi { font-size: 36px; line-height: 40px; }
.new-option span { font-size: 13px; } .new-option span { font-size: 13px; }
.new-option:hover:not(.disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); } .new-option:hover:not(.disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
.new-option.disabled { opacity: .4; cursor: not-allowed; } .new-option.disabled { opacity: .4; cursor: not-allowed; }
+1 -4
View File
@@ -51,10 +51,6 @@
<div id="new-modal"> <div id="new-modal">
<h2>Nieuw</h2> <h2>Nieuw</h2>
<div class="new-options"> <div class="new-options">
<button class="new-option" data-kind="woning" data-template="amstel">
<svg viewBox="0 0 48 48"><rect x="6" y="10" width="36" height="14" fill="none" stroke="currentColor" stroke-width="2"/><path d="M6 24 L6 38 L38 38 L38 24" fill="none" stroke="currentColor" stroke-width="2"/><rect x="38" y="24" width="4" height="14" fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2"/><line x1="22" y1="24" x2="22" y2="38" stroke="currentColor" stroke-width="1"/></svg>
<span>Woning Amstel</span>
</button>
<button class="new-option" data-kind="woning" data-template="single"> <button class="new-option" data-kind="woning" data-template="single">
<svg viewBox="0 0 48 48"><rect x="8" y="14" width="32" height="20" fill="none" stroke="currentColor" stroke-width="2"/><line x1="8" y1="24" x2="40" y2="24" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2"/></svg> <svg viewBox="0 0 48 48"><rect x="8" y="14" width="32" height="20" fill="none" stroke="currentColor" stroke-width="2"/><line x1="8" y1="24" x2="40" y2="24" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2"/></svg>
<span>Woning</span> <span>Woning</span>
@@ -101,6 +97,7 @@
<script src="js/floor/floor-model.js"></script> <script src="js/floor/floor-model.js"></script>
<script src="js/floor/floor-pdf.js"></script> <script src="js/floor/floor-pdf.js"></script>
<script src="js/roof/roof-model.js"></script> <script src="js/roof/roof-model.js"></script>
<script src="js/roof/post-model.js"></script>
<script src="js/lighting/lighting-model.js"></script> <script src="js/lighting/lighting-model.js"></script>
<script src="js/elektra/elektra-model.js"></script> <script src="js/elektra/elektra-model.js"></script>
<script src="js/sandwich/sandwich-model.js"></script> <script src="js/sandwich/sandwich-model.js"></script>
+4 -2
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md. * Zie LICENTIE.md.
*/ */
const APP_VERSION = '0.6.0'; const APP_VERSION = '0.6.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 = [];
@@ -58,7 +58,7 @@ function appRibbonTabs() {
big('Openen', 'folder2-open', `data-app="triggerImport" title="JSON-ontwerp openen" ${disabledUnless('triggerImport')}`) + big('Openen', 'folder2-open', `data-app="triggerImport" title="JSON-ontwerp openen" ${disabledUnless('triggerImport')}`) +
big('Opslaan', 'save', `data-app="exportDesign" title="Opslaan als JSON + PDF (Ctrl+S)" ${disabledUnless('exportDesign')}`) }, big('Opslaan', 'save', `data-app="exportDesign" title="Opslaan als JSON + PDF (Ctrl+S)" ${disabledUnless('exportDesign')}`) },
{ label: 'Snel nieuw', html: { label: 'Snel nieuw', html:
small('Woning Amstel', 'house-heart', 'data-app="new-kind" data-kind="woning" data-template="amstel"') + Object.entries(HouseModel.MODELS).map(([key, m]) => small(m.label, m.icon, `data-app="new-kind" data-kind="woning" data-template="${key}"`)).join('') +
small('Woning', 'house', 'data-app="new-kind" data-kind="woning" data-template="single"') + small('Woning', 'house', 'data-app="new-kind" data-kind="woning" data-template="single"') +
small('Woning L-vorm', 'bounding-box-circles', 'data-app="new-kind" data-kind="woning" data-template="L"') + small('Woning L-vorm', 'bounding-box-circles', 'data-app="new-kind" data-kind="woning" data-template="L"') +
small('Los: muur', 'bricks', 'data-app="new-kind" data-kind="muur"') + small('Los: muur', 'bricks', 'data-app="new-kind" data-kind="muur"') +
@@ -213,6 +213,8 @@ function wireAbout() {
function wireNewModal() { function wireNewModal() {
const overlay = document.getElementById('new-overlay'); const overlay = document.getElementById('new-overlay');
overlay.querySelector('.new-options').insertAdjacentHTML('afterbegin', Object.entries(HouseModel.MODELS).map(([key, m]) =>
`<button class="new-option" data-kind="woning" data-template="${key}"><i class="bi bi-${m.icon}"></i><span>${m.label}</span></button>`).join(''));
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('open'); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('open'); });
document.querySelectorAll('.new-option:not(.disabled)').forEach(opt => { document.querySelectorAll('.new-option:not(.disabled)').forEach(opt => {
opt.addEventListener('click', () => { opt.addEventListener('click', () => {
+4 -1
View File
@@ -120,7 +120,10 @@ window.COST_DEFAULTS = {
nokOplegging: { label: 'Oplegging nokbalk (stalen schoen + bouten), 2 per balk', unit: '€/st.', value: 45 }, nokOplegging: { label: 'Oplegging nokbalk (stalen schoen + bouten), 2 per balk', unit: '€/st.', value: 45 },
nokPlaatsen: { label: 'Stalen nokbalk plaatsen, stellen en vastzetten', unit: 's', value: 2700 }, nokPlaatsen: { label: 'Stalen nokbalk plaatsen, stellen en vastzetten', unit: 's', value: 2700 },
nokVulhoutMontage: { label: 'Vulhout in de holling van de nokbalk aanbrengen (beide zijden)', unit: 's/m', value: 240 }, nokVulhoutMontage: { label: 'Vulhout in de holling van de nokbalk aanbrengen (beide zijden)', unit: 's/m', value: 240 },
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 },
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 }
} }
}, },
+14 -10
View File
@@ -206,13 +206,13 @@ window.CostModel = (() => {
const rf = newEl(`${u.id}-D`, `Unit ${u.id} dak`, u.id); const rf = newEl(`${u.id}-D`, `Unit ${u.id} dak`, u.id);
const rs = u.roof.specs, D = S.dak; const rs = u.roof.specs, D = S.dak;
add(rf, 'Hout', 'Zagen', 'Dakbalken', rs.joistWoodM3, 'm³', joistWoodCost(rs.joistWoodM3, u.roof.planes[0] && u.roof.planes[0].data.calcData), 0);
u.roof.planes.forEach((pl, i) => joistLayer(rf, pl.data.calcData, Math.round(Math.abs(pl.theta || 0) * 180 / Math.PI * 10) / 10, u.roof.planes.length > 1 ? `Dak ${i + 1} ` : 'Dak', true));
const cap = u.roof.unitRoof && u.roof.unitRoof.cap; const cap = u.roof.unitRoof && u.roof.unitRoof.cap;
add(rf, 'Hout', 'Zagen', 'Dakbalken', rs.joistWoodM3, 'm³', joistWoodCost(rs.joistWoodM3, u.roof.planes[0] ? u.roof.planes[0].data.calcData : cap), 0);
u.roof.planes.forEach((pl, i) => joistLayer(rf, pl.data.calcData, Math.round(Math.abs(pl.theta || 0) * 180 / Math.PI * 10) / 10, u.roof.planes.length > 1 ? `Dak ${i + 1} ` : 'Dak', true));
if (cap) { if (cap) {
const n = cap.beams.length; const n = cap.beams.length, join = cap.kind === 'join';
add(rf, 'Hout', 'Zagen', `${sawLabel}: wolfseinden ${n} balken`, 2 * n, 'sneden', 0, cap.beams.reduce((t, bm) => t + sawSec([bm.angle, bm.angle], 1), 0)); add(rf, 'Hout', 'Zagen', `${sawLabel}: ${join ? 'dak (sporen, kepers)' : 'wolfseinden'} ${n} balken`, 2 * n, 'sneden', 0, cap.beams.reduce((t, bm) => t + sawSec([bm.angle, bm.angle], 1), 0));
addJoints(rf, n * V.verbindingenPerVloerbalk, 'wolfseindbalken', true); addJoints(rf, n * V.verbindingenPerVloerbalk, join ? 'dakbalken' : 'wolfseindbalken', true);
} }
addSheet(rf, 'Plaatwerk', 'OSB plafond', rs.osbAreaM2, P.osbM2, P.osbMontage); addSheet(rf, 'Plaatwerk', 'OSB plafond', rs.osbAreaM2, P.osbM2, P.osbMontage);
addSheet(rf, 'Plaatwerk', `Dakbeschot OSB ${RoofModel.BESCHOT_MM} mm`, rs.beschotM2 || 0, D.beschotM2, D.beschotMontage); addSheet(rf, 'Plaatwerk', `Dakbeschot OSB ${RoofModel.BESCHOT_MM} mm`, rs.beschotM2 || 0, D.beschotM2, D.beschotMontage);
@@ -230,7 +230,7 @@ window.CostModel = (() => {
} }
}); });
const wo = newEl('WON', 'Woning (dakrand, fundering, verlichting)', null); const wo = newEl('WON', 'Woning (dakrand, overkapping, fundering, verlichting)', null);
const D = S.dak; const D = S.dak;
if (totals.roofEdge) { if (totals.roofEdge) {
const e = totals.roofEdge; const e = totals.roofEdge;
@@ -238,6 +238,11 @@ window.CostModel = (() => {
add(wo, 'Dak', 'Dak', 'Onderplaat boeideel', e.soffitM2, 'm²', e.soffitM2 * D.onderplaatM2, e.soffitM2 * D.onderplaatMontage); add(wo, 'Dak', 'Dak', 'Onderplaat boeideel', e.soffitM2, 'm²', e.soffitM2 * D.onderplaatM2, e.soffitM2 * D.onderplaatMontage);
add(wo, 'Dak', 'Dak', `Daktrim ${e.trimHeight} mm`, e.trimLengthM, 'm', e.trimLengthM * D.daktrimM, e.trimLengthM * D.daktrimMontage); add(wo, 'Dak', 'Dak', `Daktrim ${e.trimHeight} mm`, e.trimLengthM, 'm', e.trimLengthM * D.daktrimM, e.trimLengthM * D.daktrimMontage);
} }
if (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, 'Staal', 'Dak', 'Paalvoeten', pp.count, 'st.', pp.count * D.paalvoet, 0);
}
if (parts.sandwich) { if (parts.sandwich) {
const sw = parts.sandwich; const sw = parts.sandwich;
add(wo, 'Vloer', 'Vloer', `Sandwichpanelen (${sw.panelCount} platen)`, sw.areaM2, 'm²', sw.areaM2 * S.vloer.sandwichM2, sw.panelCount * S.vloer.sandwichMontage); add(wo, 'Vloer', 'Vloer', `Sandwichpanelen (${sw.panelCount} platen)`, sw.areaM2, 'm²', sw.areaM2 * S.vloer.sandwichM2, sw.panelCount * S.vloer.sandwichMontage);
@@ -324,11 +329,10 @@ window.CostModel = (() => {
const m = f.w / 1000, crew = IN.personeel; const m = f.w / 1000, crew = IN.personeel;
const perM = (what, price, sec) => add(el, 'Inrichting', 'Inrichting', `${what} ${f.w} mm`, m, 'm', m * (price || 0), m * (sec || 0), crew); const perM = (what, price, sec) => add(el, 'Inrichting', 'Inrichting', `${what} ${f.w} mm`, m, 'm', m * (price || 0), m * (sec || 0), crew);
if (f.cost === 'keuken') perM('Keukenblok', IN.keukenM, IN.keukenMontage); if (f.cost === 'keuken') perM('Keukenblok', IN.keukenM, IN.keukenMontage);
else if (f.cost === 'kookeiland') { else if (f.cost === 'kookeiland') perM('Kookeiland', IN.kookeilandM, IN.kookeilandMontage);
perM('Kookeiland', IN.kookeilandM, IN.kookeilandMontage); else if (f.cost === 'kast') perM(f.label, IN.kastM, IN.kastMontage);
if (f.opts && f.opts.kookplaat) add(el, 'Inrichting', 'Inrichting', 'Inductiekookplaat', 1, 'st.', IN.kookplaat || 0, 0);
} else if (f.cost === 'kast') perM(f.label, IN.kastM, IN.kastMontage);
else add(el, 'Inrichting', 'Inrichting', f.label, 1, 'st.', IN[f.cost] || 0, IN[f.cost + 'Montage'] || 0, crew); else add(el, 'Inrichting', 'Inrichting', f.label, 1, 'st.', IN[f.cost] || 0, IN[f.cost + 'Montage'] || 0, crew);
if ((f.cost === 'keuken' || f.cost === 'kookeiland') && f.opts && f.opts.kookplaat) add(el, 'Inrichting', 'Inrichting', 'Inductiekookplaat', 1, 'st.', IN.kookplaat || 0, 0);
}); });
const sumLines = (lines) => lines.reduce((a, l) => ({ mat: a.mat + l.mat, sec: a.sec + l.sec, clock: a.clock + l.clock }), { mat: 0, sec: 0, clock: 0 }); const sumLines = (lines) => lines.reduce((a, l) => ({ mat: a.mat + l.mat, sec: a.sec + l.sec, clock: a.clock + l.clock }), { mat: 0, sec: 0, clock: 0 });
+15 -6
View File
@@ -12,6 +12,7 @@ window.ElektraModel = (() => {
switchHeight: 1500, switchHeight: 1500,
wcdHeight: 400, wcdHeight: 400,
wcdPerUnit: 2, wcdPerUnit: 2,
plateColor: 'wit', // schakelaars, wcd's, apparaat-wcd's en vloerdozen
slack: 10, // % slack: 10, // %
reserve: 300, reserve: 300,
meterkastReserve: 500, meterkastReserve: 500,
@@ -32,6 +33,11 @@ window.ElektraModel = (() => {
droger: { label: 'Droger', code: 'DR', breaker: 'B16', cable: 'wcd', height: 1100, zone: 'techniek', material: 'Wandcontactdoos enkel met randaarde' }, droger: { label: 'Droger', code: 'DR', breaker: 'B16', cable: 'wcd', height: 1100, zone: 'techniek', material: 'Wandcontactdoos enkel met randaarde' },
warmtepomp: { label: 'Warmtepomp', code: 'WP', breaker: 'B16', cable: 'wcd', height: 1200, zone: 'techniek', material: 'Werkschakelaar 2-polig (warmtepomp)' } warmtepomp: { label: 'Warmtepomp', code: 'WP', breaker: 'B16', cable: 'wcd', height: 1200, zone: 'techniek', material: 'Werkschakelaar 2-polig (warmtepomp)' }
}; };
const PLATE_COLORS = {
wit: { label: 'Wit', hex: 0xf6f6f2 },
antraciet: { label: 'Antraciet', hex: 0x3a3d41 },
zwart: { label: 'Zwart', hex: 0x161718 }
};
const BOX_PITCH = 71; const BOX_PITCH = 71;
const PLATE = 80; const PLATE = 80;
const WCD_W = 150; const WCD_W = 150;
@@ -62,6 +68,7 @@ window.ElektraModel = (() => {
s.removed = Array.isArray(i.removed) ? i.removed.slice() : []; s.removed = Array.isArray(i.removed) ? i.removed.slice() : [];
s.vloerWcd = Array.isArray(i.vloerWcd) ? i.vloerWcd.filter(o => o && o.unitId).map(o => Object.assign({}, o)) : []; s.vloerWcd = Array.isArray(i.vloerWcd) ? i.vloerWcd.filter(o => o && o.unitId).map(o => Object.assign({}, o)) : [];
s.enabled = s.enabled !== false; s.enabled = s.enabled !== false;
if (!PLATE_COLORS[s.plateColor]) s.plateColor = DEFAULT_SETTINGS.plateColor;
// v2: schakelaars 1050 -> 1500 mm, wcd 300 -> 400 mm (oude standaard). // v2: schakelaars 1050 -> 1500 mm, wcd 300 -> 400 mm (oude standaard).
if (!i.v) { if (!i.v) {
if (i.switchHeight === 1050) s.switchHeight = DEFAULT_SETTINGS.switchHeight; if (i.switchHeight === 1050) s.switchHeight = DEFAULT_SETTINGS.switchHeight;
@@ -561,6 +568,7 @@ window.ElektraModel = (() => {
out.combos.forEach(c => { frames[c.switches.length] = (frames[c.switches.length] || 0) + 1; }); out.combos.forEach(c => { frames[c.switches.length] = (frames[c.switches.length] || 0) + 1; });
const m = out.materials; const m = out.materials;
const add = (naam, aantal, eenheid) => { if (aantal > 0) m.push({ naam, aantal, eenheid: eenheid || 'st.' }); }; const add = (naam, aantal, eenheid) => { if (aantal > 0) m.push({ naam, aantal, eenheid: eenheid || 'st.' }); };
const kleur = ', ' + PLATE_COLORS[s.plateColor].label.toLowerCase();
if (out.meterkast) { if (out.meterkast) {
add('Groepenkast 3-fase (aansluiting 3×25 A)', 1); add('Groepenkast 3-fase (aansluiting 3×25 A)', 1);
add('Hoofdschakelaar 4-polig 40 A', 1); add('Hoofdschakelaar 4-polig 40 A', 1);
@@ -571,13 +579,13 @@ window.ElektraModel = (() => {
add('Installatieautomaat B16 1P+N (wcd)', nWcdG); add('Installatieautomaat B16 1P+N (wcd)', nWcdG);
add('Installatieautomaat B16 1P+N (apparaten)', out.apps.filter(a => (APPLIANCES[a.app].poles || 1) === 1).length); add('Installatieautomaat B16 1P+N (apparaten)', out.apps.filter(a => (APPLIANCES[a.app].poles || 1) === 1).length);
add('Installatieautomaat 2× B16, 2-fase (kookgroep)', out.apps.filter(a => APPLIANCES[a.app].poles === 2).length); add('Installatieautomaat 2× B16, 2-fase (kookgroep)', out.apps.filter(a => APPLIANCES[a.app].poles === 2).length);
add('Schakelaar enkelpolig (inbouw)', out.switches.length); add('Schakelaar enkelpolig (inbouw)' + kleur, out.switches.length);
Object.keys(frames).forEach(n => add(`Afdekraam ${n}-voudig`, frames[n])); Object.keys(frames).forEach(n => add(`Afdekraam ${n}-voudig` + kleur, frames[n]));
add('Wandcontactdoos dubbel met randaarde', out.wcds.length); add('Wandcontactdoos dubbel met randaarde' + kleur, out.wcds.length);
add('Vloerdoos met dubbele wcd (randaarde)', out.floorWcds.length); add('Vloerdoos met dubbele wcd (randaarde)' + kleur, out.floorWcds.length);
const appMat = {}; const appMat = {};
out.apps.forEach(a => { const n = APPLIANCES[a.app].material; appMat[n] = (appMat[n] || 0) + 1; }); out.apps.forEach(a => { const n = APPLIANCES[a.app].material; appMat[n] = (appMat[n] || 0) + 1; });
Object.keys(appMat).forEach(n => add(n, appMat[n])); Object.keys(appMat).forEach(n => add(n + kleur, appMat[n]));
add('Luchtdichte hollewanddoos (schakelaar/wcd/apparaat)', out.switches.length + 2 * out.wcds.length + out.apps.length); add('Luchtdichte hollewanddoos (schakelaar/wcd/apparaat)', out.switches.length + 2 * out.wcds.length + out.apps.length);
add('Luchtdichte verdeeldoos (plafond)', out.boxes.length); add('Luchtdichte verdeeldoos (plafond)', out.boxes.length);
add('Lasklem 3-voudig (schatting)', out.boxes.length * 3 + out.switches.length + out.wcds.length); add('Lasklem 3-voudig (schatting)', out.boxes.length * 3 + out.switches.length + out.wcds.length);
@@ -731,6 +739,7 @@ window.ElektraModel = (() => {
const toM = 0.001, group = new THREE.Group(); const toM = 0.001, group = new THREE.Group();
if (!el || !el.enabled) return group; if (!el || !el.enabled) return group;
const mats = materials || createMaterials(); const mats = materials || createMaterials();
mats.plateMaterial.color.setHex(PLATE_COLORS[el.settings.plateColor].hex);
const st = BuildStages.clamp(stage), show = (key) => BuildStages.shows(st, key); const st = BuildStages.clamp(stage), show = (key) => BuildStages.shows(st, key);
const rough = show('elektra') && !show('beplating'); const rough = show('elektra') && !show('beplating');
const box = (w, h, d, c, rotY, mat) => { const box = (w, h, d, c, rotY, mat) => {
@@ -864,5 +873,5 @@ window.ElektraModel = (() => {
}; };
} }
return { DEFAULT_SETTINGS, CABLES, CORE_COLORS, APPLIANCES, PLATE, WCD_W, cableInfo, normalizeSettings, compute, snapToWall, wallCoords, clipPlan, roofDeckMap, createMaterials, build3D }; return { DEFAULT_SETTINGS, PLATE_COLORS, CABLES, CORE_COLORS, APPLIANCES, PLATE, WCD_W, cableInfo, normalizeSettings, compute, snapToWall, wallCoords, clipPlan, roofDeckMap, createMaterials, build3D };
})(); })();
+653 -50
View File
@@ -73,12 +73,17 @@ window.HouseModel = (() => {
return { exterior: 'vochtdoek', stone: null, stuc: null, cladding, trasraam, rabatColor: state.settings.facade.rabatColor }; return { exterior: 'vochtdoek', stone: null, stuc: null, cladding, trasraam, rabatColor: state.settings.facade.rabatColor };
} }
function newModelState(naam) {
const st = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] };
st.project.naam = naam || '';
return st;
}
// TIJDELIJK (testen): nieuwe woning = voorbeeldontwerp (json/premade/vlinderwoning.json); false = één unit. // TIJDELIJK (testen): nieuwe woning = voorbeeldontwerp (json/premade/vlinderwoning.json); false = één unit.
const TEST_DEFAULT = true; const TEST_DEFAULT = true;
function createVlinderwoning() { function createVlinderwoning() {
const st = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] }; const st = newModelState('Vlinderwoning');
st.project.naam = 'Vlinderwoning';
st.settings.trasraam.finish = 'K490'; st.settings.trasraam.finish = 'K490';
const A = createUnitEntry('A', null, { width: 10000, depth: 4000, windowOn: [] }); const A = createUnitEntry('A', null, { width: 10000, depth: 4000, windowOn: [] });
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 10000, depth: 3600, windowOn: [] }); const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 10000, depth: 3600, windowOn: [] });
@@ -112,10 +117,35 @@ window.HouseModel = (() => {
return st; return st;
} }
// Bouwstenen voor de standaardmodellen. Gevelopeningen: [type, w, h, y, anchor, offset, fill];
// binnenwanden: [type, w, h, anchor, offset, fill] vanaf de vloer.
function modelKit(st, H, T) {
const K = (type, extra) => Object.assign(KozijnModel.defaultFill(type === 'raam' ? 'raam' : 'deur'), { out: 'zwart', in: 'zwart' }, extra || {});
const binnen = (extra) => Object.assign(KozijnModel.defaultFill('deur'), { out: 'wit', in: 'wit', glas: false }, extra || {});
const ops = (ws, list) => { ws.openings = list.map(([type, w, h, y, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y, anchor, offset, fill })); };
const iw = (x1, z1, x2, z2, openings, t) => {
const wall = interiorWallDefaults(t || T, H);
wall.openings = (openings || []).map(([type, w, h, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y: 0, anchor, offset, fill }));
st.interiorWalls.push({ id: st.interiorWalls.length + 1, x1, z1, x2, z2, wall });
};
// zaadpunt t.o.v. de noordwesthoek van de unit
const room = (unitId, x, z, type, name, extra) =>
st.rooms.push(Object.assign(RoomModel.applyType({ id: st.rooms.length + 1, unitId, x, z, name: '' }, type), { name: name || '' }, extra || {}));
// Midden voetafdruk t.o.v. de noordwesthoek van de unit; rot = richting van de rug (0 = noord, 90 = oost).
// anchor 'oost': x opgeven vanaf de westgevel, het meubel schuift mee met de oostgevel.
const fx = (type, unitId, x, z, rot, size, opts) => {
const f = FixtureModel.create(st, type, unitId, x, z, rot);
if (size && size.anchor === 'oost') f.x = Math.round(st.units.find(u => u.id === unitId).unit.floor.floor.width - x);
Object.assign(f, size || {});
Object.assign(f.opts, opts || {});
st.fixtures.push(f);
};
return { K, binnen, ops, iw, room, fx };
}
// Model Amstel (examples/ontwerp_amstel.pdf). Alles vanaf de westgevel gemaat, zodat de woning in de lengte kan groeien. // Model Amstel (examples/ontwerp_amstel.pdf). Alles vanaf de westgevel gemaat, zodat de woning in de lengte kan groeien.
function createAmstel() { function createAmstel() {
const st = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] }; const st = newModelState('Amstel');
st.project.naam = 'Amstel';
const S = st.settings, H = 2800; const S = st.settings, H = 2800;
Object.assign(S.facade, { type: 'stuc', stucColor: 'wit', rabatColor: 'naturel' }); Object.assign(S.facade, { type: 'stuc', stucColor: 'wit', rabatColor: 'naturel' });
S.cladding = 'verticaal'; S.cladding = 'verticaal';
@@ -131,9 +161,7 @@ window.HouseModel = (() => {
setHouseRoof(st, true); setHouseRoof(st, true);
Object.assign(S.roof.house, { form: { type: 'plat', low: H, high: H }, cover: true }); Object.assign(S.roof.house, { form: { type: 'plat', low: H, high: H }, cover: true });
const K = (type, extra) => Object.assign(KozijnModel.defaultFill(type === 'raam' ? 'raam' : 'deur'), { out: 'zwart', in: 'zwart' }, extra || {}); const { K, binnen, ops, iw, room, fx } = modelKit(st, H, 90);
const binnen = (extra) => Object.assign(KozijnModel.defaultFill('deur'), { out: 'wit', in: 'wit', glas: false }, extra || {});
const ops = (ws, list) => { ws.openings = list.map(([type, w, h, y, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y, anchor, offset, fill })); };
// Noordgevel: 'right' = vanaf de westhoek. // Noordgevel: 'right' = vanaf de westhoek.
ops(A.unit.walls.voor, [ ops(A.unit.walls.voor, [
['deur', 1474, 2650, 0, 'right', 150, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })], ['deur', 1474, 2650, 0, 'right', 150, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })],
@@ -152,13 +180,6 @@ window.HouseModel = (() => {
B.facade.walls.rechts = { type: 'rabat' }; B.facade.walls.rechts = { type: 'rabat' };
B.lighting.walls.achter = 'openingen'; B.lighting.walls.achter = 'openingen';
const T = 90;
let iwId = 0;
const iw = (x1, z1, x2, z2, openings) => {
const wall = interiorWallDefaults(T, H);
wall.openings = (openings || []).map(([type, w, h, anchor, offset, fill], i) => ({ id: i + 1, type, w, h, y: 0, anchor, offset, fill }));
st.interiorWalls.push({ id: ++iwId, x1, z1, x2, z2, wall });
};
const D = 2115; const D = 2115;
iw(2555, 3700, 2555, 4800, [['deur', 850, D, 'left', 80, binnen({ hinge: 'links', swing: 'buiten' })]]); // hal | logeerkamer iw(2555, 3700, 2555, 4800, [['deur', 850, D, 'left', 80, binnen({ hinge: 'links', swing: 'buiten' })]]); // hal | logeerkamer
iw(1980, 4845, 3070, 4845); // boven boilerkast en garderobe iw(1980, 4845, 3070, 4845); // boven boilerkast en garderobe
@@ -180,25 +201,14 @@ window.HouseModel = (() => {
fill: Object.assign(KozijnModel.defaultFill('deur'), { type: 'schuifdeur', leaves: 2, mount: 'opbouw', side: 'binnen', glas: true, out: 'zwart', in: 'zwart' }) }]; fill: Object.assign(KozijnModel.defaultFill('deur'), { type: 'schuifdeur', leaves: 2, mount: 'opbouw', side: 'binnen', glas: true, out: 'zwart', in: 'zwart' }) }];
} }
// zaadpunt t.o.v. de noordwesthoek van de unit room('A', 5600, 1200, 'woonkeuken', 'Woonkamer/keuken');
const room = (id, unitId, x, z, type, name) => st.rooms.push(Object.assign(RoomModel.applyType({ id, unitId, x, z, name: '' }, type), { name })); room('B', 1000, 2000, 'slaapkamer', 'Logeerkamer');
room(1, 'A', 5600, 1200, 'woonkeuken', 'Woonkamer/keuken'); room('B', 3600, 2600, 'hal', 'Entree');
room(2, 'B', 1000, 2000, 'slaapkamer', 'Logeerkamer'); room('B', 2480, 2800, 'toilet');
room(3, 'B', 3600, 2600, 'hal', 'Entree'); room('B', 5300, 2700, 'badkamer');
room(4, 'B', 2480, 2800, 'toilet', ''); room('B', 8300, 1800, 'slaapkamer', 'Slaapkamer');
room(5, 'B', 5300, 2700, 'badkamer', ''); room('B', 2245, 1650, 'techniek', 'Boilerkast');
room(6, 'B', 8300, 1800, 'slaapkamer', 'Slaapkamer');
room(7, 'B', 2245, 1650, 'techniek', 'Boilerkast');
// Midden voetafdruk t.o.v. de noordwesthoek van de unit; rot = richting van de rug (0 = noord, 90 = oost).
const xA = 11240, xB = 10600;
const fx = (type, unitId, x, z, rot, size, opts) => {
const f = FixtureModel.create(st, type, unitId, x, z, rot);
if (size && size.anchor === 'oost') f.x = Math.round((unitId === 'A' ? xA : xB) - x);
Object.assign(f, size || {});
Object.assign(f.opts, opts || {});
st.fixtures.push(f);
};
fx('keuken', 'A', 4746, 556, 0, { w: 2660 }, { front: 'antraciet', hogeKast: 'links', spoelbak: 0.49 }); fx('keuken', 'A', 4746, 556, 0, { w: 2660 }, { front: 'antraciet', hogeKast: 'links', spoelbak: 0.49 });
fx('kookeiland', 'A', 4746, 2000, 0, { w: 2660, d: 600 }, { front: 'antraciet', kookplaat: true }); fx('kookeiland', 'A', 4746, 2000, 0, { w: 2660, d: 600 }, { front: 'antraciet', kookplaat: true });
fx('eettafel', 'A', 1600, 1820, 90, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 }); fx('eettafel', 'A', 1600, 1820, 90, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 });
@@ -220,10 +230,442 @@ window.HouseModel = (() => {
return st; return st;
} }
// Model Wetering (examples/ontwerp_wetering.pdf): 10,5 × 4,8 m, één rechthoek zonder naadwand (open woonkamer
// over de volle diepte). 4,8 m is te diep voor één unit, dus twee units (2,6 + 2,2 m). Alles vanaf de westgevel en
// de noordgevel gemaat (buitenwerks); de entreegevel met openslaande deuren en voordeur is het zuiden.
function createWetering() {
const st = newModelState('Wetering');
const S = st.settings, H = 2800, W = 10500, DA = 2600, DB = 2200;
Object.assign(S.facade, { type: 'rabat', rabatColor: 'grijs' });
S.cladding = 'verticaal';
S.trasraam.enabled = false;
S.roof.fasciaColor = 'zwart';
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 });
const A = createUnitEntry('A', null, { width: W, depth: DA, height: H, windowOn: [] });
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: W, depth: DB, height: H, windowOn: [] });
st.units.push(A, B);
setHouseRoof(st, true);
Object.assign(S.roof.house, { form: { type: 'plat', low: H, high: H } });
const { K, binnen, ops, iw, room, fx } = modelKit(st, H, 90);
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
ops(A.unit.walls.voor, [['raam', 500, 700, 1400, 'right', 5670, K('raam')]]);
ops(A.unit.walls.links, [['raam', 700, 2100, 0, 'left', 300, K('raam')], ['raam', 700, 2100, 0, 'left', 1500, K('raam')]]);
ops(A.unit.walls.rechts, [['raam', 500, 1400, 700, 'right', 270, K('raam')]]);
ops(B.unit.walls.rechts, [['raam', 500, 1400, 700, 'right', 540, K('raam')]]);
ops(B.unit.walls.achter, [
['deur', 824, 2100, 0, 'left', 1200, K('deur', { hinge: 'links', swing: 'buiten', volglas: true })],
['deur', 824, 2100, 0, 'left', 2176, K('deur', { hinge: 'rechts', swing: 'buiten', volglas: true })],
['voordeur', 1100, 2100, 0, 'left', 5350, K('deur', { hinge: 'rechts', swing: 'buiten', volglas: true })]]);
B.lighting.walls.achter = 'openingen';
const D = 2115;
iw(4675, 190, 4675, 2210); // keuken | badkamer
iw(4630, 2255, 7120, 2255, [['deur', 930, D, 'left', 1390, binnen({ hinge: 'rechts', swing: 'buiten' })]]); // badkamer | entree
iw(7165, 190, 7165, 4610, [ // slaapkamer
['deur', 980, 2130, 'left', 360, Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'binnen', dir: 'rechts' })],
['deur', 930, D, 'left', 2277, binnen({ hinge: 'links', swing: 'buiten' })]]);
iw(6455, 3765, 7120, 3765); // kapstoknis entree
// Geen naadwand: woonkamer en slaapkamer lopen over beide units door (Unit › Naadwand toevoegen zet hem terug).
room('A', 2000, 1500, 'woonkeuken', 'Woonkamer/keuken');
room('A', 5900, 1500, 'badkamer');
room('A', 8700, 1500, 'slaapkamer', 'Slaapkamer');
const zB = (z) => z - DA;
fx('keuken', 'A', 4330, 1245, 90, { w: 2110 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.85, kookplaat: true, kookplaatPlek: 0.2 });
fx('keuken', 'A', 4630, 2600, 0, { w: 1200 }, { front: 'antraciet', hogeKast: 'rechts', spoel: false, vaatwasser: false });
fx('boiler', 'A', 5515, 2525, 0);
fx('eettafel', 'A', 1620, 1160, 0, { w: 1000, d: 1000 }, { vorm: 'ovaal', stoelen: 4 });
fx('bank', 'B', 615, zB(3640), 270, { w: 1600, d: 850 }, { chaise: 'geen' });
fx('salontafel', 'B', 1770, zB(3510), 0, { w: 600, d: 600 }, { vorm: 'rond' });
fx('fauteuil', 'B', 2760, zB(4060), 150);
fx('douche', 'A', 5245, 640, 270, { w: 900, d: 1050 }, { glas: 'links', glasLengte: 800 });
fx('toilet', 'A', 5065, 1760, 270, { w: 900 });
fx('wastafel', 'A', 6150, 420, 0, { w: 600 }, { spiegel: false });
fx('bed', 'A', 9260, 2050, 90, { w: 1600, d: 2100, anchor: 'oost' }, { nachtkastjes: false });
fx('kledingkast', 'B', 8760, zB(4310), 180, { w: 3100 });
computeParts(st, { skip3D: true });
return st;
}
// Model IJssel (examples/ontwerp_ijssel.pdf): L-vorm 14 × 4,6 m met een vleugel van 4,6 × 3 m aan de zuidoostkant,
// samen 14 × 7,6 m. Twee units: hoofdvolume met zadeldak west–oost, vleugel met zadeldak noord–zuid. Gelijke
// goot, nok en overspanning, dus de daken sluiten onder 45° aan (dakaansluiting, zie roofJoins): kil in de
// binnenhoek en, omdat de vleugel gelijk ligt met de oostgevel, een hoekkeper (de oostkop wordt een schild).
// Geen naadwanden; de wand tussen hal/badkamer en slaapkamer is een gewone binnenwand. Binnenwanden volgen het
// dak nog niet, dus alle ruimtes krijgen een vlak plafond op wandhoogte. Alles buitenwerks vanaf de noordwesthoek.
function createIJssel() {
const st = newModelState('IJssel');
const S = st.settings, LO = 3000, HI = 4600, H = 2900, D = 4600, ROOF = 'riet_001';
Object.assign(S.facade, { type: 'stuc', stucColor: 'wit' });
Object.assign(S.trasraam, { enabled: true, height: 600, finish: 'R510' });
S.roof.fasciaColor = 'zwart';
S.lighting.mode = 'spots';
S.lighting.direction = 'both';
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
const A = createUnitEntry('A', null, { width: 14000, depth: D, height: LO, windowOn: [] });
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'end', offset: 0 }, { width: 4600, depth: 3000, height: LO, windowOn: [] });
st.units.push(A, B);
UnitModel.applyRoofForm(A.unit, { type: 'zadel', ridge: 'x', low: LO, high: HI });
UnitModel.applyRoofForm(B.unit, { type: 'zadel', ridge: 'z', low: LO, high: HI });
[A, B].forEach(u => { u.roof.covering = ROOF; });
const { K, binnen, ops, iw, room, fx } = modelKit(st, H, 90);
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
ops(A.unit.walls.voor, [['raam', 1500, 500, 1600, 'right', 5445, K('raam')], ['raam', 500, 700, 1400, 'right', 9950, K('raam')]]);
ops(A.unit.walls.links, [
['deur', 1424, 2100, 0, 'left', 610, K('deur', { leaf: 'rechts', hinge: 'links', swing: 'buiten', volglas: true })],
['deur', 1424, 2100, 0, 'left', 2376, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })]]);
ops(A.unit.walls.rechts, [['raam', 1400, 1100, 1000, 'right', 608, K('raam')], ['raam', 1200, 500, 1600, 'right', 3101, K('raam')]]);
ops(A.unit.walls.achter, [
['raam', 1800, 2800, 0, 'left', 3215, K('raam')],
['voordeur', 1100, 2100, 0, 'left', 8240, K('deur', { hinge: 'rechts', swing: 'buiten', glas: true })]]);
ops(B.unit.walls.achter, [['raam', 500, 1400, 700, 'left', 410, K('raam')], ['raam', 500, 1400, 700, 'right', 1100, K('raam')]]);
A.lighting.walls.achter = 'openingen';
const DH = 2115;
iw(9580, 190, 9580, 3220, [], 160); // keuken | toilet, hal
iw(8060, 3265, 9660, 3265); // woonkamer | hal
iw(8105, 3310, 8105, 4410, [['deur', 930, DH, 'left', 86, binnen({ hinge: 'rechts', swing: 'buiten' })]]); // hal → woonkamer
iw(9660, 1633, 10760, 1633, [['deur', 930, DH, 'left', 85, binnen({ hinge: 'links', swing: 'buiten' })]]); // toilet | hal
iw(10805, 190, 10805, 4410, [['deur', 930, DH, 'left', 1725, binnen({ hinge: 'rechts', swing: 'binnen' })]]); // hal | berging, badkamer
iw(10850, 2965, 13810, 2965); // berging | badkamer
iw(9590, 4455, 13810, 4455, [ // hal, badkamer | slaapkamer
['deur', 930, DH, 'left', 115, binnen({ hinge: 'links', swing: 'buiten' })],
['deur', 980, DH, 'left', 2350, Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]);
const plafond = { ceiling: H };
room('A', 3000, 1500, 'woonkeuken', 'Woonkamer/keuken', plafond);
room('A', 10210, 900, 'toilet');
room('A', 12300, 1500, 'berging', 'Berging', plafond);
room('A', 9000, 3800, 'hal', 'Hal', plafond);
room('A', 12300, 3700, 'badkamer');
room('B', 2000, 1400, 'slaapkamer', 'Slaapkamer', plafond);
const xB = (x) => x - 9400, zB = (z) => z - D;
fx('bank', 'A', 2030, 690, 0, { w: 3600, d: 1000 }, { chaise: 'links', chaiseLengte: 1700 });
fx('salontafel', 'A', 2410, 1980, 0, { w: 800, d: 800 }, { vorm: 'rond' });
fx('fauteuil', 'A', 3120, 2950, 150);
fx('tvmeubel', 'A', 1060, 4210, 180, { w: 1200 });
fx('eettafel', 'A', 6100, 1115, 0, { w: 2000, d: 1000 }, { vorm: 'ovaal', stoelen: 6 });
fx('keuken', 'A', 9200, 1705, 90, { w: 3030 }, { front: 'antraciet', hogeKast: 'rechts', spoelbak: 0.16, kookplaat: true, kookplaatPlek: 0.8 });
fx('toilet', 'A', 10210, 535, 0, { w: 1100 });
fx('boiler', 'A', 11040, 415, 0);
fx('douche', 'A', 11300, 3710, 0, { w: 900, d: 1400 }, { glas: 'rechts', glasLengte: 1400 });
fx('wastafel', 'A', 12300, 3240, 0, { w: 800 });
fx('toilet', 'A', 13355, 3355, 0, { w: 900 });
fx('bed', 'B', xB(11400), zB(6360), 180, { w: 1600, d: 2100 }, { nachtkastjes: false });
fx('kledingkast', 'B', xB(13510), zB(5955), 90, { w: 2910, anchor: 'oost' });
computeParts(st, { skip3D: true });
return st;
}
// Model Waal (examples/ontwerp_waal.pdf): 12,85 × 7 m, twee units van 3,5 m met één flauw zadeldak over de
// woning (nok west–oost op de naad). Het dak loopt aan de oostkop 1,5 m door als veranda (10,5 m²) op drie
// eiken palen (PostModel). Geen naadwand: slaapkamer en woonkamer/keuken lopen over beide units door; bij de
// naad staat aan elke kant een binnenwand (badkamer/keuken | hal). Alles buitenwerks vanaf de noordwesthoek.
function createWaal() {
const st = newModelState('Waal');
const S = st.settings, LO = 2800, HI = 3400, W = 12850, D = 3500;
Object.assign(S.facade, { type: 'rabat', rabatColor: 'grijs' });
S.cladding = 'verticaal';
Object.assign(S.trasraam, { enabled: true, height: 600, finish: 'R510' });
Object.assign(S.roof, { fasciaColor: 'wit', overhang: RoofModel.OVERHANG_MAX, fasciaHeight: 350 });
S.roof.posts = PostModel.normalizeSettings({ enabled: true, size: 200, wood: 'eiken' });
S.lighting.mode = 'spots';
S.lighting.direction = 'both';
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
const A = createUnitEntry('A', null, { width: W, depth: D, height: LO, windowOn: [] });
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: W, depth: D, height: LO, windowOn: [] });
st.units.push(A, B);
setHouseRoof(st, true);
Object.assign(S.roof.house, { form: { type: 'zadel', ridge: 'x', low: LO, high: HI } });
[A, B].forEach(u => { u.roof.extend = { f: 0, r: 1500, b: 0, l: 0 }; });
const { K, binnen, ops, iw, room, fx } = modelKit(st, LO, 90);
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
ops(A.unit.walls.voor, [
['raam', 800, 500, 1600, 'right', 3490, K('raam')],
['raam', 800, 1100, 1000, 'right', 6530, K('raam')],
['raam', 1400, 1100, 1000, 'right', 8510, K('raam')]]);
ops(A.unit.walls.links, [['raam', 500, 1400, 700, 'left', 1170, K('raam')]]);
ops(B.unit.walls.links, [['raam', 500, 1400, 700, 'left', 390, K('raam')], ['raam', 1400, 1100, 1000, 'left', 1570, K('raam')]]);
// Oostkop onder de veranda: schuifdeur bij de keuken, openslaande deuren bij de woonkamer.
ops(A.unit.walls.rechts, [['deur', 1800, 2100, 0, 'right', 880,
K('deur', { type: 'schuifdeur', leaves: 2, mount: 'opbouw', side: 'binnen', glas: true })]]);
ops(B.unit.walls.rechts, [
['deur', 900, 2100, 0, 'right', 860, K('deur', { leaf: 'rechts', hinge: 'rechts', swing: 'buiten', volglas: true })],
['deur', 900, 2100, 0, 'right', 1760, K('deur', { leaf: 'links', hinge: 'links', swing: 'buiten', volglas: true })]]);
ops(B.unit.walls.achter, [
['raam', 500, 1400, 700, 'left', 3530, K('raam')],
['voordeur', 1100, 2100, 0, 'left', 4310, K('deur', { hinge: 'links', swing: 'buiten', glas: true })],
...[7130, 8230, 10470, 11570].map(x => ['raam', 500, 2100, 0, 'left', x, K('raam')])]);
B.lighting.walls.achter = 'openingen';
const DH = 2115;
// Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant.
iw(3235, 190, 3235, 3430, [['deur', 980, 2130, 'left', 1530, // slaapkamer | badkamer
Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]);
iw(5325, 190, 5325, 3430); // badkamer | keuken
iw(3280, 3510, 6535, 3510, [], 160); // badkamer, keuken | hal (over de naad)
iw(3235, 3590, 3235, 4655, [['deur', 930, DH, 'left', 75, binnen({ hinge: 'links', swing: 'buiten' })]]); // hal → slaapkamer
iw(6490, 3590, 6490, 6810, [['deur', 930, DH, 'left', 72, binnen({ hinge: 'links', swing: 'binnen' })]]); // hal, entree | woonkamer
iw(190, 4700, 6445, 4700, [ // slaapkamer, hal | berging, toilet, entree
['deur', 930, DH, 'left', 120, binnen({ hinge: 'links', swing: 'binnen' })],
['deur', 930, DH, 'left', 4200, binnen({ hinge: 'rechts', swing: 'buiten' })]]);
iw(2248, 4745, 2248, 5819); // berging | toilet
iw(2203, 5864, 3607, 5864); // toilet | berging, entree
iw(2938, 5909, 2938, 6810); // berging | entree
iw(3562, 4745, 3562, 5819, [['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'binnen' })]]); // toilet → entree
const plafond = { ceiling: LO };
room('A', 9500, 1800, 'woonkeuken', 'Woonkamer/keuken', plafond);
room('A', 1500, 1800, 'slaapkamer', 'Slaapkamer', plafond);
room('A', 4300, 2200, 'badkamer', '', plafond);
room('B', 4800, 600, 'hal', 'Hal', plafond);
room('B', 2900, 1800, 'toilet', '', plafond);
room('B', 4700, 2700, 'hal', 'Entree', plafond);
room('B', 1200, 2400, 'berging', 'Berging', plafond);
const zB = (z) => z - D;
fx('kledingkast', 'A', 1690, 490, 0, { w: 3000 });
fx('bed', 'A', 1240, 2770, 270, { w: 1600, d: 2100 }, { nachtkastjes: false });
fx('douche', 'A', 4585, 640, 90, { w: 900, d: 1390 }, { glas: 'rechts', glasLengte: 700 });
fx('wastafel', 'A', 5050, 1920, 90, { w: 800 });
fx('toilet', 'A', 3790, 3085, 180, { w: 1000 });
fx('keuken', 'A', 5670, 1810, 270, { w: 3240 }, { front: 'antraciet', hogeKast: 'links', spoel: false, vaatwasser: false, kookplaat: true, kookplaatPlek: 0.45 });
fx('keuken', 'A', 7025, 490, 0, { w: 2110 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.5, kookplaat: false });
fx('eettafel', 'A', 10230, 1730, 0, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 });
fx('bank', 'B', 9820, zB(5365), 90, { w: 2500, d: 900 }, { chaise: 'rechts', chaiseLengte: 1300 });
fx('fauteuil', 'B', 8420, zB(3910), 330);
fx('salontafel', 'B', 8180, zB(5180), 0, { w: 800, d: 800 }, { vorm: 'rond' });
fx('tvmeubel', 'B', 6735, zB(5815), 270, { w: 1500, d: 400 });
fx('kledingkast', 'B', 6145, zB(5777), 90, { w: 2060 });
fx('toilet', 'B', 2638, zB(5312), 270, { w: 1000 });
fx('boiler', 'B', 1810, zB(4970), 0);
fx('wasmachine', 'B', 2500, zB(6510), 180);
computeParts(st, { skip3D: true });
return st;
}
// Model Maas (examples/ontwerp_maas.pdf): vierkant 9,2 × 9,2 m onder één flauw zadeldak (nok west–oost op de
// naad). Twee units van 4,6 m diep: noord 6,7 m breed (oost uitgelijnd), zuid 9,2 m. De overkapping van het
// woningdak maakt van de noordwesthoek een inpandige veranda (2,5 × 4,6 m) op eiken palen. Maten buitenwerks vanaf
// de noordwesthoek van de woning; unit A begint 2,5 m oostelijker (X = interieur-coördinaten).
function createMaas() {
const st = newModelState('Maas');
const S = st.settings, LO = 2800, HI = 3500, D = 4600, V = 2500;
Object.assign(S.facade, { type: 'rabat', rabatColor: 'naturel' });
S.cladding = 'verticaal';
S.trasraam.enabled = false;
Object.assign(S.roof, { fasciaColor: 'antraciet', overhang: 300, fasciaHeight: 300 });
S.roof.posts = PostModel.normalizeSettings({ enabled: true, size: 200, wood: 'eiken' });
S.lighting.mode = 'spots';
S.lighting.direction = 'both';
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
const A = createUnitEntry('A', null, { width: 9200 - V, depth: D, height: LO, windowOn: [] });
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'end', offset: 0 }, { width: 9200, depth: D, height: LO, windowOn: [] });
st.units.push(A, B);
setHouseRoof(st, true);
Object.assign(S.roof.house, { form: { type: 'zadel', ridge: 'x', low: LO, high: HI }, cover: true });
const { K, binnen, ops, iw, room, fx } = modelKit(st, LO, 90);
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
ops(A.unit.walls.voor, [
['raam', 800, 1500, 600, 'right', 970, K('raam')],
['raam', 800, 1500, 600, 'right', 2715, K('raam')],
['raam', 800, 1100, 1000, 'right', 4815, K('raam')]]);
// Westgevel onder de veranda: openslaande deuren met een vast zijlicht aan beide kanten (samen 3000 × 2100).
ops(A.unit.walls.links, [
['raam', 600, 2100, 0, 'left', 770, K('raam')],
['deur', 900, 2100, 0, 'left', 1370, K('deur', { leaf: 'rechts', hinge: 'links', swing: 'buiten', volglas: true })],
['deur', 900, 2100, 0, 'left', 2270, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })],
['raam', 600, 2100, 0, 'left', 3170, K('raam')]]);
ops(B.unit.walls.voor, [['raam', 1100, 1500, 600, 'right', 315, K('raam')]]);
ops(B.unit.walls.links, [['raam', 800, 1500, 600, 'left', 200, K('raam')]]);
ops(B.unit.walls.rechts, [['raam', 800, 1100, 1000, 'right', 210, K('raam')]]);
ops(B.unit.walls.achter, [
['raam', 500, 700, 1400, 'left', 3525, K('raam')],
['voordeur', 1100, 2100, 0, 'left', 5675, K('deur', { hinge: 'links', swing: 'buiten' })],
['raam', 500, 1500, 600, 'left', 8375, K('raam')]]);
ensureSeamWalls(st);
const seam = st.interiorWalls.find(w => w.seam);
if (seam) {
const tA = WallModel.thicknessOf(WallModel.configFromState(A.unit.walls.links));
seam.wall.openings = [{ id: 1, type: 'deur', w: 930, h: 2115, y: 0, anchor: 'left', offset: Math.round(5790 - V - tA),
fill: binnen({ hinge: 'rechts', swing: 'binnen' }) }];
}
const DH = 2115, X = (x) => x - V;
// Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant.
iw(X(3250), 4700, X(3250), 9010, [ // slaapkamer | hal, toilet, badkamer
['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'binnen' })],
['deur', 980, 2130, 'left', 2780, Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]);
iw(X(3295), 5740, X(5620), 5740, [['deur', 930, DH, 'left', 665, binnen({ hinge: 'links', swing: 'binnen' })]]); // hal | toilet, schacht
iw(X(5075), 5785, X(5075), 6865); // toilet | schacht
iw(X(3295), 6910, X(5530), 6910); // toilet, schacht | badkamer
iw(X(5575), 5785, X(5575), 9010); // schacht, badkamer | entree
iw(X(6865), 4700, X(6865), 9010, [ // entree | logeerkamer, berging
['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'buiten' })],
['deur', 930, DH, 'left', 3185, binnen({ hinge: 'rechts', swing: 'buiten' })]]);
iw(X(6910), 6915, X(9010), 6915); // logeerkamer | berging
const plafond = { ceiling: LO };
room('A', 2500, 2000, 'woonkeuken', 'Woonkamer/keuken');
room('B', 1500, 2000, 'slaapkamer', 'Slaapkamer', plafond);
room('B', 4100, 1700, 'toilet');
room('B', 4000, 3300, 'badkamer');
room('B', 6200, 2500, 'hal', 'Entree', plafond);
room('B', 7900, 1100, 'slaapkamer', 'Logeerkamer', plafond);
room('B', 7900, 3300, 'berging', 'Berging', plafond);
const zB = (z) => z - D;
fx('keuken', 'A', X(7620), 490, 0, { w: 1580 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.58, kookplaat: false });
fx('keuken', 'A', X(8710), 2290, 90, { w: 4200 }, { front: 'antraciet', hogeKast: 'rechts', spoel: false, vaatwasser: false, kookplaat: true, kookplaatPlek: 0.43 });
fx('eettafel', 'A', X(5860), 1090, 0, { w: 1000, d: 1000 }, { vorm: 'ovaal', stoelen: 4 });
fx('bank', 'A', X(4020), 3770, 180, { w: 2550, d: 900 }, { chaise: 'rechts', chaiseLengte: 1300 });
fx('salontafel', 'A', X(4200), 2710, 0, { w: 800, d: 800 }, { vorm: 'rond' });
fx('fauteuil', 'A', X(3480), 1460, 330);
fx('bed', 'B', 1240, zB(6520), 270, { w: 1600, d: 2100 }, { nachtkastjes: false });
fx('kledingkast', 'B', 1700, zB(8710), 180, { w: 3000 });
fx('toilet', 'B', 3600, zB(6325), 270, { w: 1000 });
fx('wastafel', 'B', 3760, zB(7205), 0, { w: 800 });
fx('toilet', 'B', 3760, zB(8700), 180, { w: 1000 });
fx('douche', 'B', 5090, zB(8265), 180, { w: 900, d: 1500 }, { glas: 'rechts', glasLengte: 700 });
fx('bed', 'B', 7960, zB(6430), 90, { w: 900, d: 2100 }, { nachtkastjes: false });
fx('wasmachine', 'B', 7280, zB(7260), 0);
fx('boiler', 'B', 8700, zB(7260), 0);
computeParts(st, { skip3D: true });
return st;
}
// Model Rijn (examples/ontwerp_rijn.pdf): L-vorm 13 × 10 m, hoofdvolume 13 × 6,54 m (twee units van 3,27 m) met
// een vleugel van 4,2 × 3,46 m aan de zuidwestkant. Flauw zadeldak over het hoofdvolume als twee lessenaars die
// op de naad de nok vormen (een woningdak zou ook de vleugel overspannen); de vleugel heeft een plat dak op
// goothoogte. Alleen een naadwand naar de vleugel; de 160 mm wand langs de nok is een gewone binnenwand.
// Steenstrips rondom, in de oostgevel een donkere strook waar de schoorsteen staat. Maten buitenwerks vanaf de
// noordwesthoek, zoals de tekening (de steenstrips komen daar in de configurator buiten de units nog bij).
function createRijn() {
const st = newModelState('Rijn');
const S = st.settings, LO = 2800, HI = 3500, D = 3270, BRICK = 'R752', SCHOORSTEEN = 'R777';
Object.assign(S.facade, { type: 'steen', stone: BRICK });
Object.assign(S.trasraam, { enabled: true, height: 500, finish: 'R669' });
Object.assign(S.roof, { fasciaColor: 'wit', overhang: 300, fasciaHeight: 350 });
S.lighting.mode = 'spots';
S.lighting.direction = 'both';
if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true });
const A = createUnitEntry('A', null, { width: 13000, depth: D, height: LO, windowOn: [] });
const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 13000, depth: D, height: LO, windowOn: [] });
const C = createUnitEntry('C', { to: 'B', side: 'achter', align: 'start', offset: 0 }, { width: 4200, depth: 3460, height: LO, windowOn: [] });
st.units.push(A, B, C);
UnitModel.applyRoofForm(A.unit, { type: 'lessenaar', dir: 'achter', low: LO, high: HI });
UnitModel.applyRoofForm(B.unit, { type: 'lessenaar', dir: 'voor', low: LO, high: HI });
const { K, binnen, ops, iw, room, fx } = modelKit(st, LO, 90);
// Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand.
ops(A.unit.walls.voor, [
['raam', 500, 1400, 700, 'right', 1045, K('raam')],
['raam', 500, 1400, 700, 'right', 3630, K('raam')],
['raam', 1200, 500, 1600, 'right', 4760, K('raam')],
['raam', 1400, 1400, 700, 'right', 7560, K('raam')]]);
ops(A.unit.walls.rechts, [['raam', 1800, 2100, 0, 'right', 785, K('raam')]]);
ops(B.unit.walls.rechts, [
['deur', 900, 2100, 0, 'right', 730, K('deur', { leaf: 'rechts', hinge: 'rechts', swing: 'buiten', volglas: true })],
['deur', 900, 2100, 0, 'right', 1630, K('deur', { leaf: 'links', hinge: 'links', swing: 'buiten', volglas: true })]]);
ops(B.unit.walls.links, [['raam', 1400, 1100, 1000, 'left', 465, K('raam')]]);
ops(B.unit.walls.achter, [
['raam', 1100, 2100, 0, 'left', 4680, K('raam')],
['raam', 1800, 2100, 0, 'left', 10490, K('raam')]]);
ops(C.unit.walls.links, [['raam', 500, 700, 1400, 'left', 365, K('raam')]]);
ops(C.unit.walls.rechts, [['voordeur', 1100, 2100, 0, 'right', 70, K('deur', { hinge: 'links', swing: 'buiten', glas: true })]]);
ops(C.unit.walls.achter, [['raam', 1400, 1100, 1000, 'left', 1505, K('raam')]]);
// Schoorsteen midden in de oostgevel (z 2870–3670). u loopt van buiten gezien van links (zuid) naar rechts.
A.facade.walls.rechts = { type: 'steen', stone: SCHOORSTEEN };
A.unit.walls.rechts.splits = [{ u: 400, facade: { type: 'steen', stone: BRICK } }];
B.unit.walls.rechts.splits = [{ u: 2870, facade: { type: 'steen', stone: SCHOORSTEEN } }];
const DH = 2115;
// Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant.
iw(190, 3266, 6513, 3266, [['deur', 930, DH, 'left', 2800, binnen({ hinge: 'links', swing: 'buiten' })]], 160); // slaapkamer, badkamer | keuken (langs de nok)
iw(4380, 190, 4380, 3186, [['deur', 980, 2130, 'left', 1136, // slaapkamer | badkamer
Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]);
iw(6466, 190, 6466, 3186); // badkamer | woonkamer
iw(5800, 1135, 6421, 1135); // muurtje naast de douche
iw(2762, 3346, 2762, 6440, [['deur', 930, DH, 'left', 104, binnen({ hinge: 'links', swing: 'buiten' })]]); // berging | keuken
iw(1880, 6640, 1880, 7655, [['deur', 930, DH, 'left', 40, binnen({ hinge: 'links', swing: 'binnen' })]]); // toilet | entree
iw(190, 7700, 3107, 7700, [['deur', 930, DH, 'left', 1820, binnen({ hinge: 'links', swing: 'buiten' })]]); // toilet, entree | logeerkamer
iw(3062, 7745, 3062, 8335); // kapstoknis entree
iw(3107, 8290, 4010, 8290); // kapstoknis | kast logeerkamer
ensureSeamWalls(st, 'C');
const seam = st.interiorWalls.find(w => w.seam);
if (seam) {
const tC = WallModel.thicknessOf(WallModel.configFromState(C.unit.walls.links));
seam.wall.openings = [{ id: 1, type: 'deur', w: 930, h: DH, y: 0, anchor: 'left', offset: Math.round(2915 - tC),
fill: binnen({ hinge: 'links', swing: 'buiten' }) }];
}
const plafond = { ceiling: LO };
room('A', 9500, 1500, 'woonkeuken', 'Woonkamer/keuken');
room('A', 2600, 1700, 'slaapkamer', 'Slaapkamer', plafond);
room('A', 5400, 2000, 'badkamer', '', plafond);
room('B', 1400, 1500, 'berging', 'Berging', plafond);
room('C', 1000, 500, 'toilet');
room('C', 2400, 500, 'hal', 'Entree');
room('C', 1500, 2300, 'slaapkamer', 'Logeerkamer');
const zB = (z) => z - D, zC = (z) => z - 2 * D;
fx('kledingkast', 'A', 493, 1688, 270, { w: 2990 });
fx('bed', 'A', 2615, 1240, 0, { w: 1600, d: 2100 }, { nachtkastjes: false });
fx('douche', 'A', 5725, 640, 0, { w: 1400, d: 900 }, { glas: 'links', glasLengte: 900 });
fx('wastafel', 'A', 6191, 2000, 90, { w: 800 });
fx('toilet', 'A', 4998, 2680, 180);
fx('bank', 'A', 11195, 800, 0, { w: 3000, d: 1000 }, { chaise: 'rechts', chaiseLengte: 2100 });
fx('fauteuil', 'A', 8880, 1720, 315);
fx('salontafel', 'A', 10410, 2280, 0, { w: 800, d: 800 }, { vorm: 'rond' });
fx('keuken', 'B', 4945, zB(4350), 90, { w: 2000 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.15, kookplaat: true, kookplaatPlek: 0.85 });
fx('kookeiland', 'B', 5545, zB(4350), 270, { w: 2000, d: 600 }, { front: 'antraciet', kookplaat: false });
fx('keuken', 'B', 3110, zB(5080), 270, { w: 1200 }, { front: 'antraciet', hogeKast: 'rechts', spoel: false, vaatwasser: false });
fx('eettafel', 'B', 10010, zB(4990), 0, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 });
fx('wasmachine', 'B', 620, zB(6140), 180);
fx('wasmachine', 'B', 1450, zB(6140), 180); // droger
fx('boiler', 'B', 2350, zB(6215), 180);
fx('toilet', 'C', 535, zC(7150), 270);
fx('bed', 'C', 650, zC(8745), 0, { w: 900, d: 2000 }, { nachtkastjes: false });
fx('kledingkast', 'C', 3703, zC(9072), 90, { w: 1450 });
computeParts(st, { skip3D: true });
return st;
}
// Standaardmodellen: Bestand › Snel nieuw en het venster Nieuw. De sleutel is de template.
const MODELS = {
amstel: { label: 'Woning Amstel', icon: 'house-heart', create: createAmstel },
wetering: { label: 'Woning Wetering', icon: 'house-door', create: createWetering },
ijssel: { label: 'Woning IJssel', icon: 'houses', create: createIJssel },
waal: { label: 'Woning Waal', icon: 'house-add', create: createWaal },
maas: { label: 'Woning Maas', icon: 'house-check', create: createMaas },
rijn: { label: 'Woning Rijn', icon: 'house-gear', create: createRijn }
};
function createDefaultState(template) { function createDefaultState(template) {
if (template === 'amstel') return createAmstel(); if (MODELS[template]) return MODELS[template].create();
if (TEST_DEFAULT && (!template || template === 'single')) return createVlinderwoning(); if (TEST_DEFAULT && (!template || template === 'single')) return createVlinderwoning();
const state = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [], rooms: [], fixtures: [] }; const state = newModelState();
state.units.push(createUnitEntry('A', null, template === 'L' ? { width: 8000, depth: 3600 } : { width: 6000, depth: 3000 })); state.units.push(createUnitEntry('A', null, template === 'L' ? { width: 8000, depth: 3600 } : { width: 6000, depth: 3000 }));
if (template === 'L') { if (template === 'L') {
state.units.push(createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 4500, depth: 3600, windowOn: [] })); state.units.push(createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 4500, depth: 3600, windowOn: [] }));
@@ -443,7 +885,9 @@ window.HouseModel = (() => {
ws.materials = Object.assign({}, ws.materials, facadeMaterials(state, facadeFor(state, u, side))); ws.materials = Object.assign({}, ws.materials, facadeMaterials(state, facadeFor(state, u, side)));
}); });
if (u.roof.followFloor) { if (u.roof.followFloor) {
const e = cover[u.id] || { l: 0, r: 0, f: 0, b: 0 }; // Overkapping van het woningdak plus de eigen verlenging (veranda) van deze unit.
const c = cover[u.id] || {}, x = u.roof.extend || {};
const e = Object.fromEntries(['l', 'r', 'f', 'b'].map(k => [k, (c[k] || 0) + (x[k] || 0)]));
u.roof.deck.floor.width = u.unit.floor.floor.width + e.l + e.r; u.roof.deck.floor.width = u.unit.floor.floor.width + e.l + e.r;
u.roof.deck.floor.depth = u.unit.floor.floor.depth + e.f + e.b; u.roof.deck.floor.depth = u.unit.floor.floor.depth + e.f + e.b;
u.roof.offsetX = (e.r - e.l) / 2; u.roof.offsetX = (e.r - e.l) / 2;
@@ -452,6 +896,12 @@ window.HouseModel = (() => {
}); });
state.interiorWalls.forEach(iw => { iw.wall.wall.minSpacing = sp.interior.min; iw.wall.wall.maxSpacing = sp.interior.max; }); state.interiorWalls.forEach(iw => { iw.wall.wall.minSpacing = sp.interior.min; iw.wall.wall.maxSpacing = sp.interior.max; });
applyHouseRoof(state); applyHouseRoof(state);
applyRoofJoins(state);
}
const EXTEND_MAX = 4000;
function normalizeExtend(e) {
return Object.fromEntries(['f', 'r', 'b', 'l'].map(k => [k, Math.max(0, Math.min(EXTEND_MAX, Math.round(Number((e || {})[k]) || 0)))]));
} }
const houseRoofOn = (state) => !!(state.settings.roof.house && state.settings.roof.house.enabled && state.settings.roof.house.form); const houseRoofOn = (state) => !!(state.settings.roof.house && state.settings.roof.house.enabled && state.settings.roof.house.form);
@@ -527,7 +977,6 @@ window.HouseModel = (() => {
const shape = houseRoofShape(state, rects); const shape = houseRoofShape(state, rects);
if (!shape) return; if (!shape) return;
const form = state.settings.roof.house.form, ln = shape.line; const form = state.settings.roof.house.form, ln = shape.line;
if (form.type === 'mansarde' && ln.length === 5) Object.assign(form, { kneeIn: Math.round(ln[1].u), kneeH: Math.round(ln[1].h) });
if (form.type === 'wolf' && ln.length === 4) form.cut = Math.round(ln[1].h); if (form.type === 'wolf' && ln.length === 4) form.cut = Math.round(ln[1].h);
const planes = RoofModel.shapeLines(shape); const planes = RoofModel.shapeLines(shape);
state.units.forEach((u, i) => { state.units.forEach((u, i) => {
@@ -556,16 +1005,134 @@ window.HouseModel = (() => {
const a = pts[k], b = pts[k + 2]; const a = pts[k], b = pts[k + 2];
return q.h > a.h + (q.x - a.x) / (b.x - a.x) * (b.h - a.h) + 0.5; return q.h > a.h + (q.x - a.x) / (b.x - a.x) * (b.h - a.h) + 0.5;
}); });
const wall = u.unit.walls[wl.side].wall; setWallTop(u.unit.walls[wl.side].wall, W, pts[0].h, pts[pts.length - 1].h, inner);
const hL = Math.round(pts[0].h), hR = Math.round(pts[pts.length - 1].h);
const mid = inner.length === 1 && Math.abs(inner[0].x - W / 2) < 0.5;
Object.assign(wall, { heightLeft: hL, heightRight: hR, heightAll: Math.min(hL, hR), heightMid: mid ? Math.round(inner[0].h) : null });
if (inner.length && !mid) wall.topPts = inner.map(q => ({ f: q.x / W, h: Math.round(q.h) }));
else delete wall.topPts;
}); });
}); });
} }
function setWallTop(wall, W, h0, h1, inner) {
const hL = Math.round(h0), hR = Math.round(h1);
const mid = inner.length === 1 && Math.abs(inner[0].x - W / 2) < 0.5;
Object.assign(wall, { heightLeft: hL, heightRight: hR, heightAll: Math.min(hL, hR), heightMid: mid ? Math.round(inner[0].h) : null });
if (inner.length && !mid) wall.topPts = inner.map(q => ({ f: q.x / W, h: Math.round(q.h) }));
else delete wall.topPts;
}
// Dakaansluiting (L/T): een vleugel met nokdak (zadel, wolfsdak), nok haaks op de naad, tegen de
// langszijde van een hoofdvolume met nokdak loopt het hoofddak in tot zijn nok (kilkepers). Het wolfseind van de
// vleugel aan de naad vervalt. Ligt de vleugel gelijk met een kop van het hoofdvolume, dan wordt die kop een
// schild in de vlakken van de vleugel (hoekkeper, goot loopt door; een wolfseind daar vervalt); hooguit één kop,
// anders heeft het hoofdvolume geen kopgevel meer om zijn eigen dakvorm uit af te leiden. Een vleugel die in een
// wolfseind van het hoofdvolume zou steken (niet gelijk met de kop) sluit niet aan.
// Vlakken in wereldmaten; h = min(verre vlakken, wolfseinden, schildvlakken, max(min(nabije vlakken), vleugels)),
// zie RoofModel.computeJoinedRoof.
const JOIN_TYPES = ['zadel', 'wolf'];
function roofJoins(state, rects) {
const out = {};
if (houseRoofOn(state)) return out;
const rs = rects || unitRects(state);
if (rs.some(r => r.error) || overlappingPairs(rs).length) return out;
const at = (pl, [x, z]) => pl.a + pl.bx * x + pl.bz * z;
const info = state.units.map((u, i) => {
const r = rs[i], shape = RoofModel.computeShape(u.unit, UnitModel.computeLayout(u.unit, {}));
const planes = [];
if (!JOIN_TYPES.includes(shape.type)) return { u, r, shape, planes };
const sx = shape.axis === 'x';
const s = shape.type === 'wolf' && shape.hip
? Object.assign({}, shape, { hip: Object.assign({}, shape.hip, sx ? { q0: -r.D / 2, q1: r.D / 2 } : { q0: -r.W / 2, q1: r.W / 2 }) })
: shape;
const corners = [[r.x0, r.z0], [r.x1, r.z0], [r.x0, r.z1], [r.x1, r.z1]];
RoofModel.shapeLines(s).forEach(l => {
const [bx, bz] = sx ? [l.bp, l.bq] : [l.bq, l.bp];
const pl = { a: l.a - bx * r.cx - bz * r.cz, bx, bz };
// Wolfsdak: onder- en bovenstuk liggen (op afronding na) in één vlak.
if (!planes.some(q => corners.every(c => Math.abs(at(q, c) - at(pl, c)) < 2))) planes.push(pl);
});
return { u, r, shape, planes };
});
const groups = new Map(), role = {};
info.forEach(M => info.forEach(W => {
if (M === W || !M.planes.length || !W.planes.length || M.shape.axis === W.shape.axis) return;
if (role[M.u.id] === 'vleugel' || role[W.u.id]) return;
const seamX = M.shape.axis === 'z'; // naad evenwijdig aan de nok van M
const [m0, m1, w0, w1] = seamX ? [M.r.x0, M.r.x1, W.r.x0, W.r.x1] : [M.r.z0, M.r.z1, W.r.z0, W.r.z1];
const [a0, a1, b0, b1] = seamX ? [M.r.z0, M.r.z1, W.r.z0, W.r.z1] : [M.r.x0, M.r.x1, W.r.x0, W.r.x1];
const seam = Math.abs(a1 - b0) < EPS ? a1 : Math.abs(b1 - a0) < EPS ? a0 : null;
if (seam === null || w0 < m0 - EPS || w1 > m1 + EPS || (w0 < m0 + EPS && w1 > m1 - EPS)) return;
const xz = (t) => seamX ? [t, seam] : [seam, t];
const dir = seam === a1 ? 1 : -1; // vleugel aan de +kant (dwars op de naad) van M
const across = (pl) => seamX ? pl.bz : pl.bx, along = (pl) => seamX ? pl.bx : pl.bz;
const g = groups.get(M) || { M, seamX, wings: [], flush: null, drop: [] };
const end = Math.abs(w1 - m1) < EPS ? 1 : Math.abs(w0 - m0) < EPS ? 0 : -1;
const toEnd = (pl) => end ? along(pl) < 0 : along(pl) > 0; // daalt naar die kop
const flush = end >= 0 && !g.flush ? {
planes: W.planes.filter(pl => Math.abs(along(pl)) > 1e-9 && toEnd(pl)),
drop: M.planes.filter(pl => Math.abs(across(pl)) <= 1e-9 && toEnd(pl)),
side: seamX ? (end ? 'rechts' : 'links') : (end ? 'achter' : 'voor')
} : null;
const wingTop = Math.max(...W.shape.profile.map(q => q.h));
const hips = M.planes.filter(pl => Math.abs(across(pl)) <= 1e-9 && !g.drop.includes(pl) && !(flush && flush.drop.includes(pl)));
if (hips.some(pl => Math.min(at(pl, xz(w0)), at(pl, xz(w1))) < wingTop - 1)) return;
if (flush) { g.flush = flush; g.drop.push(...flush.drop); }
g.wings.push({ W, sign: -dir, roof: W.planes.filter(pl => Math.abs(along(pl)) > 1e-9 || across(pl) * dir < 0) });
groups.set(M, g);
role[M.u.id] = 'hoofd';
role[W.u.id] = 'vleugel';
}));
groups.forEach(g => {
const planes = [], idx = (pl) => planes.includes(pl) ? planes.indexOf(pl) : planes.push(pl) - 1;
const cross = (pl) => g.seamX ? pl.bz : pl.bx, terms = [];
[-1, 1].forEach(sg => {
const side = g.M.planes.filter(pl => Math.abs(cross(pl)) > 1e-9 && Math.sign(cross(pl)) === sg);
const ws = g.wings.filter(w => w.sign === sg);
if (!ws.length) side.forEach(pl => terms.push(idx(pl)));
else terms.push(['max', ['min', ...side.map(idx)], ...ws.map(w => ['min', ...w.roof.map(idx)])]);
});
g.M.planes.filter(pl => Math.abs(cross(pl)) <= 1e-9 && !g.drop.includes(pl)).forEach(pl => terms.push(idx(pl)));
if (g.flush) g.flush.planes.forEach(pl => terms.push(idx(pl)));
const expr = ['min', ...terms];
[g.M, ...g.wings.map(w => w.W)].forEach(X => { out[X.u.id] = { planes, expr, flush: X === g.M && g.flush ? g.flush.side : null }; });
});
return out;
}
// De kop die een schild wordt volgt het dak; vervalt de aansluiting, dan krijgt de unit zijn eigen dakvorm terug.
function applyRoofJoins(state) {
const rects = unitRects(state);
const joins = roofJoins(state, rects);
state.units.forEach((u, i) => {
const join = joins[u.id], side = join && join.flush;
if (Object.entries(u.unit.walls).some(([k, ws]) => ws.wall.joinTop && k !== side)) {
Object.values(u.unit.walls).forEach(ws => { delete ws.wall.joinTop; });
UnitModel.applyRoofForm(u.unit, UnitModel.roofForm(u.unit));
}
if (!side) return;
const r = rects[i];
const wl = UnitModel.computeLayout(u.unit, {}).walls.find(w => w.side === side);
const s = UnitModel.WALL_SIDES.find(x => x.key === side), alongX = s.axis === 'x';
const start = (alongX ? wl.cx + r.cx : wl.cz + r.cz) - s.dir * wl.width / 2;
const perp = alongX ? wl.cz + r.cz : wl.cx + r.cx, out = alongX ? wl.normal.y : wl.normal.x;
const faces = [perp + out * wl.thickness / 2, perp - out * wl.thickness / 2];
const pos = (x, f) => alongX ? { x: start + s.dir * x, z: f } : { x: f, z: start + s.dir * x };
const W = wl.width, xs = [0, W];
// Knikken alleen waar twee vlakken langs een wandvlak even hoog zijn.
faces.forEach(f => join.planes.forEach((p1, j) => join.planes.slice(j + 1).forEach(p2 => {
const h = (pl, x) => { const q = pos(x, f); return pl.a + pl.bx * q.x + pl.bz * q.z; };
const d0 = h(p1, 0) - h(p2, 0), d1 = h(p1, W) - h(p2, W);
if (Math.abs(d1 - d0) > 1e-9) { const x = -d0 / (d1 - d0) * W; if (x > 1 && x < W - 1) xs.push(x); }
})));
const hAt = (x) => Math.min(...faces.map(f => { const q = pos(x, f); return RoofModel.joinValue(join, q); }));
const pts = [...new Set(xs.map(x => Math.round(x * 10) / 10))].sort((a, b) => a - b).map(x => ({ x, h: hAt(x) }));
const inner = pts.slice(1, -1).filter((q, k) => {
const a = pts[k], b = pts[k + 2];
return Math.abs(q.h - (a.h + (q.x - a.x) / (b.x - a.x) * (b.h - a.h))) > 0.5;
});
const wall = u.unit.walls[side].wall;
setWallTop(wall, W, pts[0].h, pts[pts.length - 1].h, inner);
wall.joinTop = true;
});
}
function setHouseRoof(state, on) { function setHouseRoof(state, on) {
const hr = state.settings.roof.house; const hr = state.settings.roof.house;
if (!!on === houseRoofOn(state)) return; if (!!on === houseRoofOn(state)) return;
@@ -638,11 +1205,12 @@ window.HouseModel = (() => {
const { cuts, seams } = computeSeams(rects, wallThickness); const { cuts, seams } = computeSeams(rects, wallThickness);
const houseShape = houseRoofShape(state, rects); const houseShape = houseRoofShape(state, rects);
const coverExt = coverExtensions(state, rects); const coverExt = coverExtensions(state, rects);
const joins = roofJoins(state, rects);
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc) { function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop) {
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, materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop,
painted: state.settings.interiorPaint && shows('afwerking'), stage, rabatColor: state.settings.facade.rabatColor painted: state.settings.interiorPaint && shows('afwerking'), stage, 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) };
@@ -696,9 +1264,14 @@ window.HouseModel = (() => {
const deckFrame = FloorModel.computeFrame(u.roof.deck.floor); const deckFrame = FloorModel.computeFrame(u.roof.deck.floor);
const roofW = deckFrame.width, roofD = deckFrame.depth; const roofW = deckFrame.width, roofD = deckFrame.depth;
const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0; const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0;
const ur = RoofModel.computeUnitRoof(u.roof, shape, { x0: ox - roofW / 2, x1: ox + roofW / 2, z0: oz - roofD / 2, z1: oz + roofD / 2 }, rims); const roofRect = { x0: ox - roofW / 2, x1: ox + roofW / 2, z0: oz - roofD / 2, z1: oz + roofD / 2 };
const join = joins[u.id] && {
planes: joins[u.id].planes.map(pl => ({ a: pl.a + pl.bx * rect.cx + pl.bz * rect.cz, bx: pl.bx, bz: pl.bz })),
expr: joins[u.id].expr
};
const ur = join ? RoofModel.computeJoinedRoof(u.roof, join, roofRect, shape) : RoofModel.computeUnitRoof(u.roof, shape, roofRect, rims);
const roofCx = rect.cx + ox, roofCz = rect.cz + oz; const roofCx = rect.cx + ox, roofCz = rect.cz + oz;
const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel', mansarde: 'mansarde', wolf: 'wolfsdak' }[shape.type]; const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel', wolf: 'wolfsdak' }[shape.type];
const multi = ur.planes.length > 1; const multi = ur.planes.length > 1;
ur.planes.forEach((pl, i) => { ur.planes.forEach((pl, i) => {
pl.element = `${u.id}-D${i + 1}`; pl.element = `${u.id}-D${i + 1}`;
@@ -923,6 +1496,24 @@ window.HouseModel = (() => {
}); });
} }
// Buitenlagen tot de bovenkant van het dak, per punt: een steil vlak (wolfseind) is verticaal dikker.
function skinTopLine(p, roof) {
const cfg = p.config, W = cfg.wallWidth, tl = WallModel.topLine(cfg), zF = p.place.thickness / 2;
const xs = new Set([0, W].concat((cfg.wallTop || []).map(q => q.x)));
for (let x = 100; x < W; x += 100) xs.add(x);
const pts = [...xs].sort((a, b) => a - b).map(x => {
const top = tl.at(x - W / 2), w = UnitModel.localToWorld(p.place, x - W / 2, zF), wallTop = p.place.floorTop + top;
return { x, h: top + Math.max(0, roof.topAt(w.x, w.z) - Math.min(wallTop, roof.ceilingAt(w.x, w.z))) };
});
const L = pts[0], R = pts[pts.length - 1];
const inner = WallModel.wallTopPoints({ topPts: pts.slice(1, -1).map(q => ({ f: q.x / W, h: q.h })) }, W, Math.round(L.h), Math.round(R.h));
const line = [L, ...inner, R];
const at = (x) => { let i = 0; while (i < line.length - 2 && x > line[i + 1].x) i++; const a = line[i], b = line[i + 1]; return a.h + (x - a.x) / (b.x - a.x) * (b.h - a.h); };
// Alleen een bolle daklijn: een kil of sprong (binnenhoek L-vorm) zou de lijn erboven trekken; dan vast skinAbove.
if (pts.some(q => at(q.x) > q.h + 5)) return null;
return { hL: Math.round(L.h), hR: Math.round(R.h), pts: inner };
}
function topWedge(p, roof) { function topWedge(p, roof) {
const cfg = p.config, half = cfg.wallWidth / 2, zF = cfg.studDepth / 2, tl = WallModel.topLine(cfg); const cfg = p.config, half = cfg.wallWidth / 2, zF = cfg.studDepth / 2, tl = WallModel.topLine(cfg);
const xs = new Set([-half, half].concat(tl.kinks)); const xs = new Set([-half, half].concat(tl.kinks));
@@ -961,6 +1552,7 @@ window.HouseModel = (() => {
p.skinAbove = covered !== null p.skinAbove = covered !== null
? Math.max(0, Math.round(covered - 3 - wallTop)) ? Math.max(0, Math.round(covered - 3 - wallTop))
: Math.max(0, Math.round(u.roof.topAt(m.x, m.z) - Math.min(wallTop, u.roof.ceilingAt(m.x, m.z)))); : Math.max(0, Math.round(u.roof.topAt(m.x, m.z) - Math.min(wallTop, u.roof.ceilingAt(m.x, m.z))));
p.skinTop = covered !== null ? null : skinTopLine(p, u.roof);
p.underRoof = covered !== null; p.underRoof = covered !== null;
p.config.topWedge = topWedge(p, u.roof); p.config.topWedge = topWedge(p, u.roof);
// Binnenhoek-doorloop: tot de bovenkant van het dak van de buur geen buitenlagen (trimTop). // Binnenhoek-doorloop: tot de bovenkant van het dak van de buur geen buitenlagen (trimTop).
@@ -1017,7 +1609,7 @@ window.HouseModel = (() => {
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); 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);
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;
@@ -1040,6 +1632,10 @@ window.HouseModel = (() => {
const elektraGroup = build ? ElektraModel.build3D(elektra, o.elektraMaterials, stage) : null; const elektraGroup = build ? ElektraModel.build3D(elektra, o.elektraMaterials, stage) : null;
if (elektraGroup) elektraGroup.visible = o.elektraVisible !== false && shows('elektra'); if (elektraGroup) elektraGroup.visible = o.elektraVisible !== false && shows('elektra');
const groundY = units.length ? Math.max(...units.map(u => u.layout.floorTop)) - (state.settings.groundLevel || 0) : 0;
const posts = PostModel.compute(state.settings.roof.posts, units, groundY);
const postsGroup = build ? PostModel.build3D(posts, o.postMaterials) : null;
const sandwich = state.settings.sandwich && units.length const sandwich = state.settings.sandwich && units.length
? SandwichModel.compute(rects, Math.max(...units.map(u => u.layout.floorTop))) : null; ? SandwichModel.compute(rects, Math.max(...units.map(u => u.layout.floorTop))) : null;
const sandwichGroup = build && sandwich ? SandwichModel.build3D(sandwich, o.sandwichMaterials || SandwichModel.createMaterials(), { const sandwichGroup = build && sandwich ? SandwichModel.build3D(sandwich, o.sandwichMaterials || SandwichModel.createMaterials(), {
@@ -1052,7 +1648,7 @@ window.HouseModel = (() => {
const fixturesGroup = build ? FixtureModel.build3D(fixtures) : null; const fixturesGroup = build ? FixtureModel.build3D(fixtures) : null;
if (fixturesGroup) fixturesGroup.visible = shows('afwerking'); if (fixturesGroup) fixturesGroup.visible = shows('afwerking');
return { rects, seams, overlaps: overlappingPairs(rects), units, interior, roofEdge, roofEdgeGroup, lighting, lightingGroup, elektra, elektraGroup, sandwich, sandwichGroup, rooms, roomsGroup, fixtures, fixturesGroup }; return { rects, seams, overlaps: overlappingPairs(rects), units, interior, roofEdge, roofEdgeGroup, lighting, lightingGroup, elektra, elektraGroup, sandwich, sandwichGroup, posts, postsGroup, rooms, roomsGroup, fixtures, fixturesGroup };
} }
function allWallPieces(parts) { function allWallPieces(parts) {
@@ -1191,6 +1787,7 @@ window.HouseModel = (() => {
kg: u.floorSpecs.totalKg + u.roof.specs.totalKg + u.walls.reduce((a, w) => a + w.pieces.reduce((b, p) => b + p.specs.totalKg, 0), 0) kg: u.floorSpecs.totalKg + u.roof.specs.totalKg + u.walls.reduce((a, w) => a + w.pieces.reduce((b, p) => b + p.specs.totalKg, 0), 0)
})); }));
if (parts.interior.length) weights.push({ label: 'Binnenwanden', element: 'BW', kg: parts.interior.reduce((a, p) => a + p.specs.totalKg, 0) }); if (parts.interior.length) weights.push({ label: 'Binnenwanden', element: 'BW', kg: parts.interior.reduce((a, p) => a + p.specs.totalKg, 0) });
if (parts.posts.count) weights.push({ label: 'Palen overkapping', element: 'PAAL', kg: parts.posts.kg });
const isolatie = parts.units.map(u => ({ const isolatie = parts.units.map(u => ({
label: `Unit ${u.id}`, element: u.id, label: `Unit ${u.id}`, element: u.id,
@@ -1270,6 +1867,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 ? {
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
} : null,
roofEdge: parts.roofEdge ? { roofEdge: parts.roofEdge ? {
fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000, fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000,
fasciaHeight: parts.roofEdge.fasciaHeight, fasciaHeight: parts.roofEdge.fasciaHeight,
@@ -1357,9 +1958,11 @@ window.HouseModel = (() => {
state.settings.elektra = ElektraModel.normalizeSettings(state.settings.elektra); state.settings.elektra = ElektraModel.normalizeSettings(state.settings.elektra);
state.units.forEach(u => { state.units.forEach(u => {
if (!u.unit.layout) u.unit.layout = { corners: 'voor-achter' }; if (!u.unit.layout) u.unit.layout = { corners: 'voor-achter' };
UnitModel.dropMansarde(u.unit);
if (!u.roof) { u.roof = RoofModel.createDefaultRoof(u.unit.floor); delete u.roof.fascia; } if (!u.roof) { u.roof = RoofModel.createDefaultRoof(u.unit.floor); delete u.roof.fascia; }
if (u.roof.fascia === undefined) u.roof.fascia = oldRoof.edge !== 'daktrim'; if (u.roof.fascia === undefined) u.roof.fascia = oldRoof.edge !== 'daktrim';
if (!RoofModel.COVERINGS[u.roof.covering]) u.roof.covering = 'dakleer'; if (!RoofModel.COVERINGS[u.roof.covering]) u.roof.covering = 'dakleer';
u.roof.extend = normalizeExtend(u.roof.extend);
u.lighting = { walls: (u.lighting && u.lighting.walls) || {} }; u.lighting = { walls: (u.lighting && u.lighting.walls) || {} };
u.facade = { walls: (u.facade && u.facade.walls) || {} }; u.facade = { walls: (u.facade && u.facade.walls) || {} };
if (!hadFacade) Object.entries(u.unit.walls).forEach(([side, ws]) => { if (!hadFacade) Object.entries(u.unit.walls).forEach(([side, ws]) => {
@@ -1371,9 +1974,9 @@ window.HouseModel = (() => {
return { return {
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor, facadeMaterials, ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor, facadeMaterials,
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs, clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, MODELS, createDefaultState, unitRects, computeSeams, overlappingPairs,
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls, syncSeamWalls, missingSeamWalls, interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls, syncSeamWalls, missingSeamWalls,
houseRoofShape, houseRoofFits, setHouseRoof, coverExtensions, houseRoofShape, houseRoofFits, setHouseRoof, coverExtensions, EXTEND_MAX,
computeParts, allWallPieces, computeTotals, computeDimensions, dimensionRows, TRANSPORT, TRANSPORT_CLASSES, computeTransport, validateState, fromUnitDesign, normalize computeParts, allWallPieces, computeTotals, computeDimensions, dimensionRows, TRANSPORT, TRANSPORT_CLASSES, computeTransport, validateState, fromUnitDesign, normalize
}; };
})(); })();
+26 -11
View File
@@ -32,7 +32,8 @@ window.HousePdf = (() => {
list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u }); list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u });
u.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }))); u.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })));
u.roof.planes.forEach(pl => list.push({ kind: 'roof', element: pl.element, label: pl.label, plane: pl })); u.roof.planes.forEach(pl => list.push({ kind: 'roof', element: pl.element, label: pl.label, plane: pl }));
if (u.roof.unitRoof && u.roof.unitRoof.cap) list.push({ kind: 'cap', element: `${u.id}-D${u.roof.planes.length + 1}`, label: `Unit ${u.id} dak bovenste deel (wolfsdak)`, unit: u }); const cap = u.roof.unitRoof && u.roof.unitRoof.cap;
if (cap) list.push({ kind: 'cap', element: `${u.id}-D${u.roof.planes.length + 1}`, label: cap.kind === 'join' ? `Unit ${u.id} dak (aansluiting)` : `Unit ${u.id} dak bovenste deel (wolfsdak)`, unit: u });
}); });
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })); parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' }); list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
@@ -134,6 +135,10 @@ window.HousePdf = (() => {
parts.roofEdge.spots.forEach(sp => doc.circle(px(sp.x), py(sp.z), 0.8, 'FD')); parts.roofEdge.spots.forEach(sp => doc.circle(px(sp.x), py(sp.z), 0.8, 'FD'));
} }
doc.setLineDashPattern([], 0); doc.setLineDashPattern([], 0);
doc.setDrawColor(...BLACK);
doc.setLineWidth(0.2);
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'));
doc.setDrawColor(...BLACK); doc.setDrawColor(...BLACK);
doc.setFillColor(255, 211, 107); doc.setFillColor(255, 211, 107);
@@ -235,10 +240,10 @@ window.HousePdf = (() => {
const W = p.place.width, T = p.place.thickness; const W = p.place.width, T = p.place.thickness;
const c = WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc), e = p.skinExt || {}; const c = WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc), e = p.skinExt || {};
const xl = -W / 2 - (e.left > 0 ? e.left + c : 0), xr = W / 2 + (e.right > 0 ? e.right + c : 0); const xl = -W / 2 - (e.left > 0 ? e.left + c : 0), xr = W / 2 + (e.right > 0 ? e.right + c : 0);
const tl = WallModel.topLine(p.config), up = p.skinAbove || 0; const tl = WallModel.topLine(WallModel.skinConfig(p.config, p.skinAbove || 0, p.skinTop));
const top = (x) => tl.at(Math.max(-W / 2, Math.min(W / 2, x))) + up; const top = (x) => Math.min(tl.at(x), tl.at(Math.max(-W / 2, Math.min(W / 2, x))));
const loc = [{ x: xl, y: y0 }, { x: xr, y: y0 }, { x: xr, y: top(xr) }] const loc = [{ x: xl, y: y0 }, { x: xr, y: y0 }, { x: xr, y: top(xr) }]
.concat(tl.pts.slice().reverse().filter(q => q.x > xl + 1 && q.x < xr - 1).map(q => ({ x: q.x, y: q.y + up }))) .concat(tl.pts.slice().reverse().filter(q => q.x > xl + 1 && q.x < xr - 1).map(q => ({ x: q.x, y: q.y })))
.concat([{ x: xl, y: top(xl) }]); .concat([{ x: xl, y: top(xl) }]);
return loc; return loc;
}; };
@@ -282,7 +287,7 @@ window.HousePdf = (() => {
}; };
RoofModel.capRegions(r.unitRoof).forEach(rg => { RoofModel.capRegions(r.unitRoof).forEach(rg => {
pushRoof(rg.poly.map(pt => { pushRoof(rg.poly.map(pt => {
const x = u.rect.cx + (sh.axis === 'x' ? pt.x : pt.z), z = u.rect.cz + (sh.axis === 'x' ? pt.z : pt.x); const ax = r.unitRoof.alongX, x = u.rect.cx + (ax ? pt.x : pt.z), z = u.rect.cz + (ax ? pt.z : pt.x);
return { x, y: u.layout.floorTop + rg.under(pt) + rg.vThick, z }; return { x, y: u.layout.floorTop + rg.under(pt) + rg.vThick, z };
})); }));
}); });
@@ -457,7 +462,7 @@ window.HousePdf = (() => {
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts); PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
} }
const CAP_KINDS = { spoor: 'Spoor', hoekkeper: 'Hoekkeper', nokbalk: 'Nokbalk', kopbalk: 'Kopbalk' }; const CAP_KINDS = { spoor: 'Spoor', hoekkeper: 'Hoekkeper', kilkeper: 'Kilkeper', nokbalk: 'Nokbalk', knikbalk: 'Knikbalk', kopbalk: 'Kopbalk' };
function drawCapSheet(doc, u, sheetOpts) { function drawCapSheet(doc, u, sheetOpts) {
const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS; const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS;
const ur = u.roof.unitRoof, c = ur.cap; const ur = u.roof.unitRoof, c = ur.cap;
@@ -474,7 +479,8 @@ window.HousePdf = (() => {
}; };
doc.setTextColor(...BLACK); doc.setTextColor(...BLACK);
doc.setFontSize(11); doc.setFontSize(11);
doc.text('Bovenaanzicht bovenste deel (wolfseinden en nok)', X0, 20); const join = c.kind === 'join';
doc.text(join ? 'Bovenaanzicht dak (aansluiting: kil- en hoekkepers, kreupele sporen)' : 'Bovenaanzicht bovenste deel (wolfseinden en nok)', X0, 20);
doc.setDrawColor(...GREY); doc.setDrawColor(...GREY);
doc.setLineWidth(0.15); doc.setLineWidth(0.15);
c.regions.forEach(rg => poly(rg.poly.map(xz), 'S')); c.regions.forEach(rg => poly(rg.poly.map(xz), 'S'));
@@ -505,17 +511,22 @@ window.HousePdf = (() => {
const H = (p) => RoofModel.profileHeight(ur.shape.profile, p); const H = (p) => RoofModel.profileHeight(ur.shape.profile, p);
const deg = (t) => String(Math.round(Math.atan(t) * 1800 / Math.PI) / 10).replace('.', ','); const deg = (t) => String(Math.round(Math.atan(t) * 1800 / Math.PI) / 10).replace('.', ',');
const hip = c.regions.find(rg => rg.key === 'h1'); const hip = c.regions.find(rg => rg.key === 'h1');
const facts = [ const facts = (join ? [
['Goot (onderkant dak)', `${Math.round(c.eave)} mm`],
['Nok (onderkant dak)', `${Math.round(c.ridgeH)} mm`],
['Helling dakvlakken', `${deg(c.slope)}°`]
] : [
['Wolfseind (onderkant dak)', `${Math.round(c.hipH)} mm`], ['Wolfseind (onderkant dak)', `${Math.round(c.hipH)} mm`],
['Nok (onderkant dak)', `${Math.round(H(c.pR))} mm`], ['Nok (onderkant dak)', `${Math.round(H(c.pR))} mm`],
['Helling dakvlakken', `${deg(c.slope)}°`], ['Helling dakvlakken', `${deg(c.slope)}°`],
['Helling wolfseinden', hip ? `${deg(Math.tan(Math.acos(hip.cos)))}°` : '-'], ['Helling wolfseinden', hip ? `${deg(Math.tan(Math.acos(hip.cos)))}°` : '-']
]).concat([
['Balken', `${c.joistW} x ${c.joistH} mm`], ['Balken', `${c.joistW} x ${c.joistH} mm`],
['Oppervlak (schuin)', `${fmt(c.specs.osbAreaM2, 2)} m²`], ['Oppervlak (schuin)', `${fmt(c.specs.osbAreaM2, 2)} m²`],
['Hout', `${fmt(c.specs.joistWoodM3, 3)} m³`] ['Hout', `${fmt(c.specs.joistWoodM3, 3)} m³`]
]; ]);
doc.setTextColor(...BLACK); doc.setTextColor(...BLACK);
let y = PdfSheet.table(doc, 290, 20, [0, 52], ['Gegeven', 'Waarde'], facts, { title: 'Wolfsdak, bovenste deel', fontSize: 7, rowH: 4.6, lastColW: 40 }); let y = PdfSheet.table(doc, 290, 20, [0, 52], ['Gegeven', 'Waarde'], facts, { title: join ? 'Dak met aansluiting' : 'Wolfsdak, bovenste deel', fontSize: 7, rowH: 4.6, lastColW: 40 });
PdfSheet.table(doc, 290, y + 6, [0, 32, 56, 78], ['Soort', 'Lengte (mm)', 'Hoek (°)', 'Aantal'], rows, { title: 'Stuklijst balken', fontSize: 7, rowH: 4.6, lastColW: 14, maxY: 240 }); PdfSheet.table(doc, 290, y + 6, [0, 32, 56, 78], ['Soort', 'Lengte (mm)', 'Hoek (°)', 'Aantal'], rows, { title: 'Stuklijst balken', fontSize: 7, rowH: 4.6, lastColW: 14, maxY: 240 });
doc.setFontSize(7); doc.setFontSize(7);
doc.setTextColor(...GREY); doc.setTextColor(...GREY);
@@ -579,6 +590,10 @@ window.HousePdf = (() => {
rows.push(['Boeideel-spots', e.spots + ' stuks']); rows.push(['Boeideel-spots', e.spots + ' stuks']);
rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]); rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]);
rows.push(['Binnenspots', `${L.indoor} stuks (plafond)`]); rows.push(['Binnenspots', `${L.indoor} stuks (plafond)`]);
if (totals.posts) {
const pp = totals.posts;
rows.push(['Palen overkapping', `${pp.count} stuks ${pp.wood.toLowerCase()} ${pp.size}x${pp.size} mm, ${fmt(pp.lengthM, 2)} m, ${fmt(pp.m3, 3)} m3, op paalvoeten`]);
}
PdfSheet.table(doc, 120, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & verlichting')); PdfSheet.table(doc, 120, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & verlichting'));
} }
+53 -31
View File
@@ -327,6 +327,7 @@ function mountHouseEditor(container, callbacks) {
const roofMaterials = RoofModel.createMaterials(); const roofMaterials = RoofModel.createMaterials();
const lightingMaterials = LightingModel.createMaterials(); const lightingMaterials = LightingModel.createMaterials();
const sandwichMaterials = SandwichModel.createMaterials(); const sandwichMaterials = SandwichModel.createMaterials();
const postMaterials = PostModel.createMaterials();
const elektraMaterials = ElektraModel.createMaterials(); const elektraMaterials = ElektraModel.createMaterials();
let houseGroup = null; let houseGroup = null;
let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls); let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls);
@@ -975,6 +976,12 @@ function mountHouseEditor(container, callbacks) {
ctx.beginPath(); ctx.arc(q.x, q.y, 3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(q.x, q.y, 3, 0, Math.PI * 2); ctx.fill();
}); });
} }
ctx.setLineDash([]);
ctx.fillStyle = '#a07c54'; ctx.strokeStyle = '#e8d3b0'; ctx.lineWidth = 1;
parts.posts.posts.forEach(pp => {
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.restore(); ctx.restore();
ctx.strokeStyle = '#ffe9a8'; ctx.lineWidth = 1.5; ctx.strokeStyle = '#ffe9a8'; ctx.lineWidth = 1.5;
@@ -1228,7 +1235,7 @@ function mountHouseEditor(container, callbacks) {
toggle('hs-roof-house', 'Over hele woning', hr.enabled, { hint: 'Eén dakvorm over alle units (bijv. een grote wolfskap); uit = dak per unit. De daken per unit blijven bewaard.' }) + toggle('hs-roof-house', 'Over hele woning', hr.enabled, { hint: 'Eén dakvorm over alle units (bijv. een grote wolfskap); uit = dak per unit. De daken per unit blijven bewaard.' }) +
(houseRoofUi ? sel('hs-roof-cover', 'Bedekking', covOpts, RoofModel.coveringFor(state.units[0].roof.covering, shapeType), { hint: 'Voor alle units (per unit aan te passen)' }) : '') + (houseRoofUi ? sel('hs-roof-cover', 'Bedekking', covOpts, RoofModel.coveringFor(state.units[0].roof.covering, shapeType), { hint: 'Voor alle units (per unit aan te passen)' }) : '') +
toggle('hs-steel-ridge', 'Stalen nokbalk', state.units.some(u => u.roof.steelRidge !== false), { hint: 'IPE onder de nok van zadel-, mansarde- en wolfsdaken (alle units); uit = verwijderen' }) } toggle('hs-steel-ridge', 'Stalen nokbalk', state.units.some(u => u.roof.steelRidge !== false), { hint: 'IPE onder de nok van zadel- en wolfsdaken (alle units); uit = verwijderen' }) }
]; ];
if (houseRoofUi) groups.push(...houseRoofUi.groups()); if (houseRoofUi) groups.push(...houseRoofUi.groups());
else groups.push({ label: 'Dakvorm', html: note('Vorm, hoogtes, helling en bedekking zijn per unit: selecteer een unit.') }); else groups.push({ label: 'Dakvorm', html: note('Vorm, hoogtes, helling en bedekking zijn per unit: selecteer een unit.') });
@@ -1238,6 +1245,12 @@ function mountHouseEditor(container, callbacks) {
UI.range('hs-fascia-h', 'Hoogte', r.fasciaHeight, { min: 150, max: 500, step: 10 }) + UI.range('hs-fascia-h', 'Hoogte', r.fasciaHeight, { min: 150, max: 500, step: 10 }) +
sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor) }, sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor) },
{ 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:
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) +
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-min', 'Min. uitkraging', r.posts.minExt, { step: 100, hint: 'Kleinere overstekken krijgen geen palen' }) : '') },
{ label: 'Daken per unit', html: `<div data-live="roof-list" class="rb-cols"></div>` }); { label: 'Daken per unit', html: `<div data-live="roof-list" class="rb-cols"></div>` });
return groups; return groups;
}, },
@@ -1252,11 +1265,17 @@ function mountHouseEditor(container, callbacks) {
UI.onRange('hs-fascia-h', v => { if (v > 0) r.fasciaHeight = v; }, after); UI.onRange('hs-fascia-h', v => { if (v > 0) r.fasciaHeight = v; }, after);
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.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.onNum('hs-posts-span', v => { if (v >= 1000) r.posts.maxSpan = v; }, after, pos);
UI.onNum('hs-posts-min', v => { if (v >= 400) r.posts.minExt = v; }, after, pos);
const list = () => { const list = () => {
const el = body.querySelector('[data-live="roof-list"]'); const el = body.querySelector('[data-live="roof-list"]');
if (!el || !parts) return; if (!el || !parts) return;
el.innerHTML = parts.units.map(u => { el.innerHTML = parts.units.map(u => {
const pl = u.roof.planes[0], deg = Math.abs(pl ? pl.theta : 0) * 180 / Math.PI; const pl = u.roof.planes[0], cap = u.roof.unitRoof.cap;
const deg = (pl ? Math.abs(pl.theta) : cap ? Math.atan(cap.slope) : 0) * 180 / Math.PI;
const cov = (RoofModel.COVERINGS[u.roof.covering] || {}).label || ''; const cov = (RoofModel.COVERINGS[u.roof.covering] || {}).label || '';
return kv(`Unit ${u.id}`, `${(UnitModel.ROOF_TYPES[u.roof.shape.type] || u.roof.shape.type).toLowerCase()}${deg > 0.05 ? ' ' + fmtDeg(deg) + '°' : ''} · ${cov.toLowerCase()}`); return kv(`Unit ${u.id}`, `${(UnitModel.ROOF_TYPES[u.roof.shape.type] || u.roof.shape.type).toLowerCase()}${deg > 0.05 ? ' ' + fmtDeg(deg) + '°' : ''} · ${cov.toLowerCase()}`);
}).join(''); }).join('');
@@ -1392,7 +1411,8 @@ function mountHouseEditor(container, callbacks) {
groups.push({ label: 'Meterkast', html: num('he-mk-w', 'Breedte', e.meterkast.w, { step: 10 }) + num('he-mk-h', 'Hoogte', e.meterkast.h, { step: 10 }) + groups.push({ label: 'Meterkast', html: num('he-mk-w', 'Breedte', e.meterkast.w, { step: 10 }) + num('he-mk-h', 'Hoogte', e.meterkast.h, { step: 10 }) +
num('he-mk-d', 'Diepte', e.meterkast.d, { step: 10 }) + num('he-mk-y', 'Onderkant', e.meterkast.y, { step: 10, hint: 'Vanaf bovenkant vloer; zo dicht mogelijk bij de voordeur' }) }); num('he-mk-d', 'Diepte', e.meterkast.d, { step: 10 }) + num('he-mk-y', 'Onderkant', e.meterkast.y, { step: 10, hint: 'Vanaf bovenkant vloer; zo dicht mogelijk bij de voordeur' }) });
groups.push({ label: 'Punten', html: num('he-sw-h', 'Schakelaar', e.switchHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) + groups.push({ label: 'Punten', html: num('he-sw-h', 'Schakelaar', e.switchHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) +
num('he-wcd-h', 'Wcd', e.wcdHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) + num('he-wcd-n', 'Wcd per unit', e.wcdPerUnit, { step: 1, hint: 'Dubbele wcd' }) }); num('he-wcd-h', 'Wcd', e.wcdHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) + num('he-wcd-n', 'Wcd per unit', e.wcdPerUnit, { step: 1, hint: 'Dubbele wcd' }) +
sel('he-plate', 'Kleur', Object.fromEntries(Object.entries(ElektraModel.PLATE_COLORS).map(([k, c]) => [k, c.label])), e.plateColor || 'wit', { hint: 'Schakelaars, wcd\'s en vloerdozen' }) });
groups.push({ label: 'Apparaten', html: Object.entries(ElektraModel.APPLIANCES).map(([k, A]) => groups.push({ label: 'Apparaten', html: Object.entries(ElektraModel.APPLIANCES).map(([k, A]) =>
check(`he-app-${k}`, `${A.label} (${A.code})`, !!e.apparaten[k])).join('') }); check(`he-app-${k}`, `${A.label} (${A.code})`, !!e.apparaten[k])).join('') });
const nManual = Object.keys(e.place).length; const nManual = Object.keys(e.place).length;
@@ -1422,6 +1442,7 @@ function mountHouseEditor(container, callbacks) {
UI.onNum('he-sw-h', v => { e.switchHeight = v; }, after, pos); UI.onNum('he-sw-h', v => { e.switchHeight = v; }, after, pos);
UI.onNum('he-wcd-h', v => { e.wcdHeight = v; }, after, pos); UI.onNum('he-wcd-h', v => { e.wcdHeight = v; }, after, pos);
UI.onNum('he-wcd-n', v => { e.wcdPerUnit = Math.max(0, Math.round(v)); }, after); UI.onNum('he-wcd-n', v => { e.wcdPerUnit = Math.max(0, Math.round(v)); }, after);
UI.onSel('he-plate', v => { e.plateColor = v; }, rerender);
UI.onNum('he-slack', v => { e.slack = v; }, after); UI.onNum('he-slack', v => { e.slack = v; }, after);
UI.onNum('he-res', v => { e.reserve = v; }, after); UI.onNum('he-res', v => { e.reserve = v; }, after);
UI.onNum('he-mk-res', v => { e.meterkastReserve = v; }, after); UI.onNum('he-mk-res', v => { e.meterkastReserve = v; }, after);
@@ -1522,11 +1543,13 @@ function mountHouseEditor(container, callbacks) {
} }
const FX_OPT_UI = { const FX_OPT_UI = {
front: { label: 'Front', kind: 'sel', options: () => Object.fromEntries(Object.entries(FixtureModel.FRONTS).map(([k, f]) => [k, f.label])) }, front: { label: 'Front', kind: 'sel', options: () => Object.fromEntries(Object.entries(FixtureModel.FRONTS).map(([k, f]) => [k, f.label])) },
spoelbak: { label: 'Spoelbak', kind: 'range', min: 0, max: 1, step: 0.01, unit: '', hint: '0 = links, 1 = rechts (van voren gezien)' }, spoel: { label: 'Spoelbak', kind: 'toggle' },
spoelbak: { label: 'Plek spoelbak', kind: 'range', min: 0, max: 1, step: 0.01, unit: '', hint: '0 = links, 1 = rechts (van voren gezien)' },
hogeKast: { label: 'Hoge kast', kind: 'sel', options: () => ({ geen: 'Geen', links: 'Links', rechts: 'Rechts' }), hint: 'Inbouwkoelkast + oven, 600 mm' }, hogeKast: { label: 'Hoge kast', kind: 'sel', options: () => ({ geen: 'Geen', links: 'Links', rechts: 'Rechts' }), hint: 'Inbouwkoelkast + oven, 600 mm' },
vaatwasser: { label: 'Vaatwasser', kind: 'toggle' }, vaatwasser: { label: 'Vaatwasser', kind: 'toggle' },
bovenkasten: { label: 'Bovenkasten', kind: 'toggle' }, bovenkasten: { label: 'Bovenkasten', kind: 'toggle' },
kookplaat: { label: 'Kookplaat', kind: 'toggle', hint: 'Inductie, 4 zones' }, kookplaat: { label: 'Kookplaat', kind: 'toggle', hint: 'Inductie, 4 zones' },
kookplaatPlek: { label: 'Plek kookplaat', kind: 'range', min: 0, max: 1, step: 0.01, unit: '', hint: '0 = links, 1 = rechts (van voren gezien)' },
glas: { label: 'Glaswand', kind: 'sel', options: () => ({ links: 'Links', rechts: 'Rechts', geen: 'Geen' }), hint: 'Van voren gezien' }, glas: { label: 'Glaswand', kind: 'sel', options: () => ({ links: 'Links', rechts: 'Rechts', geen: 'Geen' }), hint: 'Van voren gezien' },
glasLengte: { label: 'Lengte glas', kind: 'num', step: 50 }, glasLengte: { label: 'Lengte glas', kind: 'num', step: 50 },
spiegel: { label: 'Spiegel', kind: 'toggle' }, spiegel: { label: 'Spiegel', kind: 'toggle' },
@@ -1752,10 +1775,6 @@ function mountHouseEditor(container, callbacks) {
const LIM = UnitModel.LIMITS; const LIM = UnitModel.LIMITS;
const runOf = () => UnitModel.hasRidge(form.type) ? o.span() / 2 : o.span(); const runOf = () => UnitModel.hasRidge(form.type) ? o.span() / 2 : o.span();
const degOf = () => sloped() ? Math.atan(Math.max(0, form.high - form.low) / runOf()) * 180 / Math.PI : 0; const degOf = () => sloped() ? Math.atan(Math.max(0, form.high - form.low) / runOf()) * 180 / Math.PI : 0;
const mansardeDeg = () => {
const kIn = Math.max(1, form.kneeIn || 1), rest = Math.max(1, runOf() - kIn);
return [Math.atan(Math.max(0, form.kneeH - form.low) / kIn), Math.atan(Math.max(0, form.high - form.kneeH) / rest)].map(r => r * 180 / Math.PI);
};
const f = (k) => `${id}-roof-${k}`; const f = (k) => `${id}-roof-${k}`;
const groups = () => { const groups = () => {
const g = [ const g = [
@@ -1766,12 +1785,8 @@ function mountHouseEditor(container, callbacks) {
]; ];
if (sloped()) g.push({ label: 'Helling', html: if (sloped()) g.push({ label: 'Helling', html:
UI.range(f('high'), UnitModel.hasRidge(form.type) ? 'Nok' : 'Hoog', form.high, { min: 2100, max: LIM.ridge, step: 10, hint: `Max ${LIM.ridge} mm` }) + UI.range(f('high'), UnitModel.hasRidge(form.type) ? 'Nok' : 'Hoog', form.high, { min: 2100, max: LIM.ridge, step: 10, hint: `Max ${LIM.ridge} mm` }) +
(form.type === 'mansarde' ? '' : UI.range(f('deg'), 'Helling', Math.round(degOf() * 10) / 10, { min: 1, max: 60, step: 0.5, unit: '°', hint: 'Zet de hoge zijde/nok bij de huidige goothoogte' })) + UI.range(f('deg'), 'Helling', Math.round(degOf() * 10) / 10, { min: 1, max: 60, step: 0.5, unit: '°', hint: 'Zet de hoge zijde/nok bij de huidige goothoogte' }) +
(o.gable ? `<div data-live="roof-gable">${kv('Kopgevel', `wand ${SIDE_SHORT[UnitModel.gableOf(form)].toLowerCase()} + overstaande`)}</div>` : '') }); (o.gable ? `<div data-live="roof-gable">${kv('Kopgevel', `wand ${SIDE_SHORT[UnitModel.gableOf(form)].toLowerCase()} + overstaande`)}</div>` : '') });
if (form.type === 'mansarde') g.push({ label: 'Mansarde', html:
UI.range(f('knee-h'), 'Knik', form.kneeH, { min: 2100, max: LIM.ridge, step: 10, hint: 'Hoogte van de knik tussen het steile en het flauwe dakvlak' }) +
UI.range(f('knee-in'), 'Knik in', form.kneeIn, { min: 100, max: 3000, step: 10, hint: 'Horizontale afstand van de gevel tot de knik' }) +
`<div data-live="roof-mansarde">${kv('Hellingen', mansardeDeg().map(d => fmtDeg(d) + '°').join(' / '))}</div>` });
if (form.type === 'wolf') g.push({ label: 'Wolfsdak', html: if (form.type === 'wolf') g.push({ label: 'Wolfsdak', html:
UI.range(f('cut'), 'Wolfseind', form.cut, { min: 2100, max: LIM.ridge, step: 10, hint: 'Hoogte waarop de kopgevels stoppen; daarboven driehoekige wolfseinden met de dakhelling tot de nok' }) }); UI.range(f('cut'), 'Wolfseind', form.cut, { min: 2100, max: LIM.ridge, step: 10, hint: 'Hoogte waarop de kopgevels stoppen; daarboven driehoekige wolfseinden met de dakhelling tot de nok' }) });
return g; return g;
@@ -1787,13 +1802,6 @@ function mountHouseEditor(container, callbacks) {
if (v !== 'plat' && !(form.high > form.low)) form.high = form.low + 700; if (v !== 'plat' && !(form.high > form.low)) form.high = form.low + 700;
if (v === 'lessenaar' && !form.dir) form.dir = 'voor'; if (v === 'lessenaar' && !form.dir) form.dir = 'voor';
if (UnitModel.hasRidge(v) && !form.ridge) form.ridge = 'z'; if (UnitModel.hasRidge(v) && !form.ridge) form.ridge = 'z';
if (v === 'mansarde') {
const run = runOf();
form.kneeIn = Math.round(run * 0.35);
form.kneeH = Math.round(form.low + form.kneeIn * 2.1);
form.high = Math.min(LIM.ridge, Math.max(form.high, Math.round(form.kneeH + (run - form.kneeIn) * 0.5)));
form.kneeRatio = Math.min(0.9, (form.kneeH - form.low) / (form.high - form.low));
}
if (v === 'wolf') { if (v === 'wolf') {
if (form.high - form.low < 1000) form.high = form.low + 1500; if (form.high - form.low < 1000) form.high = form.low + 1500;
form.cutRatio = 0.6; form.cutRatio = 0.6;
@@ -1804,13 +1812,9 @@ function mountHouseEditor(container, callbacks) {
UI.onSel(f('ridge'), v => { form.ridge = v; apply(); }, rerender); UI.onSel(f('ridge'), v => { form.ridge = v; apply(); }, rerender);
const syncDeg = () => { const syncDeg = () => {
const now = o.readBack(); const now = o.readBack();
if (now && now.type === form.type) ['kneeH', 'kneeIn', 'cut'].forEach(k => { if (now[k] !== undefined) form[k] = now[k]; }); if (now && now.type === form.type) ['cut'].forEach(k => { if (now[k] !== undefined) form[k] = now[k]; });
UI.setRange(f('deg'), Math.round(degOf() * 10) / 10); UI.setRange(f('deg'), Math.round(degOf() * 10) / 10);
UI.setRange(f('knee-h'), form.kneeH);
UI.setRange(f('knee-in'), form.kneeIn);
UI.setRange(f('cut'), form.cut); UI.setRange(f('cut'), form.cut);
const md = body.querySelector('[data-live="roof-mansarde"]');
if (md && form.type === 'mansarde') md.innerHTML = kv('Hellingen', mansardeDeg().map(d => fmtDeg(d) + '°').join(' / '));
}; };
UI.onRange(f('low'), v => { UI.onRange(f('low'), v => {
if (!(v >= 500)) return; if (!(v >= 500)) return;
@@ -1830,8 +1834,6 @@ function mountHouseEditor(container, callbacks) {
apply(); syncDeg(); apply(); syncDeg();
}, after); }, after);
const ratioOf = (v) => Math.max(0.05, Math.min(0.95, (v - form.low) / Math.max(1, form.high - form.low))); const ratioOf = (v) => Math.max(0.05, Math.min(0.95, (v - form.low) / Math.max(1, form.high - form.low)));
UI.onRange(f('knee-h'), v => { if (v >= 500) { form.kneeRatio = ratioOf(v); apply(); syncDeg(); } }, after);
UI.onRange(f('knee-in'), v => { if (v >= 50) { form.kneeIn = v; apply(); syncDeg(); } }, after);
UI.onRange(f('cut'), v => { if (v >= 500) { form.cutRatio = ratioOf(v); apply(); syncDeg(); } }, after); UI.onRange(f('cut'), v => { if (v >= 500) { form.cutRatio = ratioOf(v); apply(); syncDeg(); } }, after);
}; };
return { groups, wire }; return { groups, wire };
@@ -1875,9 +1877,12 @@ function mountHouseEditor(container, callbacks) {
pair(sel('hp-roof-cover', 'Bedekking', covOpts, covNow, { hint: shapeNow === 'plat' ? 'Riet en felsplaten alleen bij een hellend dak' : `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²` }), pair(sel('hp-roof-cover', 'Bedekking', covOpts, covNow, { hint: shapeNow === 'plat' ? 'Riet en felsplaten alleen bij een hellend dak' : `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²` }),
thumb(RoofModel.coveringThumbUrl(covNow, 64), c.label)) + thumb(RoofModel.coveringThumbUrl(covNow, 64), c.label)) +
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim' }) + toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim' }) +
(pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'mansarde', 'wolf'].includes(shapeNow) (pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'wolf'].includes(shapeNow)
? toggle('hp-steel-ridge', 'Stalen nokbalk', u.roof.steelRidge !== false, { hint: pu && pu.roof.unitRoof.ridgeBeam ? `${pu.roof.unitRoof.ridgeBeam.profile.name}, ${Math.round(pu.roof.unitRoof.ridgeBeam.steelKg)} kg; uit = verwijderen` : 'Uit = verwijderd' }) : '') + ? toggle('hp-steel-ridge', 'Stalen nokbalk', u.roof.steelRidge !== false, { hint: pu && pu.roof.unitRoof.ridgeBeam ? `${pu.roof.unitRoof.ridgeBeam.profile.name}, ${Math.round(pu.roof.unitRoof.ridgeBeam.steelKg)} kg; uit = verwijderen` : 'Uit = verwijderd' }) : '') +
toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) }); toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) });
if (u.roof.followFloor) groups.push({ label: 'Overkapping', html:
['f', 'r', 'b', 'l'].map(k => num('hp-ext-' + k, { f: 'Noord', r: 'Oost', b: 'Zuid', l: 'West' }[k], (u.roof.extend || {})[k] || 0,
{ hint: k === 'f' ? `Dak verlengen voorbij de gevel (veranda), max. ${HouseModel.EXTEND_MAX} mm; palen volgen vanzelf` : '' })).join('') });
if (!u.roof.followFloor) groups.push({ label: 'Dakmaat', html: if (!u.roof.followFloor) groups.push({ label: 'Dakmaat', html:
num('hp-roof-w', 'Breedte', u.roof.deck.floor.width, { hint: 'West–oost' }) + num('hp-roof-d', 'Diepte', u.roof.deck.floor.depth, { hint: 'Noord–zuid' }) + num('hp-roof-w', 'Breedte', u.roof.deck.floor.width, { hint: 'West–oost' }) + num('hp-roof-d', 'Diepte', u.roof.deck.floor.depth, { hint: 'Noord–zuid' }) +
num('hp-roof-ox', 'Versch. oost', u.roof.offsetX || 0) + num('hp-roof-oz', 'Versch. zuid', u.roof.offsetZ || 0) }); num('hp-roof-ox', 'Versch. oost', u.roof.offsetX || 0) + num('hp-roof-oz', 'Versch. zuid', u.roof.offsetZ || 0) });
@@ -1915,6 +1920,9 @@ function mountHouseEditor(container, callbacks) {
UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender); UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender);
UI.onToggle('hp-steel-ridge', v => { u.roof.steelRidge = v; }, rerender); UI.onToggle('hp-steel-ridge', v => { u.roof.steelRidge = v; }, rerender);
UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender); UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender);
['f', 'r', 'b', 'l'].forEach(k => UI.onNum('hp-ext-' + k, v => {
u.roof.extend = Object.assign({ f: 0, r: 0, b: 0, l: 0 }, u.roof.extend, { [k]: Math.max(0, Math.min(HouseModel.EXTEND_MAX, v)) });
}, after));
UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, after); UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, after);
UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, after); UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, after);
UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, after); UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, after);
@@ -2142,6 +2150,12 @@ function mountHouseEditor(container, callbacks) {
rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.'], ['Binnenspots', totals.lamps.indoor, 'st.']); rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.'], ['Binnenspots', totals.lamps.indoor, 'st.']);
cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows)); cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows));
} }
if (totals.posts) {
const pp = totals.posts;
cards.push(UI.specCard('Overkapping', 'columns', [
[`${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.']]));
}
// 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;
cards.push(UI.specCard('Zaagmachine (tijdelijk)', 'scissors', [ cards.push(UI.specCard('Zaagmachine (tijdelijk)', 'scissors', [
@@ -2184,7 +2198,7 @@ function mountHouseEditor(container, callbacks) {
const painted = state.settings.interiorPaint && BuildStages.shows(view3D.stage, 'afwerking'); const painted = state.settings.interiorPaint && BuildStages.shows(view3D.stage, 'afwerking');
wallMaterials.spaanplaatMaterial.color.set(painted ? state.settings.interiorColor : SPAANPLAAT_COLOR); wallMaterials.spaanplaatMaterial.color.set(painted ? state.settings.interiorColor : SPAANPLAAT_COLOR);
roofMaterials.ceilingMaterial.color.set(state.settings.ceilingColor || '#f4f3ef'); roofMaterials.ceilingMaterial.color.set(state.settings.ceilingColor || '#f4f3ef');
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, elektraMaterials, stage: view3D.stage, lampenVisible: view3D.lampen, elektraVisible: view3D.elektra }); parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, postMaterials, elektraMaterials, stage: view3D.stage, lampenVisible: view3D.lampen, elektraVisible: view3D.elektra });
reportDirty = true; reportDirty = true;
if (houseGroup) { scene.remove(houseGroup); disposeGroup(houseGroup); } if (houseGroup) { scene.remove(houseGroup); disposeGroup(houseGroup); }
@@ -2218,6 +2232,7 @@ function mountHouseEditor(container, callbacks) {
houseGroup.add(u.roof.group); 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.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);
@@ -2273,7 +2288,7 @@ function mountHouseEditor(container, callbacks) {
renderNav(parts); renderNav(parts);
} }
const SIGN = { ahead: 4000, aside: 1800, poleH: 2200, poleW: 80, plateW: 1500, plateD: 30 }; const SIGN = { ahead: 4000, aside: 1800, clear: 1200, poleH: 2200, poleW: 80, plateW: 1500, plateD: 30 };
let signPos = null; let signPos = null;
let signMaterial = null; let signMaterial = null;
const signPoleMat = new THREE.MeshStandardMaterial({ color: 0x2b2b2b, roughness: 0.6, metalness: 0.3 }); const signPoleMat = new THREE.MeshStandardMaterial({ color: 0x2b2b2b, roughness: 0.6, metalness: 0.3 });
@@ -2302,7 +2317,14 @@ function mountHouseEditor(container, callbacks) {
function buildLogoSign(groundY) { function buildLogoSign(groundY) {
const { d, n } = frontDoor(); const { d, n } = frontDoor();
const t = { x: n.z, z: -n.x }; const t = { x: n.z, z: -n.x };
signPos = { x: d.x + n.x * SIGN.ahead + t.x * SIGN.aside, z: d.z + n.z * SIGN.ahead + t.z * SIGN.aside }; // Voordeur in een binnenhoek: opzij kan het bord onder een andere unit vallen, dan verder opzij of de andere kant op.
const at = (a) => ({ x: d.x + n.x * SIGN.ahead + t.x * a, z: d.z + n.z * SIGN.ahead + t.z * a });
const free = (q) => !parts.rects.some(r => !r.error && q.x > r.x0 - SIGN.clear && q.x < r.x1 + SIGN.clear && q.z > r.z0 - SIGN.clear && q.z < r.z1 + SIGN.clear);
signPos = at(SIGN.aside);
for (let a = SIGN.aside; a <= 30000; a += 500) {
const q = [at(a), at(-a)].find(free);
if (q) { signPos = q; break; }
}
const toM = 0.001, plateH = SIGN.plateW / 3; const toM = 0.001, plateH = SIGN.plateW / 3;
const g = new THREE.Group(); const g = new THREE.Group();
const pole = new THREE.Mesh(new THREE.BoxGeometry(SIGN.poleW * toM, SIGN.poleH * toM, SIGN.poleW * toM), signPoleMat); const pole = new THREE.Mesh(new THREE.BoxGeometry(SIGN.poleW * toM, SIGN.poleH * toM, SIGN.poleW * toM), signPoleMat);
+27 -19
View File
@@ -162,8 +162,8 @@ window.FixtureModel = (() => {
}, },
keuken: { keuken: {
label: 'Keukenblok', cat: 'keuken', w: 2660, d: 600, h: 910, resize: ['w', 'd', 'h'], minW: 600, wall: true, cost: 'keuken', label: 'Keukenblok', cat: 'keuken', w: 2660, d: 600, h: 910, resize: ['w', 'd', 'h'], minW: 600, wall: true, cost: 'keuken',
opts: { front: 'antraciet', spoelbak: 0.6, hogeKast: 'links', vaatwasser: true, bovenkasten: false }, opts: { front: 'antraciet', spoel: true, spoelbak: 0.6, kookplaat: false, kookplaatPlek: 0.2, hogeKast: 'links', vaatwasser: true, bovenkasten: false },
hint: 'Onderkasten met werkblad en spoelbak (plek: 0 = links, 1 = rechts, van voren gezien); hoge kast 600 mm met inbouwkoelkast en oven', hint: 'Onderkasten met werkblad, spoelbak en eventueel kookplaat (plek: 0 = links, 1 = rechts, van voren gezien); hoge kast 600 mm met inbouwkoelkast en oven',
build(f, g) { kitchenRun(g, f, false); }, build(f, g) { kitchenRun(g, f, false); },
plan(f) { return kitchenPlan(f, false); } plan(f) { return kitchenPlan(f, false); }
}, },
@@ -386,25 +386,32 @@ window.FixtureModel = (() => {
boxAt(g, front, a + 2, b - 2, 1790, 2195, zf - 22, zf - 2); boxAt(g, front, a + 2, b - 2, 1790, 2195, zf - 22, zf - 2);
} }
if (!island) { if (!island) {
// Spoelbak + kraan if (f.opts.spoel !== false) {
const sx = spoelX(f, K); const sx = spoelX(f, K);
boxAt(g, MAT.chroom(), sx - 380, sx + 380, top - 2, top + 0.5, zb + 90, zf - 70); boxAt(g, MAT.chroom(), sx - 380, sx + 380, top - 2, top + 0.5, zb + 90, zf - 70);
boxAt(g, MAT.rubber(), sx - 360, sx + 360, top - 1, top + 1, zb + 105, zf - 85); boxAt(g, MAT.rubber(), sx - 360, sx + 360, top - 1, top + 1, zb + 105, zf - 85);
ecyl(g, MAT.zwartMetaal(), sx + 200, zb + 55, 22, 22, top, top + 300); ecyl(g, MAT.zwartMetaal(), sx + 200, zb + 55, 22, 22, top, top + 300);
boxAt(g, MAT.zwartMetaal(), sx + 190, sx + 210, top + 280, top + 300, zb + 55, zb + 280); boxAt(g, MAT.zwartMetaal(), sx + 190, sx + 210, top + 280, top + 300, zb + 55, zb + 280);
}
if (f.opts.bovenkasten) boxAt(g, front, K.x0, K.x1, 1450, 2150, zb, zb + 350); if (f.opts.bovenkasten) boxAt(g, front, K.x0, K.x1, 1450, 2150, zb, zb + 350);
} else if (f.opts.kookplaat) { }
const hw = Math.min(780, K.x1 - K.x0 - 100), hd = Math.min(520, f.d - 80); if (f.opts.kookplaat) {
boxAt(g, MAT.kookplaat(), -hw / 2, hw / 2, top, top + 4, -hd / 2, hd / 2); const { hx, hw, hd } = hobOf(f, K, island);
hobZones(hw, hd).forEach(z => ecyl(g, MAT.ring(), z.x, z.z, z.r, z.r, top + 4, top + 5)); boxAt(g, MAT.kookplaat(), hx - hw / 2, hx + hw / 2, top, top + 4, -hd / 2, hd / 2);
hobZones(hw, hd).forEach(z => ecyl(g, MAT.ring(), hx + z.x, z.z, z.r, z.r, top + 4, top + 5));
} }
} }
const hobZones = (hw, hd) => [[-0.25, -0.25, 0.19], [0.25, -0.25, 0.14], [-0.25, 0.25, 0.14], [0.25, 0.25, 0.19]] const hobZones = (hw, hd) => [[-0.25, -0.25, 0.19], [0.25, -0.25, 0.14], [-0.25, 0.25, 0.14], [0.25, 0.25, 0.19]]
.map(([a, b, r]) => ({ x: a * hw, z: b * hd, r: r * Math.min(hw, hd * 1.4) })); .map(([a, b, r]) => ({ x: a * hw, z: b * hd, r: r * Math.min(hw, hd * 1.4) }));
function spoelX(f, K) { function plekX(K, v, def) {
const t = Math.max(0, Math.min(1, f.opts.spoelbak === undefined ? 0.6 : f.opts.spoelbak)); const t = Math.max(0, Math.min(1, v === undefined ? def : v));
return Math.max(K.x0 + 400, Math.min(K.x1 - 400, K.x0 + t * (K.x1 - K.x0))); return Math.max(K.x0 + 400, Math.min(K.x1 - 400, K.x0 + t * (K.x1 - K.x0)));
} }
const spoelX = (f, K) => plekX(K, f.opts.spoelbak, 0.6);
// Kookeiland: kookplaat in het midden; keukenblok: op kookplaatPlek.
const hobOf = (f, K, island) => ({
hx: island ? 0 : plekX(K, f.opts.kookplaatPlek, 0.2), hw: Math.min(780, K.x1 - K.x0 - 100), hd: Math.min(520, f.d - 80)
});
function kitchenPlan(f, island) { function kitchenPlan(f, island) {
const zb = -f.d / 2, zf = f.d / 2, K = kitchenModules(f, island), out = []; const zb = -f.d / 2, zf = f.d / 2, K = kitchenModules(f, island), out = [];
out.push(rectP(K.x0, K.x1, zb, zf)); out.push(rectP(K.x0, K.x1, zb, zf));
@@ -413,13 +420,14 @@ window.FixtureModel = (() => {
const a = K.tall === 'links' ? -f.w / 2 : f.w / 2 - 600; const a = K.tall === 'links' ? -f.w / 2 : f.w / 2 - 600;
out.push(rectP(a, a + 600, zb, zf), lineP(a, zb, a + 600, zf, 'thin'), lineP(a, zf, a + 600, zb, 'thin')); out.push(rectP(a, a + 600, zb, zf), lineP(a, zb, a + 600, zf, 'thin'), lineP(a, zf, a + 600, zb, 'thin'));
} }
if (!island) { if (!island && f.opts.spoel !== false) {
const sx = spoelX(f, K); const sx = spoelX(f, K);
out.push(rrectP(sx - 360, sx + 360, zb + 105, zf - 85, 40, 'thin'), circP(sx + 200, zb + 55, 22, 'thin')); out.push(rrectP(sx - 360, sx + 360, zb + 105, zf - 85, 40, 'thin'), circP(sx + 200, zb + 55, 22, 'thin'));
} else if (f.opts.kookplaat) { }
const hw = Math.min(780, K.x1 - K.x0 - 100), hd = Math.min(520, f.d - 80); if (f.opts.kookplaat) {
out.push(rectP(-hw / 2, hw / 2, -hd / 2, hd / 2, 'thin')); const { hx, hw, hd } = hobOf(f, K, island);
hobZones(hw, hd).forEach(z => out.push(circP(z.x, z.z, z.r, 'thin'))); out.push(rectP(hx - hw / 2, hx + hw / 2, -hd / 2, hd / 2, 'thin'));
hobZones(hw, hd).forEach(z => out.push(circP(hx + z.x, z.z, z.r, 'thin')));
} }
return out; return out;
} }
+42 -16
View File
@@ -10,7 +10,7 @@ window.LightingModel = (() => {
mode: 'spots', mode: 'spots',
direction: 'both', direction: 'both',
lampHeight: 1950, lampHeight: 1950,
lampMin: 1500, lampMin: 500,
lampMax: 3000, lampMax: 3000,
minWallWidth: 2000, minWallWidth: 2000,
endOffset: 300, endOffset: 300,
@@ -39,14 +39,16 @@ window.LightingModel = (() => {
calculateSegmentPositions(len, min, max).forEach(p => out.push({ x: A.x + (B.x - A.x) * p / len, z: A.z + (B.z - A.z) * p / len })); calculateSegmentPositions(len, min, max).forEach(p => out.push({ x: A.x + (B.x - A.x) * p / len, z: A.z + (B.z - A.z) * p / len }));
} }
function enforceMinSpacing(positions, min) { // Te dicht bij elkaar: samen naar het midden, tenzij daar een opening zit (dan vervalt de tweede).
function enforceMinSpacing(positions, min, blockedAt) {
let pts = positions.slice().sort((a, b) => a - b); let pts = positions.slice().sort((a, b) => a - b);
let changed = true; let changed = true;
while (changed && pts.length > 1) { while (changed && pts.length > 1) {
changed = false; changed = false;
for (let i = 0; i < pts.length - 1; i++) { for (let i = 0; i < pts.length - 1; i++) {
if (pts[i + 1] - pts[i] < min - 0.5) { if (pts[i + 1] - pts[i] < min - 0.5) {
pts.splice(i, 2, (pts[i] + pts[i + 1]) / 2); const mid = (pts[i] + pts[i + 1]) / 2;
pts.splice(i, 2, blockedAt && blockedAt(mid) ? pts[i] : mid);
changed = true; changed = true;
break; break;
} }
@@ -55,6 +57,27 @@ window.LightingModel = (() => {
return pts; return pts;
} }
// Naast openingen: op openingOffset van elke opening; is de tussenruimte daarvoor te klein, dan één lamp in
// het midden ervan (of geen, als die tegen de kozijnen zou hangen). Posities in u (mm vanaf de linkerkant).
function besideOpenings(cfg, s) {
const W = cfg.wallWidth, off = s.openingOffset, clear = cfg.studWidth + 150;
const ops = cfg.openings.map(op => [op.x, op.x + op.w]).sort((p, q) => p[0] - q[0]);
const out = [];
ops.forEach(([x0, x1], i) => {
const next = ops[i + 1];
if (i === 0) { const u = Math.max(x0 - off, s.endOffset); if (x0 - u >= clear) out.push(u); }
if (next) {
const gap = next[0] - x1;
if (gap >= 2 * off + 1000) out.push(x1 + off, next[0] - off);
else if (gap >= 2 * clear) out.push((x1 + next[0]) / 2);
} else {
const u = Math.min(x1 + off, W - s.endOffset);
if (u - x1 >= clear) out.push(u);
}
});
return out;
}
function freeIntervals(lo, hi, blocked) { function freeIntervals(lo, hi, blocked) {
const free = []; const free = [];
let cursor = lo; let cursor = lo;
@@ -113,7 +136,7 @@ window.LightingModel = (() => {
}; };
if (s.mode === 'gevel') { if (s.mode === 'gevel') {
runs.forEach(run => { runs.forEach(run => {
const blocked = []; const blocked = [], openings = [];
const pos = []; const pos = [];
// Delen achter een andere wand (bijv. een stuk dat de naad in loopt) zijn geen zichtbare gevel. // Delen achter een andere wand (bijv. een stuk dat de naad in loopt) zijn geen zichtbare gevel.
segs.filter(o => !run.segs.includes(o)).forEach(o => { segs.filter(o => !run.segs.includes(o)).forEach(o => {
@@ -126,26 +149,29 @@ window.LightingModel = (() => {
const cfg = sg.piece.config, sW = cfg.studWidth; const cfg = sg.piece.config, sW = cfg.studWidth;
const low = Math.min(cfg.wallHeightLeft, cfg.wallHeightRight) < H + 100; const low = Math.min(cfg.wallHeightLeft, cfg.wallHeightRight) < H + 100;
if (sg.override === 'geen' || low) { blocked.push([sg.s0, sg.s1]); return; } if (sg.override === 'geen' || low) { blocked.push([sg.s0, sg.s1]); return; }
if (sg.override === 'openingen') { const own = [];
blocked.push([sg.s0, sg.s1]);
cfg.openings.forEach(op => [op.x - s.openingOffset, op.x + op.w + s.openingOffset]
.filter(u => u >= 0 && u <= cfg.wallWidth).forEach(u => pos.push(uToS(sg, u))));
return;
}
cfg.openings.filter(op => op.y - sW <= H && WallModel.openingTopMax(op) + sW >= H).forEach(op => { cfg.openings.filter(op => op.y - sW <= H && WallModel.openingTopMax(op) + sW >= H).forEach(op => {
const a = uToS(sg, op.x - sW), b = uToS(sg, op.x + op.w + sW); const a = uToS(sg, op.x - sW), b = uToS(sg, op.x + op.w + sW);
blocked.push([Math.min(a, b), Math.max(a, b)]); own.push([Math.min(a, b), Math.max(a, b)]);
}); });
openings.push(...own);
if (sg.override === 'openingen') {
blocked.push([sg.s0, sg.s1]);
besideOpenings(cfg, s).forEach(u => pos.push(uToS(sg, u)));
return;
}
blocked.push(...own);
}); });
// Automatisch: per vrij wandstuk gelijk verdeeld, midden in gelijke delen van ca. lampMax (één lamp
// midden tussen twee ramen), niet tegen de kozijnen of de hoek aan.
freeIntervals(run.s0, run.s1, blocked).forEach(([a, b]) => { freeIntervals(run.s0, run.s1, blocked).forEach(([a, b]) => {
if (b - a < s.minWallWidth) return; if (b - a < s.minWallWidth) return;
const first = a + s.endOffset, last = b - s.endOffset; const k = Math.max(1, Math.round((b - a) / s.lampMax));
pos.push(first); for (let i = 0; i < k; i++) pos.push(a + (b - a) * (i + 0.5) / k);
calculateSegmentPositions(last - first, s.lampMin, s.lampMax).forEach(p => pos.push(first + p));
pos.push(last);
}); });
// Min. h.o.h. geldt over alle lampen van de gevel samen (ook overrides en de grens tussen wanden). // Min. h.o.h. geldt over alle lampen van de gevel samen (ook overrides en de grens tussen wanden).
enforceMinSpacing(pos, s.lampMin).forEach(v => addLamp(run, v)); const inOpening = (v) => openings.some(([a, b]) => v > a && v < b);
enforceMinSpacing(pos, s.lampMin, inOpening).filter(v => !inOpening(v)).forEach(v => addLamp(run, v));
}); });
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => p.lamps.sort((a, b) => a.u - b.u)))); units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => p.lamps.sort((a, b) => a.u - b.u))));
} }
+139
View File
@@ -0,0 +1,139 @@
/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// 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
// uitkomt rust op die wand. Daartussen zo weinig palen dat de h.o.h.-afstand onder maxSpan blijft.
window.PostModel = (() => {
const EPS = 0.5;
const SHOE_MM = 100; // verzinkte paalvoet tussen maaiveld en paal
const WOODS = {
eiken: { label: 'Eiken', hex: 0xa07c54, kgM3: 750 },
douglas: { label: 'Douglas', hex: 0xb98a5e, kgM3: 530 }
};
const DEFAULT_SETTINGS = { enabled: true, size: 200, wood: 'eiken', maxSpan: 3600, minExt: 1000 };
function normalizeSettings(settings) {
const s = Object.assign({}, DEFAULT_SETTINGS, settings || {});
s.enabled = s.enabled !== false;
if (!WOODS[s.wood]) s.wood = DEFAULT_SETTINGS.wood;
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.maxSpan = num(s.maxSpan, DEFAULT_SETTINGS.maxSpan, 1000, 8000);
s.minExt = num(s.minExt, DEFAULT_SETTINGS.minExt, 400, 5000);
return s;
}
// units: [{ rect (vloer), roof: { rect, ceilingAt } }] in wereld-mm; groundY = maaiveld (absoluut).
function compute(settings, units, groundY) {
const s = normalizeSettings(settings);
const out = { posts: [], count: 0, size: s.size, wood: s.wood, lengthMM: 0, m3: 0, kg: 0 };
if (!s.enabled || !units.length) return out;
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 underRoof = (x, z) => units.some(u => { const r = u.roof.rect; return x > r.x0 && x < r.x1 && z > r.z0 && z < r.z1; });
// Vrije stukken dakrand: buitenrand van de daken samen, recht tegenover een gevel op minstens minExt (loodrecht
// op de rand gemeten); de zijkanten van een veranda krijgen alleen de hoekpalen. In een binnenhoek (veranda
// tussen twee units) loopt het stuk door tot de andere gevel en rust daar op.
const runs = [];
units.forEach(u => {
const R = u.roof.rect;
[
{ axis: 'z', at: R.x1, n: 1, s0: R.z0, s1: R.z1 }, { axis: 'z', at: R.x0, n: -1, s0: R.z0, s1: R.z1 },
{ axis: 'x', at: R.z1, n: 1, s0: R.x0, s1: R.x1 }, { axis: 'x', at: R.z0, n: -1, s0: R.x0, s1: R.x1 }
].forEach(e => {
const pt = (t, d) => (e.axis === 'z' ? { x: e.at - e.n * d, z: t } : { x: t, z: e.at - e.n * d });
const facing = (p) => Math.min(...floors.map(f => {
const [lo, hi, c0, c1, v, c] = e.axis === 'z' ? [f.z0, f.z1, f.x0, f.x1, p.z, p.x] : [f.x0, f.x1, f.z0, f.z1, p.x, p.z];
if (!(v > lo + EPS && v < hi - EPS)) return Infinity;
const d = e.n > 0 ? c - c1 : c0 - c;
return d > -EPS ? d : Infinity;
}));
const free = (t) => {
const p = pt(t, 0), o = pt(t, -1), d = facing(p);
return !underRoof(o.x, o.z) && d >= s.minExt - EPS && d < Infinity;
};
const cuts = [e.s0, e.s1].concat(floors.flatMap(f => (e.axis === 'z' ? [f.z0, f.z1] : [f.x0, f.x1]))
.filter(t => t > e.s0 + EPS && t < e.s1 - EPS)).sort((a, b) => a - b);
for (let i = 0; i < cuts.length - 1; i++) {
const a = cuts[i], b = cuts[i + 1];
if (b - a < EPS || !free((a + b) / 2)) continue;
const last = runs[runs.length - 1];
if (last && last.axis === e.axis && last.n === e.n && Math.abs(last.at - e.at) < EPS && Math.abs(last.b - a) < EPS) last.b = b;
else runs.push({ axis: e.axis, at: e.at, n: e.n, a, b });
}
});
});
// Aansluitende stukken van verschillende units (bijv. twee units met dezelfde veranda) samenvoegen.
runs.sort((p, q) => p.axis.localeCompare(q.axis) || p.n - q.n || Math.round(p.at) - Math.round(q.at) || p.a - q.a);
const merged = [];
runs.forEach(r => {
const m = merged.find(o => o.axis === r.axis && o.n === r.n && Math.abs(o.at - r.at) < EPS && Math.abs(o.b - r.a) < EPS);
if (m) m.b = r.b; else merged.push(Object.assign({}, r));
});
const half = s.size / 2;
const pts = [];
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 });
// 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 fa = endFree(r.a), fb = endFree(r.b);
const pa = fa ? r.a + half : r.a, pb = fb ? r.b - half : r.b;
if (pb - pa < EPS) return;
const n = Math.max(1, Math.ceil((pb - pa) / s.maxSpan - 1e-9));
for (let k = 0; k <= n; k++) {
if ((k === 0 && !fa) || (k === n && !fb)) continue;
pts.push(pos(pa + (pb - pa) * k / n));
}
});
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;
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 (!roof) return;
// 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))));
if (!(top > groundY + SHOE_MM)) return;
out.posts.push({ x: Math.round(p.x), z: Math.round(p.z), y0: groundY, y1: Math.round(top), size: s.size });
});
out.count = out.posts.length;
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.kg = out.m3 * WOODS[s.wood].kgM3;
return out;
}
function createMaterials() {
return {
wood: Object.fromEntries(Object.entries(WOODS).map(([k, w]) => [k, new THREE.MeshStandardMaterial({ color: w.hex, roughness: 0.78 })])),
shoe: new THREE.MeshStandardMaterial({ color: 0x9ba0a4, roughness: 0.45, metalness: 0.6 })
};
}
function build3D(data, materials) {
const toM = 0.001, group = new THREE.Group();
if (!data || !data.posts.length) return group;
const m = materials || createMaterials();
const woodMat = m.wood[data.wood] || m.wood.eiken;
data.posts.forEach(p => {
const h = p.y1 - p.y0 - SHOE_MM;
const post = new THREE.Mesh(new THREE.BoxGeometry(p.size * toM, h * toM, p.size * toM), woodMat);
post.position.set(p.x * toM, (p.y0 + SHOE_MM + h / 2) * toM, p.z * toM);
group.add(post);
const plate = new THREE.Mesh(new THREE.BoxGeometry((p.size + 40) * toM, 10 * toM, (p.size + 40) * toM), m.shoe);
plate.position.set(p.x * toM, (p.y0 + 5) * toM, p.z * toM);
group.add(plate);
const stem = new THREE.Mesh(new THREE.BoxGeometry(p.size * 0.35 * toM, (SHOE_MM - 10) * toM, p.size * 0.35 * toM), m.shoe);
stem.position.set(p.x * toM, (p.y0 + 10 + (SHOE_MM - 10) / 2) * toM, p.z * toM);
group.add(stem);
});
return group;
}
return { WOODS, SHOE_MM, DEFAULT_SETTINGS, normalizeSettings, compute, createMaterials, build3D };
})();
+266 -71
View File
@@ -92,7 +92,7 @@ window.RoofModel = (() => {
const deck = clone(floorState); const deck = clone(floorState);
deck.chassis = { enabled: false, wheelDiameter: 400, trackWidth: 1200, wheelbase: 1200 }; deck.chassis = { enabled: false, wheelDiameter: 400, trackWidth: 1200, wheelbase: 1200 };
deck.openings = []; deck.openings = [];
return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, covering: 'dakleer', deck }; return { followFloor: true, offsetX: 0, offsetZ: 0, extend: { f: 0, r: 0, b: 0, l: 0 }, fascia: true, covering: 'dakleer', deck };
} }
function thickness(calcData, covering) { function thickness(calcData, covering) {
@@ -212,7 +212,7 @@ window.RoofModel = (() => {
return { type: 'wolf', axis, profile: [L, A, ridge, B, R], hip: { h: A.h } }; return { type: 'wolf', axis, profile: [L, A, ridge, B, R], hip: { h: A.h } };
} }
} }
const type = profile.length === 2 ? 'lessenaar' : profile.length === 5 ? 'mansarde' : 'zadel'; const type = profile.length === 2 ? 'lessenaar' : 'zadel';
return { type, axis, profile }; return { type, axis, profile };
} }
@@ -267,8 +267,9 @@ window.RoofModel = (() => {
finishKNm2: 0.3, // afwerking/installaties onder het dak finishKNm2: 0.3, // afwerking/installaties onder het dak
gammaG: 1.2, gammaQ: 1.5, deflection: 250 gammaG: 1.2, gammaQ: 1.5, deflection: 250
}; };
const ridgeTypes = ['zadel', 'mansarde', 'wolf']; const ridgeTypes = ['zadel', 'wolf'];
function computeRidgeBeam(roofState, shape, rect, alongX, cd, kgM2) { // span: [q0, q1] waar de nok echt loopt (dakaansluiting: tot de hoekkeper).
function computeRidgeBeam(roofState, shape, rect, alongX, cd, kgM2, span) {
if (roofState.steelRidge === false || !ridgeTypes.includes(shape.type)) return null; if (roofState.steelRidge === false || !ridgeTypes.includes(shape.type)) return null;
const pr = shape.profile, inner = pr.slice(1, -1); const pr = shape.profile, inner = pr.slice(1, -1);
if (!inner.length) return null; if (!inner.length) return null;
@@ -283,6 +284,7 @@ window.RoofModel = (() => {
q0 = Math.max(uq0, Math.min(g.qM, g.q0 + g.inset)); q0 = Math.max(uq0, Math.min(g.qM, g.q0 + g.inset));
q1 = Math.min(uq1, Math.max(g.qM, g.q1 - g.inset)); q1 = Math.min(uq1, Math.max(g.qM, g.q1 - g.inset));
} }
if (span) { q0 = Math.max(q0, span[0]); q1 = Math.min(q1, span[1]); }
const L = q1 - q0; const L = q1 - q0;
if (L < 100) return null; if (L < 100) return null;
// q in N/mm = kN/m. // q in N/mm = kN/m.
@@ -393,7 +395,7 @@ window.RoofModel = (() => {
}); });
}); });
// Mansardeknik: het steilere vlak is verticaal dikker, dus verstek: onderkant op de knik, // Knik in de daklijn: het steilere vlak is verticaal dikker, dus verstek: onderkant op de knik,
// bovenkant op het snijpunt van de bovenvlakken. // bovenkant op het snijpunt van de bovenvlakken.
planes.forEach((pl, i) => { pl.miter0 = 0; pl.miter1 = 0; }); planes.forEach((pl, i) => { pl.miter0 = 0; pl.miter1 = 0; });
planes.slice(0, -1).forEach((pi, i) => { planes.slice(0, -1).forEach((pi, i) => {
@@ -418,11 +420,7 @@ window.RoofModel = (() => {
} }
const specs = sumSpecs(outPlanes.map(pl => pl.specs).concat(cap ? [cap.specs] : [])); const specs = sumSpecs(outPlanes.map(pl => pl.specs).concat(cap ? [cap.specs] : []));
const ridgeBeam = computeRidgeBeam(roofState, shape, rect, alongX, flat.calcData, specs.totalKg / Math.max(0.1, W * D / 1e6)); const ridgeBeam = computeRidgeBeam(roofState, shape, rect, alongX, flat.calcData, specs.totalKg / Math.max(0.1, W * D / 1e6));
Object.assign(specs, { addRidgeSpecs(specs, ridgeBeam);
nokStaalM: ridgeBeam ? ridgeBeam.length / 1000 : 0, nokStaalKg: ridgeBeam ? ridgeBeam.steelKg : 0,
nokVulhoutM3: ridgeBeam ? ridgeBeam.fillM3 : 0, nokVulhoutKg: ridgeBeam ? ridgeBeam.fillKg : 0
});
specs.totalKg += specs.nokStaalKg + specs.nokVulhoutKg;
return { return {
shape, covering, planes: outPlanes, specs, ridgeBeam, shape, covering, planes: outPlanes, specs, ridgeBeam,
undersideAt, topAt, cap, rect, alongX, undersideAt, topAt, cap, rect, alongX,
@@ -431,6 +429,185 @@ window.RoofModel = (() => {
}; };
} }
function addRidgeSpecs(specs, ridgeBeam) {
Object.assign(specs, {
nokStaalM: ridgeBeam ? ridgeBeam.length / 1000 : 0, nokStaalKg: ridgeBeam ? ridgeBeam.steelKg : 0,
nokVulhoutM3: ridgeBeam ? ridgeBeam.fillM3 : 0, nokVulhoutKg: ridgeBeam ? ridgeBeam.fillKg : 0
});
specs.totalKg += specs.nokStaalKg + specs.nokVulhoutKg;
}
// Dakaansluiting (HouseModel.roofJoins): vlakken h = a + bx·x + bz·z, samengevoegd volgens expr
// (vlaknummer of ['min' | 'max', ...]). extra(k): ophoging per vlak (bovenkant dak).
const planeAt = (pl, p) => pl.a + pl.bx * p.x + pl.bz * p.z;
function joinValue(join, p, extra) {
const at = (e) => Array.isArray(e) ? Math[e[0]](...e.slice(1).map(at)) : planeAt(join.planes[e], p) + (extra ? extra(e) : 0);
return at(join.expr);
}
function convexHull(pts) {
const sorted = pts.slice().sort((a, b) => a.x - b.x || a.z - b.z);
const cross = (o, a, b) => (a.x - o.x) * (b.z - o.z) - (a.z - o.z) * (b.x - o.x);
const half = (list) => list.reduce((h, q) => {
while (h.length >= 2 && cross(h[h.length - 2], h[h.length - 1], q) <= 0.01 * Math.hypot(q.x - h[h.length - 2].x, q.z - h[h.length - 2].z)) h.pop();
h.push(q);
return h;
}, []);
const lo = half(sorted), up = half(sorted.slice().reverse());
const out = lo.slice(0, -1).concat(up.slice(0, -1));
return out.filter((p, k) => { const q = out[(k + 1) % out.length]; return Math.hypot(q.x - p.x, q.z - p.z) > 0.5; });
}
// Dak van één unit in een dakaansluiting (join in unit-lokale maten): per vlak de convexe stukken binnen het dek,
// sporen haaks op de goot (kreupel tegen kil- en hoekkepers), kepers en nokken op de grenzen tussen de vlakken.
// Alles als houtwerk zoals het bovenste deel van een wolfsdak (cap); de eigen nok wordt de stalen nokbalk.
function computeJoinedRoof(roofState, join, rect, shape) {
const fp = roofState.deck.floor;
const joistW = fp.joistWidth || 70, joistH = fp.joistHeight || 220, underlay = fp.underlayThick || 22;
const covering = coveringFor(roofState.covering, shape.type);
const cov = COVERINGS[covering] || COVERINGS.dakleer;
const pls = join.planes.map((pl, k) => {
const slope = Math.hypot(pl.bx, pl.bz), cos = 1 / Math.sqrt(1 + slope * slope);
const tV = underlay / cos, jV = joistH / cos, bV = BESCHOT_MM / cos, covV = cov.thickMM / cos;
const under = (p) => planeAt(pl, p);
return { key: k, pl, slope, cos, tV, jV, bV, covV, vThick: tV + jV + bV + covV, under, f: under, along: Math.abs(pl.bx) >= Math.abs(pl.bz) ? 'p' : 'q' };
});
const activeAt = (p) => {
const h = joinValue(join, p);
return pls.reduce((a, b) => Math.abs(b.under(p) - h) < Math.abs(a.under(p) - h) ? b : a);
};
const box = [{ x: rect.x0, z: rect.z0 }, { x: rect.x1, z: rect.z0 }, { x: rect.x1, z: rect.z1 }, { x: rect.x0, z: rect.z1 }];
const clipBox = (poly) => [(p) => p.x - rect.x0, (p) => rect.x1 - p.x, (p) => p.z - rect.z0, (p) => rect.z1 - p.z]
.reduce((pl, f) => pl.length >= 3 ? clipPoly(pl, f) : pl, poly);
const pairs = [];
pls.forEach((A, i) => pls.slice(i + 1).forEach(B => {
if (Math.hypot(A.pl.bx - B.pl.bx, A.pl.bz - B.pl.bz) > 1e-9) pairs.push({ A, B, f: (p) => A.under(p) - B.under(p) });
}));
// Afgeronde wandhoogtes maken bijna samenvallende lijnen: splinters (gemiddeld < 0,5 mm breed) vallen weg.
const perim = (poly) => poly.reduce((t, p, k) => { const q = poly[(k + 1) % poly.length]; return t + Math.hypot(q.x - p.x, q.z - p.z); }, 0);
const sliver = (poly) => quadArea(poly) < Math.max(100, 0.25 * perim(poly));
let cells = [box];
pairs.forEach(({ f }) => {
cells = cells.flatMap(c => [clipPoly(c, f), clipPoly(c, p => -f(p))]).filter(c => c.length >= 3 && !sliver(c));
});
const centroid = (poly) => ({ x: poly.reduce((t, p) => t + p.x, 0) / poly.length, z: poly.reduce((t, p) => t + p.z, 0) / poly.length });
const regions = [];
pls.forEach(pl => {
const list = cells.filter(c => activeAt(centroid(c)) === pl).map(convexHull);
// Aangrenzende cellen samenvoegen zolang het geheel convex blijft.
let merged = true;
while (merged) {
merged = false;
for (let i = 0; i < list.length && !merged; i++) {
for (let j = i + 1; j < list.length && !merged; j++) {
const h = convexHull(list[i].concat(list[j]));
if (quadArea(h) - quadArea(list[i]) - quadArea(list[j]) < 0.5 * perim(h)) { list[i] = h; list.splice(j, 1); merged = true; }
}
}
}
list.filter(poly => poly.length >= 3).forEach(poly => regions.push(Object.assign({}, pl, { poly })));
});
// Grenzen tussen twee vlakken: bol = nok (waterpas) of hoekkeper, hol = kilkeper.
const members = [];
pairs.forEach(({ A, B, f }) => {
const gx = A.pl.bx - B.pl.bx, gz = A.pl.bz - B.pl.bz, g = Math.hypot(gx, gz), c0 = f({ x: 0, z: 0 });
const o = { x: -c0 * gx / (g * g), z: -c0 * gz / (g * g) }, d = { x: -gz / g, z: gx / g }, n = { x: gx / g, z: gz / g };
let t0 = -Infinity, t1 = Infinity;
[['x', rect.x0, rect.x1], ['z', rect.z0, rect.z1]].forEach(([k, lo, hi]) => {
if (Math.abs(d[k]) < 1e-9) { if (o[k] < lo || o[k] > hi) t1 = -Infinity; return; }
const a = (lo - o[k]) / d[k], b = (hi - o[k]) / d[k];
t0 = Math.max(t0, Math.min(a, b));
t1 = Math.min(t1, Math.max(a, b));
});
if (!(t1 - t0 >= 100)) return;
const pt = (t) => ({ x: o.x + d.x * t, z: o.z + d.z * t });
const ts = [t0, t1];
pairs.forEach(q => {
const dn = q.f(pt(1)) - q.f(pt(0));
if (Math.abs(dn) < 1e-9) return;
const t = -q.f(pt(0)) / dn;
if (t > t0 && t < t1) ts.push(t);
});
ts.sort((a, b) => a - b);
const segs = [];
let cur = null;
ts.slice(0, -1).forEach((ta, k) => {
const tb = ts[k + 1];
if (tb - ta < 1) return;
const m = pt((ta + tb) / 2), off = (s) => ({ x: m.x + n.x * s, z: m.z + n.z * s });
const s1 = activeAt(off(2)), s2 = activeAt(off(-2));
if (Math.abs(joinValue(join, m) - A.under(m)) > 0.5 || !((s1 === A && s2 === B) || (s1 === B && s2 === A))) { cur = null; return; }
const onA = off(s1 === A ? 2 : -2), convex = B.under(onA) > A.under(onA);
if (cur && cur.convex === convex) { cur.tb = tb; return; }
cur = { ta, tb, convex };
segs.push(cur);
});
// Waterpas en bol: nok als de vlakken van elkaar af aflopen, anders een knik.
const ridge = A.pl.bx * B.pl.bx + A.pl.bz * B.pl.bz < 0;
segs.forEach(sg => {
const P = pt(sg.ta), Q = pt(sg.tb), level = Math.abs(A.under(P) - A.under(Q)) < 1;
members.push({ kind: !sg.convex ? 'kilkeper' : !level ? 'hoekkeper' : ridge ? 'nokbalk' : 'knikbalk', A: P, B: Q, rg: A });
});
});
const alongX = shape.axis === 'x';
const own = members.find(m => m.kind === 'nokbalk' && (alongX ? Math.abs(m.A.x - m.B.x) < 1 : Math.abs(m.A.z - m.B.z) < 1));
const steel = own && roofState.steelRidge !== false;
const { hw, beams, awayFrom, addBeam } = beamTools(joistW);
members.forEach(m => { if (!(steel && m === own)) addBeam(m.kind, m.rg, m.A, m.B, [], true); });
pls.forEach(pl => {
const polys = regions.filter(rg => rg.key === pl.key).map(rg => rg.poly);
if (!polys.length) return;
const fixed = pl.along === 'p' ? 'z' : 'x';
const [lo, hi] = fixed === 'z' ? [rect.z0, rect.z1] : [rect.x0, rect.x1];
joistSpread(fp, hi - lo).map(v => lo + v).forEach(v => {
const spans = polys.map(poly => cutLine(poly, fixed, v)).filter(Boolean).sort((a, b) => a[0] - b[0]);
const runs = [];
spans.forEach(sp => {
const last = runs[runs.length - 1];
if (last && sp[0] <= last[1] + 1) last[1] = Math.max(last[1], sp[1]);
else runs.push(sp.slice());
});
runs.forEach(span => {
const pt = (t) => fixed === 'z' ? { x: t, z: v } : { x: v, z: t };
const A = pt(span[0]), B = pt(span[1]), mid = pt((span[0] + span[1]) / 2);
const clips = members.filter(m => distToSeg(A, m) < hw + 1 || distToSeg(B, m) < hw + 1).map(m => awayFrom(m, mid));
addBeam('spoor', pl, A, B, clips);
});
});
});
const ownBeams = clipBeams(beams, clipBox);
const capSpecs = regionSpecs(ownBeams, regions, joistW, joistH, underlay, covering);
const kgM2 = capSpecs.totalKg / Math.max(0.1, (rect.x1 - rect.x0) * (rect.z1 - rect.z0) / 1e6);
const q = own ? (alongX ? [own.A.z, own.B.z] : [own.A.x, own.B.x]) : null;
// Wolfsdak: de nok loopt hier tot de aansluiting, de inkorting van de wolfseinden volgt uit span.
const ridgeShape = shape.type === 'wolf' ? Object.assign({}, shape, { type: 'zadel' }) : shape;
const ridgeBeam = steel ? computeRidgeBeam(roofState, ridgeShape, rect, alongX, { underlayThick: underlay, joistH }, kgM2, [Math.min(...q), Math.max(...q)]) : null;
if (ridgeBeam) ridgeBeam.alongX = alongX;
const specs = Object.assign({}, capSpecs);
addRidgeSpecs(specs, ridgeBeam);
const heights = regions.flatMap(rg => rg.poly.map(p => rg.under(p)));
const maxTop = Math.max(...regions.map(rg => Math.max(...rg.poly.map(p => rg.under(p) + rg.vThick))));
const cap = {
kind: 'join', pA: rect.x0, pB: rect.x1, q0: rect.z0, q1: rect.z1, eave: Math.min(...heights), ridgeH: Math.max(...heights),
slope: Math.max(...regions.map(rg => rg.slope)), joistW, joistH, regions, beams: ownBeams, specs: capSpecs, maxTop
};
// Knikpunten van de daklijn op de randen (nok, knik, wolfseind, kepers): daar knikt ook het boeideel.
const ridges = [];
members.forEach(m => [m.A, m.B].forEach(p => {
const onX = Math.abs(p.x - rect.x0) < 1 || Math.abs(p.x - rect.x1) < 1, onZ = Math.abs(p.z - rect.z0) < 1 || Math.abs(p.z - rect.z1) < 1;
const r = onX && !onZ ? { axis: 'z', value: p.z } : onZ && !onX ? { axis: 'x', value: p.x } : null;
if (r && !ridges.some(q => q.axis === r.axis && Math.abs(q.value - r.value) < 1)) ridges.push(r);
}));
return {
shape, covering, planes: [], specs, ridgeBeam, cap, rect, alongX: true, ridges, maxTop,
undersideAt: (x, z) => joinValue(join, { x, z }),
topAt: (x, z) => joinValue(join, { x, z }, k => pls[k].vThick)
};
}
const DENSITY_WOOD_KGM3 = 450, DENSITY_OSB_KGM3 = 650; // zelfde als FloorModel const DENSITY_WOOD_KGM3 = 450, DENSITY_OSB_KGM3 = 650; // zelfde als FloorModel
function computeCap(roofState, shape, rect, alongX, covering) { function computeCap(roofState, shape, rect, alongX, covering) {
const H = (p) => profileHeight(shape.profile, p); const H = (p) => profileHeight(shape.profile, p);
@@ -465,53 +642,7 @@ window.RoofModel = (() => {
.filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100); .filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
if (!clipped.some(rg => rg.along === 'q')) return null; if (!clipped.some(rg => rg.along === 'q')) return null;
const cutLine = (poly, fixed, v) => { const { hw, beams, awayFrom, addBeam } = beamTools(joistW);
const ts = [];
poly.forEach((a, k) => {
const b = poly[(k + 1) % poly.length], av = a[fixed], bv = b[fixed];
if ((av - v) * (bv - v) > 0 || Math.abs(bv - av) < 1e-9) return;
const t = (v - av) / (bv - av), o = fixed === 'x' ? 'z' : 'x';
ts.push(a[o] + (b[o] - a[o]) * t);
});
return ts.length >= 2 ? [Math.min(...ts), Math.max(...ts)] : null;
};
const hw = joistW / 2;
const rectAround = (A, B) => {
const L = Math.hypot(B.x - A.x, B.z - A.z), nx = -(B.z - A.z) / L * hw, nz = (B.x - A.x) / L * hw;
return [{ x: A.x - nx, z: A.z - nz }, { x: B.x - nx, z: B.z - nz }, { x: B.x + nx, z: B.z + nz }, { x: A.x + nx, z: A.z + nz }];
};
const awayFrom = (m, from) => {
const L = Math.hypot(m.B.x - m.A.x, m.B.z - m.A.z), nx = -(m.B.z - m.A.z) / L, nz = (m.B.x - m.A.x) / L;
const d = (p) => (p.x - m.A.x) * nx + (p.z - m.A.z) * nz;
const sgn = d(from) >= 0 ? 1 : -1;
return (p) => sgn * d(p) - hw;
};
const distToSeg = (p, m) => {
const dx = m.B.x - m.A.x, dz = m.B.z - m.A.z, L2 = dx * dx + dz * dz;
const t = Math.max(0, Math.min(1, ((p.x - m.A.x) * dx + (p.z - m.A.z) * dz) / L2));
return Math.hypot(p.x - (m.A.x + dx * t), p.z - (m.A.z + dz * t));
};
const beams = [];
const addBeam = (kind, rg, A, B, clips, lineHeight) => {
if (Math.hypot(B.x - A.x, B.z - A.z) < 100) return null;
let poly = rectAround(A, B);
(clips || []).forEach(f => { if (poly.length >= 3) poly = clipPoly(poly, f); });
if (poly.length < 3) return null;
const dx = B.x - A.x, dz = B.z - A.z, L = Math.hypot(dx, dz);
const yA = rg.under(A) + rg.tV, yB = rg.under(B) + rg.tV;
const yAt = lineHeight
? (p) => yA + (yB - yA) * Math.max(0, Math.min(1, ((p.x - A.x) * dx + (p.z - A.z) * dz) / (L * L)))
: (p) => rg.under(p) + rg.tV;
const ts = poly.map(p => ((p.x - A.x) * dx + (p.z - A.z) * dz) / L);
const plan = Math.max(...ts) - Math.min(...ts), slope = Math.abs(yB - yA) / L;
if (plan < 50) return null;
const bm = {
kind, A, B, poly, yAt, jV: rg.jV, slope,
length: Math.round(plan * Math.sqrt(1 + slope * slope)), angle: Math.round(Math.atan(slope) * 1800 / Math.PI) / 10
};
beams.push(bm);
return bm;
};
const mOf = (x) => regions.find(rg => rg.key === (x < pR ? 'm1' : 'm2')) || regions[0]; const mOf = (x) => regions.find(rg => rg.key === (x < pR ? 'm1' : 'm2')) || regions[0];
const hOf = (z) => regions.find(rg => rg.key === (z < qM ? 'h1' : 'h2')) || regions[0]; const hOf = (z) => regions.find(rg => rg.key === (z < qM ? 'h1' : 'h2')) || regions[0];
const ends = [{ x: pR, z: Math.min(qM, q0 + inset) }, { x: pR, z: Math.max(qM, q1 - inset) }]; const ends = [{ x: pR, z: Math.min(qM, q0 + inset) }, { x: pR, z: Math.max(qM, q1 - inset) }];
@@ -534,8 +665,7 @@ window.RoofModel = (() => {
if (ridgeBeam && roofState.steelRidge !== false) beams.splice(beams.indexOf(ridgeBeam), 1); if (ridgeBeam && roofState.steelRidge !== false) beams.splice(beams.indexOf(ridgeBeam), 1);
} }
const members = kops.concat(hips, ridgeBeam ? [ridgeBeam] : []); const members = kops.concat(hips, ridgeBeam ? [ridgeBeam] : []);
const spread = (len) => FloorModel.computeFrame({ width: len, depth: 1000, joistWidth: joistW, joistHeight: joistH, minSpacing: fp.minSpacing, maxSpacing: fp.maxSpacing, underlayThick: underlay }).joists.map(x => x + len / 2); const qs = joistSpread(fp, q1 - q0).map(v => q0 + v), ps = joistSpread(fp, pB - pA).map(v => pA + v);
const qs = spread(q1 - q0).map(v => q0 + v), ps = spread(pB - pA).map(v => pA + v);
regions.forEach(rg => { regions.forEach(rg => {
const fixed = rg.along === 'p' ? 'z' : 'x'; const fixed = rg.along === 'p' ? 'z' : 'x';
(rg.along === 'p' ? qs : ps).forEach(v => { (rg.along === 'p' ? qs : ps).forEach(v => {
@@ -548,8 +678,69 @@ window.RoofModel = (() => {
}); });
}); });
const ownBeams = beams.map(bm => { const ownBeams = clipBeams(beams, clipBox);
const poly = clipBox(bm.poly); const specs = regionSpecs(ownBeams, clipped, joistW, joistH, underlay, covering);
const maxTop = Math.max(...clipped.map(rg => Math.max(...rg.poly.map(pt => rg.under(pt) + rg.vThick))));
return { pA: cp0, pR, pB: cp1, q0: cq0, q1: cq1, hipH, slope: sA, joistW, joistH, regions: clipped, beams: ownBeams, specs, maxTop };
}
// Houtwerk op dakvlakken met een eigen omtrek (wolfseinden, dakaansluiting); balken als rechthoek in bovenaanzicht.
function cutLine(poly, fixed, v) {
const ts = [];
poly.forEach((a, k) => {
const b = poly[(k + 1) % poly.length], av = a[fixed], bv = b[fixed];
if ((av - v) * (bv - v) > 0 || Math.abs(bv - av) < 1e-9) return;
const t = (v - av) / (bv - av), o = fixed === 'x' ? 'z' : 'x';
ts.push(a[o] + (b[o] - a[o]) * t);
});
return ts.length >= 2 ? [Math.min(...ts), Math.max(...ts)] : null;
}
function distToSeg(p, m) {
const dx = m.B.x - m.A.x, dz = m.B.z - m.A.z, L2 = dx * dx + dz * dz;
const t = Math.max(0, Math.min(1, ((p.x - m.A.x) * dx + (p.z - m.A.z) * dz) / L2));
return Math.hypot(p.x - (m.A.x + dx * t), p.z - (m.A.z + dz * t));
}
function joistSpread(fp, len) {
return FloorModel.computeFrame({ width: len, depth: 1000, joistWidth: fp.joistWidth || 70, joistHeight: fp.joistHeight || 220, minSpacing: fp.minSpacing, maxSpacing: fp.maxSpacing, underlayThick: fp.underlayThick || 22 })
.joists.map(x => x + len / 2);
}
function beamTools(joistW) {
const hw = joistW / 2, beams = [];
const rectAround = (A, B) => {
const L = Math.hypot(B.x - A.x, B.z - A.z), nx = -(B.z - A.z) / L * hw, nz = (B.x - A.x) / L * hw;
return [{ x: A.x - nx, z: A.z - nz }, { x: B.x - nx, z: B.z - nz }, { x: B.x + nx, z: B.z + nz }, { x: A.x + nx, z: A.z + nz }];
};
const awayFrom = (m, from) => {
const L = Math.hypot(m.B.x - m.A.x, m.B.z - m.A.z), nx = -(m.B.z - m.A.z) / L, nz = (m.B.x - m.A.x) / L;
const d = (p) => (p.x - m.A.x) * nx + (p.z - m.A.z) * nz;
const sgn = d(from) >= 0 ? 1 : -1;
return (p) => sgn * d(p) - hw;
};
const addBeam = (kind, rg, A, B, clips, lineHeight) => {
if (Math.hypot(B.x - A.x, B.z - A.z) < 100) return null;
let poly = rectAround(A, B);
(clips || []).forEach(f => { if (poly.length >= 3) poly = clipPoly(poly, f); });
if (poly.length < 3) return null;
const dx = B.x - A.x, dz = B.z - A.z, L = Math.hypot(dx, dz);
const yA = rg.under(A) + rg.tV, yB = rg.under(B) + rg.tV;
const yAt = lineHeight
? (p) => yA + (yB - yA) * Math.max(0, Math.min(1, ((p.x - A.x) * dx + (p.z - A.z) * dz) / (L * L)))
: (p) => rg.under(p) + rg.tV;
const ts = poly.map(p => ((p.x - A.x) * dx + (p.z - A.z) * dz) / L);
const plan = Math.max(...ts) - Math.min(...ts), slope = Math.abs(yB - yA) / L;
if (plan < 50) return null;
const bm = {
kind, A, B, poly, yAt, jV: rg.jV, slope,
length: Math.round(plan * Math.sqrt(1 + slope * slope)), angle: Math.round(Math.atan(slope) * 1800 / Math.PI) / 10
};
beams.push(bm);
return bm;
};
return { hw, beams, awayFrom, addBeam };
}
function clipBeams(beams, clip) {
return beams.map(bm => {
const poly = clip(bm.poly);
if (poly.length < 3 || quadArea(poly) < 100) return null; if (poly.length < 3 || quadArea(poly) < 100) return null;
const dx = bm.B.x - bm.A.x, dz = bm.B.z - bm.A.z, L = Math.hypot(dx, dz); const dx = bm.B.x - bm.A.x, dz = bm.B.z - bm.A.z, L = Math.hypot(dx, dz);
const ts = poly.map(p => ((p.x - bm.A.x) * dx + (p.z - bm.A.z) * dz) / L); const ts = poly.map(p => ((p.x - bm.A.x) * dx + (p.z - bm.A.z) * dz) / L);
@@ -557,20 +748,19 @@ window.RoofModel = (() => {
if (plan < 50) return null; if (plan < 50) return null;
return Object.assign({}, bm, { poly, length: Math.round(plan * Math.sqrt(1 + slope * slope)) }); return Object.assign({}, bm, { poly, length: Math.round(plan * Math.sqrt(1 + slope * slope)) });
}).filter(Boolean); }).filter(Boolean);
}
function regionSpecs(beams, regions, joistW, joistH, underlay, covering) {
const W = joistW * 1e-3, Hh = joistH * 1e-3; const W = joistW * 1e-3, Hh = joistH * 1e-3;
const woodM3 = ownBeams.reduce((t, bm) => t + bm.length * 1e-3 * W * Hh, 0); const woodM3 = beams.reduce((t, bm) => t + bm.length * 1e-3 * W * Hh, 0);
const areaM2 = clipped.reduce((t, rg) => t + quadArea(rg.poly) / rg.cos, 0) / 1e6; const areaM2 = regions.reduce((t, rg) => t + quadArea(rg.poly) / rg.cos, 0) / 1e6;
const cs = coverSpecs(covering, areaM2), osbVol = areaM2 * underlay * 1e-3, bVol = areaM2 * BESCHOT_MM * 1e-3; const cs = coverSpecs(covering, areaM2), osbVol = areaM2 * underlay * 1e-3, bVol = areaM2 * BESCHOT_MM * 1e-3;
const specs = Object.assign({ return Object.assign({
joistWoodM3: woodM3, joistWoodKg: woodM3 * DENSITY_WOOD_KGM3, joistWoodM3: woodM3, joistWoodKg: woodM3 * DENSITY_WOOD_KGM3,
osbAreaM2: areaM2, osbVolM3: osbVol, osbKg: osbVol * DENSITY_OSB_KGM3, osbAreaM2: areaM2, osbVolM3: osbVol, osbKg: osbVol * DENSITY_OSB_KGM3,
beschotM2: areaM2, beschotVolM3: bVol, beschotKg: bVol * DENSITY_OSB_KGM3, beschotM2: areaM2, beschotVolM3: bVol, beschotKg: bVol * DENSITY_OSB_KGM3,
voidM3: Math.max(0, areaM2 * Hh - woodM3), voidM3: Math.max(0, areaM2 * Hh - woodM3),
totalKg: woodM3 * DENSITY_WOOD_KGM3 + (osbVol + bVol) * DENSITY_OSB_KGM3 + cs.coverKg totalKg: woodM3 * DENSITY_WOOD_KGM3 + (osbVol + bVol) * DENSITY_OSB_KGM3 + cs.coverKg
}, cs); }, cs);
const maxTop = Math.max(...clipped.map(rg => Math.max(...rg.poly.map(pt => rg.under(pt) + rg.vThick))));
return { pA: cp0, pR, pB: cp1, q0: cq0, q1: cq1, hipH, slope: sA, joistW, joistH, regions: clipped, beams: ownBeams, specs, maxTop };
} }
// Afschuiving i.p.v. kanteling (lokaal +Z = oplopende as): plan = z·cos θ, Y = y/cos θ + z·sin θ. // Afschuiving i.p.v. kanteling (lokaal +Z = oplopende as): plan = z·cos θ, Y = y/cos θ + z·sin θ.
@@ -581,7 +771,8 @@ window.RoofModel = (() => {
if (unitRoof.cap) group.add(buildCap3D(unitRoof, opts)); if (unitRoof.cap) group.add(buildCap3D(unitRoof, opts));
if (unitRoof.ridgeBeam) { if (unitRoof.ridgeBeam) {
const r = unitRoof.rect; const r = unitRoof.rect;
group.add(buildRidgeBeam3D(unitRoof.ridgeBeam, (r.x0 + r.x1) / 2, (r.z0 + r.z1) / 2, unitRoof.alongX, opts.materials)); const rb = unitRoof.ridgeBeam;
group.add(buildRidgeBeam3D(rb, (r.x0 + r.x1) / 2, (r.z0 + r.z1) / 2, rb.alongX !== undefined ? rb.alongX : unitRoof.alongX, opts.materials));
} }
unitRoof.planes.forEach(pl => { unitRoof.planes.forEach(pl => {
const g = new THREE.Group(); const g = new THREE.Group();
@@ -647,7 +838,7 @@ window.RoofModel = (() => {
const T = poly.map(pt => P(pt, bottom(pt) + h)), B = poly.map(pt => P(pt, bottom(pt))); const T = poly.map(pt => P(pt, bottom(pt) + h)), B = poly.map(pt => P(pt, bottom(pt)));
const UV = poly.map(pt => uvOf ? uvOf(pt) : [0, 0]), z2 = [0, 0]; const UV = poly.map(pt => uvOf ? uvOf(pt) : [0, 0]), z2 = [0, 0];
const tri = (a, b, cc, ua, ub, uc) => { pos.push(...a, ...b, ...cc); uv.push(...ua, ...ub, ...uc); }; const tri = (a, b, cc, ua, ub, uc) => { pos.push(...a, ...b, ...cc); uv.push(...ua, ...ub, ...uc); };
const up = (T[1][0] - T[0][0]) * (T[2][2] - T[0][2]) - (T[1][2] - T[0][2]) * (T[2][0] - T[0][0]) < 0; const up = T.reduce((t, a, k) => { const b = T[(k + 1) % n]; return t + a[0] * b[2] - b[0] * a[2]; }, 0) < 0;
for (let k = 1; k < n - 1; k++) up ? tri(T[0], T[k], T[k + 1], UV[0], UV[k], UV[k + 1]) : tri(T[0], T[k + 1], T[k], UV[0], UV[k + 1], UV[k]); for (let k = 1; k < n - 1; k++) up ? tri(T[0], T[k], T[k + 1], UV[0], UV[k], UV[k + 1]) : tri(T[0], T[k + 1], T[k], UV[0], UV[k + 1], UV[k]);
const topCount = pos.length / 3; const topCount = pos.length / 3;
for (let k = 1; k < n - 1; k++) up ? tri(B[0], B[k + 1], B[k], z2, z2, z2) : tri(B[0], B[k], B[k + 1], z2, z2, z2); for (let k = 1; k < n - 1; k++) up ? tri(B[0], B[k + 1], B[k], z2, z2, z2) : tri(B[0], B[k], B[k + 1], z2, z2, z2);
@@ -879,7 +1070,11 @@ window.RoofModel = (() => {
s.overhang = Math.min(OVERHANG_MAX, Math.max(OVERHANG_MIN, s.overhang)); s.overhang = Math.min(OVERHANG_MAX, Math.max(OVERHANG_MIN, s.overhang));
delete s.edge; delete s.spots; // oud (v0.3.0): nu per unit delete s.edge; delete s.spots; // oud (v0.3.0): nu per unit
const h = s.house || {}; const h = s.house || {};
s.house = { enabled: !!h.enabled, form: h.form && UnitModel.ROOF_TYPES[h.form.type] ? Object.assign({}, h.form) : null, cover: !!h.cover }; const form = h.form ? Object.assign({}, h.form) : null;
// v0.6.2: mansarde vervallen, wordt een zadeldak met dezelfde nok.
if (form && form.type === 'mansarde') { form.type = 'zadel'; delete form.kneeH; delete form.kneeIn; delete form.kneeRatio; }
s.house = { enabled: !!h.enabled, form: form && UnitModel.ROOF_TYPES[form.type] ? form : null, cover: !!h.cover };
s.posts = PostModel.normalizeSettings(s.posts);
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor; if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
return s; return s;
} }
@@ -887,7 +1082,7 @@ window.RoofModel = (() => {
return { return {
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, buildUnit3D, capRegions, computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, joinValue, buildUnit3D, capRegions,
computeEdge, buildEdge3D, normalizeSettings computeEdge, buildEdge3D, normalizeSettings
}; };
})(); })();
+12 -19
View File
@@ -306,8 +306,8 @@ window.UnitModel = (() => {
return Math.max(w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : 0, ...(w.topPts || []).map(q => q.h)); return Math.max(w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : 0, ...(w.topPts || []).map(q => q.h));
} }
// kneeRatio / cutRatio gaan voor kneeH / cut, zodat ze meeschalen als goot of nok verandert. // cutRatio gaat voor cut, zodat ze meeschalen als goot of nok verandert.
const LIMITS = { ridge: 5000, eave: 3000, short: 4300, long: 14000 }; const LIMITS = { ridge: 5000, eave: 3000, short: 4700, long: 14000 };
function clampSize(floor, keep) { function clampSize(floor, keep) {
floor.width = Math.min(LIMITS.long, floor.width); floor.width = Math.min(LIMITS.long, floor.width);
floor.depth = Math.min(LIMITS.long, floor.depth); floor.depth = Math.min(LIMITS.long, floor.depth);
@@ -317,9 +317,9 @@ window.UnitModel = (() => {
} }
return floor; return floor;
} }
const ROOF_TYPES = { plat: 'Plat', lessenaar: 'Lessenaar', zadel: 'Zadel', mansarde: 'Mansarde', wolf: 'Wolfsdak' }; const ROOF_TYPES = { plat: 'Plat', lessenaar: 'Lessenaar', zadel: 'Zadel', wolf: 'Wolfsdak' };
const RIDGES = { z: 'Noord–zuid', x: 'West–oost' }; const RIDGES = { z: 'Noord–zuid', x: 'West–oost' };
const hasRidge = (type) => type === 'zadel' || type === 'mansarde' || type === 'wolf'; const hasRidge = (type) => type === 'zadel' || type === 'wolf';
function gableOf(form) { function gableOf(form) {
if (hasRidge(form.type)) return form.ridge === 'x' ? 'links' : 'achter'; if (hasRidge(form.type)) return form.ridge === 'x' ? 'links' : 'achter';
if (form.type === 'lessenaar') return form.dir === 'links' || form.dir === 'rechts' ? 'achter' : 'links'; if (form.type === 'lessenaar') return form.dir === 'links' || form.dir === 'rechts' ? 'achter' : 'links';
@@ -333,10 +333,8 @@ window.UnitModel = (() => {
const full = fullTopLine(w, gableSpan(unitState, side)); const full = fullTopLine(w, gableSpan(unitState, side));
const F = full[full.length - 1].u, inner = full.slice(1, -1); const F = full[full.length - 1].u, inner = full.slice(1, -1);
const L = full[0], R = full[full.length - 1], low = Math.round(Math.min(L.h, R.h)); const L = full[0], R = full[full.length - 1], low = Math.round(Math.min(L.h, R.h));
if (inner.length === 3) { // v0.6.2: mansarde vervallen; een opgeslagen mansarde wordt een zadeldak met dezelfde nok.
const high = Math.round(inner[1].h); if (inner.length === 3) return { type: 'zadel', ridge, low, high: Math.round(inner[1].h) };
return { type: 'mansarde', ridge, low, high, kneeH: Math.round(inner[0].h), kneeIn: Math.round(inner[0].u), kneeRatio: (inner[0].h - low) / Math.max(1, high - low) };
}
if (inner.length === 2 && Math.abs(inner[0].h - inner[1].h) < 0.5) { if (inner.length === 2 && Math.abs(inner[0].h - inner[1].h) < 0.5) {
const slope = (inner[0].h - L.h) / Math.max(1, inner[0].u); const slope = (inner[0].h - L.h) / Math.max(1, inner[0].u);
const high = Math.round(L.h + slope * F / 2); const high = Math.round(L.h + slope * F / 2);
@@ -351,6 +349,10 @@ window.UnitModel = (() => {
const h = Math.max(...Object.values(W).map(wallTopOf)); const h = Math.max(...Object.values(W).map(wallTopOf));
return { type: 'plat', low: h, high: h }; return { type: 'plat', low: h, high: h };
} }
function dropMansarde(unitState) {
const f = roofForm(unitState), side = f.type === 'zadel' ? gableOf(f) : null;
if (side && fullTopLine(unitState.walls[side].wall, gableSpan(unitState, side)).length === 5) applyRoofForm(unitState, f);
}
function applyRoofForm(unitState, form) { function applyRoofForm(unitState, form) {
const side = form.type !== 'plat' ? gableOf(form) : 'achter'; const side = form.type !== 'plat' ? gableOf(form) : 'achter';
const g = gableSpan(unitState, side); const g = gableSpan(unitState, side);
@@ -365,16 +367,7 @@ window.UnitModel = (() => {
const half = F / 2; const half = F / 2;
let line = [{ u: 0, h: low }, { u: F, h: low }]; let line = [{ u: 0, h: low }, { u: F, h: low }];
if (sloped && form.type === 'zadel') line = [{ u: 0, h: low }, { u: half, h: high }, { u: F, h: low }]; if (sloped && form.type === 'zadel') line = [{ u: 0, h: low }, { u: half, h: high }, { u: F, h: low }];
else if (sloped && form.type === 'mansarde') { else if (sloped && form.type === 'wolf') {
// Knik altijd boven de rechte lijn goot–nok, anders wordt het een hol dak.
const kIn = Math.max(eMin + 100, Math.min(half - 100, Math.round(form.kneeIn || F * 0.18)));
const minK = low + (high - low) * kIn / half + 20;
const want = form.kneeRatio > 0 ? low + form.kneeRatio * (high - low) : form.kneeH;
const kH = Math.round(Math.min(high - 20, Math.max(minK, want || minK)));
line = kH > minK - 1 && high - kH >= 20
? [{ u: 0, h: low }, { u: kIn, h: kH }, { u: half, h: high }, { u: F - kIn, h: kH }, { u: F, h: low }]
: [{ u: 0, h: low }, { u: half, h: high }, { u: F, h: low }];
} else if (sloped && form.type === 'wolf') {
const minR = (eMin + 50) / half; const minR = (eMin + 50) / half;
const ratio = form.cutRatio > 0 ? form.cutRatio : form.cut ? (form.cut - low) / (high - low) : 0.6; const ratio = form.cutRatio > 0 ? form.cutRatio : form.cut ? (form.cut - low) / (high - low) : 0.6;
const cut = Math.round(Math.max(low + 50, Math.min(high - 50, low + Math.max(0.05, minR, Math.min(0.95, ratio)) * (high - low)))); const cut = Math.round(Math.max(low + 50, Math.min(high - 50, low + Math.max(0.05, minR, Math.min(0.95, ratio)) * (high - low))));
@@ -401,5 +394,5 @@ window.UnitModel = (() => {
} }
} }
return { WALL_SIDES, CORNER_MODES, ROOF_TYPES, RIDGES, LIMITS, clampSize, hasRidge, clone, createDefaultState, localToWorld, computeLayout, normalizeSplits, splitFacadeAt, topLineOn, topParamsOn, topParamsFromLayout, wallPieces, syncWallHeights, wallTopOf, gableOf, roofForm, applyRoofForm, formLine, validateState }; return { WALL_SIDES, CORNER_MODES, ROOF_TYPES, RIDGES, LIMITS, clampSize, hasRidge, clone, createDefaultState, localToWorld, computeLayout, normalizeSplits, splitFacadeAt, topLineOn, topParamsOn, topParamsFromLayout, wallPieces, syncWallHeights, wallTopOf, gableOf, roofForm, dropMansarde, applyRoofForm, formLine, validateState };
})(); })();
+19 -12
View File
@@ -269,6 +269,17 @@ window.WallModel = (() => {
}; };
} }
function raiseTop(CONFIG, d) {
return d ? Object.assign({}, CONFIG, {
wallHeightLeft: CONFIG.wallHeightLeft + d, wallHeightRight: CONFIG.wallHeightRight + d,
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + d }))
}) : CONFIG;
}
// Bovenrand van de buitenlagen: per punt op het dak (skinTop) of de wandrand + skinAbove.
function skinConfig(CONFIG, skinAbove, skinTop) {
return skinTop ? Object.assign({}, CONFIG, { wallHeightLeft: skinTop.hL, wallHeightRight: skinTop.hR, wallTop: skinTop.pts }) : raiseTop(CONFIG, skinAbove);
}
function topPlateOutlines(tl) { function topPlateOutlines(tl) {
const n = tl.segs.length, L = tl.pts[0], R = tl.pts[n]; const n = tl.segs.length, L = tl.pts[0], R = tl.pts[n];
return tl.segs.map((sg, i) => [ return tl.segs.map((sg, i) => [
@@ -769,16 +780,10 @@ window.WallModel = (() => {
const startXM = -halfW; const startXM = -halfW;
const topYAt = tl.at; const topYAt = tl.at;
const skinAbove = opts.skinAbove > 0 ? opts.skinAbove : 0; const skinAbove = opts.skinAbove > 0 ? opts.skinAbove : 0;
const CONFIG_OUT = skinAbove ? Object.assign({}, CONFIG, { const CONFIG_OUT = skinConfig(CONFIG, skinAbove, opts.skinTop);
wallHeightLeft: CONFIG.wallHeightLeft + skinAbove, wallHeightRight: CONFIG.wallHeightRight + skinAbove, const tlOut = CONFIG_OUT === CONFIG ? tl : topLine(CONFIG_OUT, toM);
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + skinAbove }))
}) : CONFIG;
const tlOut = skinAbove ? topLine(CONFIG_OUT, toM) : tl;
const battenAbove = typeof opts.battenAbove === 'number' ? opts.battenAbove : skinAbove; const battenAbove = typeof opts.battenAbove === 'number' ? opts.battenAbove : skinAbove;
const CONFIG_BAT = battenAbove ? Object.assign({}, CONFIG, { const CONFIG_BAT = raiseTop(CONFIG_OUT, battenAbove - skinAbove);
wallHeightLeft: CONFIG.wallHeightLeft + battenAbove, wallHeightRight: CONFIG.wallHeightRight + battenAbove,
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + battenAbove }))
}) : CONFIG;
const underPts = tl.underPts; const underPts = tl.underPts;
// LET OP: xMin/xMax zijn wereld-coördinaten (-halfW..+halfW), niet lokaal (0..wallWidth). // LET OP: xMin/xMax zijn wereld-coördinaten (-halfW..+halfW), niet lokaal (0..wallWidth).
@@ -940,7 +945,9 @@ window.WallModel = (() => {
uv.needsUpdate = true; uv.needsUpdate = true;
return geo; return geo;
}; };
const topClampedOut = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x))); // Voorbij het wandeinde (hoekdoorloop) de daklijn volgen: vlak doorgetrokken steekt hij boven een steil dakvlak uit.
const beyondEnd = (T) => (x) => Math.min(T.at(x), T.at(Math.max(-halfW, Math.min(halfW, x))));
const topClampedOut = beyondEnd(tlOut);
const layerShapes = (a, b, y0, yCap) => { const layerShapes = (a, b, y0, yCap) => {
if (b - a <= edgeEps) return []; if (b - a <= edgeEps) return [];
const capped = yCap === Infinity; const capped = yCap === Infinity;
@@ -1216,7 +1223,7 @@ window.WallModel = (() => {
const rExt = extM(rabatDepthMM); const rExt = extM(rabatDepthMM);
const rxL = -halfW - rExt.left, rxR = halfW + rExt.right; const rxL = -halfW - rExt.left, rxR = halfW + rExt.right;
const rY0 = Math.max(skinStartY(trasraam), skinFrom) * toM; const rY0 = Math.max(skinStartY(trasraam), skinFrom) * toM;
const rTop = (x) => tlBat.at(Math.max(-halfW, Math.min(halfW, x))); const rTop = beyondEnd(tlBat);
const rabatShapes = (rxR - rxL > edgeEps && tlBat.maxY > rY0 + edgeEps) const rabatShapes = (rxR - rxL > edgeEps && tlBat.maxY > rY0 + edgeEps)
? columnMinusRects(rxL, rxR, rY0, rTop, outerHoles.map(holeRect), { extraXs: tlBat.kinks.concat([-halfW, halfW]) }).map(piece => { ? columnMinusRects(rxL, rxR, rY0, rTop, outerHoles.map(holeRect), { extraXs: tlBat.kinks.concat([-halfW, halfW]) }).map(piece => {
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y))); const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
@@ -1546,7 +1553,7 @@ 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, STUC, STUC_COLORS, isStucColor, stucHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
sheathingHoles, skinSegmentsAtY, topLine, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY, 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
}; };
+1 -1
View File
@@ -1380,7 +1380,7 @@ function mountWallEditor(container, callbacks) {
const inputs = document.querySelectorAll('#tab-wall input'); const inputs = document.querySelectorAll('#tab-wall input');
inputs.forEach(input => { inputs.forEach(input => {
// Hoogte met de hand gewijzigd: de knikken van een mansarde-/wolfsdak vervallen. // Hoogte met de hand gewijzigd: de knikken van een wolfsdak vervallen.
if (/^inp-wall-(h-|peak)/.test(input.id)) input.addEventListener('input', () => { wallTopPts = null; }); if (/^inp-wall-(h-|peak)/.test(input.id)) input.addEventListener('input', () => { wallTopPts = null; });
input.addEventListener('input', updateAll); input.addEventListener('input', updateAll);
}); });
+1 -1
View File
@@ -48,7 +48,7 @@
"mode": "spots", "mode": "spots",
"direction": "both", "direction": "both",
"lampHeight": 1950, "lampHeight": 1950,
"lampMin": 1500, "lampMin": 500,
"lampMax": 3000, "lampMax": 3000,
"minWallWidth": 2000, "minWallWidth": 2000,
"endOffset": 300, "endOffset": 300,
+1
View File
@@ -40,6 +40,7 @@ dak.daktrimMontage;Montage daktrim;s/m;300
dak.nokPlaatsen;Stalen nokbalk plaatsen, stellen en vastzetten;s;2700 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
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
1 code omschrijving eenheid waarde
40 dak.nokPlaatsen Stalen nokbalk plaatsen, stellen en vastzetten s 2700
41 dak.nokVulhoutMontage Vulhout in de holling van de nokbalk aanbrengen (beide zijden) s/m 240
42 dak.nokPersoneel Personeel bij plaatsen nokbalk (i.p.v. de bezetting) man 3
43 dak.paalMontage Paal plaatsen: stellen, aftoppen, vastzetten aan het dak s/st. 2700
44 vloer.afwerkingMontage Montage vloerafwerking s/m² 1200
45 vloer.sandwichMontage Montage per sandwichpaneel s 600
46 afwerking.valsPlafondMontage Montage vals plafond s/m² 1800
+2
View File
@@ -21,6 +21,8 @@ dak.onderplaatM2;Onderplaat boeideel;€/m²;30;;;
dak.daktrimM;Daktrim;€/m;12;;; dak.daktrimM;Daktrim;€/m;12;;;
dak.staalKg;Stalen nokbalk IPE S235 (op maat gezaagd, geboord, gegrond);€/kg;2,9;;; dak.staalKg;Stalen nokbalk IPE S235 (op maat gezaagd, geboord, gegrond);€/kg;2,9;;;
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.paalvoet;Paalvoet verzinkt, verstelbaar (incl. ankers);€/st.;40;;;
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;;;
1 code omschrijving eenheid prijs leverancier artikelnummer bijgewerkt
21 dak.daktrimM Daktrim €/m 12
22 dak.staalKg Stalen nokbalk IPE S235 (op maat gezaagd, geboord, gegrond) €/kg 2,9
23 dak.nokOplegging Oplegging nokbalk (stalen schoen + bouten), 2 per balk €/st. 45
24 dak.paalHoutM3 Paalhout overkapping (eiken, geschaafd) €/m³ 2400
25 dak.paalvoet Paalvoet verzinkt, verstelbaar (incl. ankers) €/st. 40
26 vloer.afwerkingM2 Vloerafwerking €/m² 35
27 vloer.sandwichM2 Sandwichpaneel €/m² 35
28 afwerking.valsPlafondM2 Vals plafond (regelwerk + gipsplaat, afgewerkt) €/m² 38