v0.5.3: elektra-buizen min. 50 mm uit elkaar (gebundeld alleen naar de meterkast), vloer-wcd met leiding door de vloer, elektra op de PDF-bladen per wand/vloer/dakvlak, kosten elektra (buis + draad per meter, boorgaten 90 s geteld op stijlen/regels/balken, dozen, groepenkast, keuring), dakbeschot OSB 18 mm bovenop de sporen, felsplaten als dakbedekking

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-06 20:54:49 +02:00
co-authored by Claude Opus 5.5
parent e1a688728c
commit b5510d56c7
13 changed files with 668 additions and 64 deletions
+77 -31
View File
@@ -38,6 +38,7 @@
window.RoofModel = (() => {
const DAKLEER_MM = 2; // visuele dikte van het dakleer
const BESCHOT_MM = 18; // dakbeschot (OSB) bovenop de sporen, onder de bedekking
const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2
const TRIM_THICK_MM = 25; // daktrim-profiel, uitstekend buiten de dakrand
const EDGE_ABOVE_MM = 80; // bovenkant boeideel/daktrim ligt zoveel boven het dakleer
@@ -72,11 +73,28 @@ window.RoofModel = (() => {
// hier toevoegen); zelfde opbouw (180 mm, 3,8 kg/m2).
// colors: gemiddelde kleur per textuur (vlakke weergave, PDF-aanzichten).
const RIET = { thickMM: 180, kgM2: 3.8, tileMM: 1000, nrs: ['001', '002', '003'], colors: { '001': '#947d69', '002': '#7c4c3f', '003': '#735e43' } };
// Felsplaten (staande naad, zink/staal op scheidingslaag): 25 mm hoge
// felsnaden elke seamMM langs de helling; alleen hellend.
const FELS = { thickMM: 25, kgM2: 6.5, seamMM: 500, colors: { zink: ['Felsplaat zink', '#8e9497'], antraciet: ['Felsplaat antraciet', '#3d4146'], zwart: ['Felsplaat zwart', '#1f2022'] } };
// kind: soort bedekking (dakleer | riet | fels) voor specificaties en kosten.
const COVERINGS = Object.assign({
dakleer: { label: 'Dakleer (EPDM)', color: '#2a2a2a', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
dakleer: { label: 'Dakleer (EPDM)', kind: 'dakleer', color: '#2a2a2a', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
}, ...RIET.nrs.map(nr => ({
['riet_' + nr]: { label: 'Riet ' + nr, color: RIET.colors[nr] || '#8a7560', thickMM: RIET.thickMM, kgM2: RIET.kgM2, file: `assets/textures/dak/riet_${nr}.jpg`, tileMM: RIET.tileMM, sloped: true }
['riet_' + nr]: { label: 'Riet ' + nr, kind: 'riet', color: RIET.colors[nr] || '#8a7560', thickMM: RIET.thickMM, kgM2: RIET.kgM2, file: `assets/textures/dak/riet_${nr}.jpg`, tileMM: RIET.tileMM, sloped: true }
})), ...Object.entries(FELS.colors).map(([k, [label, color]]) => ({
['fels_' + k]: { label, kind: 'fels', color, thickMM: FELS.thickMM, kgM2: FELS.kgM2, seamMM: FELS.seamMM, sloped: true }
})));
const coverKind = (key) => (COVERINGS[key] || COVERINGS.dakleer).kind;
// Hoeveelheden per soort bedekking (zelfde sleutels in elke specs).
function coverSpecs(covering, areaM2) {
const c = COVERINGS[covering] || COVERINGS.dakleer, kg = areaM2 * c.kgM2, k = c.kind;
return {
dakleerM2: k === 'dakleer' ? areaM2 : 0, dakleerKg: k === 'dakleer' ? kg : 0,
rietM2: k === 'riet' ? areaM2 : 0, rietKg: k === 'riet' ? kg : 0,
felsM2: k === 'fels' ? areaM2 : 0, felsKg: k === 'fels' ? kg : 0,
coverKg: kg
};
}
// Voorvertoning (assets/textures/dak/thumb/<px>/<naam>.png) of null.
function coveringThumbUrl(key, px) {
const c = COVERINGS[key];
@@ -90,8 +108,13 @@ window.RoofModel = (() => {
const coveringMaterials = {};
function coveringMaterial(key) {
const c = COVERINGS[key];
if (!c || !c.file) return null;
if (!c) return null;
if (coveringMaterials[key]) return coveringMaterials[key];
if (c.kind === 'fels') {
coveringMaterials[key] = new THREE.MeshStandardMaterial({ color: c.color, roughness: 0.4, metalness: 0.55, side: THREE.DoubleSide });
return coveringMaterials[key];
}
if (!c.file) return null;
const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 });
if (typeof document !== 'undefined' && THREE.TextureLoader) {
new THREE.TextureLoader().load(c.file, (tex) => {
@@ -118,9 +141,9 @@ window.RoofModel = (() => {
return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, covering: 'dakleer', deck };
}
// Dikte dek + bedekking (mm, haaks op het dakvlak).
// Dikte dek (plafondplaat + sporen + dakbeschot) + bedekking (mm, haaks op het dakvlak).
function thickness(calcData, covering) {
return calcData.joistH + calcData.underlayThick + (COVERINGS[covering] || COVERINGS.dakleer).thickMM;
return calcData.joistH + calcData.underlayThick + BESCHOT_MM + (COVERINGS[covering] || COVERINGS.dakleer).thickMM;
}
function createMaterials() {
@@ -135,15 +158,17 @@ window.RoofModel = (() => {
// 3D van één dak (meters, gecentreerd op het dakmidden, Y = 0 onderkant
// plafondplaat). Het vloer-3D (FloorModel.build3D) wordt gespiegeld
// (scale.y = -1; three.js corrigeert de vlakoriëntatie zelf), daarna
// komt het dakleer er bovenop.
// komen het dakbeschot (OSB, fase beplating) en de bedekking (fase gevel)
// er bovenop. Felsplaten: dunne plaat met staande naden langs de helling.
function build3D(calcData, resolvedOpenings, opts) {
const m = opts.materials;
const toM = 0.001;
const group = new THREE.Group();
// Plafondplaat én bedekking zichtbaar: de sporen ertussen zijn ingesloten.
// Plafondplaat én dakbeschot zichtbaar (fase beplating): de sporen ertussen zijn ingesloten.
const deck = FloorModel.build3D(calcData, resolvedOpenings, { wheels: [] }, {
materials: m, osbVisible: opts.osbVisible !== false,
outerJoistsOnly: opts.osbVisible !== false && opts.dakleerVisible !== false
outerJoistsOnly: opts.osbVisible !== false
});
const deckH = (calcData.joistH + calcData.underlayThick) * toM;
deck.scale.y = -1;
@@ -168,25 +193,46 @@ window.RoofModel = (() => {
// 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 + cov.thickMM * toM;
leer.castShadow = cov.thickMM > DAKLEER_MM;
if (opts.dakleerVisible !== false) group.add(leer);
const bT = BESCHOT_MM * toM;
if (opts.osbVisible !== false) {
const beschot = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: bT, bevelEnabled: false }), m.osbMaterial);
beschot.rotation.x = Math.PI / 2;
beschot.position.y = deckH + bT;
beschot.castShadow = true;
group.add(beschot);
}
if (opts.dakleerVisible !== false) {
// Felsplaat: dunne plaat + naden; anders de volle laag (dakleer, riet).
const slabT = (cov.kind === 'fels' ? 1.5 : cov.thickMM) * toM;
const leer = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: slabT, bevelEnabled: false }), covMat);
leer.rotation.x = Math.PI / 2;
leer.position.y = deckH + bT + slabT;
leer.castShadow = cov.thickMM > DAKLEER_MM;
group.add(leer);
if (cov.kind === 'fels') {
const n = Math.max(0, Math.floor(calcData.width / cov.seamMM));
const x0 = -(n * cov.seamMM) / 2;
for (let i = 0; i <= n; i++) {
const seam = new THREE.Mesh(new THREE.BoxGeometry(0.01, cov.thickMM * toM, dM), covMat);
seam.position.set((x0 + i * cov.seamMM) * toM, deckH + bT + cov.thickMM * toM / 2, 0);
seam.castShadow = true;
group.add(seam);
}
}
}
return group;
}
// Specificaties: die van het dek (FloorModel) + de bedekking (dakleer of
// bijv. riet: coveringM2/Kg, dakleer dan 0).
// Specificaties: die van het dek (FloorModel; osb = plafondplaat) + het
// dakbeschot (beschot*) + de bedekking (coverSpecs: dakleer/riet/fels).
function computeSpecs(calcData, resolvedOpenings, covering) {
const deck = FloorModel.computeSpecs(calcData, resolvedOpenings);
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
const cs = coverSpecs(covering, areaM2);
const bVol = areaM2 * BESCHOT_MM * 1e-3, bKg = bVol * DENSITY_OSB_KGM3;
return Object.assign({}, deck, cs, {
beschotM2: areaM2, beschotVolM3: bVol, beschotKg: bKg,
totalKg: deck.totalKg + bKg + cs.coverKg
});
}
@@ -385,7 +431,7 @@ window.RoofModel = (() => {
const inset = (top - hipH) / tH; // nokeinde vanaf de kopgevel
const plane = (key, slope, f, along) => {
const cos = Math.cos(Math.atan(slope));
return { key, f, along, cos, tV: underlay / cos, jV: joistH / cos, covV: cov.thickMM / cos };
return { key, f, along, cos, tV: underlay / cos, jV: joistH / cos, bV: BESCHOT_MM / cos, covV: cov.thickMM / cos };
};
const fns = [
plane('m1', sA, (p) => H(pA) + (p.x - pA) * sA, 'p'),
@@ -399,7 +445,7 @@ window.RoofModel = (() => {
fns.forEach((fj, j) => { if (j !== i && poly.length >= 3) poly = clipPoly(poly, (p) => fj.f(p) - fi.f(p)); });
// Samenvallende punten (afsnijden door de top) weg.
poly = poly.filter((p, k) => { const q = poly[(k + 1) % poly.length]; return Math.hypot(q.x - p.x, q.z - p.z) > 0.5; });
return Object.assign({}, fi, { poly, under: fi.f, vThick: fi.tV + fi.jV + fi.covV });
return Object.assign({}, fi, { poly, under: fi.f, vThick: fi.tV + fi.jV + fi.bV + fi.covV });
}).filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
// Snijden van een lijn (vaste p of vaste q) met een convexe veelhoek.
@@ -499,15 +545,14 @@ window.RoofModel = (() => {
const W = joistW * 1e-3, Hh = joistH * 1e-3;
const woodM3 = beams.reduce((t, bm) => t + bm.length * 1e-3 * W * Hh, 0);
const areaM2 = regions.reduce((t, rg) => t + quadArea(rg.poly) / rg.cos, 0) / 1e6;
const isLeer = !COVERINGS[covering] || covering === 'dakleer';
const covKg = areaM2 * cov.kgM2, osbVol = areaM2 * underlay * 1e-3;
const specs = {
const cs = coverSpecs(covering, areaM2), osbVol = areaM2 * underlay * 1e-3, bVol = areaM2 * BESCHOT_MM * 1e-3;
const specs = Object.assign({
joistWoodM3: woodM3, joistWoodKg: woodM3 * DENSITY_WOOD_KGM3,
osbAreaM2: areaM2, osbVolM3: osbVol, osbKg: osbVol * DENSITY_OSB_KGM3,
beschotM2: areaM2, beschotVolM3: bVol, beschotKg: bVol * DENSITY_OSB_KGM3,
voidM3: Math.max(0, areaM2 * Hh - woodM3),
dakleerM2: isLeer ? areaM2 : 0, dakleerKg: isLeer ? covKg : 0, rietM2: isLeer ? 0 : areaM2, rietKg: isLeer ? 0 : covKg,
totalKg: woodM3 * DENSITY_WOOD_KGM3 + osbVol * DENSITY_OSB_KGM3 + covKg
};
totalKg: woodM3 * DENSITY_WOOD_KGM3 + (osbVol + bVol) * DENSITY_OSB_KGM3 + cs.coverKg
}, cs);
const maxTop = top + Math.max(...regions.map(rg => rg.vThick));
return { pA, pR, pB, q0, q1, hipH, joistW, joistH, regions, beams, specs, maxTop };
}
@@ -625,13 +670,14 @@ window.RoofModel = (() => {
}
c.regions.forEach(rg => {
if (osbOn) slab(rg.poly, rg.under, rg.tV, m.osbMaterial, m.osbMaterial);
if (covOn) slab(rg.poly, (pt) => rg.under(pt) + rg.tV + rg.jV, rg.covV, covMat, covMat,
if (osbOn) slab(rg.poly, (pt) => rg.under(pt) + rg.tV + rg.jV, rg.bV, m.osbMaterial, m.osbMaterial); // dakbeschot
if (covOn) slab(rg.poly, (pt) => rg.under(pt) + rg.tV + rg.jV + rg.bV, rg.covV, covMat, covMat,
(pt) => rg.along === 'p' ? [pt.z * toM, pt.x * toM / rg.cos] : [pt.x * toM, pt.z * toM / rg.cos]);
});
// Balken: hun (afgezaagde) veelhoek in de plattegrond, onderkant op
// bm.yAt, hoogte jV verticaal.
c.beams.forEach(bm => {
if (osbOn && covOn && bm.kind !== 'kopbalk') return;
if (osbOn && bm.kind !== 'kopbalk') return; // ingesloten tussen plafondplaat en dakbeschot
slab(bm.poly, bm.yAt, bm.jV, m.woodMaterial, m.woodMaterial);
});
return group;
@@ -882,7 +928,7 @@ window.RoofModel = (() => {
}
return {
DAKLEER_MM, EDGE_ABOVE_MM, RIET, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
DAKLEER_MM, BESCHOT_MM, EDGE_ABOVE_MM, RIET, FELS, coverKind, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D, capRegions,
computeEdge, buildEdge3D, normalizeSettings