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
+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