From 86fcd57b964fc933c35c6219ac53701b7cd26dcf Mon Sep 17 00:00:00 2001 From: Machiel Date: Sun, 11 Oct 2026 14:26:06 +0200 Subject: [PATCH] =?UTF-8?q?v0.6.3:=20modellen=20Waal=20(12,85=20=C3=97=207?= =?UTF-8?q?=20m,=20flauw=20zadeldak=20dat=20aan=20de=20oostkop=201,5=20m?= =?UTF-8?q?=20doorloopt=20als=20veranda),=20Maas=20(9,2=20=C3=97=209,2=20m?= =?UTF-8?q?,=20woningdak=20met=20inpandige=20veranda=20in=20de=20noordwest?= =?UTF-8?q?hoek)=20en=20Rijn=20(L-vorm=2013=20=C3=97=2010=20m,=20zadeldak?= =?UTF-8?q?=20als=20twee=20lessenaars=20op=20de=20naad,=20vleugel=20met=20?= =?UTF-8?q?plat=20dak,=20steenstrips=20met=20schoorsteenstrook,=20kookeila?= =?UTF-8?q?nd,=20logeerkamer,=20spots=20in=20de=20boeidelen);=20overkappin?= =?UTF-8?q?g=20per=20unit=20(dak=20verlengen=20voorbij=20de=20gevel)=20met?= =?UTF-8?q?=20palen=20(PostModel:=20hout,=20maat,=20h.o.h.,=20op=20de=20pl?= =?UTF-8?q?attegrond/PDF=20en=20in=20de=20kosten);=20kleur=20schakelaars/w?= =?UTF-8?q?cd's=20(wit,=20antraciet,=20zwart);=20gevellampen=20naast=20ope?= =?UTF-8?q?ningen=20en=20per=20vrij=20wandstuk=20gelijk=20verdeeld,=20nooi?= =?UTF-8?q?t=20in=20een=20opening;=20logobord=20niet=20meer=20onder=20een?= =?UTF-8?q?=20unit=20bij=20een=20voordeur=20in=20een=20binnenhoek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- README.md | 14 +- index.html | 1 + js/app.js | 2 +- js/cost/cost-defaults.js | 5 +- js/cost/cost-model.js | 7 +- js/elektra/elektra-model.js | 21 ++- js/house/house-model.js | 314 +++++++++++++++++++++++++++++++++- js/house/house-pdf.js | 8 + js/house/house.js | 48 +++++- js/lighting/lighting-model.js | 56 ++++-- js/roof/post-model.js | 139 +++++++++++++++ js/roof/roof-model.js | 3 +- kosten/intern.csv | 1 + kosten/leveranciers.csv | 2 + 14 files changed, 586 insertions(+), 35 deletions(-) create mode 100644 js/roof/post-model.js diff --git a/README.md b/README.md index 4a7f256..609a20a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ js/core/ gedeelde helpers: geometrie, bouwfasen, ribbon, UI-kit, PDF- js/wall/ WallModel (logica) + wand-PDF + wand-editor js/floor/ FloorModel (logica) + vloer-PDF + vloer-editor js/roof/ RoofModel: dak (plat/lessenaar/zadel) + dakrand (boeidelen/daktrim) + PostModel: palen onder een overkapping/veranda (dak per unit te verlengen) js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots js/elektra/ ElektraModel + ElektraPdf: meterkast, schakelaars (letters), wcd, groepen, leidingen js/sandwich/ SandwichModel: sandwichpanelen rondom @@ -292,7 +293,18 @@ hout.balk.38x235;Constructiehout vuren C24 38×235;€/m;4,91;Houthandel Y;;2026 (`HouseModel.createIJssel`, naar `examples/ontwerp_ijssel.pdf`): L-vorm 14 × 4,6 m met een vleugel van 4,6 × 3 m, twee units met zadeldak en riet die met een kil en een hoekkeper op elkaar aansluiten, stucwerk wit, - openslaande deuren in de westgevel. De standaardmodellen staan in + openslaande deuren in de westgevel. Model **Waal** + (`HouseModel.createWaal`, naar `examples/ontwerp_waal.pdf`): 12,85 × 7 m, + flauw zadeldak dat aan de oostkop 1,5 m doorloopt als veranda op eiken + palen (Overkapping per unit). Model **Maas** (`HouseModel.createMaas`, + naar `examples/ontwerp_maas.pdf`): vierkant 9,2 × 9,2 m, flauw zadeldak + over de hele woning; de overkapping maakt van de noordwesthoek een + inpandige veranda (2,5 × 4,6 m) op eiken palen. Model **Rijn** + (`HouseModel.createRijn`, naar `examples/ontwerp_rijn.pdf`): L-vorm + 13 × 10 m, hoofdvolume met flauw zadeldak (twee lessenaars die op de naad + de nok vormen) en een vleugel met plat dak, steenstrips rondom met een + donkere schoorsteenstrook in de oostgevel, kookeiland, logeerkamer en + apart toilet in de vleugel. De standaardmodellen staan in `HouseModel.MODELS`; Bestand › Snel nieuw en het venster Nieuw worden daaruit opgebouwd (nieuw model = één regel plus een `create`-functie die `modelKit` gebruikt). De L-woning (8 × 3,6 m + 4,5 × 3,6 m) blijft diff --git a/index.html b/index.html index 78a7c7c..988c781 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,7 @@ + diff --git a/js/app.js b/js/app.js index 066d548..2255d6b 100644 --- a/js/app.js +++ b/js/app.js @@ -4,7 +4,7 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ -const APP_VERSION = '0.6.2'; +const APP_VERSION = '0.6.3'; // 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang. let tabs = []; diff --git a/js/cost/cost-defaults.js b/js/cost/cost-defaults.js index 0e60b23..ee80304 100644 --- a/js/cost/cost-defaults.js +++ b/js/cost/cost-defaults.js @@ -120,7 +120,10 @@ window.COST_DEFAULTS = { 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 }, nokVulhoutMontage: { label: 'Vulhout in de holling van de nokbalk aanbrengen (beide zijden)', unit: 's/m', value: 240 }, - nokPersoneel: { label: 'Personeel bij plaatsen nokbalk (i.p.v. de bezetting)', unit: 'man', value: 3 } + nokPersoneel: { label: 'Personeel bij plaatsen nokbalk (i.p.v. de bezetting)', unit: 'man', value: 3 }, + paalHoutM3: { label: 'Paalhout overkapping (eiken, geschaafd)', unit: '€/m³', value: 2400 }, + paalvoet: { label: 'Paalvoet verzinkt, verstelbaar (incl. ankers)', unit: '€/st.', value: 40 }, + paalMontage: { label: 'Paal plaatsen: stellen, aftoppen, vastzetten aan het dak', unit: 's/st.', value: 2700 } } }, diff --git a/js/cost/cost-model.js b/js/cost/cost-model.js index 83d0877..d948155 100644 --- a/js/cost/cost-model.js +++ b/js/cost/cost-model.js @@ -230,7 +230,7 @@ window.CostModel = (() => { } }); - const wo = newEl('WON', 'Woning (dakrand, fundering, verlichting)', null); + const wo = newEl('WON', 'Woning (dakrand, overkapping, fundering, verlichting)', null); const D = S.dak; if (totals.roofEdge) { const e = totals.roofEdge; @@ -238,6 +238,11 @@ window.CostModel = (() => { add(wo, 'Dak', 'Dak', 'Onderplaat boeideel', e.soffitM2, 'm²', e.soffitM2 * D.onderplaatM2, e.soffitM2 * D.onderplaatMontage); add(wo, 'Dak', 'Dak', `Daktrim ${e.trimHeight} mm`, e.trimLengthM, 'm', e.trimLengthM * D.daktrimM, e.trimLengthM * D.daktrimMontage); } + if (totals.posts) { + const pp = totals.posts; + add(wo, 'Hout', 'Dak', `${pp.wood} palen ${pp.size}×${pp.size} mm (${pp.count} st.)`, pp.lengthM, 'm', pp.m3 * D.paalHoutM3, pp.count * D.paalMontage); + add(wo, 'Staal', 'Dak', 'Paalvoeten', pp.count, 'st.', pp.count * D.paalvoet, 0); + } if (parts.sandwich) { const sw = parts.sandwich; add(wo, 'Vloer', 'Vloer', `Sandwichpanelen (${sw.panelCount} platen)`, sw.areaM2, 'm²', sw.areaM2 * S.vloer.sandwichM2, sw.panelCount * S.vloer.sandwichMontage); diff --git a/js/elektra/elektra-model.js b/js/elektra/elektra-model.js index 836e3ca..ca09e7f 100644 --- a/js/elektra/elektra-model.js +++ b/js/elektra/elektra-model.js @@ -12,6 +12,7 @@ window.ElektraModel = (() => { switchHeight: 1500, wcdHeight: 400, wcdPerUnit: 2, + plateColor: 'wit', // schakelaars, wcd's, apparaat-wcd's en vloerdozen slack: 10, // % reserve: 300, meterkastReserve: 500, @@ -32,6 +33,11 @@ window.ElektraModel = (() => { droger: { label: 'Droger', code: 'DR', breaker: 'B16', cable: 'wcd', height: 1100, zone: 'techniek', material: 'Wandcontactdoos enkel met randaarde' }, warmtepomp: { label: 'Warmtepomp', code: 'WP', breaker: 'B16', cable: 'wcd', height: 1200, zone: 'techniek', material: 'Werkschakelaar 2-polig (warmtepomp)' } }; + const PLATE_COLORS = { + wit: { label: 'Wit', hex: 0xf6f6f2 }, + antraciet: { label: 'Antraciet', hex: 0x3a3d41 }, + zwart: { label: 'Zwart', hex: 0x161718 } + }; const BOX_PITCH = 71; const PLATE = 80; const WCD_W = 150; @@ -62,6 +68,7 @@ window.ElektraModel = (() => { s.removed = Array.isArray(i.removed) ? i.removed.slice() : []; s.vloerWcd = Array.isArray(i.vloerWcd) ? i.vloerWcd.filter(o => o && o.unitId).map(o => Object.assign({}, o)) : []; s.enabled = s.enabled !== false; + if (!PLATE_COLORS[s.plateColor]) s.plateColor = DEFAULT_SETTINGS.plateColor; // v2: schakelaars 1050 -> 1500 mm, wcd 300 -> 400 mm (oude standaard). if (!i.v) { if (i.switchHeight === 1050) s.switchHeight = DEFAULT_SETTINGS.switchHeight; @@ -561,6 +568,7 @@ window.ElektraModel = (() => { out.combos.forEach(c => { frames[c.switches.length] = (frames[c.switches.length] || 0) + 1; }); const m = out.materials; const add = (naam, aantal, eenheid) => { if (aantal > 0) m.push({ naam, aantal, eenheid: eenheid || 'st.' }); }; + const kleur = ', ' + PLATE_COLORS[s.plateColor].label.toLowerCase(); if (out.meterkast) { add('Groepenkast 3-fase (aansluiting 3×25 A)', 1); add('Hoofdschakelaar 4-polig 40 A', 1); @@ -571,13 +579,13 @@ window.ElektraModel = (() => { add('Installatieautomaat B16 1P+N (wcd)', nWcdG); add('Installatieautomaat B16 1P+N (apparaten)', out.apps.filter(a => (APPLIANCES[a.app].poles || 1) === 1).length); add('Installatieautomaat 2× B16, 2-fase (kookgroep)', out.apps.filter(a => APPLIANCES[a.app].poles === 2).length); - add('Schakelaar enkelpolig (inbouw)', out.switches.length); - Object.keys(frames).forEach(n => add(`Afdekraam ${n}-voudig`, frames[n])); - add('Wandcontactdoos dubbel met randaarde', out.wcds.length); - add('Vloerdoos met dubbele wcd (randaarde)', out.floorWcds.length); + add('Schakelaar enkelpolig (inbouw)' + kleur, out.switches.length); + Object.keys(frames).forEach(n => add(`Afdekraam ${n}-voudig` + kleur, frames[n])); + add('Wandcontactdoos dubbel met randaarde' + kleur, out.wcds.length); + add('Vloerdoos met dubbele wcd (randaarde)' + kleur, out.floorWcds.length); const appMat = {}; out.apps.forEach(a => { const n = APPLIANCES[a.app].material; appMat[n] = (appMat[n] || 0) + 1; }); - Object.keys(appMat).forEach(n => add(n, appMat[n])); + Object.keys(appMat).forEach(n => add(n + kleur, appMat[n])); add('Luchtdichte hollewanddoos (schakelaar/wcd/apparaat)', out.switches.length + 2 * out.wcds.length + out.apps.length); add('Luchtdichte verdeeldoos (plafond)', out.boxes.length); add('Lasklem 3-voudig (schatting)', out.boxes.length * 3 + out.switches.length + out.wcds.length); @@ -731,6 +739,7 @@ window.ElektraModel = (() => { const toM = 0.001, group = new THREE.Group(); if (!el || !el.enabled) return group; const mats = materials || createMaterials(); + mats.plateMaterial.color.setHex(PLATE_COLORS[el.settings.plateColor].hex); const st = BuildStages.clamp(stage), show = (key) => BuildStages.shows(st, key); const rough = show('elektra') && !show('beplating'); const box = (w, h, d, c, rotY, mat) => { @@ -864,5 +873,5 @@ window.ElektraModel = (() => { }; } - return { DEFAULT_SETTINGS, CABLES, CORE_COLORS, APPLIANCES, PLATE, WCD_W, cableInfo, normalizeSettings, compute, snapToWall, wallCoords, clipPlan, roofDeckMap, createMaterials, build3D }; + return { DEFAULT_SETTINGS, PLATE_COLORS, CABLES, CORE_COLORS, APPLIANCES, PLATE, WCD_W, cableInfo, normalizeSettings, compute, snapToWall, wallCoords, clipPlan, roofDeckMap, createMaterials, build3D }; })(); diff --git a/js/house/house-model.js b/js/house/house-model.js index 4d8851c..5c98cf2 100644 --- a/js/house/house-model.js +++ b/js/house/house-model.js @@ -306,7 +306,7 @@ window.HouseModel = (() => { Object.assign(S.facade, { type: 'stuc', stucColor: 'wit' }); Object.assign(S.trasraam, { enabled: true, height: 600, finish: 'R510' }); S.roof.fasciaColor = 'zwart'; - S.lighting.mode = 'gevel'; + S.lighting.mode = 'spots'; S.lighting.direction = 'both'; if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true }); @@ -368,11 +368,298 @@ window.HouseModel = (() => { return st; } + // Model Waal (examples/ontwerp_waal.pdf): 12,85 × 7 m, twee units van 3,5 m met één flauw zadeldak over de + // woning (nok west–oost op de naad). Het dak loopt aan de oostkop 1,5 m door als veranda (10,5 m²) op drie + // eiken palen (PostModel). Geen naadwand: slaapkamer en woonkamer/keuken lopen over beide units door; bij de + // naad staat aan elke kant een binnenwand (badkamer/keuken | hal). Alles buitenwerks vanaf de noordwesthoek. + function createWaal() { + const st = newModelState('Waal'); + const S = st.settings, LO = 2800, HI = 3400, W = 12850, D = 3500; + Object.assign(S.facade, { type: 'rabat', rabatColor: 'grijs' }); + S.cladding = 'verticaal'; + Object.assign(S.trasraam, { enabled: true, height: 600, finish: 'R510' }); + Object.assign(S.roof, { fasciaColor: 'wit', overhang: RoofModel.OVERHANG_MAX, fasciaHeight: 350 }); + S.roof.posts = PostModel.normalizeSettings({ enabled: true, size: 200, wood: 'eiken' }); + S.lighting.mode = 'spots'; + S.lighting.direction = 'both'; + if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true }); + + const A = createUnitEntry('A', null, { width: W, depth: D, height: LO, windowOn: [] }); + const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: W, depth: D, height: LO, windowOn: [] }); + st.units.push(A, B); + setHouseRoof(st, true); + Object.assign(S.roof.house, { form: { type: 'zadel', ridge: 'x', low: LO, high: HI } }); + [A, B].forEach(u => { u.roof.extend = { f: 0, r: 1500, b: 0, l: 0 }; }); + + const { K, binnen, ops, iw, room, fx } = modelKit(st, LO, 90); + // Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand. + ops(A.unit.walls.voor, [ + ['raam', 800, 500, 1600, 'right', 3490, K('raam')], + ['raam', 800, 1100, 1000, 'right', 6530, K('raam')], + ['raam', 1400, 1100, 1000, 'right', 8510, K('raam')]]); + ops(A.unit.walls.links, [['raam', 500, 1400, 700, 'left', 1170, K('raam')]]); + ops(B.unit.walls.links, [['raam', 500, 1400, 700, 'left', 390, K('raam')], ['raam', 1400, 1100, 1000, 'left', 1570, K('raam')]]); + // Oostkop onder de veranda: schuifdeur bij de keuken, openslaande deuren bij de woonkamer. + ops(A.unit.walls.rechts, [['deur', 1800, 2100, 0, 'right', 880, + K('deur', { type: 'schuifdeur', leaves: 2, mount: 'opbouw', side: 'binnen', glas: true })]]); + ops(B.unit.walls.rechts, [ + ['deur', 900, 2100, 0, 'right', 860, K('deur', { leaf: 'rechts', hinge: 'rechts', swing: 'buiten', volglas: true })], + ['deur', 900, 2100, 0, 'right', 1760, K('deur', { leaf: 'links', hinge: 'links', swing: 'buiten', volglas: true })]]); + ops(B.unit.walls.achter, [ + ['raam', 500, 1400, 700, 'left', 3530, K('raam')], + ['voordeur', 1100, 2100, 0, 'left', 4310, K('deur', { hinge: 'links', swing: 'buiten', glas: true })], + ...[7130, 8230, 10470, 11570].map(x => ['raam', 500, 2100, 0, 'left', x, K('raam')])]); + B.lighting.walls.achter = 'openingen'; + + const DH = 2115; + // Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant. + iw(3235, 190, 3235, 3430, [['deur', 980, 2130, 'left', 1530, // slaapkamer | badkamer + Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]); + iw(5325, 190, 5325, 3430); // badkamer | keuken + iw(3280, 3510, 6535, 3510, [], 160); // badkamer, keuken | hal (over de naad) + iw(3235, 3590, 3235, 4655, [['deur', 930, DH, 'left', 75, binnen({ hinge: 'links', swing: 'buiten' })]]); // hal → slaapkamer + iw(6490, 3590, 6490, 6810, [['deur', 930, DH, 'left', 72, binnen({ hinge: 'links', swing: 'binnen' })]]); // hal, entree | woonkamer + iw(190, 4700, 6445, 4700, [ // slaapkamer, hal | berging, toilet, entree + ['deur', 930, DH, 'left', 120, binnen({ hinge: 'links', swing: 'binnen' })], + ['deur', 930, DH, 'left', 4200, binnen({ hinge: 'rechts', swing: 'buiten' })]]); + iw(2248, 4745, 2248, 5819); // berging | toilet + iw(2203, 5864, 3607, 5864); // toilet | berging, entree + iw(2938, 5909, 2938, 6810); // berging | entree + iw(3562, 4745, 3562, 5819, [['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'binnen' })]]); // toilet → entree + + const plafond = { ceiling: LO }; + room('A', 9500, 1800, 'woonkeuken', 'Woonkamer/keuken', plafond); + room('A', 1500, 1800, 'slaapkamer', 'Slaapkamer', plafond); + room('A', 4300, 2200, 'badkamer', '', plafond); + room('B', 4800, 600, 'hal', 'Hal', plafond); + room('B', 2900, 1800, 'toilet', '', plafond); + room('B', 4700, 2700, 'hal', 'Entree', plafond); + room('B', 1200, 2400, 'berging', 'Berging', plafond); + + const zB = (z) => z - D; + fx('kledingkast', 'A', 1690, 490, 0, { w: 3000 }); + fx('bed', 'A', 1240, 2770, 270, { w: 1600, d: 2100 }, { nachtkastjes: false }); + fx('douche', 'A', 4585, 640, 90, { w: 900, d: 1390 }, { glas: 'rechts', glasLengte: 700 }); + fx('wastafel', 'A', 5050, 1920, 90, { w: 800 }); + fx('toilet', 'A', 3790, 3085, 180, { w: 1000 }); + fx('keuken', 'A', 5670, 1810, 270, { w: 3240 }, { front: 'antraciet', hogeKast: 'links', spoel: false, vaatwasser: false, kookplaat: true, kookplaatPlek: 0.45 }); + fx('keuken', 'A', 7025, 490, 0, { w: 2110 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.5, kookplaat: false }); + fx('eettafel', 'A', 10230, 1730, 0, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 }); + fx('bank', 'B', 9820, zB(5365), 90, { w: 2500, d: 900 }, { chaise: 'rechts', chaiseLengte: 1300 }); + fx('fauteuil', 'B', 8420, zB(3910), 330); + fx('salontafel', 'B', 8180, zB(5180), 0, { w: 800, d: 800 }, { vorm: 'rond' }); + fx('tvmeubel', 'B', 6735, zB(5815), 270, { w: 1500, d: 400 }); + fx('kledingkast', 'B', 6145, zB(5777), 90, { w: 2060 }); + fx('toilet', 'B', 2638, zB(5312), 270, { w: 1000 }); + fx('boiler', 'B', 1810, zB(4970), 0); + fx('wasmachine', 'B', 2500, zB(6510), 180); + + computeParts(st, { skip3D: true }); + return st; + } + + // Model Maas (examples/ontwerp_maas.pdf): vierkant 9,2 × 9,2 m onder één flauw zadeldak (nok west–oost op de + // naad). Twee units van 4,6 m diep: noord 6,7 m breed (oost uitgelijnd), zuid 9,2 m. De overkapping van het + // woningdak maakt van de noordwesthoek een inpandige veranda (2,5 × 4,6 m) op eiken palen. Maten buitenwerks vanaf + // de noordwesthoek van de woning; unit A begint 2,5 m oostelijker (X = interieur-coördinaten). + function createMaas() { + const st = newModelState('Maas'); + const S = st.settings, LO = 2800, HI = 3500, D = 4600, V = 2500; + Object.assign(S.facade, { type: 'rabat', rabatColor: 'naturel' }); + S.cladding = 'verticaal'; + S.trasraam.enabled = false; + Object.assign(S.roof, { fasciaColor: 'antraciet', overhang: 300, fasciaHeight: 300 }); + S.roof.posts = PostModel.normalizeSettings({ enabled: true, size: 200, wood: 'eiken' }); + S.lighting.mode = 'spots'; + S.lighting.direction = 'both'; + if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true }); + + const A = createUnitEntry('A', null, { width: 9200 - V, depth: D, height: LO, windowOn: [] }); + const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'end', offset: 0 }, { width: 9200, depth: D, height: LO, windowOn: [] }); + st.units.push(A, B); + setHouseRoof(st, true); + Object.assign(S.roof.house, { form: { type: 'zadel', ridge: 'x', low: LO, high: HI }, cover: true }); + + const { K, binnen, ops, iw, room, fx } = modelKit(st, LO, 90); + // Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand. + ops(A.unit.walls.voor, [ + ['raam', 800, 1500, 600, 'right', 970, K('raam')], + ['raam', 800, 1500, 600, 'right', 2715, K('raam')], + ['raam', 800, 1100, 1000, 'right', 4815, K('raam')]]); + // Westgevel onder de veranda: openslaande deuren met een vast zijlicht aan beide kanten (samen 3000 × 2100). + ops(A.unit.walls.links, [ + ['raam', 600, 2100, 0, 'left', 770, K('raam')], + ['deur', 900, 2100, 0, 'left', 1370, K('deur', { leaf: 'rechts', hinge: 'links', swing: 'buiten', volglas: true })], + ['deur', 900, 2100, 0, 'left', 2270, K('deur', { leaf: 'links', hinge: 'rechts', swing: 'buiten', volglas: true })], + ['raam', 600, 2100, 0, 'left', 3170, K('raam')]]); + ops(B.unit.walls.voor, [['raam', 1100, 1500, 600, 'right', 315, K('raam')]]); + ops(B.unit.walls.links, [['raam', 800, 1500, 600, 'left', 200, K('raam')]]); + ops(B.unit.walls.rechts, [['raam', 800, 1100, 1000, 'right', 210, K('raam')]]); + ops(B.unit.walls.achter, [ + ['raam', 500, 700, 1400, 'left', 3525, K('raam')], + ['voordeur', 1100, 2100, 0, 'left', 5675, K('deur', { hinge: 'links', swing: 'buiten' })], + ['raam', 500, 1500, 600, 'left', 8375, K('raam')]]); + + ensureSeamWalls(st); + const seam = st.interiorWalls.find(w => w.seam); + if (seam) { + const tA = WallModel.thicknessOf(WallModel.configFromState(A.unit.walls.links)); + seam.wall.openings = [{ id: 1, type: 'deur', w: 930, h: 2115, y: 0, anchor: 'left', offset: Math.round(5790 - V - tA), + fill: binnen({ hinge: 'rechts', swing: 'binnen' }) }]; + } + + const DH = 2115, X = (x) => x - V; + // Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant. + iw(X(3250), 4700, X(3250), 9010, [ // slaapkamer | hal, toilet, badkamer + ['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'binnen' })], + ['deur', 980, 2130, 'left', 2780, Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]); + iw(X(3295), 5740, X(5620), 5740, [['deur', 930, DH, 'left', 665, binnen({ hinge: 'links', swing: 'binnen' })]]); // hal | toilet, schacht + iw(X(5075), 5785, X(5075), 6865); // toilet | schacht + iw(X(3295), 6910, X(5530), 6910); // toilet, schacht | badkamer + iw(X(5575), 5785, X(5575), 9010); // schacht, badkamer | entree + iw(X(6865), 4700, X(6865), 9010, [ // entree | logeerkamer, berging + ['deur', 930, DH, 'left', 70, binnen({ hinge: 'links', swing: 'buiten' })], + ['deur', 930, DH, 'left', 3185, binnen({ hinge: 'rechts', swing: 'buiten' })]]); + iw(X(6910), 6915, X(9010), 6915); // logeerkamer | berging + + const plafond = { ceiling: LO }; + room('A', 2500, 2000, 'woonkeuken', 'Woonkamer/keuken'); + room('B', 1500, 2000, 'slaapkamer', 'Slaapkamer', plafond); + room('B', 4100, 1700, 'toilet'); + room('B', 4000, 3300, 'badkamer'); + room('B', 6200, 2500, 'hal', 'Entree', plafond); + room('B', 7900, 1100, 'slaapkamer', 'Logeerkamer', plafond); + room('B', 7900, 3300, 'berging', 'Berging', plafond); + + const zB = (z) => z - D; + fx('keuken', 'A', X(7620), 490, 0, { w: 1580 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.58, kookplaat: false }); + fx('keuken', 'A', X(8710), 2290, 90, { w: 4200 }, { front: 'antraciet', hogeKast: 'rechts', spoel: false, vaatwasser: false, kookplaat: true, kookplaatPlek: 0.43 }); + fx('eettafel', 'A', X(5860), 1090, 0, { w: 1000, d: 1000 }, { vorm: 'ovaal', stoelen: 4 }); + fx('bank', 'A', X(4020), 3770, 180, { w: 2550, d: 900 }, { chaise: 'rechts', chaiseLengte: 1300 }); + fx('salontafel', 'A', X(4200), 2710, 0, { w: 800, d: 800 }, { vorm: 'rond' }); + fx('fauteuil', 'A', X(3480), 1460, 330); + fx('bed', 'B', 1240, zB(6520), 270, { w: 1600, d: 2100 }, { nachtkastjes: false }); + fx('kledingkast', 'B', 1700, zB(8710), 180, { w: 3000 }); + fx('toilet', 'B', 3600, zB(6325), 270, { w: 1000 }); + fx('wastafel', 'B', 3760, zB(7205), 0, { w: 800 }); + fx('toilet', 'B', 3760, zB(8700), 180, { w: 1000 }); + fx('douche', 'B', 5090, zB(8265), 180, { w: 900, d: 1500 }, { glas: 'rechts', glasLengte: 700 }); + fx('bed', 'B', 7960, zB(6430), 90, { w: 900, d: 2100 }, { nachtkastjes: false }); + fx('wasmachine', 'B', 7280, zB(7260), 0); + fx('boiler', 'B', 8700, zB(7260), 0); + + computeParts(st, { skip3D: true }); + return st; + } + + // Model Rijn (examples/ontwerp_rijn.pdf): L-vorm 13 × 10 m, hoofdvolume 13 × 6,54 m (twee units van 3,27 m) met + // een vleugel van 4,2 × 3,46 m aan de zuidwestkant. Flauw zadeldak over het hoofdvolume als twee lessenaars die + // op de naad de nok vormen (een woningdak zou ook de vleugel overspannen); de vleugel heeft een plat dak op + // goothoogte. Alleen een naadwand naar de vleugel; de 160 mm wand langs de nok is een gewone binnenwand. + // Steenstrips rondom, in de oostgevel een donkere strook waar de schoorsteen staat. Maten buitenwerks vanaf de + // noordwesthoek, zoals de tekening (de steenstrips komen daar in de configurator buiten de units nog bij). + function createRijn() { + const st = newModelState('Rijn'); + const S = st.settings, LO = 2800, HI = 3500, D = 3270, BRICK = 'R752', SCHOORSTEEN = 'R777'; + Object.assign(S.facade, { type: 'steen', stone: BRICK }); + Object.assign(S.trasraam, { enabled: true, height: 500, finish: 'R669' }); + Object.assign(S.roof, { fasciaColor: 'wit', overhang: 300, fasciaHeight: 350 }); + S.lighting.mode = 'spots'; + S.lighting.direction = 'both'; + if (S.elektra && S.elektra.apparaten) Object.assign(S.elektra.apparaten, { koken: true, oven: true, vaatwasser: true }); + + const A = createUnitEntry('A', null, { width: 13000, depth: D, height: LO, windowOn: [] }); + const B = createUnitEntry('B', { to: 'A', side: 'achter', align: 'start', offset: 0 }, { width: 13000, depth: D, height: LO, windowOn: [] }); + const C = createUnitEntry('C', { to: 'B', side: 'achter', align: 'start', offset: 0 }, { width: 4200, depth: 3460, height: LO, windowOn: [] }); + st.units.push(A, B, C); + UnitModel.applyRoofForm(A.unit, { type: 'lessenaar', dir: 'achter', low: LO, high: HI }); + UnitModel.applyRoofForm(B.unit, { type: 'lessenaar', dir: 'voor', low: LO, high: HI }); + + const { K, binnen, ops, iw, room, fx } = modelKit(st, LO, 90); + // Noord 'right' en zuid 'left' = vanaf de westhoek; west 'left' en oost 'right' = vanaf de noordkant van de wand. + ops(A.unit.walls.voor, [ + ['raam', 500, 1400, 700, 'right', 1045, K('raam')], + ['raam', 500, 1400, 700, 'right', 3630, K('raam')], + ['raam', 1200, 500, 1600, 'right', 4760, K('raam')], + ['raam', 1400, 1400, 700, 'right', 7560, K('raam')]]); + ops(A.unit.walls.rechts, [['raam', 1800, 2100, 0, 'right', 785, K('raam')]]); + ops(B.unit.walls.rechts, [ + ['deur', 900, 2100, 0, 'right', 730, K('deur', { leaf: 'rechts', hinge: 'rechts', swing: 'buiten', volglas: true })], + ['deur', 900, 2100, 0, 'right', 1630, K('deur', { leaf: 'links', hinge: 'links', swing: 'buiten', volglas: true })]]); + ops(B.unit.walls.links, [['raam', 1400, 1100, 1000, 'left', 465, K('raam')]]); + ops(B.unit.walls.achter, [ + ['raam', 1100, 2100, 0, 'left', 4680, K('raam')], + ['raam', 1800, 2100, 0, 'left', 10490, K('raam')]]); + ops(C.unit.walls.links, [['raam', 500, 700, 1400, 'left', 365, K('raam')]]); + ops(C.unit.walls.rechts, [['voordeur', 1100, 2100, 0, 'right', 70, K('deur', { hinge: 'links', swing: 'buiten', glas: true })]]); + ops(C.unit.walls.achter, [['raam', 1400, 1100, 1000, 'left', 1505, K('raam')]]); + // Schoorsteen midden in de oostgevel (z 2870–3670). u loopt van buiten gezien van links (zuid) naar rechts. + A.facade.walls.rechts = { type: 'steen', stone: SCHOORSTEEN }; + A.unit.walls.rechts.splits = [{ u: 400, facade: { type: 'steen', stone: BRICK } }]; + B.unit.walls.rechts.splits = [{ u: 2870, facade: { type: 'steen', stone: SCHOORSTEEN } }]; + + const DH = 2115; + // Noord–zuid: 'buiten' = west, 'left' = vanaf de noordkant; west–oost: 'buiten' = zuid, 'left' = vanaf de westkant. + iw(190, 3266, 6513, 3266, [['deur', 930, DH, 'left', 2800, binnen({ hinge: 'links', swing: 'buiten' })]], 160); // slaapkamer, badkamer | keuken (langs de nok) + iw(4380, 190, 4380, 3186, [['deur', 980, 2130, 'left', 1136, // slaapkamer | badkamer + Object.assign(binnen(), { type: 'schuifdeur', leaves: 1, mount: 'opbouw', side: 'buiten', dir: 'links' })]]); + iw(6466, 190, 6466, 3186); // badkamer | woonkamer + iw(5800, 1135, 6421, 1135); // muurtje naast de douche + iw(2762, 3346, 2762, 6440, [['deur', 930, DH, 'left', 104, binnen({ hinge: 'links', swing: 'buiten' })]]); // berging | keuken + iw(1880, 6640, 1880, 7655, [['deur', 930, DH, 'left', 40, binnen({ hinge: 'links', swing: 'binnen' })]]); // toilet | entree + iw(190, 7700, 3107, 7700, [['deur', 930, DH, 'left', 1820, binnen({ hinge: 'links', swing: 'buiten' })]]); // toilet, entree | logeerkamer + iw(3062, 7745, 3062, 8335); // kapstoknis entree + iw(3107, 8290, 4010, 8290); // kapstoknis | kast logeerkamer + + ensureSeamWalls(st, 'C'); + const seam = st.interiorWalls.find(w => w.seam); + if (seam) { + const tC = WallModel.thicknessOf(WallModel.configFromState(C.unit.walls.links)); + seam.wall.openings = [{ id: 1, type: 'deur', w: 930, h: DH, y: 0, anchor: 'left', offset: Math.round(2915 - tC), + fill: binnen({ hinge: 'links', swing: 'buiten' }) }]; + } + + const plafond = { ceiling: LO }; + room('A', 9500, 1500, 'woonkeuken', 'Woonkamer/keuken'); + room('A', 2600, 1700, 'slaapkamer', 'Slaapkamer', plafond); + room('A', 5400, 2000, 'badkamer', '', plafond); + room('B', 1400, 1500, 'berging', 'Berging', plafond); + room('C', 1000, 500, 'toilet'); + room('C', 2400, 500, 'hal', 'Entree'); + room('C', 1500, 2300, 'slaapkamer', 'Logeerkamer'); + + const zB = (z) => z - D, zC = (z) => z - 2 * D; + fx('kledingkast', 'A', 493, 1688, 270, { w: 2990 }); + fx('bed', 'A', 2615, 1240, 0, { w: 1600, d: 2100 }, { nachtkastjes: false }); + fx('douche', 'A', 5725, 640, 0, { w: 1400, d: 900 }, { glas: 'links', glasLengte: 900 }); + fx('wastafel', 'A', 6191, 2000, 90, { w: 800 }); + fx('toilet', 'A', 4998, 2680, 180); + fx('bank', 'A', 11195, 800, 0, { w: 3000, d: 1000 }, { chaise: 'rechts', chaiseLengte: 2100 }); + fx('fauteuil', 'A', 8880, 1720, 315); + fx('salontafel', 'A', 10410, 2280, 0, { w: 800, d: 800 }, { vorm: 'rond' }); + fx('keuken', 'B', 4945, zB(4350), 90, { w: 2000 }, { front: 'antraciet', hogeKast: 'geen', spoelbak: 0.15, kookplaat: true, kookplaatPlek: 0.85 }); + fx('kookeiland', 'B', 5545, zB(4350), 270, { w: 2000, d: 600 }, { front: 'antraciet', kookplaat: false }); + fx('keuken', 'B', 3110, zB(5080), 270, { w: 1200 }, { front: 'antraciet', hogeKast: 'rechts', spoel: false, vaatwasser: false }); + fx('eettafel', 'B', 10010, zB(4990), 0, { w: 2000, d: 1100 }, { vorm: 'ovaal', stoelen: 6 }); + fx('wasmachine', 'B', 620, zB(6140), 180); + fx('wasmachine', 'B', 1450, zB(6140), 180); // droger + fx('boiler', 'B', 2350, zB(6215), 180); + fx('toilet', 'C', 535, zC(7150), 270); + fx('bed', 'C', 650, zC(8745), 0, { w: 900, d: 2000 }, { nachtkastjes: false }); + fx('kledingkast', 'C', 3703, zC(9072), 90, { w: 1450 }); + + computeParts(st, { skip3D: true }); + return st; + } + // Standaardmodellen: Bestand › Snel nieuw en het venster Nieuw. De sleutel is de template. const MODELS = { amstel: { label: 'Woning Amstel', icon: 'house-heart', create: createAmstel }, wetering: { label: 'Woning Wetering', icon: 'house-door', create: createWetering }, - ijssel: { label: 'Woning IJssel', icon: 'houses', create: createIJssel } + ijssel: { label: 'Woning IJssel', icon: 'houses', create: createIJssel }, + waal: { label: 'Woning Waal', icon: 'house-add', create: createWaal }, + maas: { label: 'Woning Maas', icon: 'house-check', create: createMaas }, + rijn: { label: 'Woning Rijn', icon: 'house-gear', create: createRijn } }; function createDefaultState(template) { @@ -598,7 +885,9 @@ window.HouseModel = (() => { ws.materials = Object.assign({}, ws.materials, facadeMaterials(state, facadeFor(state, u, side))); }); if (u.roof.followFloor) { - const e = cover[u.id] || { l: 0, r: 0, f: 0, b: 0 }; + // Overkapping van het woningdak plus de eigen verlenging (veranda) van deze unit. + const c = cover[u.id] || {}, x = u.roof.extend || {}; + const e = Object.fromEntries(['l', 'r', 'f', 'b'].map(k => [k, (c[k] || 0) + (x[k] || 0)])); u.roof.deck.floor.width = u.unit.floor.floor.width + e.l + e.r; u.roof.deck.floor.depth = u.unit.floor.floor.depth + e.f + e.b; u.roof.offsetX = (e.r - e.l) / 2; @@ -610,6 +899,11 @@ window.HouseModel = (() => { applyRoofJoins(state); } + const EXTEND_MAX = 4000; + function normalizeExtend(e) { + return Object.fromEntries(['f', 'r', 'b', 'l'].map(k => [k, Math.max(0, Math.min(EXTEND_MAX, Math.round(Number((e || {})[k]) || 0)))])); + } + const houseRoofOn = (state) => !!(state.settings.roof.house && state.settings.roof.house.enabled && state.settings.roof.house.form); const ROOF_HEIGHT_KEYS = ['heightAll', 'heightLeft', 'heightRight', 'heightMid', 'topPts']; @@ -1338,6 +1632,10 @@ window.HouseModel = (() => { const elektraGroup = build ? ElektraModel.build3D(elektra, o.elektraMaterials, stage) : null; if (elektraGroup) elektraGroup.visible = o.elektraVisible !== false && shows('elektra'); + const groundY = units.length ? Math.max(...units.map(u => u.layout.floorTop)) - (state.settings.groundLevel || 0) : 0; + const posts = PostModel.compute(state.settings.roof.posts, units, groundY); + const postsGroup = build ? PostModel.build3D(posts, o.postMaterials) : null; + const sandwich = state.settings.sandwich && units.length ? SandwichModel.compute(rects, Math.max(...units.map(u => u.layout.floorTop))) : null; const sandwichGroup = build && sandwich ? SandwichModel.build3D(sandwich, o.sandwichMaterials || SandwichModel.createMaterials(), { @@ -1350,7 +1648,7 @@ window.HouseModel = (() => { const fixturesGroup = build ? FixtureModel.build3D(fixtures) : null; if (fixturesGroup) fixturesGroup.visible = shows('afwerking'); - return { rects, seams, overlaps: overlappingPairs(rects), units, interior, roofEdge, roofEdgeGroup, lighting, lightingGroup, elektra, elektraGroup, sandwich, sandwichGroup, rooms, roomsGroup, fixtures, fixturesGroup }; + return { rects, seams, overlaps: overlappingPairs(rects), units, interior, roofEdge, roofEdgeGroup, lighting, lightingGroup, elektra, elektraGroup, sandwich, sandwichGroup, posts, postsGroup, rooms, roomsGroup, fixtures, fixturesGroup }; } function allWallPieces(parts) { @@ -1489,6 +1787,7 @@ window.HouseModel = (() => { kg: u.floorSpecs.totalKg + u.roof.specs.totalKg + u.walls.reduce((a, w) => a + w.pieces.reduce((b, p) => b + p.specs.totalKg, 0), 0) })); if (parts.interior.length) weights.push({ label: 'Binnenwanden', element: 'BW', kg: parts.interior.reduce((a, p) => a + p.specs.totalKg, 0) }); + if (parts.posts.count) weights.push({ label: 'Palen overkapping', element: 'PAAL', kg: parts.posts.kg }); const isolatie = parts.units.map(u => ({ label: `Unit ${u.id}`, element: u.id, @@ -1568,6 +1867,10 @@ window.HouseModel = (() => { return { unit: u.id, name: rb.profile.name, kgM: rb.profile.kgM, lengthM: rb.length / 1000, kg: rb.steelKg, fillM3: rb.fillM3, fillKg: rb.fillKg, util: rb.util, defl: rb.defl, deflMax: rb.deflMax, ok: rb.ok, below: rb.below, loadKNm: rb.loadKNm }; }), + posts: parts.posts.count ? { + count: parts.posts.count, size: parts.posts.size, wood: (PostModel.WOODS[parts.posts.wood] || {}).label || '', + lengthM: parts.posts.lengthMM / 1000, m3: parts.posts.m3, kg: parts.posts.kg + } : null, roofEdge: parts.roofEdge ? { fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000, fasciaHeight: parts.roofEdge.fasciaHeight, @@ -1659,6 +1962,7 @@ window.HouseModel = (() => { if (!u.roof) { u.roof = RoofModel.createDefaultRoof(u.unit.floor); delete u.roof.fascia; } if (u.roof.fascia === undefined) u.roof.fascia = oldRoof.edge !== 'daktrim'; if (!RoofModel.COVERINGS[u.roof.covering]) u.roof.covering = 'dakleer'; + u.roof.extend = normalizeExtend(u.roof.extend); u.lighting = { walls: (u.lighting && u.lighting.walls) || {} }; u.facade = { walls: (u.facade && u.facade.walls) || {} }; if (!hadFacade) Object.entries(u.unit.walls).forEach(([side, ws]) => { @@ -1672,7 +1976,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, + houseRoofShape, houseRoofFits, setHouseRoof, coverExtensions, EXTEND_MAX, computeParts, allWallPieces, computeTotals, computeDimensions, dimensionRows, TRANSPORT, TRANSPORT_CLASSES, computeTransport, validateState, fromUnitDesign, normalize }; })(); diff --git a/js/house/house-pdf.js b/js/house/house-pdf.js index a8030c3..d03fc32 100644 --- a/js/house/house-pdf.js +++ b/js/house/house-pdf.js @@ -135,6 +135,10 @@ window.HousePdf = (() => { parts.roofEdge.spots.forEach(sp => doc.circle(px(sp.x), py(sp.z), 0.8, 'FD')); } doc.setLineDashPattern([], 0); + doc.setDrawColor(...BLACK); + doc.setLineWidth(0.2); + doc.setFillColor(160, 124, 84); + parts.posts.posts.forEach(pp => doc.rect(px(pp.x - pp.size / 2), py(pp.z - pp.size / 2), pp.size * scale, pp.size * scale, 'FD')); doc.setDrawColor(...BLACK); doc.setFillColor(255, 211, 107); @@ -586,6 +590,10 @@ window.HousePdf = (() => { 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)`]); + if (totals.posts) { + const pp = totals.posts; + rows.push(['Palen overkapping', `${pp.count} stuks ${pp.wood.toLowerCase()} ${pp.size}x${pp.size} mm, ${fmt(pp.lengthM, 2)} m, ${fmt(pp.m3, 3)} m3, op paalvoeten`]); + } PdfSheet.table(doc, 120, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & verlichting')); } diff --git a/js/house/house.js b/js/house/house.js index fa9aab3..3f410be 100644 --- a/js/house/house.js +++ b/js/house/house.js @@ -327,6 +327,7 @@ function mountHouseEditor(container, callbacks) { const roofMaterials = RoofModel.createMaterials(); const lightingMaterials = LightingModel.createMaterials(); const sandwichMaterials = SandwichModel.createMaterials(); + const postMaterials = PostModel.createMaterials(); const elektraMaterials = ElektraModel.createMaterials(); let houseGroup = null; let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls); @@ -975,6 +976,12 @@ function mountHouseEditor(container, callbacks) { ctx.beginPath(); ctx.arc(q.x, q.y, 3, 0, Math.PI * 2); ctx.fill(); }); } + ctx.setLineDash([]); + ctx.fillStyle = '#a07c54'; ctx.strokeStyle = '#e8d3b0'; ctx.lineWidth = 1; + parts.posts.posts.forEach(pp => { + const a = toPx(pp.x - pp.size / 2, pp.z - pp.size / 2), sz = pp.size * scale; + ctx.fillRect(a.x, a.y, sz, sz); ctx.strokeRect(a.x, a.y, sz, sz); + }); ctx.restore(); ctx.strokeStyle = '#ffe9a8'; ctx.lineWidth = 1.5; @@ -1238,6 +1245,12 @@ function mountHouseEditor(container, callbacks) { 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' }) }, + { label: 'Palen overkapping', html: + toggle('hs-posts', 'Palen', r.posts.enabled, { hint: `Onder een dak dat minstens de min. uitkraging buiten de vloer steekt (veranda); per unit: Overkapping. Nu ${parts ? parts.posts.count : 0} st.` }) + + (r.posts.enabled ? sel('hs-posts-wood', 'Hout', Object.fromEntries(Object.entries(PostModel.WOODS).map(([k, w]) => [k, w.label])), r.posts.wood) + + UI.range('hs-posts-size', 'Maat', r.posts.size, { min: 100, max: 300, step: 10, hint: 'Vierkant, mm' }) + + num('hs-posts-span', 'Max. h.o.h.', r.posts.maxSpan, { step: 100, hint: 'Grootste afstand tussen palen (of paal en gevel)' }) + + num('hs-posts-min', 'Min. uitkraging', r.posts.minExt, { step: 100, hint: 'Kleinere overstekken krijgen geen palen' }) : '') }, { label: 'Daken per unit', html: `
` }); return groups; }, @@ -1252,6 +1265,11 @@ function mountHouseEditor(container, callbacks) { UI.onRange('hs-fascia-h', v => { if (v > 0) r.fasciaHeight = v; }, after); UI.onSel('hs-fascia-color', v => { r.fasciaColor = v; }, rerender); UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos); + UI.onToggle('hs-posts', v => { r.posts.enabled = v; }, rerender); + UI.onSel('hs-posts-wood', v => { r.posts.wood = v; }, rerender); + UI.onRange('hs-posts-size', v => { if (v > 0) r.posts.size = v; }, after); + UI.onNum('hs-posts-span', v => { if (v >= 1000) r.posts.maxSpan = v; }, after, pos); + UI.onNum('hs-posts-min', v => { if (v >= 400) r.posts.minExt = v; }, after, pos); const list = () => { const el = body.querySelector('[data-live="roof-list"]'); if (!el || !parts) return; @@ -1393,7 +1411,8 @@ function mountHouseEditor(container, callbacks) { groups.push({ label: 'Meterkast', html: num('he-mk-w', 'Breedte', e.meterkast.w, { step: 10 }) + num('he-mk-h', 'Hoogte', e.meterkast.h, { step: 10 }) + num('he-mk-d', 'Diepte', e.meterkast.d, { step: 10 }) + num('he-mk-y', 'Onderkant', e.meterkast.y, { step: 10, hint: 'Vanaf bovenkant vloer; zo dicht mogelijk bij de voordeur' }) }); groups.push({ label: 'Punten', html: num('he-sw-h', 'Schakelaar', e.switchHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) + - num('he-wcd-h', 'Wcd', e.wcdHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) + num('he-wcd-n', 'Wcd per unit', e.wcdPerUnit, { step: 1, hint: 'Dubbele wcd' }) }); + num('he-wcd-h', 'Wcd', e.wcdHeight, { step: 10, hint: 'Hart, vanaf bovenkant vloer' }) + num('he-wcd-n', 'Wcd per unit', e.wcdPerUnit, { step: 1, hint: 'Dubbele wcd' }) + + sel('he-plate', 'Kleur', Object.fromEntries(Object.entries(ElektraModel.PLATE_COLORS).map(([k, c]) => [k, c.label])), e.plateColor || 'wit', { hint: 'Schakelaars, wcd\'s en vloerdozen' }) }); groups.push({ label: 'Apparaten', html: Object.entries(ElektraModel.APPLIANCES).map(([k, A]) => check(`he-app-${k}`, `${A.label} (${A.code})`, !!e.apparaten[k])).join('') }); const nManual = Object.keys(e.place).length; @@ -1423,6 +1442,7 @@ function mountHouseEditor(container, callbacks) { UI.onNum('he-sw-h', v => { e.switchHeight = v; }, after, pos); UI.onNum('he-wcd-h', v => { e.wcdHeight = v; }, after, pos); UI.onNum('he-wcd-n', v => { e.wcdPerUnit = Math.max(0, Math.round(v)); }, after); + UI.onSel('he-plate', v => { e.plateColor = v; }, rerender); UI.onNum('he-slack', v => { e.slack = v; }, after); UI.onNum('he-res', v => { e.reserve = v; }, after); UI.onNum('he-mk-res', v => { e.meterkastReserve = v; }, after); @@ -1860,6 +1880,9 @@ function mountHouseEditor(container, callbacks) { (pu && pu.roof.unitRoof.ridgeBeam || u.roof.steelRidge === false && ['zadel', 'wolf'].includes(shapeNow) ? toggle('hp-steel-ridge', 'Stalen nokbalk', u.roof.steelRidge !== false, { hint: pu && pu.roof.unitRoof.ridgeBeam ? `${pu.roof.unitRoof.ridgeBeam.profile.name}, ${Math.round(pu.roof.unitRoof.ridgeBeam.steelKg)} kg; uit = verwijderen` : 'Uit = verwijderd' }) : '') + toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) }); + if (u.roof.followFloor) groups.push({ label: 'Overkapping', html: + ['f', 'r', 'b', 'l'].map(k => num('hp-ext-' + k, { f: 'Noord', r: 'Oost', b: 'Zuid', l: 'West' }[k], (u.roof.extend || {})[k] || 0, + { hint: k === 'f' ? `Dak verlengen voorbij de gevel (veranda), max. ${HouseModel.EXTEND_MAX} mm; palen volgen vanzelf` : '' })).join('') }); if (!u.roof.followFloor) groups.push({ label: 'Dakmaat', html: num('hp-roof-w', 'Breedte', u.roof.deck.floor.width, { hint: 'West–oost' }) + num('hp-roof-d', 'Diepte', u.roof.deck.floor.depth, { hint: 'Noord–zuid' }) + num('hp-roof-ox', 'Versch. oost', u.roof.offsetX || 0) + num('hp-roof-oz', 'Versch. zuid', u.roof.offsetZ || 0) }); @@ -1897,6 +1920,9 @@ 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); + ['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); @@ -2124,6 +2150,12 @@ function mountHouseEditor(container, callbacks) { 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)); } + if (totals.posts) { + const pp = totals.posts; + cards.push(UI.specCard('Overkapping', 'columns', [ + [`${pp.wood} palen ${pp.size}×${pp.size} mm`, pp.count, 'st.'], + ['Lengte totaal', f(pp.lengthM, 2), 'm'], ['Hout', f(pp.m3, 3), 'm³'], ['Gewicht', f(pp.kg), 'kg'], ['Paalvoeten', pp.count, 'st.']])); + } // TIJDELIJK (verwijderen voor productie): zaagmachine-overzicht. const sw = totals.saw, cs = cost.saw; cards.push(UI.specCard('Zaagmachine (tijdelijk)', 'scissors', [ @@ -2166,7 +2198,7 @@ function mountHouseEditor(container, callbacks) { const painted = state.settings.interiorPaint && BuildStages.shows(view3D.stage, 'afwerking'); wallMaterials.spaanplaatMaterial.color.set(painted ? state.settings.interiorColor : SPAANPLAAT_COLOR); roofMaterials.ceilingMaterial.color.set(state.settings.ceilingColor || '#f4f3ef'); - parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, elektraMaterials, stage: view3D.stage, lampenVisible: view3D.lampen, elektraVisible: view3D.elektra }); + parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, postMaterials, elektraMaterials, stage: view3D.stage, lampenVisible: view3D.lampen, elektraVisible: view3D.elektra }); reportDirty = true; if (houseGroup) { scene.remove(houseGroup); disposeGroup(houseGroup); } @@ -2200,6 +2232,7 @@ function mountHouseEditor(container, callbacks) { houseGroup.add(u.roof.group); }); if (parts.roofEdgeGroup) houseGroup.add(parts.roofEdgeGroup); + if (parts.postsGroup) houseGroup.add(parts.postsGroup); } if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup); if (parts.lightingGroup && !section) houseGroup.add(parts.lightingGroup); @@ -2255,7 +2288,7 @@ function mountHouseEditor(container, callbacks) { renderNav(parts); } - const SIGN = { ahead: 4000, aside: 1800, poleH: 2200, poleW: 80, plateW: 1500, plateD: 30 }; + const SIGN = { ahead: 4000, aside: 1800, clear: 1200, poleH: 2200, poleW: 80, plateW: 1500, plateD: 30 }; let signPos = null; let signMaterial = null; const signPoleMat = new THREE.MeshStandardMaterial({ color: 0x2b2b2b, roughness: 0.6, metalness: 0.3 }); @@ -2284,7 +2317,14 @@ function mountHouseEditor(container, callbacks) { function buildLogoSign(groundY) { const { d, n } = frontDoor(); const t = { x: n.z, z: -n.x }; - signPos = { x: d.x + n.x * SIGN.ahead + t.x * SIGN.aside, z: d.z + n.z * SIGN.ahead + t.z * SIGN.aside }; + // Voordeur in een binnenhoek: opzij kan het bord onder een andere unit vallen, dan verder opzij of de andere kant op. + const at = (a) => ({ x: d.x + n.x * SIGN.ahead + t.x * a, z: d.z + n.z * SIGN.ahead + t.z * a }); + const free = (q) => !parts.rects.some(r => !r.error && q.x > r.x0 - SIGN.clear && q.x < r.x1 + SIGN.clear && q.z > r.z0 - SIGN.clear && q.z < r.z1 + SIGN.clear); + signPos = at(SIGN.aside); + for (let a = SIGN.aside; a <= 30000; a += 500) { + const q = [at(a), at(-a)].find(free); + if (q) { signPos = q; break; } + } const toM = 0.001, plateH = SIGN.plateW / 3; const g = new THREE.Group(); const pole = new THREE.Mesh(new THREE.BoxGeometry(SIGN.poleW * toM, SIGN.poleH * toM, SIGN.poleW * toM), signPoleMat); diff --git a/js/lighting/lighting-model.js b/js/lighting/lighting-model.js index cf7c72a..f3dc974 100644 --- a/js/lighting/lighting-model.js +++ b/js/lighting/lighting-model.js @@ -39,14 +39,16 @@ window.LightingModel = (() => { calculateSegmentPositions(len, min, max).forEach(p => out.push({ x: A.x + (B.x - A.x) * p / len, z: A.z + (B.z - A.z) * p / len })); } - function enforceMinSpacing(positions, min) { + // Te dicht bij elkaar: samen naar het midden, tenzij daar een opening zit (dan vervalt de tweede). + function enforceMinSpacing(positions, min, blockedAt) { let pts = positions.slice().sort((a, b) => a - b); let changed = true; while (changed && pts.length > 1) { changed = false; for (let i = 0; i < pts.length - 1; i++) { if (pts[i + 1] - pts[i] < min - 0.5) { - pts.splice(i, 2, (pts[i] + pts[i + 1]) / 2); + const mid = (pts[i] + pts[i + 1]) / 2; + pts.splice(i, 2, blockedAt && blockedAt(mid) ? pts[i] : mid); changed = true; break; } @@ -55,6 +57,27 @@ window.LightingModel = (() => { return pts; } + // Naast openingen: op openingOffset van elke opening; is de tussenruimte daarvoor te klein, dan één lamp in + // het midden ervan (of geen, als die tegen de kozijnen zou hangen). Posities in u (mm vanaf de linkerkant). + function besideOpenings(cfg, s) { + const W = cfg.wallWidth, off = s.openingOffset, clear = cfg.studWidth + 150; + const ops = cfg.openings.map(op => [op.x, op.x + op.w]).sort((p, q) => p[0] - q[0]); + const out = []; + ops.forEach(([x0, x1], i) => { + const next = ops[i + 1]; + if (i === 0) { const u = Math.max(x0 - off, s.endOffset); if (x0 - u >= clear) out.push(u); } + if (next) { + const gap = next[0] - x1; + if (gap >= 2 * off + 1000) out.push(x1 + off, next[0] - off); + else if (gap >= 2 * clear) out.push((x1 + next[0]) / 2); + } else { + const u = Math.min(x1 + off, W - s.endOffset); + if (u - x1 >= clear) out.push(u); + } + }); + return out; + } + function freeIntervals(lo, hi, blocked) { const free = []; let cursor = lo; @@ -113,7 +136,7 @@ window.LightingModel = (() => { }; if (s.mode === 'gevel') { runs.forEach(run => { - const blocked = []; + const blocked = [], openings = []; const pos = []; // Delen achter een andere wand (bijv. een stuk dat de naad in loopt) zijn geen zichtbare gevel. segs.filter(o => !run.segs.includes(o)).forEach(o => { @@ -126,26 +149,29 @@ window.LightingModel = (() => { const cfg = sg.piece.config, sW = cfg.studWidth; const low = Math.min(cfg.wallHeightLeft, cfg.wallHeightRight) < H + 100; if (sg.override === 'geen' || low) { blocked.push([sg.s0, sg.s1]); return; } - if (sg.override === 'openingen') { - blocked.push([sg.s0, sg.s1]); - cfg.openings.forEach(op => [op.x - s.openingOffset, op.x + op.w + s.openingOffset] - .filter(u => u >= 0 && u <= cfg.wallWidth).forEach(u => pos.push(uToS(sg, u)))); - return; - } + const own = []; cfg.openings.filter(op => op.y - sW <= H && WallModel.openingTopMax(op) + sW >= H).forEach(op => { const a = uToS(sg, op.x - sW), b = uToS(sg, op.x + op.w + sW); - blocked.push([Math.min(a, b), Math.max(a, b)]); + own.push([Math.min(a, b), Math.max(a, b)]); }); + openings.push(...own); + if (sg.override === 'openingen') { + blocked.push([sg.s0, sg.s1]); + besideOpenings(cfg, s).forEach(u => pos.push(uToS(sg, u))); + return; + } + blocked.push(...own); }); + // Automatisch: per vrij wandstuk gelijk verdeeld, midden in gelijke delen van ca. lampMax (één lamp + // midden tussen twee ramen), niet tegen de kozijnen of de hoek aan. freeIntervals(run.s0, run.s1, blocked).forEach(([a, b]) => { if (b - a < s.minWallWidth) return; - const first = a + s.endOffset, last = b - s.endOffset; - pos.push(first); - calculateSegmentPositions(last - first, s.lampMin, s.lampMax).forEach(p => pos.push(first + p)); - pos.push(last); + const k = Math.max(1, Math.round((b - a) / s.lampMax)); + for (let i = 0; i < k; i++) pos.push(a + (b - a) * (i + 0.5) / k); }); // Min. h.o.h. geldt over alle lampen van de gevel samen (ook overrides en de grens tussen wanden). - enforceMinSpacing(pos, s.lampMin).forEach(v => addLamp(run, v)); + const inOpening = (v) => openings.some(([a, b]) => v > a && v < b); + enforceMinSpacing(pos, s.lampMin, inOpening).filter(v => !inOpening(v)).forEach(v => addLamp(run, v)); }); units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => p.lamps.sort((a, b) => a.u - b.u)))); } diff --git a/js/roof/post-model.js b/js/roof/post-model.js new file mode 100644 index 0000000..528f8c0 --- /dev/null +++ b/js/roof/post-model.js @@ -0,0 +1,139 @@ +/*! + * Tectum + * Copyright © 2026 Machiel Heinen. + * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. + * Zie LICENTIE.md. + */ +// Palen onder een overkapping: steekt een dak minstens minExt buiten de vloeren uit (veranda), dan komen er +// palen onder de dakrand. Hoekpalen staan met hun buitenkant gelijk met de dakhoek; een eind dat op een gevel +// uitkomt rust op die wand. Daartussen zo weinig palen dat de h.o.h.-afstand onder maxSpan blijft. + +window.PostModel = (() => { + const EPS = 0.5; + const SHOE_MM = 100; // verzinkte paalvoet tussen maaiveld en paal + const WOODS = { + eiken: { label: 'Eiken', hex: 0xa07c54, kgM3: 750 }, + douglas: { label: 'Douglas', hex: 0xb98a5e, kgM3: 530 } + }; + const DEFAULT_SETTINGS = { enabled: true, size: 200, wood: 'eiken', maxSpan: 3600, minExt: 1000 }; + + function normalizeSettings(settings) { + const s = Object.assign({}, DEFAULT_SETTINGS, settings || {}); + s.enabled = s.enabled !== false; + if (!WOODS[s.wood]) s.wood = DEFAULT_SETTINGS.wood; + const num = (v, def, min, max) => (isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : def); + s.size = num(s.size, DEFAULT_SETTINGS.size, 100, 400); + s.maxSpan = num(s.maxSpan, DEFAULT_SETTINGS.maxSpan, 1000, 8000); + s.minExt = num(s.minExt, DEFAULT_SETTINGS.minExt, 400, 5000); + return s; + } + + // units: [{ rect (vloer), roof: { rect, ceilingAt } }] in wereld-mm; groundY = maaiveld (absoluut). + function compute(settings, units, groundY) { + const s = normalizeSettings(settings); + const out = { posts: [], count: 0, size: s.size, wood: s.wood, lengthMM: 0, m3: 0, kg: 0 }; + if (!s.enabled || !units.length) return out; + const floors = units.map(u => u.rect); + const onFloor = (x, z, m) => floors.some(f => x > f.x0 - m && x < f.x1 + m && z > f.z0 - m && z < f.z1 + m); + const underRoof = (x, z) => units.some(u => { const r = u.roof.rect; return x > r.x0 && x < r.x1 && z > r.z0 && z < r.z1; }); + + // Vrije stukken dakrand: buitenrand van de daken samen, recht tegenover een gevel op minstens minExt (loodrecht + // op de rand gemeten); de zijkanten van een veranda krijgen alleen de hoekpalen. In een binnenhoek (veranda + // tussen twee units) loopt het stuk door tot de andere gevel en rust daar op. + const runs = []; + units.forEach(u => { + const R = u.roof.rect; + [ + { axis: 'z', at: R.x1, n: 1, s0: R.z0, s1: R.z1 }, { axis: 'z', at: R.x0, n: -1, s0: R.z0, s1: R.z1 }, + { axis: 'x', at: R.z1, n: 1, s0: R.x0, s1: R.x1 }, { axis: 'x', at: R.z0, n: -1, s0: R.x0, s1: R.x1 } + ].forEach(e => { + const pt = (t, d) => (e.axis === 'z' ? { x: e.at - e.n * d, z: t } : { x: t, z: e.at - e.n * d }); + const facing = (p) => Math.min(...floors.map(f => { + const [lo, hi, c0, c1, v, c] = e.axis === 'z' ? [f.z0, f.z1, f.x0, f.x1, p.z, p.x] : [f.x0, f.x1, f.z0, f.z1, p.x, p.z]; + if (!(v > lo + EPS && v < hi - EPS)) return Infinity; + const d = e.n > 0 ? c - c1 : c0 - c; + return d > -EPS ? d : Infinity; + })); + const free = (t) => { + const p = pt(t, 0), o = pt(t, -1), d = facing(p); + return !underRoof(o.x, o.z) && d >= s.minExt - EPS && d < Infinity; + }; + const cuts = [e.s0, e.s1].concat(floors.flatMap(f => (e.axis === 'z' ? [f.z0, f.z1] : [f.x0, f.x1])) + .filter(t => t > e.s0 + EPS && t < e.s1 - EPS)).sort((a, b) => a - b); + for (let i = 0; i < cuts.length - 1; i++) { + const a = cuts[i], b = cuts[i + 1]; + if (b - a < EPS || !free((a + b) / 2)) continue; + const last = runs[runs.length - 1]; + if (last && last.axis === e.axis && last.n === e.n && Math.abs(last.at - e.at) < EPS && Math.abs(last.b - a) < EPS) last.b = b; + else runs.push({ axis: e.axis, at: e.at, n: e.n, a, b }); + } + }); + }); + // Aansluitende stukken van verschillende units (bijv. twee units met dezelfde veranda) samenvoegen. + runs.sort((p, q) => p.axis.localeCompare(q.axis) || p.n - q.n || Math.round(p.at) - Math.round(q.at) || p.a - q.a); + const merged = []; + runs.forEach(r => { + const m = merged.find(o => o.axis === r.axis && o.n === r.n && Math.abs(o.at - r.at) < EPS && Math.abs(o.b - r.a) < EPS); + if (m) m.b = r.b; else merged.push(Object.assign({}, r)); + }); + + const half = s.size / 2; + const pts = []; + merged.forEach(r => { + const pos = (t) => (r.axis === 'z' ? { x: r.at - r.n * half, z: t } : { x: t, z: r.at - r.n * half }); + // Een eind dat op of tegen een vloer uitkomt rust op die gevel. + const endFree = (t) => { const p = r.axis === 'z' ? { x: r.at, z: t } : { x: t, z: r.at }; return !onFloor(p.x, p.z, s.size); }; + const fa = endFree(r.a), fb = endFree(r.b); + const pa = fa ? r.a + half : r.a, pb = fb ? r.b - half : r.b; + if (pb - pa < EPS) return; + const n = Math.max(1, Math.ceil((pb - pa) / s.maxSpan - 1e-9)); + for (let k = 0; k <= n; k++) { + if ((k === 0 && !fa) || (k === n && !fb)) continue; + pts.push(pos(pa + (pb - pa) * k / n)); + } + }); + pts.forEach(p => { + if (out.posts.some(q => Math.abs(q.x - p.x) < s.size && Math.abs(q.z - p.z) < s.size)) return; + const roof = units.map(u => u.roof).find(rf => p.x > rf.rect.x0 - EPS && p.x < rf.rect.x1 + EPS && p.z > rf.rect.z0 - EPS && p.z < rf.rect.z1 + EPS); + if (!roof) return; + // Hoogste hoek: bij een hellend dak verdwijnt de paal aan de lage kant in het dakpakket, zonder kier. + const top = Math.max(...[[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => roof.ceilingAt(p.x + a * (half - 1), p.z + b * (half - 1)))); + if (!(top > groundY + SHOE_MM)) return; + out.posts.push({ x: Math.round(p.x), z: Math.round(p.z), y0: groundY, y1: Math.round(top), size: s.size }); + }); + out.count = out.posts.length; + out.lengthMM = out.posts.reduce((t, p) => t + p.y1 - p.y0 - SHOE_MM, 0); + out.m3 = out.lengthMM * s.size * s.size / 1e9; + out.kg = out.m3 * WOODS[s.wood].kgM3; + return out; + } + + function createMaterials() { + return { + wood: Object.fromEntries(Object.entries(WOODS).map(([k, w]) => [k, new THREE.MeshStandardMaterial({ color: w.hex, roughness: 0.78 })])), + shoe: new THREE.MeshStandardMaterial({ color: 0x9ba0a4, roughness: 0.45, metalness: 0.6 }) + }; + } + + function build3D(data, materials) { + const toM = 0.001, group = new THREE.Group(); + if (!data || !data.posts.length) return group; + const m = materials || createMaterials(); + const woodMat = m.wood[data.wood] || m.wood.eiken; + data.posts.forEach(p => { + const h = p.y1 - p.y0 - SHOE_MM; + const post = new THREE.Mesh(new THREE.BoxGeometry(p.size * toM, h * toM, p.size * toM), woodMat); + post.position.set(p.x * toM, (p.y0 + SHOE_MM + h / 2) * toM, p.z * toM); + group.add(post); + const plate = new THREE.Mesh(new THREE.BoxGeometry((p.size + 40) * toM, 10 * toM, (p.size + 40) * toM), m.shoe); + plate.position.set(p.x * toM, (p.y0 + 5) * toM, p.z * toM); + group.add(plate); + const stem = new THREE.Mesh(new THREE.BoxGeometry(p.size * 0.35 * toM, (SHOE_MM - 10) * toM, p.size * 0.35 * toM), m.shoe); + stem.position.set(p.x * toM, (p.y0 + 10 + (SHOE_MM - 10) / 2) * toM, p.z * toM); + group.add(stem); + }); + return group; + } + + return { WOODS, SHOE_MM, DEFAULT_SETTINGS, normalizeSettings, compute, createMaterials, build3D }; +})(); diff --git a/js/roof/roof-model.js b/js/roof/roof-model.js index 97163f3..f824292 100644 --- a/js/roof/roof-model.js +++ b/js/roof/roof-model.js @@ -92,7 +92,7 @@ window.RoofModel = (() => { const deck = clone(floorState); deck.chassis = { enabled: false, wheelDiameter: 400, trackWidth: 1200, wheelbase: 1200 }; deck.openings = []; - return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, covering: 'dakleer', deck }; + return { followFloor: true, offsetX: 0, offsetZ: 0, extend: { f: 0, r: 0, b: 0, l: 0 }, fascia: true, covering: 'dakleer', deck }; } function thickness(calcData, covering) { @@ -1074,6 +1074,7 @@ window.RoofModel = (() => { // v0.6.2: mansarde vervallen, wordt een zadeldak met dezelfde nok. if (form && form.type === 'mansarde') { form.type = 'zadel'; delete form.kneeH; delete form.kneeIn; delete form.kneeRatio; } s.house = { enabled: !!h.enabled, form: form && UnitModel.ROOF_TYPES[form.type] ? form : null, cover: !!h.cover }; + s.posts = PostModel.normalizeSettings(s.posts); if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor; return s; } diff --git a/kosten/intern.csv b/kosten/intern.csv index 008642a..a790edf 100644 --- a/kosten/intern.csv +++ b/kosten/intern.csv @@ -40,6 +40,7 @@ dak.daktrimMontage;Montage daktrim;s/m;300 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.nokPersoneel;Personeel bij plaatsen nokbalk (i.p.v. de bezetting);man;3 +dak.paalMontage;Paal plaatsen: stellen, aftoppen, vastzetten aan het dak;s/st.;2700 vloer.afwerkingMontage;Montage vloerafwerking;s/m²;1200 vloer.sandwichMontage;Montage per sandwichpaneel;s;600 afwerking.valsPlafondMontage;Montage vals plafond;s/m²;1800 diff --git a/kosten/leveranciers.csv b/kosten/leveranciers.csv index a494928..980690b 100644 --- a/kosten/leveranciers.csv +++ b/kosten/leveranciers.csv @@ -21,6 +21,8 @@ dak.onderplaatM2;Onderplaat boeideel;€/m²;30;;; dak.daktrimM;Daktrim;€/m;12;;; dak.staalKg;Stalen nokbalk IPE S235 (op maat gezaagd, geboord, gegrond);€/kg;2,9;;; dak.nokOplegging;Oplegging nokbalk (stalen schoen + bouten), 2 per balk;€/st.;45;;; +dak.paalHoutM3;Paalhout overkapping (eiken, geschaafd);€/m³;2400;;; +dak.paalvoet;Paalvoet verzinkt, verstelbaar (incl. ankers);€/st.;40;;; vloer.afwerkingM2;Vloerafwerking;€/m²;35;;; vloer.sandwichM2;Sandwichpaneel;€/m²;35;;; afwerking.valsPlafondM2;Vals plafond (regelwerk + gipsplaat, afgewerkt);€/m²;38;;;