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