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