Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8cc39275b | ||
|
|
118ccf9098 |
@@ -171,6 +171,10 @@ assets/ logo, texturen, skybox
|
||||
(`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.
|
||||
Bij een overkapping (veranda, inpandig of uitpandig: het dak van een unit
|
||||
steekt buiten zijn vloer, `HouseModel.hasCover`) is er geen wolfsdak: de
|
||||
keuze verdwijnt en een bestaand wolfsdak wordt een zadeldak met dezelfde
|
||||
goot en nok.
|
||||
- **Dak over de hele woning:** eigenschappen Woning › Dak › "Over hele woning": één
|
||||
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
|
||||
@@ -210,7 +214,10 @@ assets/ logo, texturen, skybox
|
||||
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).
|
||||
rand (ook naast daktrim). Onder de boeidelen zit op de gevel een
|
||||
ventilatierooster van 40 mm (direct onder de onderplaat, op de
|
||||
gevelbekleding; niet boven een veranda), zwart/antraciet/wit
|
||||
(`RoofModel.VENT`, Woning › Dak › Boeideel; kosten `dak.ventRoosterM`).
|
||||
- **Gevel:** gevelbekleding per woning, met override per wand en per
|
||||
wandstuk: rabat (vochtdoek + kruislat + rabat 12 mm, kleur per woning,
|
||||
getekend als plankentextuur die over naden doorloopt), steen (181
|
||||
@@ -224,7 +231,10 @@ assets/ logo, texturen, skybox
|
||||
(bijv. stuc links, hout rechts). Steen is overal een
|
||||
steenstrip op een wit isolatieblok, samen 110 mm (`WallModel.STONE`).
|
||||
Het trasraam (cementplaat of een eigen steen) komt onder beide, dus
|
||||
twee stenen kan. Latwerk, doek en metselwerk lopen per gevel door over
|
||||
twee stenen kan. Op een stenen trasraam onder rabat, felsplaten of stuc
|
||||
(minder ver uitstekend) komt een afdekker: zetwerk 5° aflopend met een
|
||||
neus van 25 mm over de steen, onderbroken bij openingen, zwart/antraciet/wit
|
||||
(`WallModel.TRAS_CAP`, `TRIM_COLORS`; kosten `gevel.afdekkerM`). Latwerk, doek en metselwerk lopen per gevel door over
|
||||
unit-naden. Sandwichpanelen rondom, 600 mm de grond in.
|
||||
- **Ruimtes** (`room/room-model.js`): elk door wanden afgesloten gebied is
|
||||
een ruimte, bepaald als een verfemmer: alles wat bereikbaar is zonder door
|
||||
@@ -352,8 +362,10 @@ plus `<div class="tectum-light" data-model="amstel"></div>` (mount vanzelf), of
|
||||
west/oost; zadel- en wolfsdak: nok noord–zuid of west–oost); bij een hellend dak de nokhoogte (lessenaar:
|
||||
hoge kant, met de helling eronder; de goot omhoog neemt de nok mee), bij een wolfsdak ook de hoogte van het
|
||||
wolfseind (bij een dak over de hele woning één keer), dakbedekking, gevelbekleding (hout met richting, felsplaten, stuc, steenstrips; kleur),
|
||||
het accent van het model (de afwijkende gevel; of geen), trasraam (geen of steen, met hoogte; een cementplaat-trasraam van het model wordt geen), binnenspots aan/uit en het type buitenverlichting. Wolfsdak over de hele
|
||||
woning alleen als de units samen een rechthoek vormen (zoals de configurator). Geen openingen,
|
||||
het accent van het model (de afwijkende gevel; of geen), trasraam (geen of steen, met hoogte; een cementplaat-trasraam van het model wordt geen), boeidelen of daktrim
|
||||
(met kleur boeidelen; een model met beide houdt zijn indeling tot je kiest), zetwerk-kleur (afdekker trasraam en
|
||||
ventilatierooster), binnenspots aan/uit en het type buitenverlichting. Wolfsdak over de hele
|
||||
woning alleen als de units samen een rechthoek vormen en nooit bij een overkapping (zoals de configurator). Geen openingen,
|
||||
binnenwanden, inrichting of elektra; die volgen het model.
|
||||
- **Keuzes op een schone kopie:** `LightModel.build` past de keuzes elke keer toe op het model zelf; wat niet
|
||||
afwijkt blijft precies zoals in het model. Bij een andere maat schuiven binnenwanden mee met hun unit en
|
||||
@@ -371,7 +383,8 @@ plus `<div class="tectum-light" data-model="amstel"></div>` (mount vanzelf), of
|
||||
site in een Bootstrap col-12): vanaf 720 px naast elkaar, smaller 3D boven en de keuzes eronder met prijs en
|
||||
PDF vast onderin beeld. Stijl alles onder `.tectum-light` (Bootstrap-reboot geneutraliseerd); Bootstrap Icons
|
||||
nodig (`css/vendor/bootstrap-icons.min.css`). Het scrollwiel scrolt de pagina, tenzij je in de 3D hebt
|
||||
geklikt of Ctrl/⌘ ingedrukt houdt. Touch: één vinger draaien, twee vingers knijpen/schuiven (eigen
|
||||
geklikt of Ctrl/⌘ ingedrukt houdt. Zijwaarts schuiven: rechts of middelste knop slepen, Shift/Ctrl/⌘ +
|
||||
slepen, of de pijltjestoetsen met de muis boven de 3D. Touch: één vinger draaien, twee vingers knijpen/schuiven (eigen
|
||||
OrbitControls, geldt ook voor de configurator).
|
||||
- **Opslaan = PDF** (`LightPdf`): voorblad "Uw woningontwerp" (beeld uit de 3D-weergave, kerncijfers, keuzes
|
||||
in woorden via `LightModel.describe`, indicatieve prijs, contactgegevens en een QR-code naar de modelpagina
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
const APP_VERSION = '0.7.5';
|
||||
const APP_VERSION = '0.7.7';
|
||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||
|
||||
let tabs = [];
|
||||
@@ -77,7 +77,8 @@ const KEY_HELP = [
|
||||
['Beeld', [['1 / 2 / 3', '2D + 3D, alleen 2D, alleen 3D'], ['4', 'Rapport aan/uit'], ['X', 'Doorsnede aan/uit'], ['F', 'Plattegrond passend'],
|
||||
['L', 'Lopen aan/uit'], ['Ctrl+\\', 'Panelen verbergen'], ['F11', 'Volledig scherm']]],
|
||||
['Wand-editor', [['Shift+H', 'Horizontaal spiegelen']]],
|
||||
['3D rondkijken', [['Slepen', 'Draaien'], ['Rechts slepen', 'Verschuiven'], ['Scrollwiel', 'Zoomen']], true],
|
||||
['3D rondkijken', [['Slepen', 'Draaien'], ['Rechts of middelste knop slepen', 'Zijwaarts verschuiven'], ['Shift/Ctrl + slepen', 'Zijwaarts verschuiven'],
|
||||
['Pijltjestoetsen (muis boven de 3D)', 'Verschuiven (Shift = grotere stap)'], ['Scrollwiel', 'Zoomen']], true],
|
||||
['Plattegrond', [['Scrollwiel', 'Zoomen'], ['Slepen op lege plek', 'Verschuiven'], ['Middelste muisknop', 'Verschuiven']], true],
|
||||
['Lopen', [['W / S', 'Vooruit / achteruit (ook pijl op/neer)'], ['A / D', 'Opzij'], ['← / →', 'Draaien'], ['Shift', 'Rennen'], ['E', 'Deur of schakelaar bedienen'], ['L / Esc', 'Stoppen met lopen']]],
|
||||
['Rondkijken tijdens lopen', [['Slepen', 'Rondkijken']], true]
|
||||
|
||||
@@ -97,7 +97,9 @@ window.COST_DEFAULTS = {
|
||||
stucM2: { label: 'Stucwerk gevel (wapening + sierpleister op cementgebonden plaat)', unit: '€/m²', value: 45 },
|
||||
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 }
|
||||
felsMontage: { label: 'Montage felsplaten gevel (ook dagkanten en hoeken)', unit: 's/m²', value: 2100 },
|
||||
afdekkerM: { label: 'Afdekker trasraam (aluminium zetwerk, gepoedercoat)', unit: '€/m', value: 14 },
|
||||
afdekkerMontage: { label: 'Montage afdekker trasraam', unit: 's/m', value: 300 }
|
||||
}
|
||||
},
|
||||
|
||||
@@ -118,6 +120,8 @@ window.COST_DEFAULTS = {
|
||||
onderplaatMontage: { label: 'Montage onderplaat', unit: 's/m²', value: 900 },
|
||||
daktrimM: { label: 'Daktrim', unit: '€/m', value: 12 },
|
||||
daktrimMontage: { label: 'Montage daktrim', unit: 's/m', value: 300 },
|
||||
ventRoosterM: { label: 'Ventilatierooster onder boeideel (40 mm, aluminium)', unit: '€/m', value: 6 },
|
||||
ventRoosterMontage: { label: 'Montage ventilatierooster', unit: 's/m', value: 120 },
|
||||
staalKg: { label: 'Stalen nokbalk IPE S235 (op maat gezaagd, geboord, gegrond)', unit: '€/kg', value: 2.9 },
|
||||
nokOplegging: { label: 'Oplegging nokbalk (stalen schoen + bouten), 2 per balk', unit: '€/st.', value: 45 },
|
||||
nokPlaatsen: { label: 'Stalen nokbalk plaatsen, stellen en vastzetten', unit: 's', value: 2700 },
|
||||
|
||||
@@ -159,6 +159,8 @@ window.CostModel = (() => {
|
||||
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);
|
||||
add(el, 'Gevel', 'Gevel', 'Steenstrips', steen, 'm²', steen * G.steenM2, steen * G.steenMontage);
|
||||
const cap = (st.trasraamCapM || 0);
|
||||
add(el, 'Gevel', 'Gevel', 'Afdekker trasraam', cap, 'm', cap * (G.afdekkerM || 0), cap * (G.afdekkerMontage || 0));
|
||||
const stuc = st.stucAreaM2 || 0;
|
||||
add(el, 'Gevel', 'Gevel', 'Stucwerk', stuc, 'm²', stuc * (G.stucM2 || 0), stuc * (G.stucMontage || 0));
|
||||
const latM = st.battenLengthM || 0;
|
||||
@@ -239,6 +241,7 @@ window.CostModel = (() => {
|
||||
add(wo, 'Dak', 'Dak', `Boeideel ${e.fasciaHeight} mm`, e.fasciaLengthM, 'm', e.fasciaLengthM * D.boeideelM, e.fasciaLengthM * D.boeideelMontage);
|
||||
add(wo, 'Dak', 'Dak', 'Onderplaat boeideel', e.soffitM2, 'm²', e.soffitM2 * D.onderplaatM2, e.soffitM2 * D.onderplaatMontage);
|
||||
add(wo, 'Dak', 'Dak', `Daktrim ${e.trimHeight} mm`, e.trimLengthM, 'm', e.trimLengthM * D.daktrimM, e.trimLengthM * D.daktrimMontage);
|
||||
add(wo, 'Dak', 'Dak', 'Ventilatierooster onder boeideel', e.ventLengthM || 0, 'm', (e.ventLengthM || 0) * (D.ventRoosterM || 0), (e.ventLengthM || 0) * (D.ventRoosterMontage || 0));
|
||||
}
|
||||
if (totals.posts) {
|
||||
const pp = totals.posts;
|
||||
|
||||
+45
-10
@@ -36,7 +36,7 @@ window.HouseModel = (() => {
|
||||
spacing: { exterior: { min: 240, max: 480 }, interior: { min: 300, max: 600 } },
|
||||
facade: { type: 'rabat', stone: 'K490', rabatColor: 'naturel', stucColor: 'wit', felsColor: 'antraciet' },
|
||||
cladding: 'horizontaal',
|
||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM, finish: 'cementplaat' },
|
||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM, finish: 'cementplaat', capColor: 'antraciet' },
|
||||
interiorPaint: true,
|
||||
interiorColor: '#f2efe9',
|
||||
ceilingColor: '#f4f3ef',
|
||||
@@ -67,7 +67,7 @@ window.HouseModel = (() => {
|
||||
}
|
||||
function facadeMaterials(state, fc) {
|
||||
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, tr.capColor) : null;
|
||||
const cladding = state.settings.cladding;
|
||||
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, fels: null };
|
||||
@@ -1121,10 +1121,33 @@ window.HouseModel = (() => {
|
||||
}
|
||||
});
|
||||
state.interiorWalls.forEach(iw => { iw.wall.wall.minSpacing = sp.interior.min; iw.wall.wall.maxSpacing = sp.interior.max; });
|
||||
// Geen wolfsdak boven een overkapping: wordt een zadeldak met dezelfde goot en nok.
|
||||
if (houseRoofOn(state)) {
|
||||
if (state.settings.roof.house.form.type === 'wolf' && state.units.some(u => roofCovers(u, cover))) state.settings.roof.house.form.type = 'zadel';
|
||||
} else state.units.forEach(u => {
|
||||
const f = UnitModel.roofForm(u.unit);
|
||||
if (f.type === 'wolf' && roofCovers(u, cover)) UnitModel.applyRoofForm(u.unit, Object.assign(f, { type: 'zadel' }));
|
||||
});
|
||||
applyHouseRoof(state);
|
||||
applyRoofJoins(state);
|
||||
}
|
||||
|
||||
// Overkapping (veranda, inpandig of uitpandig): het dak van de unit steekt buiten zijn vloer. Een vrije dakmaat
|
||||
// telt pas vanaf de kleinste paal-uitkraging; daaronder is het overstek.
|
||||
function roofCovers(u, cover) {
|
||||
if (!u.roof.followFloor) {
|
||||
const d = u.roof.deck.floor, f = u.unit.floor.floor, m = 400;
|
||||
return (d.width - f.width) / 2 + Math.abs(u.roof.offsetX || 0) > m || (d.depth - f.depth) / 2 + Math.abs(u.roof.offsetZ || 0) > m;
|
||||
}
|
||||
const c = cover[u.id] || {}, x = u.roof.extend || {};
|
||||
return ['l', 'r', 'f', 'b'].some(k => (c[k] || 0) + (x[k] || 0) > EPS);
|
||||
}
|
||||
// Overkapping onder het dak van de unit (unitId) of ergens onder de woning (geen unitId): dan geen wolfsdak.
|
||||
function hasCover(state, unitId) {
|
||||
const cover = coverExtensions(state);
|
||||
return state.units.some(u => (!unitId || u.id === unitId) && roofCovers(u, cover));
|
||||
}
|
||||
|
||||
const EXTEND_MAX = 4000;
|
||||
function normalizeExtend(e) {
|
||||
return Object.fromEntries(['f', 'r', 'b', 'l'].map(k => [k, Math.max(0, Math.min(EXTEND_MAX, Math.round(Number((e || {})[k]) || 0)))]));
|
||||
@@ -1477,10 +1500,10 @@ window.HouseModel = (() => {
|
||||
});
|
||||
const ridgePairs = houseShape ? {} : doubleLessenaarPairs(state, rects, cuts);
|
||||
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels) {
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels, endCaps) {
|
||||
if (!build) return { group: null, stats: null, specs: null };
|
||||
const built = WallModel.build3D(config, frame, {
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels,
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop, battenAbove, stuc, skinTop, fels, endCaps,
|
||||
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) };
|
||||
@@ -1873,7 +1896,7 @@ window.HouseModel = (() => {
|
||||
};
|
||||
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;
|
||||
return e ? Math.sign(e.dot) * WallModel.claddingOffset(e.p.buitenMateriaal, e.p.stone, e.p.stuc) : null; // null: geen gevel (veranda)
|
||||
};
|
||||
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,
|
||||
@@ -1895,14 +1918,22 @@ window.HouseModel = (() => {
|
||||
// De rand begint op de dakrand, die op of iets vóór het wandvlak kan liggen.
|
||||
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);
|
||||
if (!sg) return;
|
||||
const above = sg.fels ? RoofModel.FELS_EDGE.aboveMM : roofEdge.edgeAbove;
|
||||
p.battenAbove = (p.skinAbove || 0) + above - RoofModel.edgeHeight(roofEdge, sg);
|
||||
// Daktrim: de bekleding loopt door tot het dak, de trim valt eroverheen.
|
||||
if (!sg || (!sg.fascia && !sg.fels)) return;
|
||||
p.battenAbove = (p.skinAbove || 0) + sg.above - RoofModel.edgeHeight(roofEdge, sg);
|
||||
});
|
||||
// Vrije uiteinden van binnenwanden: vlak voorbij het eind geen andere wand.
|
||||
const allPieces = extPieces.concat(interior);
|
||||
interior.forEach(p => {
|
||||
if (p.buitenMateriaal !== 'spaanplaat') return;
|
||||
const W = p.place.width / 2, against = (q) => allPieces.some(o => o !== p && pointInPoly(q,
|
||||
[[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([i, j]) => UnitModel.localToWorld(o.place, i * o.place.width / 2, j * o.place.thickness / 2))));
|
||||
p.endCaps = { left: !against(UnitModel.localToWorld(p.place, -W - 15, 0)), right: !against(UnitModel.localToWorld(p.place, W + 15, 0)) };
|
||||
});
|
||||
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
|
||||
const rooms = RoomModel.compute(state.rooms || [], units, extPieces.concat(interior));
|
||||
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, p.fels);
|
||||
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, p.endCaps);
|
||||
if (built.group && p.shiftZ) {
|
||||
const outer = new THREE.Group();
|
||||
built.group.position.z = p.shiftZ * 0.001;
|
||||
@@ -2219,6 +2250,7 @@ window.HouseModel = (() => {
|
||||
woodM3: sumU(u => u.floorSpecs.joistWoodM3 + u.roof.specs.joistWoodM3) + sumP(p => p.stats.woodVolumeM3),
|
||||
woodKg: sumU(u => u.floorSpecs.joistWoodKg + u.roof.specs.joistWoodKg) + sumP(p => p.specs.woodKg),
|
||||
battens: { count: sumP(p => p.stats.battenCount || 0), lengthM: sumP(p => p.stats.battenLengthM || 0), kg: sumP(p => p.specs.battenKg || 0) },
|
||||
trasraamCap: { lengthM: sumP(p => p.stats.trasraamCapM || 0), color: (WallModel.TRIM_COLORS[(pieces.find(p => p.stats.trasraamCapM > 0 && p.trasraam) || { trasraam: {} }).trasraam.capColor] || {}).label || '' },
|
||||
isolatie,
|
||||
isolatieM3: isolatie.reduce((a, r) => a + r.m3, 0),
|
||||
plaatwerk: [
|
||||
@@ -2292,6 +2324,8 @@ window.HouseModel = (() => {
|
||||
overhang: parts.roofEdge.overhang,
|
||||
soffitM2: parts.roofEdge.soffitM2,
|
||||
trimLengthM: parts.roofEdge.trimLengthMM / 1000,
|
||||
ventLengthM: (parts.roofEdge.ventLengthMM || 0) / 1000,
|
||||
ventColor: parts.roofEdge.vent ? (WallModel.TRIM_COLORS[parts.roofEdge.ventColor] || {}).label || '' : '',
|
||||
trimHeight: parts.roofEdge.trimHeight,
|
||||
spots: parts.roofEdge.spots.length
|
||||
} : null,
|
||||
@@ -2357,6 +2391,7 @@ window.HouseModel = (() => {
|
||||
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
||||
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
||||
if (!WallModel.isStoneFinish(state.settings.trasraam.finish)) state.settings.trasraam.finish = 'cementplaat';
|
||||
if (!WallModel.isTrimColor(state.settings.trasraam.capColor)) state.settings.trasraam.capColor = 'antraciet';
|
||||
state.settings.sandwich = state.settings.sandwich !== false;
|
||||
if (!/^#[0-9a-f]{6}$/i.test(state.settings.interiorColor || '')) state.settings.interiorColor = def.interiorColor;
|
||||
if (!/^#[0-9a-f]{6}$/i.test(state.settings.ceilingColor || '')) state.settings.ceilingColor = def.ceilingColor;
|
||||
@@ -2393,7 +2428,7 @@ window.HouseModel = (() => {
|
||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor, facadeMaterials,
|
||||
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, MODELS, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
||||
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls, syncSeamWalls, missingSeamWalls,
|
||||
houseRoofShape, houseRoofFits, setHouseRoof, coverExtensions, EXTEND_MAX,
|
||||
houseRoofShape, houseRoofFits, hasCover, setHouseRoof, coverExtensions, EXTEND_MAX,
|
||||
computeParts, placeGroups, rootGroups, groundOrigin, sectionCaps, allWallPieces, computeTotals, computeDimensions, dimensionRows, TRANSPORT, TRANSPORT_CLASSES, computeTransport, validateState, fromUnitDesign, normalize
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -271,6 +271,12 @@ window.HousePdf = (() => {
|
||||
const trColor = WallModel.isStoneFinish(tr.finish) ? (WallModel.STONE_COLORS[tr.finish] || '#a0724f') : VIEW_COLORS.cementplaat;
|
||||
const zT = p.place.thickness / 2 + (WallModel.isStoneFinish(tr.finish) ? WallModel.STONE.THICK_MM : 12);
|
||||
faces.push({ kind: 'trasraam', pts: band.map(q => toWorld(p, q.x, q.y, zT)), n, color: trColor, bias: 1 });
|
||||
// Afdekker: neus over de bovenkant van de steen (alleen in 3D onderbroken bij openingen).
|
||||
if (p.stats && p.stats.trasraamCapM > 0) {
|
||||
const C = WallModel.TRAS_CAP, yLo = tr.top - C.DROP_MM, yHi = tr.top + C.THICK_MM;
|
||||
faces.push({ kind: 'trasraam', pts: [[-W / 2, yLo], [W / 2, yLo], [W / 2, yHi], [-W / 2, yHi]].map(([lx, ly]) => toWorld(p, lx, ly, zT + C.GAP_MM + C.THICK_MM)),
|
||||
n, color: WallModel.trimHex(tr.capColor), bias: 1.5 });
|
||||
}
|
||||
}
|
||||
(p.config.openings || []).filter(op => op.type !== 'schoorsteen').forEach(op => {
|
||||
if (WallModel.openingTopMax(op) <= y0) return;
|
||||
@@ -328,6 +334,17 @@ window.HousePdf = (() => {
|
||||
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 });
|
||||
}
|
||||
// Rooster op de gevel, onder de onderplaat.
|
||||
if (sg.fascia && sg.facadeUnder && edge.vent) {
|
||||
const V = RoofModel.VENT, d = RoofModel.ventFace(sg) + V.THICK_MM - (sg.a.x * sg.normal.x + sg.a.z * sg.normal.z);
|
||||
const vTop = [];
|
||||
for (let k = 0; k <= n; k++) {
|
||||
const I = lerp(sg.a, sg.b, k / n), q = { x: I.x + sg.normal.x * d, z: I.z + sg.normal.z * d };
|
||||
vTop.push({ x: q.x, y: sg.topAt(q) - h, z: q.z });
|
||||
}
|
||||
faces.push({ kind: 'dakrand', pts: vTop.concat(vTop.map(q => ({ x: q.x, y: q.y - V.HEIGHT_MM, z: q.z })).reverse()),
|
||||
n: { x: sg.normal.x, z: sg.normal.z }, color: WallModel.trimHex(edge.ventColor), bias: 2.5 });
|
||||
}
|
||||
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 });
|
||||
});
|
||||
}
|
||||
@@ -371,7 +388,11 @@ window.HousePdf = (() => {
|
||||
items.push(WallModel.isStoneFinish(tf)
|
||||
? { label: `Trasraam (${st.trasraam.height} mm)`, value: `Steen ${tf}`, thumb: WallModel.stoneThumbUrl(tf, 64), color: WallModel.STONE_COLORS[tf] }
|
||||
: { label: `Trasraam (${st.trasraam.height} mm)`, value: 'Cementplaat', color: VIEW_COLORS.cementplaat });
|
||||
const tc = WallModel.TRIM_COLORS[st.trasraam.capColor];
|
||||
if (parts && tc && HouseModel.allWallPieces(parts).some(p => p.stats && p.stats.trasraamCapM > 0)) items.push({ label: 'Afdekker trasraam', value: tc.label, color: tc.hex });
|
||||
}
|
||||
const vc = parts && parts.roofEdge && parts.roofEdge.vent && parts.roofEdge.ventLengthMM > 0 && WallModel.TRIM_COLORS[parts.roofEdge.ventColor];
|
||||
if (vc) items.push({ label: 'Ventilatierooster', value: vc.label, color: vc.hex });
|
||||
(parts ? parts.units : state.units).forEach(u => {
|
||||
const cov = parts ? u.roof.covering : (u.roof.covering || 'dakleer');
|
||||
const c = RoofModel.COVERINGS[cov] || RoofModel.COVERINGS.dakleer;
|
||||
@@ -618,6 +639,8 @@ window.HousePdf = (() => {
|
||||
rows.push(['Onderplaat boeideel', fmt(e.soffitM2, 2) + ' m2']);
|
||||
}
|
||||
if (e.trimLengthM > 0) rows.push(['Daktrim', `${fmt(e.trimLengthM, 2)} m, ${e.trimHeight} mm hoog`]);
|
||||
if (e.ventLengthM > 0) rows.push(['Ventilatierooster', `${fmt(e.ventLengthM, 2)} m op de gevel onder de boeidelen, ${RoofModel.VENT.HEIGHT_MM} mm hoog, ${e.ventColor.toLowerCase()}`]);
|
||||
if (totals.trasraamCap && totals.trasraamCap.lengthM > 0) rows.push(['Afdekker trasraam', `${fmt(totals.trasraamCap.lengthM, 2)} m, ${totals.trasraamCap.color.toLowerCase()}`]);
|
||||
rows.push(['Boeideel-spots', e.spots + ' stuks']);
|
||||
rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]);
|
||||
rows.push(['Binnenspots', `${L.indoor} stuks (plafond)`]);
|
||||
|
||||
+26
-11
@@ -1312,6 +1312,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
const fmtDeg = (d) => String(Math.round(d * 10) / 10).replace('.', ',');
|
||||
const stoneOptions = Object.assign({}, ...WallModel.TRASRAAM_STONES.map(c => ({ [c]: `Steen ${c}` })));
|
||||
const trasraamFinishes = Object.assign({ cementplaat: 'Cementplaat' }, stoneOptions);
|
||||
const TRIM_OPTS = Object.fromEntries(Object.entries(WallModel.TRIM_COLORS).map(([k, c]) => [k, c.label]));
|
||||
const NO_WOLF = Object.fromEntries(Object.entries(UnitModel.ROOF_TYPES).filter(([k]) => k !== 'wolf'));
|
||||
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>`;
|
||||
@@ -1352,7 +1354,10 @@ function mountHouseEditor(container, callbacks) {
|
||||
(st.trasraam.enabled
|
||||
? UI.range('hs-trasraam-h', 'Hoogte', st.trasraam.height, { min: 100, max: 1000, step: 10, hint: 'Vanaf bovenkant vloer' }) +
|
||||
pair(sel('hs-trasraam-finish', 'Vormgeving', trasraamFinishes, st.trasraam.finish),
|
||||
WallModel.isStoneFinish(st.trasraam.finish) ? thumb(WallModel.stoneThumbUrl(st.trasraam.finish, 64), st.trasraam.finish) : '')
|
||||
WallModel.isStoneFinish(st.trasraam.finish) ? thumb(WallModel.stoneThumbUrl(st.trasraam.finish, 64), st.trasraam.finish) : '') +
|
||||
(WallModel.isStoneFinish(st.trasraam.finish)
|
||||
? pair(sel('hs-trasraam-cap', 'Afdekker', TRIM_OPTS, st.trasraam.capColor, { hint: 'Zetwerk op de steen waar de gevel erboven minder ver uitsteekt (rabat, felsplaat, stuc): 5° aflopend, neus 25 mm over de steen' }),
|
||||
swatchOf(WallModel.trimHex(st.trasraam.capColor))) : '')
|
||||
: '') },
|
||||
{ label: 'Onderzijde', html: toggle('hs-sandwich', 'Sandwichpanelen', st.sandwich,
|
||||
{ hint: `${SandwichModel.PANEL_WIDTH_MM} × ${SandwichModel.PANEL_LENGTH_MM} × ${SandwichModel.THICK_MM} mm rondom, vanaf bovenkant vloer ${SandwichModel.DEPTH_MM} mm naar beneden` }) +
|
||||
@@ -1371,6 +1376,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
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.onSel('hs-trasraam-finish', v => { st.trasraam.finish = v; }, rerender);
|
||||
UI.onSel('hs-trasraam-cap', v => { st.trasraam.capColor = v; }, rerender);
|
||||
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
|
||||
UI.onRange('hs-ground', v => { if (v >= 0) st.groundLevel = v; }, after);
|
||||
}
|
||||
@@ -1381,9 +1387,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
key: 'dak', label: 'Dak', order: 30,
|
||||
groups: () => {
|
||||
const r = state.settings.roof, hr = r.house;
|
||||
const wolfOk = HouseModel.houseRoofFits(state);
|
||||
const wolfOk = HouseModel.houseRoofFits(state) && !HouseModel.hasCover(state);
|
||||
houseRoofUi = hr.enabled && hr.form ? roofFormUi('hs', hr.form, {
|
||||
types: wolfOk || hr.form.type === 'wolf' ? null : Object.fromEntries(Object.entries(UnitModel.ROOF_TYPES).filter(([k]) => k !== 'wolf')),
|
||||
types: wolfOk || hr.form.type === 'wolf' ? null : NO_WOLF,
|
||||
notice: !wolfOk && hr.form.type === 'wolf' ? 'Wolfsdak alleen als de units samen een rechthoek vormen; bij deze L-/T-vorm wordt het een zadeldak.' : null,
|
||||
span: () => { const sh = HouseModel.houseRoofShape(state); return sh ? sh.span : 1000; },
|
||||
apply: () => HouseModel.applyGlobalSettings(state),
|
||||
@@ -1404,8 +1410,10 @@ function mountHouseEditor(container, callbacks) {
|
||||
{ label: 'Boeideel', html:
|
||||
UI.range('hs-overhang', 'Overstek', r.overhang, { min: RoofModel.OVERHANG_MIN, max: RoofModel.OVERHANG_MAX, step: 10, hint: 'Vanaf de gevel' }) +
|
||||
UI.range('hs-fascia-h', 'Hoogte', r.fasciaHeight, { min: 150, max: 500, step: 10 }) +
|
||||
sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor) },
|
||||
{ label: 'Daktrim', html: num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }) },
|
||||
sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor) +
|
||||
toggle('hs-vent', 'Ventilatierooster', r.vent !== false, { hint: `${RoofModel.VENT.HEIGHT_MM} mm hoog op de gevel, direct onder alle boeidelen` }) +
|
||||
(r.vent !== false ? pair(sel('hs-vent-color', 'Kleur rooster', TRIM_OPTS, r.ventColor || 'antraciet'), swatchOf(WallModel.trimHex(r.ventColor))) : '') },
|
||||
{ label: 'Daktrim', html: num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Over de gevel; steekt 30 mm boven het dak uit (units zonder boeidelen)' }) },
|
||||
{ label: 'Palen overkapping', html:
|
||||
toggle('hs-posts', 'Palen', r.posts.enabled, { hint: `Onder een dak dat minstens de min. uitkraging buiten de vloer steekt (veranda); per unit: Overkapping. Nu ${parts ? parts.posts.count : 0} st.` }) +
|
||||
(r.posts.enabled ? 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` }) +
|
||||
@@ -1426,6 +1434,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
UI.onRange('hs-overhang', v => { if (v > 0) r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); }, after);
|
||||
UI.onRange('hs-fascia-h', v => { if (v > 0) r.fasciaHeight = v; }, after);
|
||||
UI.onSel('hs-fascia-color', v => { r.fasciaColor = v; }, rerender);
|
||||
UI.onToggle('hs-vent', v => { r.vent = v; }, rerender);
|
||||
UI.onSel('hs-vent-color', v => { r.ventColor = v; }, rerender);
|
||||
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-side', v => { r.posts.sideWall = v; }, rerender);
|
||||
@@ -1889,7 +1899,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
const f = (k) => `${id}-roof-${k}`;
|
||||
const groups = () => {
|
||||
const g = [
|
||||
{ label: 'Dakvorm', html: sel(f('type'), 'Vorm', o.types || UnitModel.ROOF_TYPES, form.type) + (o.notice ? note(o.notice) : '') +
|
||||
{ label: 'Dakvorm', html: sel(f('type'), 'Vorm', (typeof o.types === 'function' ? o.types() : o.types) || UnitModel.ROOF_TYPES, form.type) + (o.notice ? note(o.notice) : '') +
|
||||
(form.type === 'lessenaar' ? sel(f('dir'), 'Hoge zijde', SIDE_SHORT, form.dir) : '') +
|
||||
(UnitModel.hasRidge(form.type) ? sel(f('ridge'), 'Nok', UnitModel.RIDGES, form.ridge) : '') +
|
||||
UI.range(f('low'), sloped() ? 'Goot' : 'Hoogte', form.low, { min: 2100, max: LIM.eave, step: 10, hint: `Vanaf bovenkant vloer tot onderkant dak (max ${LIM.eave} mm)` }) }
|
||||
@@ -1959,6 +1969,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
const form = UnitModel.roofForm(u.unit);
|
||||
const roofUi = roofFormUi('hp', form, {
|
||||
gable: true,
|
||||
types: () => HouseModel.hasCover(state, u.id) ? NO_WOLF : null,
|
||||
span: () => {
|
||||
const pu = partsOf();
|
||||
const s = UnitModel.WALL_SIDES.find(x => x.key === UnitModel.gableOf(form));
|
||||
@@ -2030,13 +2041,15 @@ function mountHouseEditor(container, callbacks) {
|
||||
UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender);
|
||||
UI.onToggle('hp-steel-ridge', v => { u.roof.steelRidge = v; }, rerender);
|
||||
UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender);
|
||||
// Een overkapping maakt van een wolfsdak een zadeldak: dan het paneel opnieuw opbouwen.
|
||||
const afterCover = () => (form.type === 'wolf' ? rerender : after)();
|
||||
['f', 'r', 'b', 'l'].forEach(k => UI.onNum('hp-ext-' + k, v => {
|
||||
u.roof.extend = Object.assign({ f: 0, r: 0, b: 0, l: 0 }, u.roof.extend, { [k]: Math.max(0, Math.min(HouseModel.EXTEND_MAX, v)) });
|
||||
}, after));
|
||||
UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, after);
|
||||
UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, after);
|
||||
UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, after);
|
||||
UI.onNum('hp-roof-oz', v => { u.roof.offsetZ = v; }, after);
|
||||
}, afterCover));
|
||||
UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, afterCover);
|
||||
UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, afterCover);
|
||||
UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, afterCover);
|
||||
UI.onNum('hp-roof-oz', v => { u.roof.offsetZ = v; }, afterCover);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2334,6 +2347,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
const e = totals.roofEdge, rows = [];
|
||||
if (e.fasciaLengthM > 0) rows.push([`Boeidelen · ${e.fasciaHeight} mm`, f(e.fasciaLengthM, 2), 'm'], ['Onderplaat boeideel', f(e.soffitM2, 2), 'm²']);
|
||||
if (e.trimLengthM > 0) rows.push([`Daktrim · ${e.trimHeight} mm`, f(e.trimLengthM, 2), 'm']);
|
||||
if (e.ventLengthM > 0) rows.push([`Ventilatierooster · ${e.ventColor.toLowerCase()}`, f(e.ventLengthM, 2), 'm']);
|
||||
if (totals.trasraamCap.lengthM > 0) rows.push([`Afdekker trasraam · ${totals.trasraamCap.color.toLowerCase()}`, f(totals.trasraamCap.lengthM, 2), 'm']);
|
||||
rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.'], ['Binnenspots', totals.lamps.indoor, 'st.']);
|
||||
cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows));
|
||||
}
|
||||
|
||||
+14
-2
@@ -77,7 +77,11 @@ window.LightModel = (() => {
|
||||
const t = base.settings.trasraam, stone = WallModel.isStoneFinish(t.finish) && t.finish !== 'cementplaat';
|
||||
return { on: !!t.enabled && stone, finish: stone ? t.finish : null, height: t.height };
|
||||
})(),
|
||||
lighting: { mode: base.settings.lighting.mode, direction: base.settings.lighting.direction, indoor: base.settings.lighting.indoor !== false }
|
||||
lighting: { mode: base.settings.lighting.mode, direction: base.settings.lighting.direction, indoor: base.settings.lighting.indoor !== false },
|
||||
// Boeidelen (aan = boeidelen, uit = daktrim) met kleur; een model met beide houdt zijn eigen indeling tot je kiest.
|
||||
fascia: { on: base.units.some(u => u.roof.fascia !== false), color: RoofModel.FASCIA_COLORS[base.settings.roof.fasciaColor] ? base.settings.roof.fasciaColor : 'antraciet' },
|
||||
// Zetwerk: één kleur voor de afdekker op het trasraam en het ventilatierooster onder de boeidelen.
|
||||
trim: WallModel.isTrimColor(base.settings.roof.ventColor) ? base.settings.roof.ventColor : 'antraciet'
|
||||
};
|
||||
const model = { key, label: def.label, base, forms, hasAccent: acc.length > 0, frame: frameOf(key, opts && opts.frames), initial: clone(choices) };
|
||||
const st = build(model, choices);
|
||||
@@ -222,6 +226,9 @@ window.LightModel = (() => {
|
||||
accents(st).forEach(o => o.set(a ? Object.assign({ type: a.type }, a.type === 'steen' ? { stone: a.color } : a.type === 'stuc' ? { stuc: a.color } : a.type === 'fels' ? { fels: a.color } : {}) : null));
|
||||
}
|
||||
Object.assign(st.settings.lighting, { mode: ch.lighting.mode, direction: ch.lighting.direction, indoor: ch.lighting.indoor });
|
||||
st.settings.trasraam.capColor = st.settings.roof.ventColor = ch.trim;
|
||||
if (ch.fascia.on !== model.initial.fascia.on) st.units.forEach(u => { u.roof.fascia = ch.fascia.on; });
|
||||
st.settings.roof.fasciaColor = ch.fascia.color;
|
||||
return st;
|
||||
}
|
||||
|
||||
@@ -326,12 +333,17 @@ window.LightModel = (() => {
|
||||
];
|
||||
const tr = ch.trasraam;
|
||||
rows.push(['Trasraam', tr.on ? `Steen ${tr.finish}, ${nl(tr.height / 1000)} m hoog` : 'Geen']);
|
||||
rows.push(['Dakrand', ch.fascia.on ? `Boeidelen ${RoofModel.FASCIA_COLORS[ch.fascia.color].label.toLowerCase()}` : 'Daktrim']);
|
||||
const trimUse = trimUses(ch);
|
||||
if (trimUse) rows.push(['Zetwerk', `${WallModel.TRIM_COLORS[ch.trim].label} (${trimUse})`]);
|
||||
if (model.hasAccent) rows.push(['Accentgevel', ch.accent ? cap(facade(ch.accent.type, ch.accent.type === 'rabat' ? fc.rabat : ch.accent.color)) : 'Geen']);
|
||||
rows.push(['Buitenverlichting', LIGHTING[ch.lighting.mode] + (ch.lighting.mode === 'gevel' ? ` (${LightingModel.DIRECTIONS[ch.lighting.direction].toLowerCase()})` : '')]);
|
||||
rows.push(['Binnenverlichting', ch.lighting.indoor ? 'Inbouwspots in het plafond' : 'Geen']);
|
||||
return rows;
|
||||
}
|
||||
const nl = (n) => n.toLocaleString('nl-NL', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
// Waar de zetwerk-kleur voor geldt (leeg: nergens).
|
||||
const trimUses = (ch) => [ch.trasraam.on ? 'afdekker trasraam' : null, ch.fascia.on ? 'ventilatierooster' : null].filter(Boolean).join(' en ');
|
||||
|
||||
return { DEFAULT_FRAME, FRAMES, STEP, PITCH, SIDES, formOf, FACADES, STONES, LIGHTING, create, build, check, fit, summary, describe, colorName };
|
||||
return { DEFAULT_FRAME, FRAMES, STEP, PITCH, SIDES, formOf, FACADES, STONES, LIGHTING, trimUses, create, build, check, fit, summary, describe, colorName };
|
||||
})();
|
||||
|
||||
+11
-8
@@ -135,20 +135,23 @@ window.LightPdf = (() => {
|
||||
], 'Kerncijfers');
|
||||
y = keyValues(doc, M, y + 6, 150, d.choices, 'Uw keuzes');
|
||||
|
||||
// Prijs
|
||||
// Prijs: rustig, als de rest van de pagina; alleen het bedrag groter en vet.
|
||||
const py = Math.max(y + 4, 196);
|
||||
doc.setFillColor(...ORANGE);
|
||||
doc.roundedRect(M, py, 150, 30, 3, 3, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setDrawColor(...LINE);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(M, py, M + 150, py);
|
||||
doc.setTextColor(...MUTED);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
doc.text('Indicatieve prijs', M + 7, py + 9);
|
||||
doc.text('Indicatieve prijs', M, py + 9);
|
||||
doc.setTextColor(...INK);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(24);
|
||||
doc.text(CostModel.eur(Math.round(i.price / 100) * 100), M + 7, py + 21);
|
||||
doc.setFontSize(20);
|
||||
doc.text(CostModel.eur(Math.round(i.price / 100) * 100), M, py + 19);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9);
|
||||
doc.text('incl. btw, onder voorbehoud', M + 7, py + 27);
|
||||
doc.setTextColor(...MUTED);
|
||||
doc.text('incl. btw, onder voorbehoud', M, py + 25);
|
||||
|
||||
// Beeld van de woning
|
||||
const bx = 190, by = 18, bw = PW - M - bx, bh = 206;
|
||||
|
||||
+14
-4
@@ -35,7 +35,7 @@ window.TectumLight = (() => {
|
||||
|
||||
// Doorsnede: snijhoogte boven de vloer (mm), zoals de doorsnede van de configurator.
|
||||
const SECTION = { start: 1500, min: 300, max: 3000 };
|
||||
const HINTS = { '3d': 'Slepen = draaien · rechts slepen = schuiven · klik en scroll = zoomen', cut: 'Slepen = draaien · klik en scroll = zoomen', plan: 'Noord boven · maten in mm' };
|
||||
const HINTS = { '3d': 'Slepen = draaien · rechts of Shift+slepen = schuiven · klik en scroll = zoomen', cut: 'Slepen = draaien · rechts of Shift+slepen = schuiven · klik en scroll = zoomen', plan: 'Noord boven · maten in mm' };
|
||||
|
||||
// tl-wrap is het raster; .tectum-light zelf is de container voor de container queries (breedte van de tool, niet
|
||||
// van het scherm: in de HUGO-site staat hij in een Bootstrap col-12).
|
||||
@@ -436,17 +436,22 @@ window.TectumLight = (() => {
|
||||
|
||||
// Dak: per unit (of één keer bij een dak over de hele woning) vorm, richting en hoogtes; de bedekking geldt overal.
|
||||
const wolfOk = !houseRoof || HouseModel.houseRoofFits(cur.st);
|
||||
const roofTypes = HouseModel.hasCover(cur.st, houseRoof ? null : unitSel)
|
||||
? Object.fromEntries(Object.entries(UnitModel.ROOF_TYPES).filter(([k]) => k !== 'wolf')) : UnitModel.ROOF_TYPES;
|
||||
const K = houseRoof ? 'house' : 'unit', c = houseRoof ? ch.house : u, f = NUM[K + 'High'].form(ch);
|
||||
const covItems = Object.entries(RoofModel.COVERINGS).map(([key, cv]) => ({ key, label: cv.label, hex: cv.color, img: RoofModel.coveringThumbUrl(key, 64) }));
|
||||
const roofHtml = (multi && !houseRoof ? field('Unit', seg('unit', Object.fromEntries(model.base.units.map(x => [x.id, x.id])), unitSel), null, 'tl-units') : '') +
|
||||
field('Dakvorm', seg(K + 'Type', UnitModel.ROOF_TYPES, c.type, (k) => k === 'wolf' && !wolfOk && 'Een wolfsdak kan alleen als de units samen een rechthoek vormen')) +
|
||||
field('Dakvorm', seg(K + 'Type', roofTypes, c.type, (k) => k === 'wolf' && !wolfOk && 'Een wolfsdak kan alleen als de units samen een rechthoek vormen')) +
|
||||
(c.type === 'lessenaar' ? field('Hoge kant', seg(K + 'Dir', LightModel.SIDES, f.dir)) : '') +
|
||||
(UnitModel.hasRidge(c.type) ? field('Nok', seg(K + 'Ridge', UnitModel.RIDGES, f.ridge)) : '') +
|
||||
slider(K + 'Low', c.type === 'plat' ? 'Hoogte' : 'Goothoogte', c.low, model.frame.height, 'Vanaf de vloer tot onderkant dak, aan de lage kant') +
|
||||
(c.type !== 'plat' ? slider(K + 'High', c.type === 'lessenaar' ? 'Hoge kant' : 'Nokhoogte', f.high,
|
||||
[Math.ceil((f.low + 300) / LightModel.STEP) * LightModel.STEP, UnitModel.LIMITS.ridge], 'Vanaf de vloer tot onderkant dak', ` · ${nl(f.pitch, 0)}°`) : '') +
|
||||
(c.type === 'wolf' ? wolfSlider(K + 'Cut', f) : '') +
|
||||
swatchField('Bedekking', swatches('covering', covItems, ch.covering, (k) => !sloped && RoofModel.COVERINGS[k].sloped && 'alleen op een hellend dak; een plat dak krijgt dakleer'));
|
||||
swatchField('Bedekking', swatches('covering', covItems, ch.covering, (k) => !sloped && RoofModel.COVERINGS[k].sloped && 'alleen op een hellend dak; een plat dak krijgt dakleer')) +
|
||||
field('Dakrand', seg('fascia', { aan: 'Boeidelen', uit: 'Daktrim' }, ch.fascia.on ? 'aan' : 'uit')) +
|
||||
(ch.fascia.on ? swatchField('Kleur boeidelen', swatches('fasciaColor',
|
||||
Object.entries(RoofModel.FASCIA_COLORS).map(([key, c]) => ({ key, label: c.label, hex: '#' + c.hex.toString(16).padStart(6, '0') })), ch.fascia.color)) : '');
|
||||
|
||||
const fc = ch.facade;
|
||||
const facadeHtml = field('Bekleding', seg('facadeType', LightModel.FACADES, fc.type)) +
|
||||
@@ -459,7 +464,9 @@ window.TectumLight = (() => {
|
||||
const tr = ch.trasraam;
|
||||
const trasraamHtml = field('Trasraam <small>stenen strook onderaan de gevel</small>', seg('trasraamType', { geen: 'Geen', steen: 'Steen' }, tr.on ? 'steen' : 'geen')) +
|
||||
(tr.on ? swatchField('Steen trasraam', swatches('trasraamStone', facadeColors('steen', tr.finish), tr.finish)) : '') +
|
||||
(tr.on ? slider('trasraamHigh', 'Hoogte trasraam', tr.height, [200, 1000], 'Vanaf bovenkant vloer') : '');
|
||||
(tr.on ? slider('trasraamHigh', 'Hoogte trasraam', tr.height, [200, 1000], 'Vanaf bovenkant vloer') : '') +
|
||||
(LightModel.trimUses(ch) ? swatchField(`Zetwerk <small>${LightModel.trimUses(ch)}</small>`,
|
||||
swatches('trim', Object.entries(WallModel.TRIM_COLORS).map(([key, c]) => ({ key, label: c.label, hex: c.hex })), ch.trim)) : '');
|
||||
|
||||
const L = ch.lighting;
|
||||
const lightHtml = field('Binnen', seg('indoor', { aan: 'Inbouwspots', uit: 'Geen' }, L.indoor ? 'aan' : 'uit')) +
|
||||
@@ -519,6 +526,9 @@ window.TectumLight = (() => {
|
||||
t.on = val === 'steen';
|
||||
if (t.on && !t.finish) t.finish = fc.type === 'steen' ? fc.stone : LightModel.STONES[0];
|
||||
} else if (key === 'trasraamStone') next.trasraam.finish = val;
|
||||
else if (key === 'trim') next.trim = val;
|
||||
else if (key === 'fascia') next.fascia.on = val === 'aan';
|
||||
else if (key === 'fasciaColor') next.fascia.color = val;
|
||||
else if (key === 'lightMode') next.lighting.mode = val;
|
||||
else if (key === 'lightDir') next.lighting.direction = val;
|
||||
else return;
|
||||
|
||||
+81
-21
@@ -12,6 +12,9 @@ window.RoofModel = (() => {
|
||||
const DENSITY_DAKLEER_KGM2 = 1.4;
|
||||
const TRIM_THICK_MM = 25;
|
||||
const EDGE_ABOVE_MM = 80;
|
||||
// Daktrim: voorkant 8 mm vóór de gevel, over de bovenste trimHeight van de gevel en 30 mm boven het dak; de opstand
|
||||
// is 20 mm diep, daarachter ligt een flens van 3 mm op het dak over de bovenkant van de bekleding.
|
||||
const TRIM = { OUT_MM: 8, ABOVE_MM: 30, DEPTH_MM: 20, FLANGE_MM: 3 };
|
||||
// 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 };
|
||||
@@ -23,9 +26,36 @@ window.RoofModel = (() => {
|
||||
fasciaHeight: 300,
|
||||
spotMin: 800,
|
||||
spotMax: 1200,
|
||||
trimHeight: 80,
|
||||
fasciaColor: 'antraciet'
|
||||
trimHeight: 30,
|
||||
fasciaColor: 'antraciet',
|
||||
vent: true,
|
||||
ventColor: 'antraciet'
|
||||
};
|
||||
// Ventilatierooster onder de boeidelen: strook op de gevel, direct onder de onderplaat; sleuven in de textuur.
|
||||
// Kleuren als de afdekker van het trasraam (WallModel.TRIM_COLORS).
|
||||
const VENT = { HEIGHT_MM: 40, THICK_MM: 12, PITCH_MM: 12 };
|
||||
const ventMaterials = {};
|
||||
function ventMaterial(key) {
|
||||
const k = WallModel.isTrimColor(key) ? key : 'antraciet';
|
||||
if (ventMaterials[k]) return ventMaterials[k];
|
||||
const base = new THREE.Color(WallModel.trimHex(k));
|
||||
const canvas = typeof document !== 'undefined' && document.createElement('canvas');
|
||||
const ctx = canvas && canvas.getContext && canvas.getContext('2d');
|
||||
let mat;
|
||||
if (!ctx) mat = new THREE.MeshStandardMaterial({ color: base, roughness: 0.5, metalness: 0.3 });
|
||||
else {
|
||||
canvas.width = 32; canvas.height = 32;
|
||||
ctx.fillStyle = '#' + base.getHexString();
|
||||
ctx.fillRect(0, 0, 32, 32);
|
||||
ctx.fillStyle = '#' + base.clone().multiplyScalar(0.18).getHexString();
|
||||
ctx.fillRect(11, 6, 11, 20); // sleuf, 4 mm breed en 25 mm hoog
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.wrapS = THREE.RepeatWrapping;
|
||||
tex.anisotropy = 8;
|
||||
mat = new THREE.MeshStandardMaterial({ color: 0xffffff, map: tex, roughness: 0.5, metalness: 0.3 });
|
||||
}
|
||||
return (ventMaterials[k] = mat);
|
||||
}
|
||||
|
||||
const FASCIA_COLORS = {
|
||||
wit: { label: 'Wit', hex: 0xf2f2f0 },
|
||||
@@ -980,15 +1010,16 @@ window.RoofModel = (() => {
|
||||
// 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 dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
|
||||
const above = fels ? FELS_EDGE.aboveMM : EDGE_ABOVE_MM;
|
||||
const above = fels ? FELS_EDGE.aboveMM : fascia ? EDGE_ABOVE_MM : TRIM.ABOVE_MM;
|
||||
return {
|
||||
a, b, owner: owner.unitId, fascia, fels, covering: owner.covering, normal,
|
||||
a, b, owner: owner.unitId, fascia, fels, covering: owner.covering, normal, above,
|
||||
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
|
||||
topAt: (p) => owner.topAt(p.x, p.z) + above, ridges: owner.ridges || [],
|
||||
overRoof: !!nb && !dropped, dropped
|
||||
};
|
||||
});
|
||||
const depRaw = segs.map(sg => (facadeDepth ? facadeDepth(sg.a, sg.b, sg.normal) || 0 : 0));
|
||||
const depIn = segs.map(sg => (facadeDepth ? facadeDepth(sg.a, sg.b, sg.normal) : 0));
|
||||
const depRaw = depIn.map(d => d || 0);
|
||||
const dep = depRaw.map(d => Math.min(0, d));
|
||||
const spotD = depRaw.map(d => Math.max(0, d) + (s.overhang - Math.max(0, d)) * SPOT_RATIO);
|
||||
const perp = (m1, m2) => Math.abs(m1.x * m2.x + m1.z * m2.z) < 0.5;
|
||||
@@ -1002,15 +1033,16 @@ window.RoofModel = (() => {
|
||||
spotA: at(sg.a, sg.normal, spotD[i], pv.normal, spotD[pi]), spotB: at(sg.b, sg.normal, spotD[i], nx.normal, spotD[ni])
|
||||
};
|
||||
});
|
||||
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i] }));
|
||||
// facadeUnder: een eigen gevel onder de rand (niet bij een veranda of een hogere gevel ertegenaan).
|
||||
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i], facadeUnder: depIn[i] !== null && depIn[i] >= 0 }));
|
||||
|
||||
// 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;
|
||||
// Rand zonder overstek (daktrim, 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) : Math.max(0, depRaw[k]) + TRIM.OUT_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]) => {
|
||||
if (!sg.fascia) 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;
|
||||
});
|
||||
@@ -1048,8 +1080,10 @@ window.RoofModel = (() => {
|
||||
return {
|
||||
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
||||
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaColor: s.fasciaColor,
|
||||
top, edgeAbove: EDGE_ABOVE_MM,
|
||||
vent: s.vent, ventColor: s.ventColor,
|
||||
top,
|
||||
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
||||
ventLengthMM: s.vent ? segments.filter(sg => sg.fascia && sg.facadeUnder).reduce((t, sg) => t + Math.hypot(sg.b.x - sg.a.x, sg.b.z - sg.a.z), 0) : 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),
|
||||
@@ -1071,22 +1105,30 @@ window.RoofModel = (() => {
|
||||
return out;
|
||||
}
|
||||
|
||||
function prismGeometry(poly, topFn, h) {
|
||||
const toM = 0.001, n = poly.length, pos = [];
|
||||
// uAt(p): met textuur, u langs de rand; v = 0 onder, 1 boven.
|
||||
function prismGeometry(poly, topFn, h, uAt) {
|
||||
const toM = 0.001, n = poly.length, pos = [], uvs = [];
|
||||
const T = poly.map(p => [p.x * toM, topFn(p) * toM, p.z * toM]);
|
||||
const B = poly.map(p => [p.x * toM, (topFn(p) - h) * toM, p.z * toM]);
|
||||
const U = uAt ? poly.map(uAt) : null;
|
||||
const tri = (a, b, c) => pos.push(...a, ...b, ...c);
|
||||
for (let k = 1; k < n - 1; k++) { tri(T[0], T[k], T[k + 1]); tri(B[0], B[k + 1], B[k]); }
|
||||
for (let k = 0; k < n; k++) { const j = (k + 1) % n; tri(T[k], B[k], B[j]); tri(T[k], B[j], T[j]); }
|
||||
const uv = (...q) => { if (U) q.forEach(([k, v]) => uvs.push(U[k], v)); };
|
||||
for (let k = 1; k < n - 1; k++) { tri(T[0], T[k], T[k + 1]); uv([0, 1], [k, 1], [k + 1, 1]); tri(B[0], B[k + 1], B[k]); uv([0, 0], [k + 1, 0], [k, 0]); }
|
||||
for (let k = 0; k < n; k++) { const j = (k + 1) % n; tri(T[k], B[k], B[j]); uv([k, 1], [k, 0], [j, 0]); tri(T[k], B[j], T[j]); uv([k, 1], [j, 0], [j, 1]); }
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||
if (U) geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
||||
geo.computeVertexNormals();
|
||||
return geo;
|
||||
}
|
||||
|
||||
// Gevelvlak onder een rand (afstand langs de normaal): de dakrand plus de bekleding die erbuiten steekt.
|
||||
function ventFace(sg) {
|
||||
return sg.a.x * sg.normal.x + sg.a.z * sg.normal.z + Math.max(0, sg.facadeDepth || 0);
|
||||
}
|
||||
// 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;
|
||||
return sg.fascia ? edge.fasciaHeight : sg.fels ? FELS_EDGE.heightMM : edge.trimHeight + TRIM.ABOVE_MM;
|
||||
}
|
||||
// Kleur van de rand in tekeningen (hex-getal).
|
||||
function edgeColor(edge, sg) {
|
||||
@@ -1116,11 +1158,27 @@ window.RoofModel = (() => {
|
||||
});
|
||||
const h = edgeHeight(edge, sg);
|
||||
const mat = sg.fascia ? materials.fasciaMaterial : sg.fels ? coveringMaterial(sg.covering) || materials.trimMaterial : materials.trimMaterial;
|
||||
polys.forEach(pl => {
|
||||
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), mat);
|
||||
const nd = (p) => p.x * sg.normal.x + p.z * sg.normal.z;
|
||||
const add = (pl, topFn, hh, m, u) => {
|
||||
const mesh = new THREE.Mesh(prismGeometry(pl, topFn, hh, u), m);
|
||||
mesh.visible = o.edgeVisible !== false;
|
||||
group.add(mesh);
|
||||
});
|
||||
};
|
||||
if (!sg.fascia && !sg.fels) {
|
||||
// Daktrim: opstand aan de voorkant, flens erachter op het dak.
|
||||
const inner = nd(sg.outerA) - TRIM.DEPTH_MM;
|
||||
polys.forEach(pl => {
|
||||
const up = clipPoly(pl, p => nd(p) - inner), fl = clipPoly(pl, p => inner - nd(p));
|
||||
if (up.length >= 3) add(up, sg.topAt, h, mat);
|
||||
if (fl.length >= 3) add(fl, p => sg.topAt(p) - TRIM.ABOVE_MM + TRIM.FLANGE_MM, TRIM.FLANGE_MM, mat);
|
||||
});
|
||||
} else polys.forEach(pl => add(pl, sg.topAt, h, mat));
|
||||
// Rooster: op de gevel (voorkant bekleding), onder de onderplaat van het boeideel.
|
||||
if (!sg.fascia || !sg.facadeUnder || !edge.vent) return;
|
||||
const face = ventFace(sg);
|
||||
const u = (p) => (alongZ ? p.z : p.x) / VENT.PITCH_MM;
|
||||
polys.map(pl => clipPoly(clipPoly(pl, p => nd(p) - face), p => face + VENT.THICK_MM - nd(p))).filter(pl => pl.length >= 3)
|
||||
.forEach(pl => add(pl, (p) => sg.topAt(p) - h, VENT.HEIGHT_MM, ventMaterial(edge.ventColor), u));
|
||||
});
|
||||
|
||||
const spotR = 0.035, spotH = 0.012;
|
||||
@@ -1145,13 +1203,15 @@ window.RoofModel = (() => {
|
||||
s.house = { enabled: !!h.enabled, form: form && UnitModel.ROOF_TYPES[form.type] ? form : null, cover: !!h.cover };
|
||||
s.posts = PostModel.normalizeSettings(s.posts);
|
||||
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
|
||||
s.vent = s.vent !== false;
|
||||
if (!WallModel.isTrimColor(s.ventColor)) s.ventColor = DEFAULT_SETTINGS.ventColor;
|
||||
return s;
|
||||
}
|
||||
|
||||
return {
|
||||
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
|
||||
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, TRIM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
|
||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, sharedRidgeBeam, joinValue, buildUnit3D, capRegions,
|
||||
FELS_EDGE, computeEdge, buildEdge3D, edgeHeight, edgeColor, normalizeSettings
|
||||
FELS_EDGE, VENT, ventFace, computeEdge, buildEdge3D, edgeHeight, edgeColor, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
Vendored
+26
-2
@@ -4,7 +4,8 @@
|
||||
// hebben om het origineel te vendoren; dekt precies de subset die wall.js
|
||||
// gebruikt (`new THREE.OrbitControls(camera, domElement)`,
|
||||
// `controls.target.set(...)`, `controls.update()`), plus interactie:
|
||||
// links-slepen = roteren, scrollwiel = zoomen, rechts-slepen = pannen;
|
||||
// links-slepen = roteren, scrollwiel = zoomen; zijwaarts schuiven (pannen) met rechts- of middelste-knop-slepen,
|
||||
// Shift/Ctrl/⌘ + links-slepen, of de pijltjestoetsen terwijl de muis boven de weergave staat;
|
||||
// touch: één vinger = roteren, twee vingers = knijpen (zoomen) en samen schuiven (pannen).
|
||||
//
|
||||
// Moet NA three.min.js geladen worden (leunt op window.THREE).
|
||||
@@ -50,7 +51,9 @@
|
||||
};
|
||||
|
||||
function onPointerDown(event) {
|
||||
if (event.button === 2) {
|
||||
const panButton = event.button === 2 || event.button === 1 || (event.button === 0 && (event.shiftKey || event.ctrlKey || event.metaKey));
|
||||
if (event.button === 1) event.preventDefault(); // geen autoscroll
|
||||
if (panButton) {
|
||||
if (!scope.enablePan) return;
|
||||
isPanning = true;
|
||||
} else {
|
||||
@@ -138,11 +141,29 @@
|
||||
|
||||
function onContextMenu(event) { event.preventDefault(); }
|
||||
|
||||
// Pijltjestoetsen: schuiven terwijl de muis boven de weergave staat (niet in een invoerveld).
|
||||
let hover = false;
|
||||
const onEnter = () => { hover = true; };
|
||||
const onLeave = () => { hover = false; };
|
||||
const KEY_PAN = { ArrowLeft: [1, 0], ArrowRight: [-1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] };
|
||||
function onKeyDown(event) {
|
||||
const d = KEY_PAN[event.key];
|
||||
if (!d || !hover || !scope.enablePan || event.altKey || event.ctrlKey || event.metaKey) return;
|
||||
const t = event.target, tag = t && t.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (t && t.isContentEditable)) return;
|
||||
event.preventDefault();
|
||||
const step = (scope.domElement.clientHeight || 400) * (event.shiftKey ? 0.25 : 0.08);
|
||||
pan(d[0] * step, d[1] * step);
|
||||
}
|
||||
|
||||
this.domElement.addEventListener('mousedown', onPointerDown);
|
||||
window.addEventListener('mousemove', onPointerMove);
|
||||
window.addEventListener('mouseup', onPointerUp);
|
||||
this.domElement.addEventListener('wheel', onWheel, { passive: false });
|
||||
this.domElement.addEventListener('contextmenu', onContextMenu);
|
||||
this.domElement.addEventListener('mouseenter', onEnter);
|
||||
this.domElement.addEventListener('mouseleave', onLeave);
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
this.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
|
||||
this.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
this.domElement.addEventListener('touchend', onTouchEnd);
|
||||
@@ -154,6 +175,9 @@
|
||||
window.removeEventListener('mouseup', onPointerUp);
|
||||
scope.domElement.removeEventListener('wheel', onWheel);
|
||||
scope.domElement.removeEventListener('contextmenu', onContextMenu);
|
||||
scope.domElement.removeEventListener('mouseenter', onEnter);
|
||||
scope.domElement.removeEventListener('mouseleave', onLeave);
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
scope.domElement.removeEventListener('touchstart', onTouchStart);
|
||||
scope.domElement.removeEventListener('touchmove', onTouchMove);
|
||||
scope.domElement.removeEventListener('touchend', onTouchEnd);
|
||||
|
||||
+70
-5
@@ -116,6 +116,23 @@ window.WallModel = (() => {
|
||||
return mat;
|
||||
}
|
||||
|
||||
// Afwerking in zetwerk (aluminium, gepoedercoat): afdekker op een stenen trasraam en het ventilatierooster onder
|
||||
// de boeidelen.
|
||||
const TRIM_COLORS = {
|
||||
zwart: { label: 'Zwart', hex: '#1f2022' },
|
||||
antraciet: { label: 'Antraciet', hex: '#3d4146' },
|
||||
wit: { label: 'Wit', hex: '#f3f2ee' }
|
||||
};
|
||||
const isTrimColor = (key) => !!TRIM_COLORS[key];
|
||||
const trimHex = (key) => (TRIM_COLORS[key] || TRIM_COLORS.antraciet).hex;
|
||||
const trimMaterials = {};
|
||||
function trimMaterial(key) {
|
||||
const k = isTrimColor(key) ? key : 'antraciet';
|
||||
return trimMaterials[k] || (trimMaterials[k] = new THREE.MeshStandardMaterial({ color: trimHex(k), roughness: 0.45, metalness: 0.3 }));
|
||||
}
|
||||
// Afdekker trasraam: bovenkant 5° aflopend (85° met de neus), neus 25 mm over de steen, 3 mm ervoor.
|
||||
const TRAS_CAP = { SLOPE_DEG: 5, DROP_MM: 25, GAP_MM: 3, THICK_MM: 2 };
|
||||
|
||||
function claddingOffset(buitenMateriaal, stone, stuc) {
|
||||
if (buitenMateriaal === 'vochtdoek') return 2 * BATTEN.thick + CLADDING_THICK_MM;
|
||||
if (buitenMateriaal !== 'cementplaat') return 0;
|
||||
@@ -175,9 +192,9 @@ window.WallModel = (() => {
|
||||
|
||||
// Hoogte vanaf het loopvlak (= onderkant van de wand).
|
||||
const TRASRAAM_DEFAULT_MM = 350;
|
||||
function trasraamFor(heightMM, finish) {
|
||||
function trasraamFor(heightMM, finish, capColor) {
|
||||
if (!(heightMM > 0)) return null;
|
||||
return { bottom: 0, top: heightMM, finish: finish || 'cementplaat' };
|
||||
return { bottom: 0, top: heightMM, finish: finish || 'cementplaat', capColor: isTrimColor(capColor) ? capColor : 'antraciet' };
|
||||
}
|
||||
|
||||
// Textuur 2062 × 1125 px op 2 mm/px. JOINT_MM: hart onderste voeg boven de onderrand (gemeten 1-2 px);
|
||||
@@ -849,7 +866,7 @@ window.WallModel = (() => {
|
||||
let trasraamAreaM2 = 0, trasraamVolumeM3 = 0;
|
||||
let stoneAreaM2 = 0, stoneIsolatieM3 = 0;
|
||||
let stucAreaM2 = 0;
|
||||
let trasraamStoneAreaM2 = 0, trasraamIsolatieM3 = 0;
|
||||
let trasraamStoneAreaM2 = 0, trasraamIsolatieM3 = 0, trasraamCapM = 0;
|
||||
let horizBeamCount = 0;
|
||||
|
||||
function createExtrudedBeam(shape, depth, positionZ) {
|
||||
@@ -1108,6 +1125,41 @@ window.WallModel = (() => {
|
||||
return { areaM2: area(mid) + area(caps), isoM3: area(mid) * tIso };
|
||||
}
|
||||
|
||||
// Afdekker op een stenen trasraam waar de gevel erboven minder ver uitsteekt (rabat, felsplaat, stuc): zetwerk
|
||||
// over de bovenkant van de steen, achter de bekleding beginnend, aan de voorkant een neus over de steen. Onderbroken
|
||||
// waar een opening door de bovenkant van het trasraam gaat; bij een buitenhoek tot voorbij de steen van de buurwand.
|
||||
function stoneCap(tr) {
|
||||
const C = TRAS_CAP, T = tr.top * toM;
|
||||
const zB = (maxDepth / 2) + skinGapM + sThickM + skinGapM, zF = zB + STONE.THICK_MM * toM;
|
||||
const zIn = zF + C.GAP_MM * toM, zOut = zIn + C.THICK_MM * toM, lip = zOut - zF;
|
||||
const yFront = T + C.THICK_MM * toM, yBack = yFront + Math.tan(C.SLOPE_DEG * Math.PI / 180) * (zOut - zB);
|
||||
const ext = extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM + STONE.THICK_MM);
|
||||
const a = -halfW - ext.left - (ext.left > 0 ? lip : 0), b = halfW + ext.right + (ext.right > 0 ? lip : 0);
|
||||
const yLo = T - C.DROP_MM * toM;
|
||||
const cuts = outerHoles.filter(h => h.hy0 < yBack && h.hy1 > yLo).map(h => [h.hx0, h.hx1]).sort((p, q) => p[0] - q[0]);
|
||||
const runs = [];
|
||||
let x = a;
|
||||
cuts.forEach(([c0, c1]) => { if (c0 > x) runs.push([x, Math.min(b, c0)]); x = Math.max(x, c1); });
|
||||
if (x < b) runs.push([x, b]);
|
||||
const shape = new THREE.Shape([
|
||||
[zB, T + 0.0005], [zIn, T + 0.0005], [zIn, yLo], [zOut, yLo], [zOut, yFront], [zB, yBack]
|
||||
].map(([pz, py]) => new THREE.Vector2(pz, py)));
|
||||
let lengthM = 0;
|
||||
runs.forEach(([r0, r1]) => {
|
||||
if (r1 - r0 <= edgeEps) return;
|
||||
lengthM += r1 - r0;
|
||||
if (!buitenVisible || !show('gevel')) return;
|
||||
// Profiel in (z, y), langs de wand uitgetrokken: -90° om Y legt de extrusie langs -X.
|
||||
const mesh = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: r1 - r0, bevelEnabled: false }), trimMaterial(tr.capColor));
|
||||
mesh.rotation.y = -Math.PI / 2;
|
||||
mesh.position.x = r1;
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
wallGroup3D.add(mesh);
|
||||
});
|
||||
return lengthM;
|
||||
}
|
||||
|
||||
function stucBlock(colorKey, y0) {
|
||||
const zB = (maxDepth / 2) + skinGapM + sThickM + skinGapM;
|
||||
const t = STUC.THICK_MM * toM;
|
||||
@@ -1205,6 +1257,17 @@ window.WallModel = (() => {
|
||||
cap.position.set(x + dir * 0.0003, h / 2, 0);
|
||||
if (spaanplaatVisible) wallGroup3D.add(cap);
|
||||
});
|
||||
// Vrij uiteinde van een binnenwand (endCaps): afgewerkt zoals de wand, geen stijl in zicht.
|
||||
const endCaps = opts.endCaps || {};
|
||||
if (buitenMateriaal === 'spaanplaat' && spaanplaatVisible) [[endCaps.left, -halfW, -1], [endCaps.right, halfW, 1]].forEach(([on, x, dir]) => {
|
||||
if (!on) return;
|
||||
const h = topYAt(x), depth = maxDepth + 2 * (skinGapM + sThickM);
|
||||
if (!(h > 0)) return;
|
||||
const cap = new THREE.Mesh(new THREE.PlaneGeometry(depth, h), spaanplaatMaterial);
|
||||
cap.rotation.y = dir * Math.PI / 2;
|
||||
cap.position.set(x + dir * 0.0003, h / 2, 0);
|
||||
wallGroup3D.add(cap);
|
||||
});
|
||||
spaanplaatAreaM2 = spaanplaatResult.areaM2;
|
||||
spaanplaatVolumeM3 = spaanplaatResult.volumeM3;
|
||||
|
||||
@@ -1229,6 +1292,7 @@ window.WallModel = (() => {
|
||||
const r = stoneBlock(trasraam.finish, trasraam.bottom * toM, trasraam.top * toM, { y: trasraam.top * toM, down: true });
|
||||
trasraamStoneAreaM2 = r.areaM2;
|
||||
trasraamIsolatieM3 = r.isoM3;
|
||||
if (stucKey) trasraamCapM = stoneCap(trasraam);
|
||||
y0 = trasraam.top * toM;
|
||||
} else if (trasraam) {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible && show('latwerk'), {
|
||||
@@ -1306,6 +1370,7 @@ window.WallModel = (() => {
|
||||
const r = stoneBlock(trasraam.finish, trasraam.bottom * toM, trasraam.top * toM, { y: trasraam.top * toM, down: true });
|
||||
trasraamStoneAreaM2 = r.areaM2;
|
||||
trasraamIsolatieM3 = r.isoM3;
|
||||
trasraamCapM = stoneCap(trasraam);
|
||||
} else if (trasraam) {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible && show('latwerk'), {
|
||||
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + (rowIndex + 1) * doekZStepM + skinGapM
|
||||
@@ -1445,7 +1510,7 @@ window.WallModel = (() => {
|
||||
stats: {
|
||||
woodVolumeM3, spaanplaatVolumeM3, spaanplaatAreaM2, cementplaatVolumeM3, cementplaatAreaM2, doekAreaM2,
|
||||
spaanplaatBuitenAreaM2, spaanplaatBuitenVolumeM3, horizBeamCount,
|
||||
battenLengthM, battenVolumeM3, battenCount, battenPieces, rabatAreaM2, felsAreaM2, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3, stucAreaM2
|
||||
battenLengthM, battenVolumeM3, battenCount, battenPieces, rabatAreaM2, felsAreaM2, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, trasraamCapM, stoneAreaM2, stoneIsolatieM3, stucAreaM2
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1674,7 +1739,7 @@ window.WallModel = (() => {
|
||||
}
|
||||
|
||||
return {
|
||||
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,
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, RABAT, RABAT_COLORS, rabatHex, FELS, isFelsColor, felsHex, TRIM_COLORS, isTrimColor, trimHex, trimMaterial, TRAS_CAP, STUC, STUC_COLORS, isStucColor, stucHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
stoneMaterial, stucMaterial, felsMaterial, claddingMaterial, sheathingHoles, skinSegmentsAtY, topLine, skinConfig, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||
createMaterials, build3D, computeSpecs
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"fasciaHeight": 300,
|
||||
"spotMin": 800,
|
||||
"spotMax": 1200,
|
||||
"trimHeight": 80,
|
||||
"trimHeight": 30,
|
||||
"fasciaColor": "antraciet"
|
||||
},
|
||||
"lighting": {
|
||||
|
||||
Reference in New Issue
Block a user