diff --git a/README.md b/README.md index 9748b56..dac15b3 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,16 @@ assets/ logo (preloader) + bestemmingsmap voor toekomstige unitrand bij een naad); spots te dicht bij een binnenwand vervallen. Woning-instellingen › Verlichting › Binnenspots. In 3D (filter Lampen), plattegrond (open cirkeltje), render en totalen. +- **Naden (v0.4.9).** Op elke naad staat standaard een tussenwand met in het + midden een deur (900 x 2150), ook bij "unit toevoegen"; verwijderen en + aanpassen kan gewoon. Hij bestaat uit twee delen, één per unit + (HouseModel.seamLeaves, elementen NW-1A / NW-1B), met balken en isolatie in + beide delen maar beplating alleen aan de kamerzijde (WallModel buitenzijde + 'geen'); de openingen gelden voor beide delen en worden gespiegeld. Vloer en + dak krijgen op een naad langs voor-/achterrand een kopbalk + (FloorModel.computeFrame rims), langs de zijranden ligt al een balk. +- **Isolatie niet meer getekend (v0.4.9).** Filter "Isolatie" en de 3D-lagen + zijn weg; de spouw-/leegte-inhoud blijft in specificaties en totalen. - **Startpunt gevellampen (v0.4.8).** Een gevel telt vanaf de zichtbare, afgewerkte hoek: elk uiteinde + afwerking van de haakse wand (52 mm bij vochtdoek), plus de bedekte kopse kant (skinExt) bij tussenpassende wanden. diff --git a/js/app.js b/js/app.js index acb3bae..eb58160 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.4.8'; +const APP_VERSION = '0.4.9'; // 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang. let tabs = []; diff --git a/js/floor/floor-model.js b/js/floor/floor-model.js index 926a1a7..0365395 100644 --- a/js/floor/floor-model.js +++ b/js/floor/floor-model.js @@ -114,8 +114,12 @@ window.FloorModel = (() => { // ---------------------------------------------------------- // Balkverdeling: zelfde algoritme (calculateSegmentPositions) als de wand. // params = state.floor (of de ruwe invoerwaarden uit de editor). + // rims (optioneel): { front, back } — kopbalk langs de voor-/achterrand + // (haaks op de balken), bijv. op een naad tussen twee units waar de + // modules weer aan elkaar gemonteerd worden. De langsbalken worden daar + // met één balkbreedte ingekort. Langs de zijranden ligt altijd al een balk. // ---------------------------------------------------------- - function computeFrame(params) { + function computeFrame(params, rims) { const width = params.width || 2000; const depth = params.depth || 2000; const joistW = params.joistWidth || 70; @@ -131,7 +135,9 @@ window.FloorModel = (() => { calculateSegmentPositions(width - joistW, minSp, maxSp).forEach(p => joists.push(leftJoistX + p)); joists = [...new Set(joists.map(x => Math.round(x)))].sort((a, b) => a - b); - return { joists, width, depth, joistW, joistH, underlayThick }; + const r = { front: !!(rims && rims.front), back: !!(rims && rims.back) }; + const joistLen = depth - (r.front ? joistW : 0) - (r.back ? joistW : 0); + return { joists, width, depth, joistW, joistH, underlayThick, rims: r, joistLen }; } // Bovenkant vloer (mm) = waar de wanden van een unit op staan. @@ -151,8 +157,10 @@ window.FloorModel = (() => { const { width, depth, joists, joistW, joistH, underlayThick } = calcData; const widthM = width * toM, depthM = depth * toM; const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM; + const rimCount = calcData.rims ? (calcData.rims.front ? 1 : 0) + (calcData.rims.back ? 1 : 0) : 0; + const joistLenM = (calcData.joistLen || depth) * toM; - const joistWoodM3 = joists.length * joistWM * joistHM * depthM; + const joistWoodM3 = joists.length * joistWM * joistHM * joistLenM + rimCount * joistWM * joistHM * widthM; const joistWoodKg = joistWoodM3 * DENSITY_WOOD_KGM3; const grossAreaM2 = widthM * depthM; @@ -197,8 +205,9 @@ window.FloorModel = (() => { } // Alles in één keer vanuit een vloer-state (handig voor de unit). - function computeFromState(state) { - const calcData = computeFrame(state.floor); + // rims: zie computeFrame. + function computeFromState(state, rims) { + const calcData = computeFrame(state.floor, rims); const resolvedOpenings = resolveFloorOpenings(state.openings || [], calcData.width, calcData.depth, calcData.joists, calcData.joistW); const chassisData = computeChassisWheels(state.chassis, calcData.width, calcData.depth); return { calcData, resolvedOpenings, chassisData }; @@ -208,9 +217,6 @@ window.FloorModel = (() => { return { woodMaterial: new THREE.MeshStandardMaterial({ color: 0xc29b62, roughness: 0.6 }), osbMaterial: new THREE.MeshStandardMaterial({ color: 0xcda86a, roughness: 0.75, side: THREE.DoubleSide }), - isolatieMaterial: new THREE.MeshStandardMaterial({ - color: 0xffe08c, roughness: 0.9, transparent: true, opacity: 0.5, side: THREE.DoubleSide - }), wheelMaterial: new THREE.MeshStandardMaterial({ color: 0x151515, roughness: 0.5 }) }; } @@ -220,12 +226,11 @@ window.FloorModel = (() => { // Z = -diepte/2), Y = dikte (balkhoogte + OSB). Balken en isolatie zijn // simpele dozen; de OSB-onderlaag is een geëxtrudeerd Shape met ronde // uitsparingen voor de doorvoeren. - // opts: { materials, osbVisible, isolatieVisible } + // opts: { materials, osbVisible } // ---------------------------------------------------------- function build3D(calcData, resolvedOpenings, chassisData, opts) { - const { woodMaterial, osbMaterial, isolatieMaterial, wheelMaterial } = opts.materials; + const { woodMaterial, osbMaterial, wheelMaterial } = opts.materials; const osbVisible = opts.osbVisible !== false; - const isolatieVisible = opts.isolatieVisible !== false; const floorGroup3D = new THREE.Group(); const toM = 0.001; @@ -233,28 +238,26 @@ window.FloorModel = (() => { const widthM = width * toM, depthM = depth * toM; const joistWM = joistW * toM, joistHM = joistH * toM, underlayM = underlayThick * toM; - // A. VLOERBALKEN — dozen, lopen over de volle diepte door. + // A. VLOERBALKEN — dozen over de diepte (ingekort bij een kopbalk), plus + // de kopbalken (rims) over de volle breedte. + const rims = calcData.rims || {}; + const joistLenM = (calcData.joistLen || depth) * toM; + const joistZ = ((rims.front ? joistW : 0) - (rims.back ? joistW : 0)) / 2 * toM; joists.forEach(xMM => { - const geo = new THREE.BoxGeometry(joistWM, joistHM, depthM); + const geo = new THREE.BoxGeometry(joistWM, joistHM, joistLenM); const mesh = new THREE.Mesh(geo, woodMaterial); - mesh.position.set(xMM * toM, joistHM / 2, 0); + mesh.position.set(xMM * toM, joistHM / 2, joistZ); + mesh.castShadow = true; + floorGroup3D.add(mesh); + }); + [['front', -1], ['back', 1]].filter(([k]) => rims[k]).forEach(([, sign]) => { + const mesh = new THREE.Mesh(new THREE.BoxGeometry(widthM, joistHM, joistWM), woodMaterial); + mesh.position.set(0, joistHM / 2, sign * (depthM - joistWM) / 2); mesh.castShadow = true; floorGroup3D.add(mesh); }); - // B. ISOLATIE — vult elk balkvak (tussen twee opeenvolgende balken) over - // de volle balkhoogte/diepte. Zelfde "leegte tussen het regelwerk"- - // gedachte als bij wall.js, hier ook echt gevisualiseerd als laag. - const bays = computeBays(joists, joistW); - bays.forEach(bay => { - const bayWidthM = (bay.right - bay.left) * toM; - if (bayWidthM <= 0) return; - const geo = new THREE.BoxGeometry(bayWidthM, joistHM, depthM); - const mesh = new THREE.Mesh(geo, isolatieMaterial); - mesh.position.set(((bay.left + bay.right) / 2) * toM, joistHM / 2, 0); - mesh.visible = isolatieVisible; - floorGroup3D.add(mesh); - }); + // (Isolatie wordt niet getekend; de leegte-inhoud zit in computeSpecs.) // C. OSB-ONDERLAAG — één geëxtrudeerd vlak over de volle vloer, met // ronde uitsparingen voor de doorvoeren. Shape wordt opgebouwd in diff --git a/js/floor/floor-pdf.js b/js/floor/floor-pdf.js index 06ccc6c..8bb9663 100644 --- a/js/floor/floor-pdf.js +++ b/js/floor/floor-pdf.js @@ -46,8 +46,15 @@ window.FloorPdf = (() => { doc.setLineWidth(0.15); doc.line(tblX[0], ty, tblX[4] + 12, ty); ty += 4; - [String(1), String(joistW), String(joistH), String(Math.round(depth)), String(joists.length)] + const rims = calcData.rims || {}, joistLen = calcData.joistLen || depth; + const rimCount = (rims.front ? 1 : 0) + (rims.back ? 1 : 0); + [String(1), String(joistW), String(joistH), String(Math.round(joistLen)), String(joists.length)] .forEach((v, i) => doc.text(v, tblX[i], ty)); + if (rimCount) { + ty += 4.3; // kopbalk(en) op een naad (FloorModel.computeFrame) + [String(2), String(joistW), String(joistH), String(Math.round(width)), String(rimCount)] + .forEach((v, i) => doc.text(v, tblX[i], ty)); + } // --- Plaatmateriaal + isolatie --- let my = 240; @@ -89,11 +96,16 @@ window.FloorPdf = (() => { doc.setLineWidth(0.3); doc.setTextColor(...BLACK); doc.setFontSize(5); + const jz0 = rims.front ? joistW : 0; joists.forEach(x => { const xLeft = x + width / 2 - joistW / 2; - doc.rect(px(xLeft), py(0), joistW * scale, depth * scale, 'S'); + doc.rect(px(xLeft), py(jz0), joistW * scale, joistLen * scale, 'S'); doc.text('1', px(xLeft + joistW / 2), py(depth / 2), { align: 'center' }); }); + [['front', 0], ['back', depth - joistW]].filter(([k]) => rims[k]).forEach(([, z]) => { + doc.rect(px(0), py(z), width * scale, joistW * scale, 'S'); + doc.text('2', px(width / 2), py(z + joistW / 2) + 1.2, { align: 'center' }); + }); doc.setDrawColor(...RED); doc.setLineWidth(0.2); diff --git a/js/floor/floor.js b/js/floor/floor.js index f929f99..180f99b 100644 --- a/js/floor/floor.js +++ b/js/floor/floor.js @@ -713,7 +713,6 @@ function mountFloorEditor(container, callbacks) { const floorMaterials = roofMode ? RoofModel.createMaterials() : FloorModel.createMaterials(); let osbVisible = true; - let isolatieVisible = true; let floorGroup3D = null; let lastResolvedOpenings = []; let lastChassisData = { wheels: [], outOfBounds: false }; @@ -721,11 +720,9 @@ function mountFloorEditor(container, callbacks) { // Weergave-filter (core/view-filter.js). Beplating = OSB-plaat (bij een // dak: plafondplaat + dakleer). createViewFilter(document.getElementById('floor-toolbar-3d'), [ - { key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' }, - { key: 'isolatie', label: 'Isolatie' } + { key: 'beplating', label: roofMode ? 'Beplating (plafond + dakleer)' : 'Beplating (OSB-plaat)' } ], (v) => { osbVisible = v.beplating; - isolatieVisible = v.isolatie; update3DScene(); }); // Omgeving (core/skybox.js: geen/dag/nacht); de vloer staat op de tegelvloer. @@ -740,8 +737,8 @@ function mountFloorEditor(container, callbacks) { function update3DScene() { if (floorGroup3D) scene.remove(floorGroup3D); floorGroup3D = roofMode - ? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, isolatieVisible, dakleerVisible: osbVisible }) - : FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible, isolatieVisible }); + ? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, dakleerVisible: osbVisible }) + : FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible }); scene.add(floorGroup3D); applySky(); // schaduw van de nieuwe vloer } diff --git a/js/house/house-model.js b/js/house/house-model.js index e680f0c..2b2c870 100644 --- a/js/house/house-model.js +++ b/js/house/house-model.js @@ -92,6 +92,7 @@ window.HouseModel = (() => { { id: 3, type: 'raam', w: 500, h: 1900, y: 250, anchor: 'right', offset: 450 } ]; } + ensureSeamWalls(state); computeParts(state, { skip3D: true }); return state; } @@ -208,15 +209,55 @@ window.HouseModel = (() => { return wall; } - // Dikte = regelwerk + 2x beplating (WallModel.thicknessOf); de diepte van - // stijlen en regels wordt daarop afgestemd. + // Dikte = regelwerk + beplating (2x, of 1x bij een naadwand-deel met + // buitenzijde 'geen'); de diepte van stijlen en regels wordt daarop afgestemd. + const skinSides = (wallState) => ((wallState.materials || {}).exterior === 'geen' ? 1 : 2); function setThickness(wallState, thickness) { - const depth = Math.max(20, Math.round(thickness - 2 * wallState.wall.sheathingThick)); + const depth = Math.max(20, Math.round(thickness - skinSides(wallState) * wallState.wall.sheathingThick)); wallState.wall.studDepth = depth; wallState.wall.plateDepth = depth; } function thicknessOf(wallState) { - return Math.max(wallState.wall.studDepth, wallState.wall.plateDepth) + 2 * wallState.wall.sheathingThick; + return Math.max(wallState.wall.studDepth, wallState.wall.plateDepth) + skinSides(wallState) * wallState.wall.sheathingThick; + } + + // ------------------------------------------------------------ + // Naadwanden: op elke naad tussen twee units staat standaard een tussenwand + // met in het midden een deur. Hij bestaat uit twee delen, één per unit + // (de modules worden los vervoerd), die met hun regelwerk tegen elkaar aan + // staan: balken en isolatie in beide delen, beplating alleen aan de + // kamerzijde (buitenMateriaal 'geen' aan de naadzijde). Eén wand-state + // (hoogte, openingen) voor beide delen; de openingen gelden voor het deel + // in unit `seam.a` en worden in het andere deel gespiegeld, zodat de deur + // recht door beide delen gaat. De gebruiker kan hem aanpassen of + // verwijderen; alleen bij een nieuwe woning en bij "unit toevoegen" worden + // ontbrekende naadwanden aangemaakt (ensureSeamWalls). + // state.interiorWalls: { id, seam: { a, b }, wall } + // ------------------------------------------------------------ + const DEFAULT_SEAM_LEAF_MM = 100; // dikte per deel (regelwerk + 1x beplating) + function seamWallDefaults(height) { + const wall = WallEditor.getDefaultState(); + wall.wall.heightAll = wall.wall.heightLeft = wall.wall.heightRight = height || wall.wall.heightAll; + wall.materials = { interior: 'spaanplaat', exterior: 'geen' }; + const d = WallModel.OPENING_TYPES.deur; + wall.openings = [{ id: 1, type: 'deur', w: d.w, h: d.h, y: d.y, anchor: 'middle', offset: 0 }]; + setThickness(wall, DEFAULT_SEAM_LEAF_MM); + return wall; + } + const sameSeam = (s, a, b) => (s.a === a && s.b === b) || (s.a === b && s.b === a); + // Maakt ontbrekende naadwanden aan (alleen naden met onlyUnitId, indien gegeven). + function ensureSeamWalls(state, onlyUnitId) { + const { seams } = computeSeams(unitRects(state)); + seams.forEach(sm => { + if (onlyUnitId && sm.a !== onlyUnitId && sm.b !== onlyUnitId) return; + if (state.interiorWalls.some(iw => iw.seam && sameSeam(iw.seam, sm.a, sm.b))) return; + const id = state.interiorWalls.reduce((m, w) => Math.max(m, w.id), 0) + 1; + const height = Math.min(...[sm.a, sm.b].map(uid => { + const w = state.units.find(u => u.id === uid).unit.walls.voor.wall; + return Math.min(w.heightLeft, w.heightRight); + })); + state.interiorWalls.push({ id, seam: { a: sm.a, b: sm.b }, wall: seamWallDefaults(height) }); + }); } function interiorLayout(iw) { @@ -233,7 +274,7 @@ window.HouseModel = (() => { // ------------------------------------------------------------ // Alle onderdelen doorgerekend (+ 3D, tenzij opts.skip3D). - // opts: { wallMaterials, floorMaterials, beplatingVisible, isolatieVisible, skip3D } + // opts: { wallMaterials, floorMaterials, beplatingVisible, skip3D } // ------------------------------------------------------------ // Past de woning-brede instellingen toe op de onderdeel-states (muteert // state): h.o.h. van alle wanden, en de dakmaat als die de vloer volgt. @@ -296,8 +337,7 @@ window.HouseModel = (() => { if (!build) return { group: null, stats: null, specs: null }; const built = WallModel.build3D(config, frame, { materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, - spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false, - isolatieVisible: o.isolatieVisible !== false + spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false }); return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) }; } @@ -305,10 +345,13 @@ window.HouseModel = (() => { const units = state.units.map(u => { const rect = rectById[u.id]; const layout = UnitModel.computeLayout(u.unit, cuts[u.id]); - const floorData = FloorModel.computeFromState(u.unit.floor); + // Kopbalk op een naad langs voor-/achterrand (haaks op de balken): daar + // worden de modules weer aan elkaar gemonteerd. Zelfde voor het dak. + const rims = { front: cuts[u.id].voor.length > 0, back: cuts[u.id].achter.length > 0 }; + const floorData = FloorModel.computeFromState(u.unit.floor, rims); const floorSpecs = FloorModel.computeSpecs(floorData.calcData, floorData.resolvedOpenings); const floorGroup = build ? FloorModel.build3D(floorData.calcData, floorData.resolvedOpenings, floorData.chassisData, { - materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false, isolatieVisible: o.isolatieVisible !== false + materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false }) : null; const walls = layout.walls.map(wl => { @@ -331,7 +374,7 @@ window.HouseModel = (() => { // Dak: dek = vloer-state (180° gespiegeld, RoofModel), op de hoogste // wandbovenkant van deze unit. - const roofData = FloorModel.computeFromState(u.roof.deck); + const roofData = FloorModel.computeFromState(u.roof.deck, rims); const roofSpecs = RoofModel.computeSpecs(roofData.calcData, roofData.resolvedOpenings); const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => Math.max(ws.wall.heightLeft, ws.wall.heightRight))); const roofW = roofData.calcData.width, roofD = roofData.calcData.depth; @@ -342,9 +385,9 @@ window.HouseModel = (() => { base: layout.floorTop + wallTop, top: layout.floorTop + wallTop + RoofModel.thickness(roofData.calcData), // Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM) - // horen bij "Beplating", de dakisolatie bij "Isolatie". + // horen bij "Beplating". group: build ? RoofModel.build3D(roofData.calcData, roofData.resolvedOpenings, { - materials: o.roofMaterials, isolatieVisible: o.isolatieVisible !== false, + materials: o.roofMaterials, osbVisible: o.beplatingVisible !== false, dakleerVisible: o.beplatingVisible !== false }) : null @@ -363,19 +406,107 @@ window.HouseModel = (() => { }))); // Binnenwanden staan op de vloer van de unit waarin hun midden valt. - const interior = state.interiorWalls.map((iw, i) => { + // Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves). + const interior = []; + let bwNr = 0, nwNr = 0; + state.interiorWalls.forEach(iw => { + if (iw.seam) { nwNr++; seamLeaves(iw, nwNr).forEach(l => interior.push(l)); return; } + bwNr++; const lay = interiorLayout(iw); const host = units.find(u => lay.cx >= u.rect.x0 && lay.cx <= u.rect.x1 && lay.cz >= u.rect.z0 && lay.cz <= u.rect.z1); const floorTop = host ? host.layout.floorTop : Math.max(...units.map(u => u.layout.floorTop)); const config = WallModel.configFromState(iw.wall); const frame = WallModel.computeFrame(config); const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat'; - return Object.assign({ - id: iw.id, element: `BW-${i + 1}`, label: `Binnenwand ${i + 1}`, config, frame, buitenMateriaal, + interior.push(Object.assign({ + id: iw.id, element: `BW-${bwNr}`, label: `Binnenwand ${bwNr}`, config, frame, buitenMateriaal, place: { cx: lay.cx, cz: lay.cz, rotY: lay.rotY, width: lay.width, thickness: WallModel.thicknessOf(config), floorTop } - }, buildWall(config, frame, buitenMateriaal)); + }, buildWall(config, frame, buitenMateriaal))); }); + // Twee delen van een naadwand. Elk deel staat met zijn regelwerk op de + // naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit, en loopt van + // wand tot wand: waar een buitenwandstuk de hartlijn van het deel kruist, + // stopt het (voetafdrukken, zoals bij de gevellampen). + function seamLeaves(iw, nr) { + const sm = seams.find(x => sameSeam(x, iw.seam.a, iw.seam.b)); + if (!sm) return []; // units niet (meer) tegen elkaar: naadwand vervalt + const horiz = Math.abs(sm.z0 - sm.z1) < EPS; + const baseConfig = WallModel.configFromState(iw.wall); + const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth); + const T1 = WallModel.thicknessOf(baseConfig, 'geen'); + const extPieces = []; + units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p)))); + const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x; + const seamPerp = horiz ? sm.z0 : sm.x0; + const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1; + + const leaves = []; + [[sm.a, 1], [sm.b, -1]].forEach(([uid, side]) => { + // side +1: unit a ligt aan de lage kant, de naad aan zijn +kant. + const line = seamPerp - side * T1 / 2; + const blocked = []; + extPieces.forEach(p => { + const cs = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([i, j]) => UnitModel.localToWorld(p.place, i * p.place.width / 2, j * p.place.thickness / 2)); + const pMin = Math.min(...cs.map(perpOf)), pMax = Math.max(...cs.map(perpOf)); + if (line > pMin + 0.5 && line < pMax - 0.5) blocked.push([Math.min(...cs.map(along)), Math.max(...cs.map(along))]); + }); + let cursor = s0, best = null; + blocked.sort((p, q) => p[0] - q[0]).concat([[s1, s1]]).forEach(([a, b]) => { + const hi = Math.min(a, s1); + if (hi - cursor > (best ? best[1] - best[0] : 0)) best = [cursor, hi]; + cursor = Math.max(cursor, b); + }); + if (!best || best[1] - best[0] < 300) return; + const width = Math.round(best[1] - best[0]), mid = (best[0] + best[1]) / 2; + const rotY = horiz ? (side > 0 ? 0 : Math.PI) : (side > 0 ? Math.PI / 2 : -Math.PI / 2); + const place = { + cx: horiz ? mid : line, cz: horiz ? line : mid, rotY, width, thickness: T1, + floorTop: units.find(u => u.id === uid).layout.floorTop + }; + leaves.push({ uid, side, place }); + }); + if (!leaves.length) return []; + + // Openingen: opgeslagen voor het eerste deel; het andere deel krijgt + // dezelfde openingen op dezelfde plek in de wereld (gespiegeld). + const first = leaves[0]; + iw.wall.wall.width = first.place.width; // voor de wand-editor + const firstConfig = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: first.place.width }), iw.wall.openings || []); + const toLocalU = (pl, q) => { + const c = Math.cos(pl.rotY), sn = Math.sin(pl.rotY); + return (q.x - pl.cx) * c - (q.z - pl.cz) * sn + pl.width / 2; + }; + return leaves.map((lf, k) => { + let config = firstConfig; + if (k > 0) { + const ops = firstConfig.openings.map(op => { + const wa = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x, 0); + const wb = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x + op.w, 0); + const u = Math.min(toLocalU(lf.place, wa), toLocalU(lf.place, wb)); + return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: Math.round(u) }; + }); + config = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: lf.place.width }), ops); + } + const frame = WallModel.computeFrame(config); + const built = buildWall(config, frame, 'geen'); + if (built.group) { + // Het regelwerk staat gecentreerd op lokaal z = 0, de beplating + // alleen aan de achterzijde: schuif zodat het midden van de totale + // dikte op place.cx/cz ligt (regelwerk op de naad). + const outer = new THREE.Group(); + built.group.position.z = (T1 - maxD) / 2 * 0.001; + outer.add(built.group); + built.group = outer; + } + return Object.assign({ + id: iw.id, seam: iw.seam, seamUnit: lf.uid, + element: `NW-${nr}${lf.uid}`, label: `Naadwand ${nr} (unit ${lf.uid})`, + config, frame, buitenMateriaal: 'geen', place: lf.place + }, built); + }); + } + // Dakrand (boeidelen/daktrim) als één doorlopende rand om alle daken; // boeidelen aan/uit per unit. const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof, @@ -428,9 +559,11 @@ window.HouseModel = (() => { } parts.units.forEach(u => { const fc = u.floorData.calcData; - fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.depth)); + fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.joistLen)); + ['front', 'back'].filter(k => fc.rims[k]).forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.width)); const rc = u.roof.data.calcData; - rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.depth)); + rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen)); + ['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width)); }); const wallBom = []; pieces.forEach(p => WallModel.computeBOM(p.config, p.frame).forEach(b => { wallBom.push(b); addPiece(b.kind, b.width, b.depth, b.length); })); @@ -568,7 +701,7 @@ window.HouseModel = (() => { return { ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs, - interiorWallDefaults, setThickness, thicknessOf, interiorLayout, + interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls, computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize }; })(); diff --git a/js/house/house-pdf.js b/js/house/house-pdf.js index 7ac398f..efcd710 100644 --- a/js/house/house-pdf.js +++ b/js/house/house-pdf.js @@ -146,7 +146,7 @@ window.HousePdf = (() => { }))); doc.setTextColor(...BLACK); parts.interior.forEach(p => { - doc.text(`${p.element}: ${p.place.width}`, px(p.place.cx), py(p.place.cz) - 1.5, { align: 'center', angle: p.place.rotY === 0 ? 0 : 90 }); + doc.text(`${p.element}: ${p.place.width}`, px(p.place.cx), py(p.place.cz) - 1.5, { align: 'center', angle: Math.abs(Math.sin(p.place.rotY)) < 0.5 ? 0 : 90 }); }); // Buitenmaten woning diff --git a/js/house/house.js b/js/house/house.js index 4e87727..8ea8c7e 100644 --- a/js/house/house.js +++ b/js/house/house.js @@ -135,7 +135,11 @@ function mountHouseEditor(container, callbacks) { if (kind === 'floor' && unitById(a)) unitById(a).unit.floor = activeEditor.getStateJson(); else if (kind === 'wall' && unitById(a)) unitById(a).unit.walls[b] = activeEditor.getStateJson(); else if (kind === 'roof' && unitById(a)) unitById(a).roof.deck = activeEditor.getStateJson(); - else if (kind === 'iw' && iwById(Number(a))) iwById(Number(a)).wall = activeEditor.getStateJson(); + else if (kind === 'iw' && iwById(Number(a))) { + const iw = iwById(Number(a)); + iw.wall = activeEditor.getStateJson(); + if (iw.seam) iw.wall.materials = Object.assign({}, iw.wall.materials, { exterior: 'geen' }); // naadzijde blijft zonder plaat + } } function showPart(part) { @@ -172,7 +176,8 @@ function mountHouseEditor(container, callbacks) { activeEditor.loadStateJson(HouseModel.clone(u.roof.deck)); } else if (kind === 'iw' && iwById(Number(a))) { HouseModel.applyGlobalSettings(state); - HouseModel.interiorLayout(iwById(Number(a))); // breedte bijwerken + if (iwById(Number(a)).seam) HouseModel.computeParts(state, { skip3D: true }); // breedte naadwand-deel + else HouseModel.interiorLayout(iwById(Number(a))); // breedte bijwerken activeEditor = WallEditor.mount(host, { cameraKey: camKey(part), lockWidth: 'De lengte van een binnenwand volgt uit de tekening (overzicht).', lockSpacing: SPACING_LOCK_MSG }); activeEditor.loadStateJson(HouseModel.clone(iwById(Number(a)).wall)); } else { @@ -205,7 +210,15 @@ function mountHouseEditor(container, callbacks) { }); if (parts.interior.length) { html += `