v0.4.15: gevelbekleding per woning (rabat/steen, override per wand), stenen trasraam (110 mm, 181 texturen), riet per unit, vloerafwerking, verfkleur binnen, boeideelkleur, gevel tot onder dakrand, binnenhoek-fixes; testontwerp als standaard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 19:47:49 +02:00
co-authored by Claude Opus 5.5
parent 7d91416004
commit 12c2579471
743 changed files with 675 additions and 99 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
const APP_VERSION = '0.4.14';
const APP_VERSION = '0.4.15';
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
let tabs = [];
+10 -1
View File
@@ -40,6 +40,15 @@ window.UI = (() => {
return field(id, label, `<select id="${id}">${opts}</select><span class="ui-unit"></span>`, o);
}
// Kleurkiezer (waarde '#rrggbb'); onColor reageert pas bij loslaten.
function color(id, label, value, o) {
return field(id, label, `<input type="color" id="${id}" value="${value}"><span class="ui-unit"></span>`, o);
}
function onColor(id, fn, after) {
const el = document.getElementById(id);
if (el) el.addEventListener('change', (e) => { fn(e.target.value); after && after(); });
}
// Aan/uit-schakelaar.
function toggle(id, label, checked, o) {
return field(id, label,
@@ -117,5 +126,5 @@ window.UI = (() => {
el.classList.toggle('fullscreen-2d');
}
return { esc, icon, hint, num, sel, toggle, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
return { esc, icon, hint, num, sel, toggle, color, onColor, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
})();
+57 -2
View File
@@ -213,6 +213,32 @@ window.FloorModel = (() => {
return { calcData, resolvedOpenings, chassisData };
}
// Vloerafwerking (bovenop de OSB). Texturen in assets/textures/vloer/,
// naadloos herhalend; tileMM = maat van één tegel van de textuur.
const FINISHES = {
geen: { label: 'Geen (OSB)' },
visgraat_001: { label: 'Visgraat (Hongaarse punt)', file: 'assets/textures/vloer/visgraat_001.jpg', tileMM: 3000 }
};
const finishMaterials = {};
function finishMaterial(key) {
const f = FINISHES[key];
if (!f || !f.file) return null;
if (finishMaterials[key]) return finishMaterials[key];
const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(f.file, (tex) => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1000 / f.tileMM, 1000 / f.tileMM);
tex.anisotropy = 8;
mat.map = tex;
mat.color.set(0xffffff);
mat.needsUpdate = true;
});
}
finishMaterials[key] = mat;
return mat;
}
function createMaterials() {
return {
woodMaterial: new THREE.MeshStandardMaterial({ color: 0xc29b62, roughness: 0.6 }),
@@ -226,7 +252,10 @@ 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 }
// opts: { materials, osbVisible, finish }
// finish (optioneel): { key (FINISHES), originX, originZ } — afwerking als
// dun vlak op de OSB; UV's in meters vanaf origin (m, woning-coördinaten
// van het vloermidden), zodat het patroon over units doorloopt.
// ----------------------------------------------------------
function build3D(calcData, resolvedOpenings, chassisData, opts) {
const { woodMaterial, osbMaterial, wheelMaterial } = opts.materials;
@@ -311,6 +340,32 @@ window.FloorModel = (() => {
floorGroup3D.add(mesh);
});
// Vloerafwerking: vlak net boven de OSB, over de hele vloer (onder de
// wanden verdwijnt het), met de doorvoeren uitgespaard.
const fin = opts.finish && finishMaterial(opts.finish.key);
if (fin) {
const shape = new THREE.Shape();
shape.moveTo(-widthM / 2, -depthM / 2);
shape.lineTo(widthM / 2, -depthM / 2);
shape.lineTo(widthM / 2, depthM / 2);
shape.lineTo(-widthM / 2, depthM / 2);
shape.closePath();
resolvedOpenings.filter(o => !o.removed && o.r > 0).forEach(o => {
const hole = new THREE.Path();
hole.absarc(o.x * toM, o.y * toM - depthM / 2, o.r * toM, 0, Math.PI * 2, false);
shape.holes.push(hole);
});
const geo = new THREE.ShapeGeometry(shape);
const ox = opts.finish.originX || 0, oz = opts.finish.originZ || 0;
const pos = geo.attributes.position, uv = geo.attributes.uv;
// Na het kantelen (-90° om X) wordt vorm-y wereld -z.
for (let k = 0; k < pos.count; k++) uv.setXY(k, pos.getX(k) + ox, -pos.getY(k) + oz);
const mesh = new THREE.Mesh(geo, fin);
mesh.rotation.x = -Math.PI / 2;
mesh.position.y = joistHM + underlayM + 0.0005;
mesh.receiveShadow = true;
floorGroup3D.add(mesh);
}
return floorGroup3D;
}
@@ -338,7 +393,7 @@ window.FloorModel = (() => {
}
return {
validateState, computeFrame, topHeight, resolveOpenings: resolveFloorOpenings, computeChassisWheels,
FINISHES, finishMaterial, validateState, computeFrame, topHeight, resolveOpenings: resolveFloorOpenings, computeChassisWheels,
computeSpecs, computeFromState, createMaterials, build3D
};
})();
+129 -19
View File
@@ -60,12 +60,23 @@ window.HouseModel = (() => {
function createDefaultSettings() {
return {
spacing: { exterior: { min: 240, max: 480 }, interior: { min: 300, max: 600 } },
cladding: 'horizontaal', // richting gevelbekleding, voorlopig voor alle buitenwanden
// Gevelbekleding van alle buitenwanden (override per wand: unit.facade):
// rabat = vochtdoek + kruislat + rabat (richting: cladding);
// steen = cement-gebonden plaat + steenblok (stone = steencode, WallModel.STONE).
// Het trasraam (eigen afwerking/steen) komt onder beide.
facade: { type: 'rabat', stone: 'K490' },
cladding: 'horizontaal', // richting rabat, voorlopig voor alle buitenwanden
// Trasraam: cement-gebonden plaat onderaan alle vochtdoek-gevels, hoogte
// vanaf het beloopbare vloervlak (WallModel.trasraamFor).
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM },
// finish: 'cementplaat' of een steencode (WallModel.TRASRAAM_STONES).
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM, finish: 'cementplaat' },
// Binnenzijde van alle wanden (voorlopig globaal): geschilderd in
// interiorColor (egaal, zonder plaatnaden) of kale spaanplaat.
interiorPaint: true,
interiorColor: '#f2efe9',
sandwich: true, // sandwichpanelen rondom (SandwichModel), vanaf loopvlak 600 mm de grond in
groundLevel: 350, // maaiveld: zoveel mm onder het loopvlak (bovenkant vloer); voor de 3D-omgeving
groundLevel: 0, // maaiveld: zoveel mm onder het loopvlak (bovenkant vloer); voor de 3D-omgeving
floorFinish: 'visgraat_001', // vloerafwerking (FloorModel.FINISHES), voorlopig globaal
roof: Object.assign({}, RoofModel.DEFAULT_SETTINGS),
lighting: Object.assign({}, LightingModel.DEFAULT_SETTINGS)
};
@@ -73,10 +84,35 @@ window.HouseModel = (() => {
function createUnitEntry(id, attach, unitOpts) {
const unit = UnitModel.createDefaultState(unitOpts);
return { id, attach, unit, roof: RoofModel.createDefaultRoof(unit.floor), lighting: LightingModel.createDefaultUnitLighting() };
return { id, attach, unit, roof: RoofModel.createDefaultRoof(unit.floor), lighting: LightingModel.createDefaultUnitLighting(), facade: { walls: {} } };
}
// Gevelbekleding: keuze voor de woning, override per wand.
const FACADE_TYPES = { rabat: 'Rabat', steen: 'Steen' };
// { type, stone } voor één wand van een unit (u = entry uit state.units).
function facadeFor(state, u, side) {
const f = state.settings.facade;
const o = (u.facade && u.facade.walls && u.facade.walls[side]) || null;
const type = o && FACADE_TYPES[o.type] ? o.type : f.type;
const stone = o && o.type === 'steen' && WallModel.isStoneFinish(o.stone) ? o.stone : f.stone;
return { type, stone, override: !!o };
}
// TIJDELIJK (testen): een nieuwe woning is standaard het testontwerp:
// L-vorm, unit A lessenaar (noord 2600, zuid 3200) met riet. Op false
// zetten om terug te gaan naar de gewone standaard (één unit).
const TEST_DEFAULT = true;
function createDefaultState(template) {
if (TEST_DEFAULT && (!template || template === 'single')) {
const st = createDefaultState('L');
const A = st.units[0], W = A.unit.walls;
const h = (k, l, r) => Object.assign(W[k].wall, { heightAll: Math.min(l, r), heightLeft: l, heightRight: r, heightMid: null });
h('achter', 3200, 3200); h('links', 2600, 3200); h('rechts', 3200, 2600);
A.roof.covering = 'riet_001';
computeParts(st, { skip3D: true });
return st;
}
const state = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [] };
state.units.push(createUnitEntry('A', null, { width: 6000, depth: 3000 }));
if (template === 'L') {
@@ -282,11 +318,14 @@ window.HouseModel = (() => {
const sp = state.settings.spacing;
const tr = state.settings.trasraam;
state.units.forEach(u => {
const trasraam = tr.enabled ? WallModel.trasraamFor(tr.height) : null;
Object.values(u.unit.walls).forEach(ws => {
const trasraam = tr.enabled ? WallModel.trasraamFor(tr.height, tr.finish) : null;
Object.entries(u.unit.walls).forEach(([side, ws]) => {
ws.wall.minSpacing = sp.exterior.min;
ws.wall.maxSpacing = sp.exterior.max;
ws.materials = Object.assign({}, ws.materials, { cladding: state.settings.cladding, trasraam });
const fc = facadeFor(state, u, side);
ws.materials = Object.assign({}, ws.materials, fc.type === 'steen'
? { exterior: 'cementplaat', stone: fc.stone, cladding: state.settings.cladding, trasraam }
: { exterior: 'vochtdoek', stone: null, cladding: state.settings.cladding, trasraam });
});
if (u.roof.followFloor) {
u.roof.deck.floor.width = u.unit.floor.floor.width;
@@ -310,14 +349,22 @@ window.HouseModel = (() => {
const W = p.place.width, T = p.place.thickness;
const normal = { x: p.place.normal.x, z: p.place.normal.y };
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z', perp = axis === 'x' ? 'z' : 'x';
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
segs.push({ piece: p, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
// Binnenhoek-doorloop (negatieve skinExt, zie trimTop): zit dat stuk
// over de hele hoogte in de buur-unit, dan begint de gevel pas daarna.
// Loopt de gevel erboven door (lager dak van de buur), dan telt het
// hele stuk mee, zodat de gevel één geheel blijft met de rest.
const e = p.skinExt || {}, tt = p.trimTop || {};
const maxY = WallModel.topLine(p.config).maxY + (p.skinAbove || 0);
const trim = (k) => e[k] < 0 && !(tt[k] > 0 && tt[k] < maxY - 1) ? -e[k] : 0;
const tL = trim('left'), tR = trim('right');
const q0 = UnitModel.localToWorld(p.place, -W / 2 + tL, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2 - tR, T / 2);
segs.push({ piece: p, trimL: tL, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
});
groupFacadeRuns(segs).forEach(run => {
const dir = Math.sign(run.segs[0].sAtW - run.segs[0].sAt0) || 1;
const sLeft = dir > 0 ? run.s0 : run.s1, sRight = dir > 0 ? run.s1 : run.s0;
const first = run.segs.reduce((best, sg) => Math.abs(sg.sAt0 - sLeft) < Math.abs(best.sAt0 - sLeft) ? sg : best);
const extLeft = (first.piece.skinExt && first.piece.skinExt.left) || 0;
const extLeft = Math.max(0, (first.piece.skinExt && first.piece.skinExt.left) || 0);
// Horizontaal latten-raster: vanaf het trasraam van een gewoon stuk
// (absolute hoogte), zodat opbouw/naadwand-stukken erop aansluiten.
const ref = run.segs.find(sg => !sg.piece.place.baseY && sg.piece.skinFrom === undefined);
@@ -326,7 +373,7 @@ window.HouseModel = (() => {
run.segs.forEach(sg => {
const p = sg.piece;
sg.piece.facade = {
u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5,
u0: Math.abs(sg.sAt0 - sLeft) + extLeft - sg.trimL, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5,
gridY0: gridAbs === undefined ? undefined : gridAbs - p.place.floorTop,
yOff: floorAbs === undefined ? 0 : p.place.floorTop - floorAbs
};
@@ -344,10 +391,11 @@ window.HouseModel = (() => {
const wallThickness = (id, side) => WallModel.thicknessOf(WallModel.configFromState(state.units.find(u => u.id === id).unit.walls[side]));
const { cuts, seams } = computeSeams(rects, wallThickness);
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow) {
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop) {
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,
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop,
painted: state.settings.interiorPaint,
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
});
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
@@ -360,7 +408,8 @@ window.HouseModel = (() => {
const ws = u.unit.walls[wl.side];
const buitenMateriaal = (ws.materials || {}).exterior || 'vochtdoek';
const cladding = (ws.materials || {}).cladding || 'horizontaal';
const trasraam = buitenMateriaal === 'vochtdoek' ? ((ws.materials || {}).trasraam || null) : null;
const stone = buitenMateriaal === 'cementplaat' ? ((ws.materials || {}).stone || null) : null;
const trasraam = buitenMateriaal === 'vochtdoek' || stone ? ((ws.materials || {}).trasraam || null) : null;
const wp = UnitModel.wallPieces(wl, ws, raised);
const pieces = wp.pieces.map(p => {
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
@@ -368,7 +417,7 @@ window.HouseModel = (() => {
return Object.assign(p, {
element: `${u.id}-W${wl.nr}${p.suffix}`,
label: `Unit ${u.id} ${wl.label.toLowerCase()}` + (base ? ` opbouw (vanaf ${base} mm)` : p.suffix ? ` (${p.suffix})` : ''),
buitenMateriaal, cladding, trasraam: base ? null : trasraam,
buitenMateriaal, cladding, trasraam: base ? null : trasraam, stone,
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop + base, baseY: base }
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
});
@@ -394,7 +443,8 @@ window.HouseModel = (() => {
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
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false,
finish: { key: state.settings.floorFinish, originX: rect.cx * 0.001, originZ: rect.cz * 0.001 }
}) : null;
const walls = layout.walls.map(wl => wallOf(u, rect, layout, wl));
@@ -643,9 +693,35 @@ window.HouseModel = (() => {
// over het blootliggende deel van een naadwand. Daarna pas bouwen.
const extPieces = [];
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
// Gevel tot onder de dakrand: tussen wandbovenkant en onderkant boeideel
// resp. daktrim zit het dakpakket (dek + bedekking, verschilt per dak).
// De buitenlagen van de wand lopen daar door (skinAbove, mm), zodat er
// geen dakrand bloot ligt, bij elke daksoort.
const roofSt = RoofModel.normalizeSettings(state.settings.roof);
const flatTopsPre = units.filter(u => u.roof.shape.type === 'plat').map(u => u.roof.top);
const flatTopPre = flatTopsPre.length ? Math.max(...flatTopsPre) : 0;
units.forEach(u => {
const su = state.units.find(x => x.id === u.id);
const below = su.roof.fascia !== false ? roofSt.fasciaHeight - RoofModel.EDGE_ABOVE_MM : roofSt.trimHeight;
u.walls.forEach(w => w.pieces.forEach(p => {
const m = UnitModel.localToWorld(p.place, 0, p.place.thickness / 2);
const top = u.roof.shape.type === 'plat' ? flatTopPre : u.roof.topAt(m.x, m.z);
p.skinAbove = Math.max(0, Math.round(top - u.roof.ceilingAt(m.x, m.z) - below));
// Binnenhoek-doorloop (negatieve skinExt): tot de bovenkant van het
// dak van de buur-unit die daar staat geen buitenlagen (trimTop).
const e = p.skinExt || {}, W = p.place.width;
p.trimTop = {};
[['left', -1], ['right', 1]].forEach(([k, d]) => {
if (!(e[k] < 0)) return;
const q = UnitModel.localToWorld(p.place, d * (W / 2 + e[k] / 2), p.place.thickness / 2 + 20);
const nb = units.find(o => o.id !== u.id && q.x > o.rect.x0 && q.x < o.rect.x1 && q.z > o.rect.z0 && q.z < o.rect.z1);
p.trimTop[k] = nb ? Math.round(nb.roof.topAt(q.x, q.z) - p.place.floorTop) : 0;
});
}));
});
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
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);
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);
if (built.group && p.shiftZ) {
const outer = new THREE.Group();
built.group.position.z = p.shiftZ * 0.001;
@@ -725,6 +801,9 @@ window.HouseModel = (() => {
const purchase = WallModel.planPurchase(wallBom);
const sumP = (fn) => pieces.reduce((acc, p) => acc + fn(p), 0);
const stonePiece = pieces.find(p => p.trasraam && WallModel.isStoneFinish(p.trasraam.finish));
const stoneCode = stonePiece ? stonePiece.trasraam.finish : null;
const stoneCodes = [...new Set(pieces.filter(p => p.stone).map(p => p.stone))].join(', ');
const sumU = (fn) => parts.units.reduce((acc, u) => acc + fn(u), 0);
const byMat = (mat) => pieces.filter(p => p.buitenMateriaal === mat);
@@ -762,10 +841,24 @@ window.HouseModel = (() => {
},
{ naam: 'OSB-plaat (vloeren + plafonds dak)', m2: sumU(u => u.floorSpecs.osbAreaM2 + u.roof.specs.osbAreaM2), m3: sumU(u => u.floorSpecs.osbVolM3 + u.roof.specs.osbVolM3), kg: sumU(u => u.floorSpecs.osbKg + u.roof.specs.osbKg) },
{ naam: 'Dakleer (EPDM)', m2: sumU(u => u.roof.specs.dakleerM2), m3: null, kg: sumU(u => u.roof.specs.dakleerKg) },
{ naam: `Riet (${RoofModel.RIET.thickMM} mm)`, m2: sumU(u => u.roof.specs.rietM2 || 0), m3: null, kg: sumU(u => u.roof.specs.rietKg || 0) },
{
naam: 'Cementplaat (buitenzijde)', m2: byMat('cementplaat').reduce((a, p) => a + p.stats.cementplaatAreaM2, 0),
m3: byMat('cementplaat').reduce((a, p) => a + p.stats.cementplaatVolumeM3, 0), kg: byMat('cementplaat').reduce((a, p) => a + p.specs.buitenKg, 0)
},
{
naam: `Steenstrips gevel${stoneCodes ? ' (' + stoneCodes + ')' : ''}`,
m2: sumP(p => p.stats.stoneAreaM2 || 0), m3: null, kg: 0 // gewicht volgt
},
{
naam: `Steenstrips trasraam${stoneCode ? ' (' + stoneCode + ')' : ''}`,
m2: sumP(p => p.stats.trasraamStoneAreaM2 || 0), m3: null, kg: 0 // gewicht volgt
},
{
naam: `Isolatieblok steen (${WallModel.STONE.THICK_MM - WallModel.STONE.STRIP_MM} mm)`,
m2: sumP(p => (p.stats.trasraamIsolatieM3 || 0) + (p.stats.stoneIsolatieM3 || 0)) / ((WallModel.STONE.THICK_MM - WallModel.STONE.STRIP_MM) / 1000),
m3: sumP(p => (p.stats.trasraamIsolatieM3 || 0) + (p.stats.stoneIsolatieM3 || 0)), kg: 0
},
{
naam: 'Cementplaat (trasraam)', m2: sumP(p => p.stats.trasraamAreaM2 || 0),
m3: sumP(p => p.stats.trasraamVolumeM3 || 0), kg: sumP(p => p.specs.trasraamKg || 0)
@@ -784,6 +877,7 @@ window.HouseModel = (() => {
roofEdge: parts.roofEdge ? {
fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000,
fasciaHeight: parts.roofEdge.fasciaHeight,
fasciaColor: (RoofModel.FASCIA_COLORS[parts.roofEdge.fasciaColor] || {}).label || '',
overhang: parts.roofEdge.overhang,
soffitM2: parts.roofEdge.soffitM2,
trimLengthM: parts.roofEdge.trimLengthMM / 1000,
@@ -837,11 +931,22 @@ window.HouseModel = (() => {
state.settings = state.settings || {};
state.settings.spacing = Object.assign(def.spacing, state.settings.spacing || {});
if (!WallModel.CLADDING_DIRECTIONS[state.settings.cladding]) state.settings.cladding = def.cladding;
// v0.4.15: gevelbekleding per woning; een wand met cementplaat wordt een
// steen-override (de rest volgt de woning, standaard rabat).
const hadFacade = !!state.settings.facade;
state.settings.facade = Object.assign(def.facade, state.settings.facade || {});
if (!FACADE_TYPES[state.settings.facade.type]) state.settings.facade.type = 'rabat';
if (!WallModel.isStoneFinish(state.settings.facade.stone)) state.settings.facade.stone = def.facade.stone;
state.settings.trasraam = Object.assign(def.trasraam, state.settings.trasraam || {});
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';
state.settings.sandwich = state.settings.sandwich !== false;
if (!(state.settings.groundLevel >= 0)) state.settings.groundLevel = def.groundLevel;
if (!/^#[0-9a-f]{6}$/i.test(state.settings.interiorColor || '')) state.settings.interiorColor = def.interiorColor;
state.settings.interiorPaint = state.settings.interiorPaint !== false;
// v0.4.15: maaiveld standaard gelijk met de bovenkant vloer (was 350 mm eronder).
if (!(state.settings.groundLevel >= 0) || (!state.settings.floorFinish && state.settings.groundLevel === 350)) state.settings.groundLevel = def.groundLevel;
if (!FloorModel.FINISHES[state.settings.floorFinish]) state.settings.floorFinish = def.floorFinish;
const oldRoof = state.settings.roof || {}; // v0.3.0: edge/spots waren woning-breed
state.settings.roof = RoofModel.normalizeSettings(oldRoof);
// v0.3.1: type/richting van de verlichting stonden per unit; nu woning-breed.
@@ -854,13 +959,18 @@ window.HouseModel = (() => {
if (!u.unit.layout) u.unit.layout = { corners: 'voor-achter' };
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.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]) => {
if ((ws.materials || {}).exterior === 'cementplaat' && !u.facade.walls[side]) u.facade.walls[side] = { type: 'steen' };
});
});
return state;
}
return {
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS,
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor,
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls,
computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize
+1 -1
View File
@@ -218,7 +218,7 @@ window.HousePdf = (() => {
const e = totals.roofEdge, L = totals.lamps;
const rows = [];
if (e.fasciaLengthM > 0) {
rows.push(['Boeidelen', `${fmt(e.fasciaLengthM, 2)} m, ${e.fasciaHeight} mm hoog, overstek ${e.overhang} mm`]);
rows.push(['Boeidelen', `${fmt(e.fasciaLengthM, 2)} m, ${e.fasciaHeight} mm hoog, overstek ${e.overhang} mm${e.fasciaColor ? ', ' + e.fasciaColor.toLowerCase() : ''}`]);
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`]);
+109 -7
View File
@@ -169,6 +169,7 @@ function mountHouseEditor(container, callbacks) {
lockWidth: 'Binnen een woning volgt de wandbreedte uit de vloer van de unit.',
lockSpacing: SPACING_LOCK_MSG,
lockCladding: 'De richting van de gevelbekleding is (voorlopig) globaal: Overzicht, niets geselecteerd.',
lockExterior: 'De gevelbekleding volgt de woning (Overzicht › Gevel); per wand aan te passen via de wand in de plattegrond.',
seamCuts: wl.cutsU,
lamps: [].concat(...wl.pieces.map(p => p.lamps.map(l => ({ u: p.u0 + l.u, y: l.y }))))
});
@@ -313,6 +314,7 @@ function mountHouseEditor(container, callbacks) {
ground.visible = false;
scene.add(ground);
const wallMaterials = WallModel.createMaterials();
const SPAANPLAAT_COLOR = wallMaterials.spaanplaatMaterial.color.getHex(); // kaal (niet geschilderd)
const floorMaterials = FloorModel.createMaterials();
const roofMaterials = RoofModel.createMaterials();
const lightingMaterials = LightingModel.createMaterials();
@@ -328,6 +330,46 @@ function mountHouseEditor(container, callbacks) {
let vloerplan = false;
const cutPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
renderer.localClippingEnabled = true;
const capMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
// Egaal witte afdekking van de snede: per wandstuk een vlak op de
// snijhoogte over de volle dikte (incl. doek/latwerk of steen aan de
// buitenzijde en de hoekdoorloop), met de openingen die door de snede
// lopen uitgespaard. cutAbs = snijhoogte (mm, absoluut).
function buildPlanCaps(cutAbs) {
const toM = 0.001, g = new THREE.Group();
const S = WallModel.STONE;
HouseModel.allWallPieces(parts).forEach(p => {
const y = cutAbs - p.place.floorTop; // snijhoogte in het stuk
if (y <= 0 || y >= WallModel.topLine(p.config).maxY) return;
const W = p.place.width, T = p.place.thickness;
// Buitenlagen op de snijhoogte (alleen buitenwanden: skinExt/normal).
let out = 0;
if (p.place.normal && p.buitenMateriaal !== 'geen') {
const tr = p.trasraam;
out = tr && y < tr.top ? (WallModel.isStoneFinish(tr.finish) ? S.THICK_MM : 0) : WallModel.claddingOffset(p.buitenMateriaal, p.stone);
}
const e = p.skinExt || {};
const xL = -W / 2 - (e.left > 0 ? e.left + out : 0), xR = W / 2 + (e.right > 0 ? e.right + out : 0);
const gaps = p.config.openings.filter(op => op.y < y && op.y + op.h > y)
.map(op => [-W / 2 + op.x, -W / 2 + op.x + op.w]).sort((a, b) => a[0] - b[0]);
const spans = [];
let cur = xL;
gaps.forEach(([a, b]) => { if (a > cur) spans.push([cur, Math.min(a, xR)]); cur = Math.max(cur, b); });
if (xR > cur) spans.push([cur, xR]);
const z0 = -T / 2, z1 = T / 2 + out;
spans.filter(([a, b]) => b - a > 1).forEach(([a, b]) => {
const m = new THREE.Mesh(new THREE.PlaneGeometry((b - a) * toM, (z1 - z0) * toM), capMaterial);
m.rotation.x = -Math.PI / 2;
m.position.set((a + b) / 2 * toM, (y - 0.5) * toM, (z0 + z1) / 2 * toM);
const holder = new THREE.Group();
holder.rotation.y = p.place.rotY;
holder.position.set(p.place.cx * toM, p.place.floorTop * toM, p.place.cz * toM);
holder.add(m);
g.add(holder);
});
});
return g;
}
let fullRender = false;
let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene);
@@ -690,13 +732,23 @@ function mountHouseEditor(container, callbacks) {
sel('hp-att-side', 'Zijde', HouseModel.SIDE_LABELS, a.side) +
sel('hp-att-align', 'Uitlijning', HouseModel.ALIGN_LABELS[axis], a.align) +
num('hp-att-offset', 'Verschuiving', a.offset || 0);
// Dakbedekking per unit; riet alleen bij een hellend dak.
const pu = parts && parts.units.find(x => x.id === u.id);
const shapeType = pu ? pu.roof.shape.type : 'plat';
const covOpts = Object.fromEntries(Object.entries(RoofModel.COVERINGS)
.filter(([, c]) => !c.sloped || shapeType !== 'plat').map(([k, c]) => [k, c.label]));
const covInfo = (k) => { const c = RoofModel.COVERINGS[k]; return `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²`; };
const bedekking = sel('hp-roof-cover', 'Bedekking', covOpts, RoofModel.coveringFor(u.roof.covering, shapeType), {
hint: shapeType === 'plat' ? 'Riet kan alleen bij een hellend dak (lessenaar/zadel)' : covInfo(RoofModel.coveringFor(u.roof.covering, shapeType)) + '; boeideel schuift mee omhoog'
});
const dak = toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) +
(u.roof.followFloor ? '' :
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', 'Verschuiving oost', u.roof.offsetX || 0) +
num('hp-roof-oz', 'Verschuiving zuid', u.roof.offsetZ || 0)) +
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' });
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' }) +
bedekking;
propsEl.innerHTML = warn + head('box', `Unit ${u.id}`, `${f.width} &times; ${f.depth} mm`) +
UI.tabs('unit', [
@@ -720,6 +772,7 @@ function mountHouseEditor(container, callbacks) {
UI.onSel('hp-att-align', v => { u.attach.align = v; }, rerender);
UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender);
UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender);
UI.onSel('hp-roof-cover', v => { u.roof.covering = v; }, rerender);
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);
@@ -744,11 +797,25 @@ function mountHouseEditor(container, callbacks) {
{ hint: `Automatisch: ${lt.endOffset} mm uit de hoek, alleen op vrije gevel van min. ${lt.minWallWidth} mm. Naast openingen: ${lt.openingOffset} mm naast elke opening.` }) +
UI.specCard('Op dit wandstuk', 'lightbulb', [['Gevellampen', piece.lamps.length, 'st.']])
: info('Gevellampen staan uit. Kies bij Woning › Verlichting het type "Gevellampen".');
const su = unitById(pu.id);
const fc = HouseModel.facadeFor(state, su, wl.side);
const ov = su.facade.walls[wl.side] || null;
const f0 = state.settings.facade;
const woningLabel = `Woning (${HouseModel.FACADE_TYPES[f0.type]}${f0.type === 'steen' ? ' ' + f0.stone : ''})`;
const gevel = sel('hw-facade', 'Type', Object.assign({ woning: woningLabel }, HouseModel.FACADE_TYPES), ov ? ov.type : 'woning',
{ hint: 'Geldt voor de hele zijde van deze unit' }) +
(ov && ov.type === 'steen' ? stonePicker('hw-facade-stone', 'Steen', fc.stone, `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik`) : '');
propsEl.innerHTML = warn + head('bricks', piece.element, piece.label) +
cols(UI.specCard('Wandstuk', 'rulers', rows), section('Gevellampen', 'lightbulb', lampen)) +
cols(UI.specCard('Wandstuk', 'rulers', rows), section('Gevelbekleding', 'grip-horizontal', gevel)) +
section('Gevellampen', 'lightbulb', lampen) +
(wl.dropped.length ? UI.warn(`${wl.dropped.length} opening(en) van deze zijde vallen in een naad en vervallen.`) : '') +
actions(button('Wand bewerken', 'pencil-square', `data-go="wall:${pu.id}:${wl.side}"`));
UI.onSel('hw-light', v => { unitById(pu.id).lighting.walls[wl.side] = v; }, rerender);
UI.onSel('hw-facade', v => {
if (v === 'woning') delete su.facade.walls[wl.side];
else su.facade.walls[wl.side] = v === 'steen' ? { type: v, stone: fc.stone } : { type: v };
}, rerender);
UI.onSel('hw-facade-stone', v => { su.facade.walls[wl.side].stone = v; }, rerender);
}
function renderIwProps(iw, warn) {
@@ -799,6 +866,13 @@ function mountHouseEditor(container, callbacks) {
// Woning-instellingen (niets geselecteerd): alles wat voor de hele woning
// geldt, in tabbladen.
// Keuzes voor de vormgeving van het trasraam: plaat of een steen.
const stoneOptions = Object.assign({}, ...WallModel.TRASRAAM_STONES.map(c => ({ [c]: `Steen ${c}` })));
const trasraamFinishes = Object.assign({ cementplaat: 'Cementplaat' }, stoneOptions);
// Steenkeuze met voorvertoning.
const stonePicker = (id, label, value, hint) => sel(id, label, stoneOptions, value, { hint }) +
`<img class="hs-stone-preview" src="${WallModel.stoneThumbUrl(value, 128)}" alt="${value}">`;
function renderHouseProps(warn) {
const st = state.settings, sp = st.spacing, r = st.roof, lt = st.lighting;
const pos = { positive: true };
@@ -822,17 +896,34 @@ function mountHouseEditor(container, callbacks) {
actions(button('Unit toevoegen', 'plus-lg', 'data-action="add-unit"'), button('Binnenwand tekenen', 'pencil', 'data-action="draw-iw"'),
button('Projectgegevens', 'card-text', 'data-action="project"')) },
{ key: 'gevel', label: 'Gevel', icon: 'bricks', html: cols(
section('Gevelbekleding', 'grip-horizontal', sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'WEO/Novalit, voorlopig voor alle buitenwanden' })) +
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Cement-gebonden plaat onderaan de gevel' }) +
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) : '')),
section('Gevelbekleding', 'grip-horizontal',
sel('hs-facade', 'Type', HouseModel.FACADE_TYPES, st.facade.type, { hint: 'Voor alle buitenwanden; per wand aan te passen (klik een wand in de plattegrond)' }) +
(st.facade.type === 'steen'
? stonePicker('hs-facade-stone', 'Steen', st.facade.stone, `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik`)
: sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'Rabat op vochtdoek en kruislat' }))) +
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Strook onderaan de gevel, onder rabat én steen (eigen steen mogelijk)' }) +
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) +
sel('hs-trasraam-finish', 'Vormgeving', trasraamFinishes, st.trasraam.finish, {
hint: WallModel.isStoneFinish(st.trasraam.finish)
? `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik`
: 'Cement-gebonden plaat'
}) +
(WallModel.isStoneFinish(st.trasraam.finish)
? `<img class="hs-stone-preview" src="${WallModel.stoneThumbUrl(st.trasraam.finish, 128)}" alt="${st.trasraam.finish}">` : '') : '')),
section('Sandwichpanelen', 'bounding-box', toggle('hs-sandwich', 'Rondom woning', st.sandwich,
{ hint: `${SandwichModel.PANEL_WIDTH_MM} × ${SandwichModel.PANEL_LENGTH_MM} × ${SandwichModel.THICK_MM} mm, vanaf bovenkant vloer ${SandwichModel.DEPTH_MM} mm naar beneden` })) +
section('Maaiveld', 'tree', num('hs-ground', 'Onder bovenkant vloer', st.groundLevel, { step: 10, hint: 'Hoogte van de tegelvloer in de 3D-weergave bij Dag/Nacht' }))) },
{ key: 'binnen', label: 'Binnen', icon: 'palette', html: cols(
section('Wanden', 'palette', toggle('hs-int-paint', 'Geschilderd', st.interiorPaint, { hint: 'Alle wanden aan de kamerzijde; egaal, zonder plaatnaden' }) +
(st.interiorPaint ? UI.color('hs-int-color', 'Verfkleur', st.interiorColor, { hint: 'Voorlopig één kleur voor de hele woning' }) : '')),
section('Vloer', 'grid-3x3', sel('hs-floor-finish', 'Afwerking', Object.fromEntries(Object.entries(FloorModel.FINISHES).map(([k, f]) => [k, f.label])), st.floorFinish, { hint: 'Alle vloeren (voorlopig één keuze)' }) +
info('Wanden uit: kale spaanplaat met plaatnaden.'))) },
{ key: 'constructie', label: 'Regelwerk', icon: 'columns-gap', html: cols(
section('Buitenwanden', 'bricks', num('hs-ext-min', 'Afstand min.', sp.exterior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-ext-max', 'Afstand max.', sp.exterior.max, { step: 10, hint: 'Hart-op-hart stijlen' })),
section('Binnenwanden', 'distribute-horizontal', num('hs-int-min', 'Afstand min.', sp.interior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-int-max', 'Afstand max.', sp.interior.max, { step: 10, hint: 'Hart-op-hart stijlen' }))) },
{ key: 'dak', label: 'Dakrand', icon: 'house', html: cols(
section('Boeideel', 'border-outer', num('hs-overhang', 'Overstek', r.overhang, { step: 10, hint: `${RoofModel.OVERHANG_MIN} – ${RoofModel.OVERHANG_MAX} mm` }) + num('hs-fascia-h', 'Hoogte', r.fasciaHeight, { step: 10 })),
section('Boeideel', 'border-outer', num('hs-overhang', 'Overstek', r.overhang, { step: 10, hint: `${RoofModel.OVERHANG_MIN} – ${RoofModel.OVERHANG_MAX} mm` }) + num('hs-fascia-h', 'Hoogte', r.fasciaHeight, { step: 10 }) +
sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor, { hint: 'Egale kleur' })),
section('Daktrim', 'border-top', num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }))) },
{ key: 'verlichting', label: 'Verlichting', icon: 'lightbulb', html:
cols(section('Buiten', 'sun', sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode)),
@@ -844,9 +935,15 @@ function mountHouseEditor(container, callbacks) {
]);
UI.onSel('hs-cladding', v => { st.cladding = v; }, rerender);
UI.onSel('hs-facade', v => { st.facade.type = v; }, rerender);
UI.onColor('hs-int-color', v => { st.interiorColor = v; }, after);
UI.onToggle('hs-int-paint', v => { st.interiorPaint = v; }, rerender);
UI.onSel('hs-floor-finish', v => { st.floorFinish = v; }, rerender);
UI.onSel('hs-facade-stone', v => { st.facade.stone = v; }, rerender);
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
UI.onNum('hs-trasraam-h', v => { st.trasraam.height = v; }, after, pos);
UI.onSel('hs-trasraam-finish', v => { st.trasraam.finish = v; }, rerender);
UI.onNum('hs-ground', v => { if (v >= 0) st.groundLevel = v; }, after);
UI.onNum('hs-ext-min', v => { sp.exterior.min = v; }, after, pos);
UI.onNum('hs-ext-max', v => { sp.exterior.max = v; }, after, pos);
@@ -854,6 +951,7 @@ function mountHouseEditor(container, callbacks) {
UI.onNum('hs-int-max', v => { sp.interior.max = v; }, after, pos);
UI.onNum('hs-overhang', v => { r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); }, after, pos);
UI.onNum('hs-fascia-h', v => { r.fasciaHeight = v; }, after, pos);
UI.onSel('hs-fascia-color', v => { r.fasciaColor = v; }, rerender);
UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos);
UI.onNum('hs-spot-min', v => { r.spotMin = v; }, after, pos);
UI.onNum('hs-spot-max', v => { r.spotMax = v; }, after, pos);
@@ -917,6 +1015,8 @@ function mountHouseEditor(container, callbacks) {
// --- Alles opnieuw doorrekenen ---
function refresh(opts) {
// Binnenzijde geschilderd: de (gedeelde) spaanplaat krijgt de verfkleur.
wallMaterials.spaanplaatMaterial.color.set(state.settings.interiorPaint ? state.settings.interiorColor : SPAANPLAAT_COLOR);
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, beplatingVisible, lampenVisible });
const totals = HouseModel.computeTotals(parts);
@@ -944,6 +1044,7 @@ function mountHouseEditor(container, callbacks) {
});
houseGroup.add(p.group);
});
if (vloerplan) houseGroup.add(buildPlanCaps(cutPlane.constant / toM));
if (dakVisible && !vloerplan) {
parts.units.forEach(u => {
u.roof.group.position.set(u.roof.rect.cx * toM, u.roof.base * toM, u.roof.rect.cz * toM);
@@ -960,7 +1061,8 @@ function mountHouseEditor(container, callbacks) {
const cx = (bx0 + bx1) / 2 * toM, cz = (bz0 + bz1) / 2 * toM;
grid.position.set(cx, 0, cz);
// Maaiveld: settings.groundLevel onder het loopvlak (bovenkant vloer).
const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel) * toM;
// 2 mm extra lager: bij maaiveld 0 niet in hetzelfde vlak als de OSB (flikkeren).
const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel - 2) * toM;
skyEnv.fitTo(houseGroup, groundY);
ground.position.set(cx, groundY - 0.002, cz);
applyRenderMode();
+5 -3
View File
@@ -122,17 +122,19 @@ window.LightingModel = (() => {
if (p.baseY) return; // opbouw boven een lager dak: geen gevellampen
const W = p.place.width;
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal);
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal, p.stone);
const normal = { x: p.place.normal.x, z: p.place.normal.y }; // place.normal is {x, y} (plattegrond)
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z';
const perp = axis === 'x' ? 'z' : 'x';
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
// Binnenhoek-doorloop (negatieve skinExt): de gevel begint pas daarna.
const tL = Math.max(0, -((p.skinExt || {}).left || 0)), tR = Math.max(0, -((p.skinExt || {}).right || 0));
const q0 = UnitModel.localToWorld(p.place, -W / 2 + tL, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2 - tR, T / 2);
// Gevel loopt tot de afgewerkte hoek, zodat endOffset altijd vanaf de
// zichtbare hoek telt: elk uiteinde + afwerking (latwerk + bekleding van
// de haakse wand die eroverheen loopt), en waar dit stuk de kopse kant
// van een doorlopende wand bedekt (skinExt) ook nog die wanddikte.
// Binnenhoeken en naden: zie de voetafdrukken in 3 en de overlap in 2.
const c = WallModel.claddingOffset(p.buitenMateriaal), e = p.skinExt || {};
const c = WallModel.claddingOffset(p.buitenMateriaal, p.stone), e = p.skinExt || {};
const extL = (e.left > 0 ? e.left : 0) + c, extR = (e.right > 0 ? e.right : 0) + c;
const up = q1[axis] >= q0[axis];
// Voetafdruk van dit stuk incl. afwerking (om bedekte delen van andere
+82 -18
View File
@@ -49,9 +49,57 @@ window.RoofModel = (() => {
fasciaHeight: 300, // mm
spotMin: 800, // mm h.o.h. boeideel-spots
spotMax: 1200,
trimHeight: 80 // mm
trimHeight: 80, // mm
fasciaColor: 'antraciet' // FASCIA_COLORS
};
// Kleur van het boeideel (egaal).
const FASCIA_COLORS = {
wit: { label: 'Wit', hex: 0xf2f2f0 },
lichtgrijs: { label: 'Lichtgrijs', hex: 0xbfc3c6 },
middelgrijs: { label: 'Middelgrijs', hex: 0x7d8286 },
antraciet: { label: 'Antraciet', hex: 0x3a3f44 },
zwart: { label: 'Zwart', hex: 0x151617 }
};
// Dakbedekking per unit (roof.covering). Riet e.d. alleen bij een hellend
// dak (lessenaar/zadel); plat valt terug op dakleer. thickMM telt mee in de
// dakdikte (vThick/topAt), dus boeideel/daktrim en een opbouw ernaast
// schuiven vanzelf mee omhoog. Texturen: assets/textures/dak/, naadloos;
// de stengels lopen langs de helling.
// Riet: één keuze per textuur assets/textures/dak/riet_<nr>.jpg (nummer
// hier toevoegen); zelfde opbouw (180 mm, 3,8 kg/m2).
const RIET = { thickMM: 180, kgM2: 3.8, tileMM: 1000, nrs: ['001', '002', '003'] };
const COVERINGS = Object.assign({
dakleer: { label: 'Dakleer (EPDM)', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
}, ...RIET.nrs.map(nr => ({
['riet_' + nr]: { label: 'Riet ' + nr, thickMM: RIET.thickMM, kgM2: RIET.kgM2, file: `assets/textures/dak/riet_${nr}.jpg`, tileMM: RIET.tileMM, sloped: true }
})));
// Werkelijke bedekking voor een dakvorm (riet niet op een plat dak).
function coveringFor(key, shapeType) {
const c = COVERINGS[key];
return c && !(c.sloped && shapeType === 'plat') ? key : 'dakleer';
}
const coveringMaterials = {};
function coveringMaterial(key) {
const c = COVERINGS[key];
if (!c || !c.file) return null;
if (coveringMaterials[key]) return coveringMaterials[key];
const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(c.file, (tex) => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1000 / c.tileMM, 1000 / c.tileMM);
tex.anisotropy = 8;
mat.map = tex;
mat.color.set(0xffffff);
mat.needsUpdate = true;
});
}
coveringMaterials[key] = mat;
return mat;
}
function clone(obj) { return JSON.parse(JSON.stringify(obj)); }
// Dak-state per unit: volgt standaard de vloermaat (followFloor), maar mag
@@ -60,11 +108,12 @@ 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, deck };
return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, covering: 'dakleer', deck };
}
function thickness(calcData) {
return calcData.joistH + calcData.underlayThick + DAKLEER_MM;
// Dikte dek + bedekking (mm, haaks op het dakvlak).
function thickness(calcData, covering) {
return calcData.joistH + calcData.underlayThick + (COVERINGS[covering] || COVERINGS.dakleer).thickMM;
}
function createMaterials() {
@@ -106,20 +155,31 @@ window.RoofModel = (() => {
hole.absarc(o.x * toM, o.y * toM - dM / 2, o.r * toM, 0, Math.PI * 2, false);
shape.holes.push(hole);
});
const leer = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: DAKLEER_MM * toM, bevelEnabled: false }), m.dakleerMaterial);
// Andere bedekking (opts.covering, bijv. riet): een laag van thickMM met
// de textuur (vorm-coördinaten in m = UV; langs de helling = v).
const cov = COVERINGS[opts.covering] || COVERINGS.dakleer;
const covMat = coveringMaterial(opts.covering) || m.dakleerMaterial;
const leer = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: cov.thickMM * toM, bevelEnabled: false }), covMat);
leer.rotation.x = Math.PI / 2;
leer.position.y = deckH + DAKLEER_MM * toM;
leer.position.y = deckH + cov.thickMM * toM;
leer.castShadow = cov.thickMM > DAKLEER_MM;
leer.visible = opts.dakleerVisible !== false;
group.add(leer);
return group;
}
// Specificaties: die van het dek (FloorModel) + dakleer.
function computeSpecs(calcData, resolvedOpenings) {
// Specificaties: die van het dek (FloorModel) + de bedekking (dakleer of
// bijv. riet: coveringM2/Kg, dakleer dan 0).
function computeSpecs(calcData, resolvedOpenings, covering) {
const deck = FloorModel.computeSpecs(calcData, resolvedOpenings);
const dakleerM2 = deck.osbAreaM2; // zelfde vlak minus doorvoeren
const dakleerKg = dakleerM2 * DENSITY_DAKLEER_KGM2;
return Object.assign({}, deck, { dakleerM2, dakleerKg, totalKg: deck.totalKg + dakleerKg });
const areaM2 = deck.osbAreaM2; // zelfde vlak minus doorvoeren
const isLeer = !COVERINGS[covering] || covering === 'dakleer';
const kg = areaM2 * (COVERINGS[covering] || COVERINGS.dakleer).kgM2;
return Object.assign({}, deck, {
dakleerM2: isLeer ? areaM2 : 0, dakleerKg: isLeer ? kg : 0,
rietM2: isLeer ? 0 : areaM2, rietKg: isLeer ? 0 : kg,
totalKg: deck.totalKg + kg
});
}
// ------------------------------------------------------------
@@ -193,6 +253,7 @@ window.RoofModel = (() => {
const cuts = [a0, ...kinks, a1];
const flat = FloorModel.computeFromState(roofState.deck, rims);
const H = (p) => profileHeight(shape.profile, p);
const covering = coveringFor(roofState.covering, shape.type);
// Doorvoeren: positie in de plattegrond (zoals in de dak-editor) -> vlak.
const holes = flat.resolvedOpenings.filter(o => !o.removed).map(o => {
@@ -223,10 +284,10 @@ window.RoofModel = (() => {
}
const mid = (p0 + p1) / 2;
return Object.assign(plane, {
data, specs: computeSpecs(data.calcData, data.resolvedOpenings),
data, specs: computeSpecs(data.calcData, data.resolvedOpenings, covering),
cx: alongX ? mid - xc : 0, cz: alongX ? 0 : mid - zc, y: (h0 + h1) / 2,
// Verticale dikte (dek + dakleer) van dit vlak.
vThick: thickness(data.calcData) / cos
// Verticale dikte (dek + bedekking) van dit vlak.
vThick: thickness(data.calcData, covering) / cos
});
});
@@ -239,7 +300,7 @@ window.RoofModel = (() => {
return H(q) + t;
};
return {
shape, planes, specs: sumSpecs(planes.map(pl => pl.specs)),
shape, covering, planes, specs: sumSpecs(planes.map(pl => pl.specs)),
undersideAt, topAt,
ridges: kinks.map(value => ({ axis: shape.axis, value })),
maxTop: Math.max(...planes.map(pl => Math.max(pl.h0, pl.h1) + pl.vThick))
@@ -272,7 +333,7 @@ window.RoofModel = (() => {
0, 1 / c, sn, 0,
0, 0, c, 0,
0, 0, 0, 1);
shear.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, opts));
shear.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, Object.assign({ covering: unitRoof.covering }, opts)));
g.add(shear);
group.add(g);
});
@@ -412,7 +473,7 @@ window.RoofModel = (() => {
const top = Math.max(...segments.map(sg => Math.max(sg.topAt(sg.a), sg.topAt(sg.b))));
return {
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaColor: s.fasciaColor,
top, edgeAbove: EDGE_ABOVE_MM,
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
@@ -461,6 +522,8 @@ window.RoofModel = (() => {
const toM = 0.001;
const group = new THREE.Group();
if (!edge) return group;
// Het boeidelen-materiaal wordt gedeeld: kleur volgens de instelling.
materials.fasciaMaterial.color.setHex((FASCIA_COLORS[edge.fasciaColor] || FASCIA_COLORS.antraciet).hex);
edge.segments.forEach(sg => {
let polys = [[sg.a, sg.b, sg.outerB, sg.outerA]];
sg.ridges.forEach(r => {
@@ -491,11 +554,12 @@ window.RoofModel = (() => {
const s = Object.assign({}, DEFAULT_SETTINGS, settings || {});
s.overhang = Math.min(OVERHANG_MAX, Math.max(OVERHANG_MIN, s.overhang));
delete s.edge; delete s.spots; // oud (v0.3.0): nu per unit
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
return s;
}
return {
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
DAKLEER_MM, EDGE_ABOVE_MM, RIET, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor,
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D,
computeEdge, buildEdge3D, normalizeSettings
+14 -5
View File
@@ -140,14 +140,17 @@ window.UnitModel = (() => {
// Resterende stukken wand; een stuk dat aan een naad grenst loopt de
// naad in door met de doorloop (ext) van die kant.
// [u0, u1, inL, inR]: inL/inR = hoever het stuk aan die kant de naad
// in loopt (binnenhoek dicht, zie computeSeams).
const pieces = [];
let cursor = 0;
let cursor = 0, inL = 0;
rawU.forEach(([a, b, e0, e1]) => {
const end = Math.min(b, a + e0);
if (end - cursor >= MIN_PIECE_MM) pieces.push([cursor, end]);
if (end - cursor >= MIN_PIECE_MM) pieces.push([cursor, end, inL, end - a]);
cursor = Math.max(cursor, b - e1);
inL = b - cursor;
});
if (width - cursor >= MIN_PIECE_MM) pieces.push([cursor, width]);
if (width - cursor >= MIN_PIECE_MM) pieces.push([cursor, width, inL, 0]);
// Werkelijk vervallende zones (voor de weergave in de wand-editor).
const cutsU = rawU.map(([a, b, e0, e1]) => [a + e0, b - e1]).filter(([a, b]) => b - a > EPS);
@@ -198,7 +201,7 @@ window.UnitModel = (() => {
const multi = wallLayout.pieces.length > 1;
const used = new Set();
const pieces = wallLayout.pieces.map(([a, b], i) => {
const pieces = wallLayout.pieces.map(([a, b, inL, inR], i) => {
const openings = fullConfig.openings
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS)
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: op.x - a }; });
@@ -211,7 +214,13 @@ window.UnitModel = (() => {
return {
suffix: multi ? String.fromCharCode(97 + i) : '', // a, b, ...
u0: a, u1: b, lxCenter: -W / 2 + (a + b) / 2,
skinExt: { left: a < EPS ? wallLayout.skinExt.left : 0, right: b > W - EPS ? wallLayout.skinExt.right : 0 },
// Loopt het stuk een naad in (binnenhoek), dan ligt dat deel binnen
// de wand van de buur-unit: buitenlagen daar niet (negatieve
// doorloop, zie WallModel.cornerExt) en een binnenplaat op de kopse kant.
skinExt: {
left: inL > EPS ? -inL : a < EPS ? wallLayout.skinExt.left : 0,
right: inR > EPS ? -inR : b > W - EPS ? wallLayout.skinExt.right : 0
},
config, frame: WallModel.computeFrame(config)
};
});
+234 -34
View File
@@ -56,8 +56,10 @@ window.WallModel = (() => {
// Hoe ver de afgewerkte gevel buiten het wandvlak ligt (mm): bij vochtdoek
// de kruislat (2 lagen) + de gevelbekleding. Gebruikt o.a. voor de
// positie van gevellampen.
function claddingOffset(buitenMateriaal) {
return buitenMateriaal === 'vochtdoek' ? 2 * BATTEN.thick + CLADDING_THICK_MM : 0;
// Bij cementplaat met steen (stone): de steenstrip.
function claddingOffset(buitenMateriaal, stone) {
if (buitenMateriaal === 'vochtdoek') return 2 * BATTEN.thick + CLADDING_THICK_MM;
return buitenMateriaal === 'cementplaat' && stone ? STONE.THICK_MM : 0;
}
// Trasraam: strook cement-gebonden plaat (montage-achterkant voor de
@@ -69,9 +71,50 @@ window.WallModel = (() => {
// Geeft null als hij uit staat. Alleen van toepassing op vochtdoek-wanden
// (cement-gebonden buitenzijde is al plaat).
const TRASRAAM_DEFAULT_MM = 350;
function trasraamFor(heightMM) {
// finish: 'cementplaat' of een steencode uit TRASRAAM_STONES (steenstrip
// op een isolatieblok, zie STONE).
function trasraamFor(heightMM, finish) {
if (!(heightMM > 0)) return null;
return { bottom: 0, top: heightMM };
return { bottom: 0, top: heightMM, finish: finish || 'cementplaat' };
}
// Steen (trasraam en steengevel): isolatieblok met steenstrip ervoor,
// samen THICK_MM dik (vóór het doek resp. de cementplaat). Texturen: assets/textures/buitenmuur/
// "<code> - Witte voeg.jpg", 2062 x 1125 px op 2 mm/px (steen + voeg
// ~62 mm per laag), naadloos herhalend. Voorvertoning in thumb/<px>/ (.png).
// JOINT_MM: hart van de onderste voeg boven de onderrand van de textuur
// (gemeten: 1-2 px); COURSE_MM: steen + voeg (31 px). De bovenrand van de
// textuur snijdt door een steen (1125 px is geen veelvoud van 31).
const STONE = { THICK_MM: 110, STRIP_MM: 20, TILE_W_MM: 4124, TILE_H_MM: 2250, JOINT_MM: 3, COURSE_MM: 62, DIR: 'assets/textures/buitenmuur/' };
const TRASRAAM_STONES = ('K490 K518 K560 K662 K706 K739 K752 K764 K773 K911P K911 K914 K915 K919 K923P K923 K941P K941 K947 K948 K960 K991 ' +
'R100 R116 R140 R200 R206 R214 R216 R220 R227 R228 R240 R268 R286 R287 R303 R307 R332 R335 R343 R356 R381 R382 R384 R385 R386 R391 ' +
'R400 R401 R435 R436 R440 R480 R483 R484 R487 R488 R489 R500 R510 R518 R535 R540 R550 R555 R560 R561 R563 R564 R565 R572DF14_64 R572DF ' +
'R580 R581 R582 R658 R659 R663 R664 R665 R669 R680 R682 R684 R685 R686 R687 R688 R689 R690 R691 R692 R-693 R694 R695 R696 R697 R698 ' +
'R700 R714 R715 R717 R719 R720 R721 R725 R726 R727 R729 R730 R731 R732 R733 R734 R735 R736 R737 R738 R739 R740 R742 R743 R744 R745X737 ' +
'R746 R747 R748 R748X764X749 R749 R750 R751 R751X759 R752 R754 R756 R757 R758 R759 R760 R761 R762 R763 R764 R764X756 R766 R767 R768 ' +
'R769 R770 R771 R772 R773 R775 R777 R788 R800 R835 R840 R911P R911 R913 R916 R917 R918 R921 R923P R931 R932 R941P R941 R942 R943 R944 ' +
'R960DF14_70 R960DF R961 R970 R980 R985 R991').split(' ');
const isStoneFinish = (finish) => TRASRAAM_STONES.includes(finish);
const stoneTextureUrl = (code) => STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg');
const stoneThumbUrl = (code, px) => STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png');
// Eén materiaal per steen (gedeeld, pas geladen als hij gebruikt wordt).
// UV's van de steenblokken zijn in meters, dus repeat = 1/tegel.
const stoneMaterials = {};
function stoneMaterial(code) {
if (stoneMaterials[code]) return stoneMaterials[code];
const mat = new THREE.MeshStandardMaterial({ color: 0xb0a090, roughness: 0.9 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(stoneTextureUrl(code), (tex) => {
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(1000 / STONE.TILE_W_MM, 1000 / STONE.TILE_H_MM);
tex.anisotropy = 8;
mat.map = tex;
mat.color.set(0xffffff);
mat.needsUpdate = true;
});
}
stoneMaterials[code] = mat;
return mat;
}
// Hoekdoorloop van de buitenlagen. Een wand die tussen twee doorlopende
// wanden past (unit-hoek) moet met zijn buitenlagen ook de kopse kant
@@ -79,9 +122,13 @@ window.WallModel = (() => {
// right } = dikte van die wand (mm, 0 = vrij uiteinde). Elke laag loopt
// daarbovenop nog door tot zijn eigen buitenvlak, zodat de hoek dicht is.
// depth = afstand van het buitenvlak van de laag tot het wandvlak (mm).
// Negatief (binnenhoek): het stuk loopt zoveel de naad in, binnen de wand
// van de buur-unit. Geen doorloop; de buitenlagen krijgen daar een
// uitsparing (build3D: trim, tot opts.trimTop).
function cornerExt(skinExt, depth) {
const e = skinExt || {};
return { left: e.left > 0 ? e.left + depth : 0, right: e.right > 0 ? e.right + depth : 0 };
const one = (v) => v > 0 ? v + depth : 0;
return { left: one(e.left), right: one(e.right) };
}
// Onderkant van vochtdoek en latwerk (wand-coördinaten, mm).
@@ -479,13 +526,13 @@ window.WallModel = (() => {
// Stukken van een horizontale lijn op hoogte y (mm) die OP de beplating
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
// uitsparingen. Geeft [[x0, x1], ...] terug (mm, t.o.v. het wandmidden).
function skinSegmentsAtY(CONFIG, y) {
function skinSegmentsAtY(CONFIG, y, extraHoles) {
if (y <= 0) return [];
// Deel van de breedte waar de daklijn boven y ligt.
const range = topLine(CONFIG).rangeAbove(y);
if (!range) return [];
let segments = [range];
sheathingHoles(CONFIG).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
sheathingHoles(CONFIG).concat(extraHoles || []).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
segments = segments.flatMap(([a, b]) => {
if (h.hx1 <= a || h.hx0 >= b) return [[a, b]];
const out = [];
@@ -513,7 +560,9 @@ window.WallModel = (() => {
// hoger begint (opbouw, blootliggende naadwand) volgt zo hetzelfde raster.
// skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het
// blootliggende deel van een naadwand boven het dak van de andere unit).
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) {
// extraHoles (optioneel, mm, zoals sheathingHoles): extra uitsparingen
// (binnenhoek-doorloop, zie build3D).
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom, extraHoles) {
const y0 = Math.max(skinStartY(trasraam), skinFrom || 0); // boven het trasraam
const ext = cornerExt(skinExt, 2 * BATTEN.thick); // over de hoekstijlen van de doorlopende wand
const xL = -CONFIG.wallWidth / 2 - ext.left, xR = CONFIG.wallWidth / 2 + ext.right;
@@ -521,7 +570,7 @@ window.WallModel = (() => {
const isEnd = facade ? facade.isEnd : true;
const halfW = CONFIG.wallWidth / 2;
const tl = topLine(CONFIG), topYAt = tl.at;
const holes = sheathingHoles(CONFIG);
const holes = sheathingHoles(CONFIG).concat(extraHoles || []);
const w = BATTEN.width, sp = BATTEN.spacing;
const top = (cladding === 'verticaal') ? 'horizontaal' : 'verticaal';
@@ -533,7 +582,7 @@ window.WallModel = (() => {
yStart = g + Math.max(0, Math.ceil((yStart - g) / sp - 1e-9)) * sp;
}
for (let y = yStart; y < tl.maxY - w / 2; y += sp) {
skinSegmentsAtY(CONFIG, y).forEach(([x0, x1]) => {
skinSegmentsAtY(CONFIG, y, extraHoles).forEach(([x0, x1]) => {
if (x0 <= -halfW + 0.5) x0 = xL;
if (x1 >= halfW - 0.5) x1 = xR;
if (x1 - x0 > w) horizontaal.push({ x0, x1, y });
@@ -627,6 +676,7 @@ window.WallModel = (() => {
? new THREE.MeshStandardMaterial({ color: 0xffffff, map: doekMap, roughness: 0.7, side: THREE.DoubleSide })
: new THREE.MeshStandardMaterial({ color: DOEK_COLOR, roughness: 0.7, side: THREE.DoubleSide }),
cementplaatMaterial: new THREE.MeshStandardMaterial({ color: 0x808285, roughness: 0.8, side: THREE.DoubleSide }),
isolatieMaterial: new THREE.MeshStandardMaterial({ color: 0xf4f4f2, roughness: 0.95 }),
battenMaterial: new THREE.MeshStandardMaterial({ color: 0xd9b77e, roughness: 0.7 })
};
}
@@ -644,10 +694,14 @@ window.WallModel = (() => {
const spaanplaatVisible = opts.spaanplaatVisible !== false;
const buitenVisible = opts.buitenVisible !== false;
const cladding = opts.cladding || 'horizontaal';
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
// Trasraam: bij rabat (vochtdoek) en bij een steengevel (cementplaat + stone).
const trasraam = buitenMateriaal === 'vochtdoek' || (buitenMateriaal === 'cementplaat' && isStoneFinish(opts.stone)) ? (opts.trasraam || null) : null;
const skinExt = opts.skinExt || null;
const facade = opts.facade || null;
const skinFrom = opts.skinFrom > 0 ? opts.skinFrom : 0; // buitenafwerking vanaf (mm), zie computeBattens
const painted = !!opts.painted; // spaanplaat geschilderd: zonder plaatnaden
// stone: steencode (TRASRAAM_STONES) voor steenstrips op een cementplaat-gevel.
const stone = buitenMateriaal === 'cementplaat' && isStoneFinish(opts.stone) ? opts.stone : null;
// spaanplaatBelow: onder skinFrom aan de buitenzijde spaanplaat (naadwand
// geheel in één unit: onder het dak van de andere unit is het een binnenwand).
const spaanplaatBelow = !!opts.spaanplaatBelow && skinFrom > 0 && buitenMateriaal === 'vochtdoek';
@@ -680,6 +734,8 @@ window.WallModel = (() => {
let spaanplaatBuitenVolumeM3 = 0;
let battenLengthM = 0, battenVolumeM3 = 0, battenCount = 0; // kruislat (alleen vochtdoek)
let trasraamAreaM2 = 0, trasraamVolumeM3 = 0; // cement-gebonden plaat onderaan (alleen vochtdoek)
let stoneAreaM2 = 0, stoneIsolatieM3 = 0; // steengevel (cementplaat + stone): strip + isolatieblok
let trasraamStoneAreaM2 = 0, trasraamIsolatieM3 = 0; // stenen trasraam: steenstrip (gevelvlak) + isolatieblok
let horizBeamCount = 0; // onderregel-segmenten + topbalk + dorpels per opening
function createExtrudedBeam(shape, depth, positionZ) {
@@ -709,6 +765,16 @@ window.WallModel = (() => {
const tl = topLine(CONFIG, toM);
const startXM = -halfW;
const topYAt = tl.at;
// Gevel doorgetrokken tot onder de dakrand (opts.skinAbove, mm): de
// buitenlagen (doek, latwerk, steen, cementplaat) volgen een bovenrand
// die zoveel hoger ligt, met dezelfde helling; regelwerk en binnenzijde
// blijven op de wandhoogte.
const skinAbove = opts.skinAbove > 0 ? opts.skinAbove : 0;
const CONFIG_OUT = skinAbove ? Object.assign({}, CONFIG, {
wallHeightLeft: CONFIG.wallHeightLeft + skinAbove, wallHeightRight: CONFIG.wallHeightRight + skinAbove,
wallPeak: CONFIG.wallPeak ? { x: CONFIG.wallPeak.x, h: CONFIG.wallPeak.h + skinAbove } : null
}) : CONFIG;
const tlOut = skinAbove ? topLine(CONFIG_OUT, toM) : tl;
// Onderkant bovenregel van xLeft naar xRight (incl. verstek bij de nok).
const underPts = tl.underPts;
@@ -858,6 +924,89 @@ window.WallModel = (() => {
const holeExtents = sheathingHoles(CONFIG).map(hole => ({
hx0: hole.hx0 * toM, hx1: hole.hx1 * toM, hy0: hole.hy0 * toM, hy1: hole.hy1 * toM
}));
// Binnenhoek-doorloop (negatieve skinExt): dat stuk zit binnen de wand
// en het dak van de buur-unit; daar geen buitenlagen, tot opts.trimTop
// ({ left, right }, mm; bovenkant dak van de buur). Daarboven loopt de
// gevel gewoon door. Als uitsparing (zoals een opening) in alle
// buitenlagen.
const trimL = skinExt && skinExt.left < 0 ? -skinExt.left * toM : 0;
const trimR = skinExt && skinExt.right < 0 ? -skinExt.right * toM : 0;
const trimTop = opts.trimTop || {};
const topOf = (v) => (v > 0 ? v : 1e7) * toM;
const trimHoles = [];
if (trimL > 0) trimHoles.push({ hx0: -halfW - 1, hx1: -halfW + trimL, hy0: -1, hy1: topOf(trimTop.left) });
if (trimR > 0) trimHoles.push({ hx0: halfW - trimR, hx1: halfW + 1, hy0: -1, hy1: topOf(trimTop.right) });
const outerHoles = holeExtents.concat(trimHoles);
const trimHolesMM = trimHoles.map(h => ({ hx0: h.hx0 / toM, hx1: h.hx1 / toM, hy0: h.hy0 / toM, hy1: h.hy1 / toM }));
// Steenblok (trasraam en steengevel): isolatieblok met een steenstrip
// ervoor, samen STONE.THICK_MM dik, vóór het wandvlak (doek resp.
// cementplaat). Van hoogte y0 tot yCap (m) of tot de daklijn, met de
// openingen uitgespaard. Hoeken: doorloop zoals de andere buitenlagen
// (cornerExt); het laatste stuk van die doorloop is de hoek zelf,
// waarvan de kopse kant zichtbaar is: dat stuk is over de volle dikte
// steen. UV's in meters in gevel-coördinaten, zodat het metselwerk
// over wandstukken en naden doorloopt; per vlakrichting gezet (ook de
// kopse kanten horizontaal, niet de standaard van ExtrudeGeometry).
// Verticaal: anchor = { y (m), down } — op hoogte y ligt een voeg;
// de gevel telt vanaf daar omhoog (hele stenen boven de naad), het
// trasraam vanaf zijn bovenkant omlaag (hele stenen onder de naad).
// Geeft { areaM2, isoM3 }.
function stoneBlock(code, y0, yCap, anchor) {
// v (m) op hoogte y: omhoog vanaf de onderste voeg van de textuur;
// omlaag vanaf een voeg een aantal hele lagen hoger in de textuur,
// zodat de strook eronder nog binnen de textuur valt.
const jointM = STONE.JOINT_MM * toM, courseM = STONE.COURSE_MM * toM;
const vOff = anchor.down
? jointM + Math.ceil(Math.max(0, anchor.y - y0) / courseM) * courseM - anchor.y
: jointM - anchor.y;
const setUV = (geo) => {
const pos = geo.attributes.position, nrm = geo.attributes.normal, uv = geo.attributes.uv;
for (let k = 0; k < pos.count; k++) {
const x = pos.getX(k) + uvShiftM, y = pos.getY(k) + vOff, z = pos.getZ(k);
const nx = Math.abs(nrm.getX(k)), ny = Math.abs(nrm.getY(k));
if (nx > 0.5) uv.setXY(k, z, y); else if (ny > 0.5) uv.setXY(k, x, z); else uv.setXY(k, x, y);
}
uv.needsUpdate = true;
return geo;
};
const S = STONE;
const zB = (maxDepth / 2) + skinGapM + sThickM + skinGapM;
const tIso = (S.THICK_MM - S.STRIP_MM) * toM, tAll = S.THICK_MM * toM;
const ext = extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM + S.THICK_MM);
const xL = -halfW - ext.left, xR = halfW + ext.right;
const capL = ext.left > 0 ? xL + tAll : xL, capR = ext.right > 0 ? xR - tAll : xR;
const topClamped = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x)));
const rects = outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 }));
const shapesIn = (a, b) => {
if (b - a <= edgeEps) return [];
const capped = yCap === Infinity;
const pieces = columnMinusRects(a, b, y0, capped ? topClamped : (x) => Math.min(yCap, topClamped(x)), rects,
capped ? { extraXs: tlOut.kinks } : { yCap, extraXs: tlOut.kinks.concat(tlOut.crossings(yCap)) });
return (pieces || []).map(piece => {
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(q => new THREE.Vector2(q.x, q.y)))));
return shape;
});
};
const add = (shapes, depth, z, mat) => {
if (!shapes.length) return;
const mesh = new THREE.Mesh(setUV(new THREE.ExtrudeGeometry(shapes, { steps: 1, depth, bevelEnabled: false })), mat);
mesh.position.set(0, 0, z);
mesh.visible = buitenVisible;
mesh.castShadow = true;
wallGroup3D.add(mesh);
};
const stoneMat = stoneMaterial(code);
const isoMat = opts.materials.isolatieMaterial || cementplaatMaterial;
const mid = shapesIn(capL, capR);
const caps = shapesIn(xL, capL).concat(shapesIn(capR, xR));
add(mid, tIso, zB, isoMat);
add(mid, tAll - tIso, zB + tIso, stoneMat);
add(caps, tAll, zB, stoneMat);
const area = (list) => list.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
return { areaM2: area(mid) + area(caps), isoM3: area(mid) * tIso };
}
// side: 'front' (buitenzijde/zuid) of 'back' (binnenzijde/noord).
// range (optioneel, m): { y0, y1, zExtra } = alleen de strook daartussen
@@ -867,11 +1016,15 @@ window.WallModel = (() => {
// komt er een losse strook zOut verder naar buiten (laag in de
// beplatingsruimte, anders zouden de hoekstijlen erdoor steken).
function buildVerticalBoardSkin(material, side, visible, range, ext) {
const bounds = boardBoundaries.slice();
// Geschilderde spaanplaat (opts.painted): één egaal vlak, geen naden.
const seamless = painted && material === spaanplaatMaterial;
const bounds = seamless ? [boardBoundaries[0], boardBoundaries[boardBoundaries.length - 1]] : boardBoundaries.slice();
if (ext && !ext.zOut) { bounds[0] -= ext.left; bounds[bounds.length - 1] += ext.right; }
const yB = range ? range.y0 : 0;
const yCap = range ? range.y1 : Infinity;
const skinTopAt = (x) => Math.min(yCap, topYAt(x));
const TL = side === 'front' && !range ? tlOut : tl; // buitenplaat: tot onder de dakrand
const HX = side === 'front' ? outerHoles : holeExtents;
const skinTopAt = (x) => Math.min(yCap, TL.at(x));
let areaM2 = 0, volumeM3 = 0;
const z = (side === 'front' ? (maxDepth / 2) + skinGapM : -(maxDepth / 2) - sThickM - skinGapM) + ((range && range.zExtra) || 0);
// Duidelijke naad tussen platen: dunne rand rond elke plaat, 50%
@@ -894,8 +1047,8 @@ window.WallModel = (() => {
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
{ yCap: range ? yCap : undefined, extraXs: range ? tl.kinks.concat(tl.crossings(yCap)) : tl.kinks });
HX.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
{ yCap: range ? yCap : undefined, extraXs: range ? TL.kinks.concat(TL.crossings(yCap)) : TL.kinks });
let boardShapes;
if (pieces) {
boardShapes = pieces.map(piece => {
@@ -910,7 +1063,7 @@ window.WallModel = (() => {
boardShape.moveTo(ex0, yB);
boardShape.lineTo(ex1, yB);
boardShape.lineTo(ex1, skinTopAt(ex1));
tl.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
TL.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
boardShape.lineTo(ex0, skinTopAt(ex0));
boardShape.closePath();
holeExtents.forEach(h => {
@@ -941,11 +1094,13 @@ window.WallModel = (() => {
board.visible = visible;
wallGroup3D.add(board);
const edgesGeo = new THREE.EdgesGeometry(boardGeo);
const edgeLines = new THREE.LineSegments(edgesGeo, edgeMaterial);
edgeLines.position.set(0, 0, z);
edgeLines.visible = visible;
wallGroup3D.add(edgeLines);
if (!seamless) {
const edgesGeo = new THREE.EdgesGeometry(boardGeo);
const edgeLines = new THREE.LineSegments(edgesGeo, edgeMaterial);
edgeLines.position.set(0, 0, z);
edgeLines.visible = visible;
wallGroup3D.add(edgeLines);
}
areaM2 += boardAreaM2;
volumeM3 += boardAreaM2 * sThickM;
@@ -975,6 +1130,18 @@ window.WallModel = (() => {
// Binnenzijde (noord): altijd spaanplaat.
const spaanplaatResult = buildVerticalBoardSkin(spaanplaatMaterial, 'back', spaanplaatVisible);
// Kopse kant van een binnenhoek-doorloop (trimL/trimR): die staat open
// naar de kamer van de buur-unit; binnenplaat erop (over regelwerk +
// beplating), net buiten het stijlvlak tegen flikkeren.
[[trimL, -halfW, -1], [trimR, halfW, 1]].forEach(([trim, x, dir]) => {
if (!(trim > 0)) return;
const h = Math.min(topYAt(x), topOf(dir < 0 ? trimTop.left : trimTop.right)), depth = maxDepth + 2 * (skinGapM + sThickM);
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);
cap.visible = spaanplaatVisible;
wallGroup3D.add(cap);
});
spaanplaatAreaM2 = spaanplaatResult.areaM2;
spaanplaatVolumeM3 = spaanplaatResult.volumeM3;
@@ -997,6 +1164,31 @@ window.WallModel = (() => {
Object.assign(extM(0), { zOut: sThickM + 2 * skinGapM }));
cementplaatAreaM2 = cementResult.areaM2;
cementplaatVolumeM3 = cementResult.volumeM3;
// Steen (opts.stone): steenblok (isolatie + strip) over de hele gevel,
// vóór de cement-gebonden plaat; onderaan het trasraam (eigen steen
// of cementplaat), de gevelsteen begint erboven.
if (stone) {
let y0 = 0;
if (trasraam && isStoneFinish(trasraam.finish)) {
const r = stoneBlock(trasraam.finish, trasraam.bottom * toM, trasraam.top * toM, { y: trasraam.top * toM, down: true });
trasraamStoneAreaM2 = r.areaM2;
trasraamIsolatieM3 = r.isoM3;
y0 = trasraam.top * toM;
} else if (trasraam) {
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, {
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + skinGapM
}, extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM));
trasraamAreaM2 = tras.areaM2;
trasraamVolumeM3 = tras.volumeM3;
y0 = trasraam.top * toM;
}
// Anker op de gevel-hoogte van de naad (facade.gridY0 = bovenkant
// trasraam t.o.v. dit stuk), zodat ook een opbouw aansluit.
const anchorY = facade && facade.gridY0 !== undefined ? facade.gridY0 * toM : y0;
const r = stoneBlock(stone, y0, Infinity, { y: anchorY, down: false });
stoneAreaM2 = r.areaM2;
stoneIsolatieM3 = r.isoM3;
}
} else {
// F. VOCHTDOEK BUITENZIJDE (zuid) — horizontale banen van 1500mm
// (rolbreedte), "oneindig" strekkend in de breedte (dus geen
@@ -1011,9 +1203,10 @@ window.WallModel = (() => {
// ligt — zoals bij een echte shingle-overlap — i.p.v. exact co-planair
// (wat flikkering/z-fighting zou geven nu de platen niet meer
// transparant zijn).
const doekZStepM = 0.0002; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tl.pts.slice().reverse()];
const maxRoofY = tl.maxY;
const doekZStepM = 0.0002;
const DOEK_OUT_M = 0.001; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tlOut.pts.slice().reverse()];
const maxRoofY = tlOut.maxY;
let rowY0 = skinFrom * toM;
let rowIndex = 0;
@@ -1023,11 +1216,11 @@ window.WallModel = (() => {
// Baan = strook [rowY0, min(rowY1, daklijn)] min uitsparingen
// (columnMinusRects, zelfde als de platen): een opening over de
// baanrand wordt een inkeping, geen reepje doek door de opening.
const roofAt = topYAt;
const [rx0, rx1] = tl.rangeAbove(rowY0) || [0, 0];
const extraXs = tl.crossings(rowY1).concat(tl.kinks);
const roofAt = tlOut.at;
const [rx0, rx1] = tlOut.rangeAbove(rowY0) || [0, 0];
const extraXs = tlOut.crossings(rowY1).concat(tlOut.kinks);
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
let rowShapes;
if (pieces) {
@@ -1068,7 +1261,10 @@ window.WallModel = (() => {
const rowAreaM2 = rowShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
const doekRow = new THREE.Mesh(rowGeo, doekMaterial);
doekRow.position.set(0, 0, (maxDepth / 2) + skinGapM + rowIndex * doekZStepM);
// 1 mm naar buiten: niet in hetzelfde vlak als de dakrand (dak op
// de wand), anders flikkert het dak door het doek heen waar de
// gevel tot onder de dakrand doorloopt (skinAbove).
doekRow.position.set(0, 0, (maxDepth / 2) + skinGapM + rowIndex * doekZStepM + DOEK_OUT_M);
doekRow.visible = buitenVisible;
wallGroup3D.add(doekRow);
@@ -1101,7 +1297,11 @@ window.WallModel = (() => {
// uitsparingen als de buitenplaten), vóór het doek
// (zelfde vlak als de latten). Het doek loopt
// er ongewijzigd achter door; het latwerk begint erboven.
if (trasraam) {
if (trasraam && isStoneFinish(trasraam.finish)) {
const r = stoneBlock(trasraam.finish, trasraam.bottom * toM, trasraam.top * toM, { y: trasraam.top * toM, down: true });
trasraamStoneAreaM2 = r.areaM2;
trasraamIsolatieM3 = r.isoM3;
} else if (trasraam) {
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, {
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + (rowIndex + 1) * doekZStepM + skinGapM
}, extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM));
@@ -1111,9 +1311,9 @@ window.WallModel = (() => {
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
// het doek, bovenste laag (haaks op de gevelbekleding) daaroverheen.
const bat = computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom);
const bat = computeBattens(CONFIG_OUT, cladding, trasraam, skinExt, facade, skinFrom, trimHolesMM);
const bT = BATTEN.thick * toM, bW = BATTEN.width * toM;
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM;
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM + DOEK_OUT_M;
const addLayer = (orientation, z) => {
bat.layers[orientation].forEach(b => {
const horiz = orientation === 'horizontaal';
@@ -1136,7 +1336,7 @@ window.WallModel = (() => {
stats: {
woodVolumeM3, spaanplaatVolumeM3, spaanplaatAreaM2, cementplaatVolumeM3, cementplaatAreaM2, doekAreaM2,
spaanplaatBuitenAreaM2, spaanplaatBuitenVolumeM3, horizBeamCount,
battenLengthM, battenVolumeM3, battenCount, trasraamAreaM2, trasraamVolumeM3
battenLengthM, battenVolumeM3, battenCount, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3
}
};
}
@@ -1346,7 +1546,7 @@ window.WallModel = (() => {
}
return {
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
createMaterials, build3D, computeSpecs
+4 -1
View File
@@ -88,7 +88,10 @@ window.WallPdf = (() => {
doc.text(`Bovenste lat ${bat.top}, gevelbekleding ${opts.cladding || 'horizontaal'} (WEO/Novalit)`, tblX[0], my);
if (opts.trasraam) {
my += 4;
doc.text(`Trasraam: cement-gebonden plaat tot ${opts.trasraam.top} mm boven bovenkant vloer`, tblX[0], my);
const finish = WallModel.isStoneFinish(opts.trasraam.finish)
? `steenstrip ${opts.trasraam.finish} op isolatieblok (${WallModel.STONE.THICK_MM} mm)`
: 'cement-gebonden plaat';
doc.text(`Trasraam: ${finish} tot ${opts.trasraam.top} mm boven bovenkant vloer`, tblX[0], my);
}
}
+7 -2
View File
@@ -253,6 +253,8 @@ function mountWallEditor(container, callbacks) {
}
// Binnen een woning is de richting van de gevelbekleding (voorlopig) globaal.
const lockCladding = callbacks && callbacks.lockCladding;
// ... en de buitenzijde (gevelbekleding per woning, override per wand).
const lockExterior = callbacks && callbacks.lockExterior;
// Binnen een woning is de h.o.h. globaal (woning-instellingen), niet per wand.
if (callbacks && callbacks.lockSpacing) {
@@ -566,7 +568,7 @@ function mountWallEditor(container, callbacks) {
function getPureState() {
return {
wall: collectWallParams(),
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam },
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone },
openings: openings.map(o => ({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset }))
};
}
@@ -591,6 +593,7 @@ function mountWallEditor(container, callbacks) {
buitenMateriaal = (state.materials && state.materials.exterior) || 'vochtdoek';
cladding = (state.materials && state.materials.cladding) || 'horizontaal';
trasraam = (state.materials && state.materials.trasraam) || null; // woning-instelling
stone = (state.materials && state.materials.stone) || null; // woning-instelling (steen)
syncMaterialButtons();
openings = state.openings.map(o => ({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset }));
@@ -1244,11 +1247,13 @@ function mountWallEditor(container, callbacks) {
let buitenMateriaal = 'vochtdoek';
let cladding = 'horizontaal'; // richting gevelbekleding (alleen bij vochtdoek)
let trasraam = null; // { bottom, top } mm, komt uit de woning-instellingen (WallModel.trasraamFor)
let stone = null; // steencode bij cementplaat, komt uit de woning-instellingen
function syncMaterialButtons() {
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => b.classList.toggle('active', b.dataset.material === buitenMateriaal));
document.querySelectorAll('.material-toggle-btn[data-cladding]').forEach(b => b.classList.toggle('active', b.dataset.cladding === cladding));
document.getElementById('cladding-section').style.display = buitenMateriaal === 'vochtdoek' ? '' : 'none';
if (lockCladding) document.querySelectorAll('.material-toggle-btn[data-cladding]').forEach(b => { b.disabled = true; b.title = lockCladding; });
if (lockExterior) document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => { b.disabled = true; b.title = lockExterior; });
}
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(btn => {
btn.addEventListener('click', () => { buitenMateriaal = btn.dataset.material; syncMaterialButtons(); updateAll(); });
@@ -1284,7 +1289,7 @@ function mountWallEditor(container, callbacks) {
function update3DScene() {
if (wallGroup3D) scene.remove(wallGroup3D);
const result = WallModel.build3D(CONFIG, calcData, {
materials: wallMaterials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, spaanplaatVisible, buitenVisible
});
wallGroup3D = result.group;
lastWallStats = result.stats;