v0.6.0: ruimtes (verfemmer: functie, netto m², vloerafwerking, FIBO-wandpanelen, eigen verfkleur, vals plafond, ruimtestaat op de PDF), inrichting (sanitair, keuken, kookeiland, kasten, bed, zithoek, boiler; slepen met snappen op het wandvlak, draaien, kosten), kozijnen met deurrichting, glasdeur en schuifdeuren (inbouw/opbouw, deuren open in de loopmodus) plus dagkanten, gevel stucwerk en wandstukken splitsen met eigen gevel, dak over de hele woning met overkapping, geschilderd plafond, model Amstel als nieuw ontwerp, kosten en prijzen uit kosten/intern.csv en kosten/leveranciers.csv (CostCsv); easter egg verwijderd; overbodige comments opgeschoond (alleen copyright, uitzonderingen en valkuilen blijven)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b5510d56c7
commit
ceda0b554d
+219
-262
@@ -4,58 +4,26 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
// ============================================================
|
||||
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica. Geen DOM.
|
||||
//
|
||||
// Een dakvlak is opgebouwd als een vloer (FloorModel: balken, isolatie,
|
||||
// doorvoeren, OSB-plaat), maar 180° gespiegeld: de OSB-plaat zit aan de
|
||||
// onderzijde (plafond), de balken erboven, met dakleer als buitenlaag
|
||||
// bovenop. Het "dek" van een dak is dus gewoon een vloer-state.
|
||||
//
|
||||
// Dakvorm volgt de wanden (computeShape): het paar tegenoverliggende wanden
|
||||
// met een schuine bovenrand of tussenpunten (nok, knikken) zijn de kopgevels,
|
||||
// het dak volgt hun daklijn (plat, lessenaar, zadel, mansarde of wolfsdak;
|
||||
// bij een wolfsdak komen daar de wolfseinden bij, zie computeShape). UnitModel.syncWallHeights
|
||||
// houdt de wanden daarvoor consistent. Het dek (in de plattegrond) wordt per
|
||||
// dakvlak omgerekend naar een schuin vlak met de balken als sporen (van
|
||||
// laag naar hoog); doorvoeren gaan mee naar het vlak waarin ze vallen.
|
||||
//
|
||||
// Dakrand (over de hele woning, doorlopend over naden tussen units): de
|
||||
// omtrek van alle daken samen, opgeknipt per ribbe (en op elke unit-hoek).
|
||||
// Per ribbe bepaalt de unit waar die ribbe bij hoort of het een boeideel of
|
||||
// een daktrim is (boeidelen aan/uit per unit). De ribben sluiten in verstek
|
||||
// op elkaar aan, dus op een naad tussen units is niets te zien.
|
||||
// - boeidelen: een doos van 200..400 mm overstek (gemeten vanaf de
|
||||
// dakrand = standaard de buitenkant gevel), standaard 300 mm hoog.
|
||||
// Spots in de onderplaat staan op 40% van de overstek (spotLine); welke
|
||||
// spots er komen bepaalt LightingModel.
|
||||
// - zonder boeidelen: een daktrim van 80 mm hoog (onder het dakleer).
|
||||
// De bovenkant van boeideel en daktrim ligt EDGE_ABOVE_MM boven het dakleer
|
||||
// (opstand rondom), dus het dakleer ligt altijd lager dan de rand.
|
||||
//
|
||||
// Plain script, geladen na floor-model.js en core/geometry.js.
|
||||
// ============================================================
|
||||
// Een dakvlak is een vloer-state, 180° gespiegeld: OSB-plaat onder (plafond), balken erboven.
|
||||
|
||||
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
|
||||
const SPOT_RATIO = 0.4; // spots op 40% van de overstek, gemeten vanaf de gevel
|
||||
const DAKLEER_MM = 2; // visuele dikte
|
||||
const BESCHOT_MM = 18;
|
||||
const DENSITY_DAKLEER_KGM2 = 1.4;
|
||||
const TRIM_THICK_MM = 25;
|
||||
const EDGE_ABOVE_MM = 80;
|
||||
const SPOT_RATIO = 0.4;
|
||||
const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
|
||||
|
||||
// Woning-brede maten van de dakrand (aan/uit per unit: roof.fascia).
|
||||
const DEFAULT_SETTINGS = {
|
||||
overhang: 300, // mm, 200..400
|
||||
fasciaHeight: 300, // mm
|
||||
spotMin: 800, // mm h.o.h. boeideel-spots
|
||||
overhang: 300,
|
||||
fasciaHeight: 300,
|
||||
spotMin: 800,
|
||||
spotMax: 1200,
|
||||
trimHeight: 80, // mm
|
||||
fasciaColor: 'antraciet' // FASCIA_COLORS
|
||||
trimHeight: 80,
|
||||
fasciaColor: 'antraciet'
|
||||
};
|
||||
|
||||
// Kleur van het boeideel (egaal).
|
||||
const FASCIA_COLORS = {
|
||||
wit: { label: 'Wit', hex: 0xf2f2f0 },
|
||||
lichtgrijs: { label: 'Lichtgrijs', hex: 0xbfc3c6 },
|
||||
@@ -64,19 +32,10 @@ window.RoofModel = (() => {
|
||||
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).
|
||||
// colors: gemiddelde kleur per textuur (vlakke weergave, PDF-aanzichten).
|
||||
// Riet e.d. alleen bij een hellend dak; plat valt terug op dakleer.
|
||||
// Riet: één keuze per textuur assets/textures/dak/riet_<nr>.jpg (nummer hier toevoegen).
|
||||
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)', kind: 'dakleer', color: '#2a2a2a', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
|
||||
}, ...RIET.nrs.map(nr => ({
|
||||
@@ -85,7 +44,6 @@ window.RoofModel = (() => {
|
||||
['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 {
|
||||
@@ -95,12 +53,10 @@ window.RoofModel = (() => {
|
||||
coverKg: kg
|
||||
};
|
||||
}
|
||||
// Voorvertoning (assets/textures/dak/thumb/<px>/<naam>.png) of null.
|
||||
function coveringThumbUrl(key, px) {
|
||||
const c = COVERINGS[key];
|
||||
return c && c.file ? c.file.replace(/[^/]+$/, (f) => `thumb/${px || 64}/` + f.replace(/\.jpg$/, '.png')) : null;
|
||||
}
|
||||
// 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';
|
||||
@@ -132,8 +88,6 @@ window.RoofModel = (() => {
|
||||
|
||||
function clone(obj) { return JSON.parse(JSON.stringify(obj)); }
|
||||
|
||||
// Dak-state per unit: volgt standaard de vloermaat (followFloor), maar mag
|
||||
// groter/kleiner en verschoven worden (override).
|
||||
function createDefaultRoof(floorState) {
|
||||
const deck = clone(floorState);
|
||||
deck.chassis = { enabled: false, wheelDiameter: 400, trackWidth: 1200, wheelbase: 1200 };
|
||||
@@ -141,7 +95,6 @@ window.RoofModel = (() => {
|
||||
return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, covering: 'dakleer', deck };
|
||||
}
|
||||
|
||||
// Dikte dek (plafondplaat + sporen + dakbeschot) + bedekking (mm, haaks op het dakvlak).
|
||||
function thickness(calcData, covering) {
|
||||
return calcData.joistH + calcData.underlayThick + BESCHOT_MM + (COVERINGS[covering] || COVERINGS.dakleer).thickMM;
|
||||
}
|
||||
@@ -151,23 +104,20 @@ window.RoofModel = (() => {
|
||||
dakleerMaterial: new THREE.MeshStandardMaterial({ color: 0x2a2a2a, roughness: 0.9, side: THREE.DoubleSide }),
|
||||
fasciaMaterial: new THREE.MeshStandardMaterial({ color: 0x3a3f44, roughness: 0.6, side: THREE.DoubleSide }),
|
||||
trimMaterial: new THREE.MeshStandardMaterial({ color: 0xb8bcc0, roughness: 0.4, metalness: 0.5, side: THREE.DoubleSide }),
|
||||
steelMaterial: new THREE.MeshStandardMaterial({ color: 0x5f666c, roughness: 0.5, metalness: 0.6 }),
|
||||
ceilingMaterial: new THREE.MeshStandardMaterial({ color: 0xf4f3ef, roughness: 0.92, side: THREE.DoubleSide }),
|
||||
spotOnMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe9a8, emissiveIntensity: 0.9 })
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
// komen het dakbeschot (OSB, fase beplating) en de bedekking (fase gevel)
|
||||
// er bovenop. Felsplaten: dunne plaat met staande naden langs de helling.
|
||||
// Vloer-3D gespiegeld met scale.y = -1 (three.js corrigeert de vlakoriëntatie zelf).
|
||||
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 deckMats = opts.ceilingMaterial ? Object.assign({}, m, { osbMaterial: opts.ceilingMaterial }) : m;
|
||||
const deck = FloorModel.build3D(calcData, resolvedOpenings, { wheels: [] }, {
|
||||
materials: m, osbVisible: opts.osbVisible !== false,
|
||||
materials: deckMats, osbVisible: opts.osbVisible !== false,
|
||||
outerJoistsOnly: opts.osbVisible !== false
|
||||
});
|
||||
const deckH = (calcData.joistH + calcData.underlayThick) * toM;
|
||||
@@ -175,8 +125,6 @@ window.RoofModel = (() => {
|
||||
deck.position.y = deckH;
|
||||
group.add(deck);
|
||||
|
||||
// Dakleer: vlak over het hele dak, met dezelfde ronde uitsparingen als
|
||||
// de doorvoeren (zelfde opbouw als de OSB-plaat in FloorModel).
|
||||
const wM = calcData.width * toM, dM = calcData.depth * toM;
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(-wM / 2, -dM / 2);
|
||||
@@ -189,8 +137,6 @@ window.RoofModel = (() => {
|
||||
hole.absarc(o.x * toM, o.y * toM - dM / 2, o.r * toM, 0, Math.PI * 2, false);
|
||||
shape.holes.push(hole);
|
||||
});
|
||||
// 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 bT = BESCHOT_MM * toM;
|
||||
@@ -202,7 +148,6 @@ window.RoofModel = (() => {
|
||||
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;
|
||||
@@ -223,11 +168,9 @@ window.RoofModel = (() => {
|
||||
return group;
|
||||
}
|
||||
|
||||
// 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 areaM2 = deck.osbAreaM2;
|
||||
const cs = coverSpecs(covering, areaM2);
|
||||
const bVol = areaM2 * BESCHOT_MM * 1e-3, bKg = bVol * DENSITY_OSB_KGM3;
|
||||
return Object.assign({}, deck, cs, {
|
||||
@@ -236,62 +179,67 @@ window.RoofModel = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Dakvorm per unit (volgt de wanden)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Daklijn van één wand als profiel langs zijn as (unit-lokaal mm):
|
||||
// [{ p, h }] gesorteerd op p, h vanaf het loopvlak. Zelfde definitie als
|
||||
// UnitModel.wallPieces: links/rechts op de uiteinden van de wand (van
|
||||
// buitenaf gezien), het middenpunt (nok) midden op de wand.
|
||||
// De daklijn hoort bij de wand zonder naden (wl.ref): is de wand door een naad langer of korter,
|
||||
// dan blijft het profiel gelijk.
|
||||
function wallProfile(unitState, layout, side) {
|
||||
const w = unitState.walls[side].wall;
|
||||
const wl = layout.walls.find(x => x.side === side);
|
||||
const s = UnitModel.WALL_SIDES.find(x => x.key === side);
|
||||
const c = s.axis === 'x' ? wl.cx : wl.cz;
|
||||
const pts = [{ p: c - s.dir * wl.width / 2, h: w.heightLeft }, { p: c + s.dir * wl.width / 2, h: w.heightRight }];
|
||||
// Tussenpunten (nok, knikken van mansarde/wolfsdak), x vanaf links.
|
||||
WallModel.wallTopPoints(w, wl.width, w.heightLeft, w.heightRight)
|
||||
.forEach(q => pts.push({ p: c - s.dir * wl.width / 2 + s.dir * q.x, h: q.h }));
|
||||
const ref = wl.ref || { w: wl.width, u0: 0 };
|
||||
const at = (x) => c - s.dir * wl.width / 2 + s.dir * (x - ref.u0);
|
||||
const pts = [{ p: at(0), h: w.heightLeft }, { p: at(ref.w), h: w.heightRight }];
|
||||
WallModel.wallTopPoints(w, ref.w, w.heightLeft, w.heightRight)
|
||||
.forEach(q => pts.push({ p: at(q.x), h: q.h }));
|
||||
return pts.sort((a, b) => a.p - b.p);
|
||||
}
|
||||
|
||||
// Dakvorm van een unit. layout = UnitModel.computeLayout-resultaat.
|
||||
// Geeft { type: 'plat'|'lessenaar'|'zadel'|'mansarde'|'wolf', axis: 'x'|'z',
|
||||
// profile, hip? }: de hoogte van de dakonderkant (vanaf het loopvlak)
|
||||
// hangt alleen af van de positie langs axis (profileHeight), behalve bij
|
||||
// een wolfsdak (hip). Kopgevels: de eerste wand met een schuine daklijn
|
||||
// (noord/zuid -> dak helt langs X, west/oost -> langs Z). Zonder schuine
|
||||
// wanden: plat op de hoogste wand.
|
||||
// zadel 3 punten (goot, nok, goot)
|
||||
// mansarde 5 punten (goot, knik, nok, knik, goot)
|
||||
// wolf 4 punten met een vlakke top: de kopgevel stopt op het
|
||||
// wolfseind. Het profiel wordt dat van het zadeldak (de
|
||||
// schuine stukken doorgetrokken tot de nok), met de knikken op
|
||||
// het wolfseind; hip = { h }: de wolfseinden lopen vanaf de
|
||||
// kopgevels op hoogte h omhoog tot de nok (computeCap).
|
||||
function computeShape(unitState, layout) {
|
||||
for (const [side, axis] of [['achter', 'x'], ['voor', 'x'], ['links', 'z'], ['rechts', 'z']]) {
|
||||
const profile = wallProfile(unitState, layout, side);
|
||||
if (!profile.some(q => Math.abs(q.h - profile[0].h) > 0.5)) continue;
|
||||
if (profile.length === 4 && Math.abs(profile[1].h - profile[2].h) < 0.5) {
|
||||
const [L, A, B, R] = profile, sA = (A.h - L.h) / (A.p - L.p), sB = (R.h - B.h) / (R.p - B.p);
|
||||
// Snijpunt van de doorgetrokken schuine stukken = nok.
|
||||
const pr = Math.abs(sA - sB) > 1e-9 ? (B.h - sB * B.p - A.h + sA * A.p) / (sA - sB) : (A.p + B.p) / 2;
|
||||
const ridge = { p: pr, h: A.h + sA * (pr - A.p) };
|
||||
if (sA > 0 && sB < 0 && pr > A.p && pr < B.p) {
|
||||
return { type: 'wolf', axis, profile: [L, A, ridge, B, R], hip: { h: A.h } };
|
||||
}
|
||||
}
|
||||
const type = profile.length === 2 ? 'lessenaar' : profile.length === 5 ? 'mansarde' : 'zadel';
|
||||
return { type, axis, profile };
|
||||
return shapeFromProfile(axis, profile);
|
||||
}
|
||||
const h = Math.max(...Object.values(unitState.walls).map(ws => UnitModel.wallTopOf(ws)));
|
||||
return { type: 'plat', axis: 'z', profile: [{ p: 0, h }] };
|
||||
}
|
||||
function shapeFromProfile(axis, profile) {
|
||||
if (profile.length === 4 && Math.abs(profile[1].h - profile[2].h) < 0.5) {
|
||||
const [L, A, B, R] = profile, sA = (A.h - L.h) / (A.p - L.p), sB = (R.h - B.h) / (R.p - B.p);
|
||||
const pr = Math.abs(sA - sB) > 1e-9 ? (B.h - sB * B.p - A.h + sA * A.p) / (sA - sB) : (A.p + B.p) / 2;
|
||||
const ridge = { p: pr, h: A.h + sA * (pr - A.p) };
|
||||
if (sA > 0 && sB < 0 && pr > A.p && pr < B.p) {
|
||||
return { type: 'wolf', axis, profile: [L, A, ridge, B, R], hip: { h: A.h } };
|
||||
}
|
||||
}
|
||||
const type = profile.length === 2 ? 'lessenaar' : profile.length === 5 ? 'mansarde' : 'zadel';
|
||||
return { type, axis, profile };
|
||||
}
|
||||
|
||||
// Hoogte van het profiel op positie p; buiten de wand loopt de laatste
|
||||
// helling door (overstek, boeideel).
|
||||
// Helling wolfseinden: die van het dak, maar minstens zo steil dat ze de nok halen vóór het midden.
|
||||
function hipGeom(shape, q0, q1) {
|
||||
const [, A, R, B] = shape.profile, hipH = shape.hip.h, top = R.h, qM = (q0 + q1) / 2;
|
||||
const sA = (top - A.h) / (R.p - A.p), sB = (top - B.h) / (B.p - R.p);
|
||||
const tH = Math.max((sA + sB) / 2, (top - hipH) / Math.max(1, qM - q0));
|
||||
return { pA: A.p, pR: R.p, pB: B.p, hipH, top, sA, sB, tH, inset: (top - hipH) / tH, q0, q1, qM };
|
||||
}
|
||||
|
||||
// De daklijn is bol, dus het profiel is het minimum van zijn (doorgetrokken) stukken.
|
||||
function shapeLines(shape) {
|
||||
const pr = shape.profile;
|
||||
if (pr.length === 1) return [{ a: pr[0].h, bp: 0, bq: 0 }];
|
||||
const out = pr.slice(0, -1).map((a, i) => {
|
||||
const b = pr[i + 1], s = (b.h - a.h) / (b.p - a.p);
|
||||
return { a: a.h - s * a.p, bp: s, bq: 0 };
|
||||
});
|
||||
if (shape.type === 'wolf' && shape.hip && shape.hip.q0 !== undefined) {
|
||||
const g = hipGeom(shape, shape.hip.q0, shape.hip.q1);
|
||||
out.push({ a: g.hipH - g.q0 * g.tH, bp: 0, bq: g.tH }, { a: g.hipH + g.q1 * g.tH, bp: 0, bq: -g.tH });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Buiten de wand loopt de laatste helling door (overstek, boeideel).
|
||||
function profileHeight(profile, p) {
|
||||
if (profile.length === 1) return profile[0].h;
|
||||
let i = 0;
|
||||
@@ -300,7 +248,98 @@ window.RoofModel = (() => {
|
||||
return a.h + (p - a.p) / (b.p - a.p) * (b.h - a.h);
|
||||
}
|
||||
|
||||
// Tel alle getallen van een lijst specificaties op.
|
||||
// Stalen nokbalk, per unit één (modules worden los gebouwd), opgelegd op de kopgevels.
|
||||
// Kleinste IPE die voldoet aan sterkte (1,2·G + 1,5·S, σ ≤ fy) en doorbuiging (G + S, ≤ L/250).
|
||||
// [naam, h, b, tw, tf (mm), kg/m, Iy (cm4), Wy (cm3)]
|
||||
const IPE = [
|
||||
['IPE 100', 100, 55, 4.1, 5.7, 8.1, 171, 34.2], ['IPE 120', 120, 64, 4.4, 6.3, 10.4, 318, 53.0],
|
||||
['IPE 140', 140, 73, 4.7, 6.9, 12.9, 541, 77.3], ['IPE 160', 160, 82, 5.0, 7.4, 15.8, 869, 109],
|
||||
['IPE 180', 180, 91, 5.3, 8.0, 18.8, 1317, 146], ['IPE 200', 200, 100, 5.6, 8.5, 22.4, 1943, 194],
|
||||
['IPE 220', 220, 110, 5.9, 9.2, 26.2, 2772, 252], ['IPE 240', 240, 120, 6.2, 9.8, 30.7, 3892, 324],
|
||||
['IPE 270', 270, 135, 6.6, 10.2, 36.1, 5790, 429], ['IPE 300', 300, 150, 7.1, 10.7, 42.2, 8356, 557],
|
||||
['IPE 330', 330, 160, 7.5, 11.5, 49.1, 11770, 713], ['IPE 360', 360, 170, 8.0, 12.7, 57.1, 16270, 904],
|
||||
['IPE 400', 400, 180, 8.6, 13.5, 66.3, 23130, 1160], ['IPE 450', 450, 190, 9.4, 14.6, 77.6, 33740, 1500],
|
||||
['IPE 500', 500, 200, 10.2, 16.0, 90.7, 48200, 1930]
|
||||
].map(([name, h, b, tw, tf, kgM, I, W]) => ({ name, h, b, tw, tf, kgM, I, W }));
|
||||
const STEEL_RIDGE = {
|
||||
fy: 235, E: 210000, // S235, N/mm2
|
||||
snowKNm2: 0.56, // sneeuw op het dak (0,8 × 0,7 kN/m2)
|
||||
finishKNm2: 0.3, // afwerking/installaties onder het dak
|
||||
gammaG: 1.2, gammaQ: 1.5, deflection: 250
|
||||
};
|
||||
const ridgeTypes = ['zadel', 'mansarde', 'wolf'];
|
||||
function computeRidgeBeam(roofState, shape, rect, alongX, cd, kgM2) {
|
||||
if (roofState.steelRidge === false || !ridgeTypes.includes(shape.type)) return null;
|
||||
const pr = shape.profile, inner = pr.slice(1, -1);
|
||||
if (!inner.length) return null;
|
||||
const R = inner.reduce((a, q) => q.h > a.h ? q : a);
|
||||
const a0 = alongX ? rect.x0 : rect.z0, a1 = alongX ? rect.x1 : rect.z1;
|
||||
if (R.p < a0 - 0.5 || R.p >= a1 - 0.5) return null;
|
||||
const uq0 = alongX ? rect.z0 : rect.x0, uq1 = alongX ? rect.z1 : rect.x1;
|
||||
let q0 = uq0, q1 = uq1;
|
||||
if (shape.type === 'wolf' && shape.hip) {
|
||||
const hq = shape.hip.q0 !== undefined ? [shape.hip.q0, shape.hip.q1] : [uq0, uq1];
|
||||
const g = hipGeom(shape, hq[0], hq[1]);
|
||||
q0 = Math.max(uq0, Math.min(g.qM, g.q0 + g.inset));
|
||||
q1 = Math.min(uq1, Math.max(g.qM, g.q1 - g.inset));
|
||||
}
|
||||
const L = q1 - q0;
|
||||
if (L < 100) return null;
|
||||
// q in N/mm = kN/m.
|
||||
const trib = (a1 - a0) / 2 / 1000;
|
||||
const gRoof = (kgM2 * 9.81e-3 + STEEL_RIDGE.finishKNm2) * trib, sQ = STEEL_RIDGE.snowKNm2 * trib;
|
||||
const check = (s) => {
|
||||
const g = gRoof + s.kgM * 9.81e-3;
|
||||
const M = (STEEL_RIDGE.gammaG * g + STEEL_RIDGE.gammaQ * sQ) * L * L / 8;
|
||||
const util = M / (s.W * 1e3 * STEEL_RIDGE.fy);
|
||||
const defl = 5 * (g + sQ) * Math.pow(L, 4) / (384 * STEEL_RIDGE.E * s.I * 1e4);
|
||||
return { util, defl, deflMax: L / STEEL_RIDGE.deflection, ok: util <= 1 && defl <= L / STEEL_RIDGE.deflection };
|
||||
};
|
||||
let sec = IPE.find(s => check(s).ok);
|
||||
const ok = !!sec;
|
||||
if (!sec) sec = IPE[IPE.length - 1];
|
||||
const c = check(sec);
|
||||
const segs = pr.slice(0, -1).map((a, i) => {
|
||||
const b = pr[i + 1], sl = (b.h - a.h) / (b.p - a.p);
|
||||
return { at: (p) => a.h + (p - a.p) * sl, f: Math.sqrt(1 + sl * sl) };
|
||||
});
|
||||
const under = cd.underlayThick, jH = cd.joistH;
|
||||
const zone = (p, t) => Math.min(...segs.map(sg => sg.at(p) + t * sg.f));
|
||||
const yTop = Math.min(zone(R.p - sec.b / 2, under + jH), zone(R.p + sec.b / 2, under + jH));
|
||||
const yBottom = yTop - sec.h;
|
||||
const below = Math.max(0, Math.round(zone(R.p, 0) - yBottom));
|
||||
const fillW = (sec.b - sec.tw) / 2, fillH = sec.h - 2 * sec.tf;
|
||||
const fillM3 = 2 * fillW * fillH * L / 1e9;
|
||||
return {
|
||||
profile: sec, ok, util: c.util, defl: c.defl, deflMax: c.deflMax, loadKNm: gRoof + sQ + sec.kgM * 9.81e-3,
|
||||
p: R.p, q0, q1, length: Math.round(L), yTop, yBottom, below,
|
||||
steelKg: sec.kgM * L / 1000, fillW, fillH, fillM3, fillKg: fillM3 * DENSITY_WOOD_KGM3
|
||||
};
|
||||
}
|
||||
|
||||
function buildRidgeBeam3D(rb, xc, zc, alongX, m) {
|
||||
const toM = 0.001, s = rb.profile, L = rb.length * toM;
|
||||
const b = s.b * toM / 2, w = s.tw * toM / 2, h = s.h * toM, t = s.tf * toM;
|
||||
const shp = new THREE.Shape();
|
||||
[[-b, 0], [b, 0], [b, t], [w, t], [w, h - t], [b, h - t], [b, h], [-b, h], [-b, h - t], [-w, h - t], [-w, t], [-b, t]]
|
||||
.forEach(([x, y], i) => i ? shp.lineTo(x, y) : shp.moveTo(x, y));
|
||||
shp.closePath();
|
||||
const g = new THREE.Group();
|
||||
const beam = new THREE.Mesh(new THREE.ExtrudeGeometry(shp, { steps: 1, depth: L, bevelEnabled: false }), m.steelMaterial);
|
||||
beam.castShadow = beam.receiveShadow = true;
|
||||
g.add(beam);
|
||||
const fw = rb.fillW * toM, fh = rb.fillH * toM;
|
||||
[-1, 1].forEach(sd => {
|
||||
const fill = new THREE.Mesh(new THREE.BoxGeometry(fw, fh, L), m.woodMaterial);
|
||||
fill.position.set(sd * (w + fw / 2), t + fh / 2, L / 2);
|
||||
g.add(fill);
|
||||
});
|
||||
// Lokaal: x dwars, y omhoog, z langs de nok (vanaf q0).
|
||||
if (alongX) g.position.set((rb.p - xc) * toM, rb.yBottom * toM, (rb.q0 - zc) * toM);
|
||||
else { g.rotation.y = Math.PI / 2; g.position.set((rb.q0 - xc) * toM, rb.yBottom * toM, (rb.p - zc) * toM); }
|
||||
return g;
|
||||
}
|
||||
|
||||
function sumSpecs(list) {
|
||||
const out = Object.assign({}, list[0]);
|
||||
list.slice(1).forEach(sp => Object.keys(sp).forEach(k => {
|
||||
@@ -309,14 +348,6 @@ window.RoofModel = (() => {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Het dak van één unit: de dakrechthoek (unit-lokaal mm { x0, x1, z0, z1 },
|
||||
// = maat van roofState.deck) geknipt op de nok in dakvlakken. Per vlak een
|
||||
// eigen dek (vloer-state) met breedte = maat dwars op de helling en diepte
|
||||
// = lengte langs de helling, zodat de balken als sporen van laag naar hoog
|
||||
// lopen. rims: zie FloorModel.computeFrame (alleen bij helling langs Z).
|
||||
// Geeft { shape, planes, specs, undersideAt(x, z), topAt(x, z), ridges, maxTop }
|
||||
// (unit-lokaal, hoogtes vanaf het loopvlak). Vlak-posities (cx, cz) zijn
|
||||
// t.o.v. het midden van de dakrechthoek.
|
||||
function computeUnitRoof(roofState, shape, rect, rims) {
|
||||
const alongX = shape.axis === 'x';
|
||||
const W = rect.x1 - rect.x0, D = rect.z1 - rect.z0;
|
||||
@@ -328,7 +359,6 @@ window.RoofModel = (() => {
|
||||
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 => {
|
||||
const px = xc + o.x, pz = rect.z0 + o.y;
|
||||
return { o, px, pz, q: alongX ? px : pz };
|
||||
@@ -341,7 +371,7 @@ window.RoofModel = (() => {
|
||||
const plane = { p0, p1, h0, h1, theta: Math.atan2(h1 - h0, run), cos, yaw: alongX ? Math.PI / 2 : 0 };
|
||||
let data;
|
||||
if (cuts.length === 2 && !alongX && Math.abs(h1 - h0) < 0.5) {
|
||||
data = flat; // plat dak: het dek zoals het is
|
||||
data = flat;
|
||||
} else {
|
||||
const deck = clone(roofState.deck);
|
||||
deck.floor.width = Math.round(alongX ? D : W);
|
||||
@@ -359,16 +389,12 @@ window.RoofModel = (() => {
|
||||
return Object.assign(plane, {
|
||||
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 + bedekking) van dit vlak.
|
||||
vThick: thickness(data.calcData, covering) / cos
|
||||
});
|
||||
});
|
||||
|
||||
// Knik tussen twee vlakken met een verschillende helling (mansarde):
|
||||
// de bovenkanten snijden elkaar vóór (het steilere vlak is verticaal
|
||||
// dikker), dus de vlakken sluiten in verstek aan: onderkant op de knik,
|
||||
// bovenkant op het snijpunt van de bovenvlakken (miter = verschuiving
|
||||
// van de bovenkant t.o.v. de knik, mm langs de as; 0 = verticaal).
|
||||
// Mansardeknik: het steilere vlak is verticaal dikker, dus verstek: onderkant op de knik,
|
||||
// bovenkant op het snijpunt van de bovenvlakken.
|
||||
planes.forEach((pl, i) => { pl.miter0 = 0; pl.miter1 = 0; });
|
||||
planes.slice(0, -1).forEach((pi, i) => {
|
||||
const pj = planes[i + 1], ai = Math.tan(pi.theta), aj = Math.tan(pj.theta);
|
||||
@@ -377,58 +403,47 @@ window.RoofModel = (() => {
|
||||
pi.miter1 = dp; pj.miter0 = dp;
|
||||
});
|
||||
const mainUnder = (x, z) => H(alongX ? x : z);
|
||||
// Bovenkant: het laagste van de (doorgetrokken) bovenvlakken; de daklijn
|
||||
// is bol, dus dat is precies het dak, ook rond een knik.
|
||||
const mainTop = (x, z) => {
|
||||
const q = alongX ? x : z;
|
||||
return Math.min(...planes.map(pl => pl.h0 + (q - pl.p0) * Math.tan(pl.theta) + pl.vThick));
|
||||
};
|
||||
// Wolfsdak: de vlakken tussen de wolfseind-knikken worden geen
|
||||
// rechthoekige dekken; dat bovenste deel (cap, computeCap) is het laagste
|
||||
// van de twee dakvlakken en de twee wolfseinden, met één top.
|
||||
let cap = null, undersideAt = mainUnder, topAt = mainTop, outPlanes = planes;
|
||||
if (shape.type === 'wolf' && shape.hip && kinks.length === 3) {
|
||||
cap = computeCap(roofState, shape, rect, alongX, kinks, covering);
|
||||
outPlanes = planes.filter(pl => !(pl.p0 >= cap.pA - 0.5 && pl.p1 <= cap.pB + 0.5));
|
||||
const at = (x, z) => ({ x: alongX ? x : z, z: alongX ? z : x }); // (p, q)
|
||||
const pA = shape.type === 'wolf' && shape.hip ? shape.profile[1].p : 0, pB = pA && shape.profile[3].p;
|
||||
if (shape.type === 'wolf' && shape.hip && Math.min(pB, a1) - Math.max(pA, a0) > 1) cap = computeCap(roofState, shape, rect, alongX, covering);
|
||||
if (cap) {
|
||||
outPlanes = planes.filter(pl => !(pl.p0 >= pA - 0.5 && pl.p1 <= pB + 0.5));
|
||||
const at = (x, z) => ({ x: alongX ? x : z, z: alongX ? z : x });
|
||||
undersideAt = (x, z) => Math.min(mainUnder(x, z), ...cap.regions.map(rg => rg.under(at(x, z))));
|
||||
topAt = (x, z) => Math.min(mainTop(x, z), ...cap.regions.map(rg => rg.under(at(x, z)) + rg.vThick));
|
||||
}
|
||||
const specs = sumSpecs(outPlanes.map(pl => pl.specs).concat(cap ? [cap.specs] : []));
|
||||
const ridgeBeam = computeRidgeBeam(roofState, shape, rect, alongX, flat.calcData, specs.totalKg / Math.max(0.1, W * D / 1e6));
|
||||
Object.assign(specs, {
|
||||
nokStaalM: ridgeBeam ? ridgeBeam.length / 1000 : 0, nokStaalKg: ridgeBeam ? ridgeBeam.steelKg : 0,
|
||||
nokVulhoutM3: ridgeBeam ? ridgeBeam.fillM3 : 0, nokVulhoutKg: ridgeBeam ? ridgeBeam.fillKg : 0
|
||||
});
|
||||
specs.totalKg += specs.nokStaalKg + specs.nokVulhoutKg;
|
||||
return {
|
||||
shape, covering, planes: outPlanes, specs: sumSpecs(outPlanes.map(pl => pl.specs).concat(cap ? [cap.specs] : [])),
|
||||
shape, covering, planes: outPlanes, specs, ridgeBeam,
|
||||
undersideAt, topAt, cap, rect, alongX,
|
||||
ridges: kinks.map(value => ({ axis: shape.axis, value })),
|
||||
maxTop: cap ? cap.maxTop : Math.max(...planes.map(pl => Math.max(pl.h0, pl.h1) + pl.vThick))
|
||||
};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Bovenste deel van een wolfsdak (cap). Coördinaten (p, q) als { x: p,
|
||||
// z: q } (unit-lokaal mm): p langs de helling van het hoofddak, q langs de
|
||||
// nok. Band p = pA..pB (wolfseind-knikken), q = q0..q1 (de kopgevels).
|
||||
// Vier vlakken (onderkant dak): m1/m2 de twee dakvlakken, h1/h2 de
|
||||
// wolfseinden: vanaf de kopgevel op hipH met de dakhelling omhoog tot de
|
||||
// nokhoogte (driehoek), daartussen een rechte nok op pR. Is de unit te kort
|
||||
// voor een nokstuk, dan worden de wolfseinden steiler en lopen ze in één
|
||||
// punt samen. Per vlak het gebied waar het het laagste is (regions).
|
||||
// Houtwerk zoals de dekken: plafondplaat, sporen (dakvlakken: op dezelfde
|
||||
// q als de balken van het onderste dek; wolfseinden: verdeeld over pA..pB),
|
||||
// hoekkepers van elke hoek van de band naar het nokeinde, nokbalk, kopbalk
|
||||
// langs de voet van een wolfseind.
|
||||
// ------------------------------------------------------------
|
||||
const DENSITY_WOOD_KGM3 = 450, DENSITY_OSB_KGM3 = 650; // zelfde als FloorModel
|
||||
function computeCap(roofState, shape, rect, alongX, kinks, covering) {
|
||||
function computeCap(roofState, shape, rect, alongX, covering) {
|
||||
const H = (p) => profileHeight(shape.profile, p);
|
||||
const [pA, pR, pB] = kinks, hipH = shape.hip.h, top = H(pR);
|
||||
const q0 = alongX ? rect.z0 : rect.x0, q1 = alongX ? rect.z1 : rect.x1, qM = (q0 + q1) / 2;
|
||||
const uq0 = alongX ? rect.z0 : rect.x0, uq1 = alongX ? rect.z1 : rect.x1;
|
||||
const a0 = alongX ? rect.x0 : rect.z0, a1 = alongX ? rect.x1 : rect.z1;
|
||||
const hq = shape.hip.q0 !== undefined ? [shape.hip.q0, shape.hip.q1] : [uq0, uq1];
|
||||
const { pA, pR, pB, hipH, top, sA, sB, tH, inset, q0, q1, qM } = hipGeom(shape, hq[0], hq[1]);
|
||||
const fp = roofState.deck.floor;
|
||||
const joistW = fp.joistWidth || 70, joistH = fp.joistHeight || 220, underlay = fp.underlayThick || 22;
|
||||
const cov = COVERINGS[covering] || COVERINGS.dakleer;
|
||||
const sA = (top - H(pA)) / (pR - pA), sB = (top - H(pB)) / (pB - pR);
|
||||
// Helling wolfseinden: die van het dak, maar minstens zo steil dat ze de
|
||||
// nokhoogte halen vóór het midden tussen de kopgevels.
|
||||
const tH = Math.max((sA + sB) / 2, (top - hipH) / Math.max(1, qM - q0));
|
||||
const inset = (top - hipH) / tH; // nokeinde vanaf de kopgevel
|
||||
const cp0 = Math.max(pA, a0), cp1 = Math.min(pB, a1), cq0 = Math.max(q0, uq0), cq1 = Math.min(q1, uq1);
|
||||
const clipBox = (poly) => [(p) => p.x - cp0, (p) => cp1 - p.x, (p) => p.z - cq0, (p) => cq1 - p.z]
|
||||
.reduce((pl, f) => pl.length >= 3 ? clipPoly(pl, f) : pl, poly);
|
||||
const plane = (key, slope, f, along) => {
|
||||
const cos = Math.cos(Math.atan(slope));
|
||||
return { key, f, along, cos, tV: underlay / cos, jV: joistH / cos, bV: BESCHOT_MM / cos, covV: cov.thickMM / cos };
|
||||
@@ -443,12 +458,13 @@ window.RoofModel = (() => {
|
||||
const regions = fns.map((fi, i) => {
|
||||
let poly = band;
|
||||
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.bV + fi.covV });
|
||||
}).filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
|
||||
const clipped = regions.map(rg => Object.assign({}, rg, { poly: clipBox(rg.poly) }))
|
||||
.filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
|
||||
if (!clipped.some(rg => rg.along === 'q')) return null;
|
||||
|
||||
// Snijden van een lijn (vaste p of vaste q) met een convexe veelhoek.
|
||||
const cutLine = (poly, fixed, v) => {
|
||||
const ts = [];
|
||||
poly.forEach((a, k) => {
|
||||
@@ -459,16 +475,11 @@ window.RoofModel = (() => {
|
||||
});
|
||||
return ts.length >= 2 ? [Math.min(...ts), Math.max(...ts)] : null;
|
||||
};
|
||||
// Balken als veelhoek in de plattegrond (rechthoek van balkbreedte rond
|
||||
// de hartlijn A-B, afgezaagd tegen de balken waar hij op aansluit) met
|
||||
// de onderkant op het dakvlak (yAt: + plafondplaat). Lengte = langs de
|
||||
// balk, hoek = helling van de balk.
|
||||
const hw = joistW / 2;
|
||||
const rectAround = (A, B) => {
|
||||
const L = Math.hypot(B.x - A.x, B.z - A.z), nx = -(B.z - A.z) / L * hw, nz = (B.x - A.x) / L * hw;
|
||||
return [{ x: A.x - nx, z: A.z - nz }, { x: B.x - nx, z: B.z - nz }, { x: B.x + nx, z: B.z + nz }, { x: A.x + nx, z: A.z + nz }];
|
||||
};
|
||||
// Halfvlak aan de kant van `from` naast het zijvlak van balk m (>= 0 blijft).
|
||||
const awayFrom = (m, from) => {
|
||||
const L = Math.hypot(m.B.x - m.A.x, m.B.z - m.A.z), nx = -(m.B.z - m.A.z) / L, nz = (m.B.x - m.A.x) / L;
|
||||
const d = (p) => (p.x - m.A.x) * nx + (p.z - m.A.z) * nz;
|
||||
@@ -488,7 +499,6 @@ window.RoofModel = (() => {
|
||||
if (poly.length < 3) return null;
|
||||
const dx = B.x - A.x, dz = B.z - A.z, L = Math.hypot(dx, dz);
|
||||
const yA = rg.under(A) + rg.tV, yB = rg.under(B) + rg.tV;
|
||||
// Hartlijnbalk (hoekkeper, nokbalk): hoogte langs de hartlijn; anders op het vlak.
|
||||
const yAt = lineHeight
|
||||
? (p) => yA + (yB - yA) * Math.max(0, Math.min(1, ((p.x - A.x) * dx + (p.z - A.z) * dz) / (L * L)))
|
||||
: (p) => rg.under(p) + rg.tV;
|
||||
@@ -496,16 +506,12 @@ window.RoofModel = (() => {
|
||||
const plan = Math.max(...ts) - Math.min(...ts), slope = Math.abs(yB - yA) / L;
|
||||
if (plan < 50) return null;
|
||||
const bm = {
|
||||
kind, A, B, poly, yAt, jV: rg.jV,
|
||||
kind, A, B, poly, yAt, jV: rg.jV, slope,
|
||||
length: Math.round(plan * Math.sqrt(1 + slope * slope)), angle: Math.round(Math.atan(slope) * 1800 / Math.PI) / 10
|
||||
};
|
||||
beams.push(bm);
|
||||
return bm;
|
||||
};
|
||||
// Raamwerk: kopbalken (binnen de kopgevel, op de bovenregel), hoekkepers
|
||||
// van elke hoek van de band naar het nokeinde, nokbalk ertussen. De
|
||||
// hoekkepers staan op de kopbalk en sluiten bovenin in verstek op elkaar
|
||||
// aan; de nokbalk is afgezaagd tegen de hoekkepers.
|
||||
const mOf = (x) => regions.find(rg => rg.key === (x < pR ? 'm1' : 'm2')) || regions[0];
|
||||
const hOf = (z) => regions.find(rg => rg.key === (z < qM ? 'h1' : 'h2')) || regions[0];
|
||||
const ends = [{ x: pR, z: Math.min(qM, q0 + inset) }, { x: pR, z: Math.max(qM, q1 - inset) }];
|
||||
@@ -516,7 +522,6 @@ window.RoofModel = (() => {
|
||||
const e = c.z < qM ? ends[0] : ends[1], sp = Math.sign(pR - c.x), sq = Math.sign(qM - c.z);
|
||||
const clips = [(p) => sp * (pR - p.x)];
|
||||
if (!hasRidge) clips.push((p) => sq * (qM - p.z));
|
||||
// Op de kopbalk: vanaf zijn binnenkant.
|
||||
clips.push((p) => sq * (c.z + sq * joistW - p.z) * -1);
|
||||
return addBeam('hoekkeper', mOf(c.x), c, e, clips, true);
|
||||
}).filter(Boolean);
|
||||
@@ -524,9 +529,11 @@ window.RoofModel = (() => {
|
||||
if (hasRidge) {
|
||||
const mid = { x: pR, z: qM };
|
||||
ridgeBeam = addBeam('nokbalk', mOf(pR - 1), ends[0], ends[1], hips.map(h => awayFrom(h, mid)), true);
|
||||
// Stalen nokbalk (computeRidgeBeam) in plaats van hout: wel om de sporen
|
||||
// tegen af te zagen, niet als houten balk.
|
||||
if (ridgeBeam && roofState.steelRidge !== false) beams.splice(beams.indexOf(ridgeBeam), 1);
|
||||
}
|
||||
const members = kops.concat(hips, ridgeBeam ? [ridgeBeam] : []);
|
||||
// Sporen: afgezaagd tegen de balk(en) waar ze op uitkomen.
|
||||
const spread = (len) => FloorModel.computeFrame({ width: len, depth: 1000, joistWidth: joistW, joistHeight: joistH, minSpacing: fp.minSpacing, maxSpacing: fp.maxSpacing, underlayThick: underlay }).joists.map(x => x + len / 2);
|
||||
const qs = spread(q1 - q0).map(v => q0 + v), ps = spread(pB - pA).map(v => pA + v);
|
||||
regions.forEach(rg => {
|
||||
@@ -541,10 +548,19 @@ window.RoofModel = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
// Specificaties (zelfde sleutels als computeSpecs).
|
||||
const ownBeams = beams.map(bm => {
|
||||
const poly = clipBox(bm.poly);
|
||||
if (poly.length < 3 || quadArea(poly) < 100) return null;
|
||||
const dx = bm.B.x - bm.A.x, dz = bm.B.z - bm.A.z, L = Math.hypot(dx, dz);
|
||||
const ts = poly.map(p => ((p.x - bm.A.x) * dx + (p.z - bm.A.z) * dz) / L);
|
||||
const plan = Math.max(...ts) - Math.min(...ts), slope = bm.slope;
|
||||
if (plan < 50) return null;
|
||||
return Object.assign({}, bm, { poly, length: Math.round(plan * Math.sqrt(1 + slope * slope)) });
|
||||
}).filter(Boolean);
|
||||
|
||||
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 woodM3 = ownBeams.reduce((t, bm) => t + bm.length * 1e-3 * W * Hh, 0);
|
||||
const areaM2 = clipped.reduce((t, rg) => t + quadArea(rg.poly) / rg.cos, 0) / 1e6;
|
||||
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,
|
||||
@@ -553,25 +569,20 @@ window.RoofModel = (() => {
|
||||
voidM3: Math.max(0, areaM2 * Hh - woodM3),
|
||||
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 };
|
||||
const maxTop = Math.max(...clipped.map(rg => Math.max(...rg.poly.map(pt => rg.under(pt) + rg.vThick))));
|
||||
return { pA: cp0, pR, pB: cp1, q0: cq0, q1: cq1, hipH, slope: sA, joistW, joistH, regions: clipped, beams: ownBeams, specs, maxTop };
|
||||
}
|
||||
|
||||
// 3D van het dak van één unit (meters, gecentreerd op de dakrechthoek,
|
||||
// Y = 0 op het loopvlak): per dakvlak build3D, zo vervormd dat de
|
||||
// onderkant van het plafond precies op de daklijn van de wanden ligt.
|
||||
// Geen kanteling maar een afschuiving (lokaal +Z = oplopende as):
|
||||
// plan = z·cos θ, Y = y/cos θ + z·sin θ
|
||||
// De onderkant ligt dan op de helling, de dikte haaks op de helling blijft
|
||||
// gelijk (verticaal = dikte/cos θ, zoals vThick), de lengte langs de
|
||||
// helling ook (balklengtes), en de kopse kanten staan loodrecht (zoals een
|
||||
// loodrechte zaagsnede aan sporen). Bij kantelen zouden de kopse kanten
|
||||
// haaks op de helling staan en ligt de bovenkant aan de hoge kant
|
||||
// dikte·sin θ terug van de dakrand.
|
||||
// Afschuiving i.p.v. kanteling (lokaal +Z = oplopende as): plan = z·cos θ, Y = y/cos θ + z·sin θ.
|
||||
// Zo blijven dikte haaks op de helling en lengte gelijk, en staan de kopse kanten loodrecht (zoals de zaagsnede aan sporen).
|
||||
function buildUnit3D(unitRoof, opts) {
|
||||
const toM = 0.001;
|
||||
const group = new THREE.Group();
|
||||
if (unitRoof.cap) group.add(buildCap3D(unitRoof, opts));
|
||||
if (unitRoof.ridgeBeam) {
|
||||
const r = unitRoof.rect;
|
||||
group.add(buildRidgeBeam3D(unitRoof.ridgeBeam, (r.x0 + r.x1) / 2, (r.z0 + r.z1) / 2, unitRoof.alongX, opts.materials));
|
||||
}
|
||||
unitRoof.planes.forEach(pl => {
|
||||
const g = new THREE.Group();
|
||||
g.position.set(pl.cx * toM, pl.y * toM, pl.cz * toM);
|
||||
@@ -593,10 +604,6 @@ window.RoofModel = (() => {
|
||||
return group;
|
||||
}
|
||||
|
||||
// Verstek op de uiteinden van een dek (vóór de afschuiving; lokaal z
|
||||
// langs de helling, -L/2 .. L/2, y = 0 onderkant .. T bovenkant): elk
|
||||
// hoekpunt op een uiteinde schuift met de hoogte mee naar de verstekhoek
|
||||
// (pl.miter0 / miter1 in mm langs de as, omgerekend langs de helling).
|
||||
function miterDeck(deck, pl, cos) {
|
||||
const toM = 0.001, cd = pl.data.calcData;
|
||||
const L = cd.depth * toM, T = pl.vThick * cos * toM;
|
||||
@@ -624,11 +631,6 @@ window.RoofModel = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// Bovenste deel van een wolfsdak (unitRoof.cap, zie computeCap): per
|
||||
// hellend vlak plafondplaat en bedekking (verticale dikte, UV langs de
|
||||
// helling), sporen/hoekkepers/kopbalken als balken met verticale kopse
|
||||
// kanten (zoals de afgeschoven dekken). Plaat én bedekking zichtbaar:
|
||||
// alleen de kopbalken (de rest is ingesloten).
|
||||
function capRegions(unitRoof) { return unitRoof.cap ? unitRoof.cap.regions : []; }
|
||||
function buildCap3D(unitRoof, opts) {
|
||||
const toM = 0.001, m = opts.materials, c = unitRoof.cap, r = unitRoof.rect;
|
||||
@@ -636,18 +638,15 @@ window.RoofModel = (() => {
|
||||
const group = new THREE.Group();
|
||||
const osbOn = opts.osbVisible !== false, covOn = opts.dakleerVisible !== false;
|
||||
const covMat = coveringMaterial(unitRoof.covering) || m.dakleerMaterial;
|
||||
// (p, q) -> wereld (m, t.o.v. het dakmidden).
|
||||
const P = (pt, y) => {
|
||||
const x = unitRoof.alongX ? pt.x : pt.z, z = unitRoof.alongX ? pt.z : pt.x;
|
||||
return [(x - xc) * toM, y * toM, (z - zc) * toM];
|
||||
};
|
||||
// Plaat onder een convexe veelhoek: onderkant bottom(pt), dikte h (verticaal).
|
||||
function slab(poly, bottom, h, topMat, sideMat, uvOf) {
|
||||
const n = poly.length, pos = [], uv = [];
|
||||
const T = poly.map(pt => P(pt, bottom(pt) + h)), B = poly.map(pt => P(pt, bottom(pt)));
|
||||
const UV = poly.map(pt => uvOf ? uvOf(pt) : [0, 0]), z2 = [0, 0];
|
||||
const tri = (a, b, cc, ua, ub, uc) => { pos.push(...a, ...b, ...cc); uv.push(...ua, ...ub, ...uc); };
|
||||
// Oriëntatie zo dat de bovenkant naar boven wijst.
|
||||
const up = (T[1][0] - T[0][0]) * (T[2][2] - T[0][2]) - (T[1][2] - T[0][2]) * (T[2][0] - T[0][0]) < 0;
|
||||
for (let k = 1; k < n - 1; k++) up ? tri(T[0], T[k], T[k + 1], UV[0], UV[k], UV[k + 1]) : tri(T[0], T[k + 1], T[k], UV[0], UV[k + 1], UV[k]);
|
||||
const topCount = pos.length / 3;
|
||||
@@ -669,26 +668,18 @@ window.RoofModel = (() => {
|
||||
group.add(mesh);
|
||||
}
|
||||
c.regions.forEach(rg => {
|
||||
if (osbOn) slab(rg.poly, rg.under, rg.tV, m.osbMaterial, m.osbMaterial);
|
||||
if (osbOn) slab(rg.poly, (pt) => rg.under(pt) + rg.tV + rg.jV, rg.bV, m.osbMaterial, m.osbMaterial); // dakbeschot
|
||||
if (osbOn) slab(rg.poly, rg.under, rg.tV, opts.ceilingMaterial || m.osbMaterial, m.osbMaterial);
|
||||
if (osbOn) slab(rg.poly, (pt) => rg.under(pt) + rg.tV + rg.jV, rg.bV, m.osbMaterial, m.osbMaterial);
|
||||
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 && bm.kind !== 'kopbalk') return; // ingesloten tussen plafondplaat en dakbeschot
|
||||
if (osbOn && bm.kind !== 'kopbalk') return;
|
||||
slab(bm.poly, bm.yAt, bm.jV, m.woodMaterial, m.woodMaterial);
|
||||
});
|
||||
return group;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Dakrand over de hele woning
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Splits de ribben van de omtrek op elke hoek van een dak-rechthoek die op
|
||||
// die ribbe ligt ("hoek van een unit"), zodat spots daar ook beginnen/eindigen.
|
||||
function splitAtCorners(pts, rects) {
|
||||
const corners = [];
|
||||
rects.forEach(r => corners.push({ x: r.x0, z: r.z0 }, { x: r.x1, z: r.z0 }, { x: r.x1, z: r.z1 }, { x: r.x0, z: r.z1 }));
|
||||
@@ -717,28 +708,14 @@ window.RoofModel = (() => {
|
||||
return Math.abs(pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + p.x * q.z - q.x * p.z; }, 0) / 2);
|
||||
}
|
||||
|
||||
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool, topAt(x, z),
|
||||
// ridges: [{ axis, value }] }] (mm, woning-assen); topAt = bovenkant
|
||||
// dakleer (absoluut, loopt buiten het dak door met de helling). Geeft per
|
||||
// ribbe (segments) de geometrie terug; edge.spots wordt door LightingModel
|
||||
// gevuld (met y = onderkant boeideel ter plaatse).
|
||||
// facadeDepth(a, b, normal) (optioneel, mm): dikte van de gevelafwerking
|
||||
// (doek + latwerk, steen) buiten het wandvlak langs die ribbe. De gevel
|
||||
// loopt achter het boeideel door tot de bovenkant van het dak; het
|
||||
// boeideel / de daktrim valt er overheen en sluit aan de dakkant aan op
|
||||
// het dak (binnenrand = dakrand, van boven dicht). De spots komen in de
|
||||
// onderplaat vóór de gevel. Negatief = gevel van een ander (hoger) deel
|
||||
// die naar dit dak toe kijkt: de rand stopt tegen die afgewerkte gevel.
|
||||
// facadeDepth negatief = gevel van een hoger deel die naar dit dak kijkt: de rand stopt tegen die gevel.
|
||||
function computeEdge(settings, roofs, facadeDepth) {
|
||||
const s = normalizeSettings(settings);
|
||||
const rects = roofs.map(r => r.rect);
|
||||
if (!roofs.length) return null;
|
||||
|
||||
// Hoogtegroepen: daken die tegen elkaar liggen horen bij één doorlopende
|
||||
// rand, tenzij het hoogteverschil langs de hele naad minstens de
|
||||
// boeideelhoogte is. Dan krijgt het hoge dak een eigen rand rondom (ook
|
||||
// boven het lage dak), en loopt de rand van het lage dak tot de gevel
|
||||
// van het hoge.
|
||||
// Daken die tegen elkaar liggen delen één rand, tenzij het hoogteverschil langs de hele naad
|
||||
// minstens de boeideelhoogte is.
|
||||
const group = roofs.map((r, i) => i);
|
||||
const find = (i) => group[i] === i ? i : (group[i] = find(group[i]));
|
||||
const sharedEdge = (A, B) => {
|
||||
@@ -778,22 +755,16 @@ window.RoofModel = (() => {
|
||||
const owner = members.find(r => onRectEdge(r.rect, mid)) || members[0];
|
||||
const fascia = owner.fascia !== false;
|
||||
const off = fascia ? offBoei : offTrim;
|
||||
// Ribbe tegen een dak van een andere groep: tegen een hoger dak
|
||||
// vervalt hij (daar staat de gevel van het hoge), boven een lager
|
||||
// dak blijft hij, maar zonder spots.
|
||||
// Tegen een hoger dak vervalt de ribbe (daar staat de gevel), boven een lager dak blijft hij zonder spots.
|
||||
const nb = others.find(r => onRectEdge(r.rect, mid));
|
||||
const dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
|
||||
return {
|
||||
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
|
||||
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
|
||||
// Bovenkant van de rand (dakleer + opstand).
|
||||
topAt: (p) => owner.topAt(p.x, p.z) + EDGE_ABOVE_MM, ridges: owner.ridges || [],
|
||||
overRoof: !!nb && !dropped, dropped
|
||||
};
|
||||
});
|
||||
// Binnenrand: alleen tegen de gevel van een hoger deel (negatieve
|
||||
// diepte) inkorten; spotlijn vóór de gevel. Op een hoek van twee haakse
|
||||
// ribben het snijpunt van beide verschoven lijnen, anders loodrecht.
|
||||
const depRaw = segs.map(sg => (facadeDepth ? facadeDepth(sg.a, sg.b, sg.normal) || 0 : 0));
|
||||
const dep = depRaw.map(d => Math.min(0, d));
|
||||
const spotD = depRaw.map(d => Math.max(0, d) + (s.overhang - Math.max(0, d)) * SPOT_RATIO);
|
||||
@@ -810,12 +781,6 @@ window.RoofModel = (() => {
|
||||
});
|
||||
segs.forEach((sg, i) => Object.assign(sg, shifted[i], { facadeDepth: depRaw[i] }));
|
||||
|
||||
// Naast een vervallen ribbe (tegen een hoger dak):
|
||||
// - ligt het verlengde van de rand in het hoge deel, dan loopt hij
|
||||
// recht door tot die gevel (spot op spotafstand van de gevel);
|
||||
// - ligt het verlengde ernaast (de rand loopt langs de zijgevel van
|
||||
// het hoge deel), dan loopt hij recht door tot de overstek, onder
|
||||
// de rand van het hoge dak (van bovenaf overlappen ze).
|
||||
const insideOther = (p) => others.some(r => p.x > r.rect.x0 + 0.5 && p.x < r.rect.x1 - 0.5 && p.z > r.rect.z0 + 0.5 && p.z < r.rect.z1 - 0.5);
|
||||
segs.forEach((sg, i) => {
|
||||
if (sg.dropped) return;
|
||||
@@ -851,8 +816,6 @@ window.RoofModel = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
// Convexe veelhoek (plattegrond, mm) knippen met een halfvlak
|
||||
// (Sutherland-Hodgman): houd de punten waar f(p) >= 0.
|
||||
function clipPoly(poly, f) {
|
||||
const out = [];
|
||||
poly.forEach((p, k) => {
|
||||
@@ -866,8 +829,6 @@ window.RoofModel = (() => {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Prisma onder een convexe veelhoek: bovenkant per hoekpunt op topFn(p),
|
||||
// onderkant h lager (mm -> meters).
|
||||
function prismGeometry(poly, topFn, h) {
|
||||
const toM = 0.001, n = poly.length, pos = [];
|
||||
const T = poly.map(p => [p.x * toM, topFn(p) * toM, p.z * toM]);
|
||||
@@ -881,17 +842,12 @@ window.RoofModel = (() => {
|
||||
return geo;
|
||||
}
|
||||
|
||||
// 3D van de dakrand: per ribbe een vierhoek (dakrand -> buitenrand, in
|
||||
// verstek), naar beneden over de hoogte van boeideel resp. daktrim. De
|
||||
// bovenkant volgt het dak (sg.topAt); op een nok wordt de ribbe geknipt.
|
||||
// Plus de boeideel-spots (edge.spots) in de onderplaat.
|
||||
// opts: { edgeVisible (volgt "Beplating"), spotsVisible (volgt "Lampen") }
|
||||
function buildEdge3D(edge, materials, opts) {
|
||||
const o = opts || {};
|
||||
const toM = 0.001;
|
||||
const group = new THREE.Group();
|
||||
if (!edge) return group;
|
||||
// Het boeidelen-materiaal wordt gedeeld: kleur volgens de instelling.
|
||||
// Gedeeld materiaal: 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]];
|
||||
@@ -899,8 +855,6 @@ window.RoofModel = (() => {
|
||||
const f = (p) => r.axis === 'x' ? p.x - r.value : p.z - r.value;
|
||||
polys = polys.flatMap(pl => [clipPoly(pl, f), clipPoly(pl, p => -f(p))]).filter(pl => pl.length >= 3);
|
||||
});
|
||||
// Boeideel: totale hoogte fasciaHeight vanaf de bovenkant van de rand;
|
||||
// daktrim: trimHeight onder het dakleer + de opstand erboven.
|
||||
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
|
||||
polys.forEach(pl => {
|
||||
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
|
||||
@@ -914,6 +868,7 @@ window.RoofModel = (() => {
|
||||
const spot = new THREE.Mesh(new THREE.CylinderGeometry(spotR, spotR, spotH, 16), materials.spotOnMaterial);
|
||||
spot.position.set(p.x * toM, p.y * toM - spotH / 2 + 0.002, p.z * toM);
|
||||
spot.visible = o.spotsVisible !== false;
|
||||
spot.userData = { ref: p, glow: true };
|
||||
group.add(spot);
|
||||
});
|
||||
return group;
|
||||
@@ -923,6 +878,8 @@ 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
|
||||
const h = s.house || {};
|
||||
s.house = { enabled: !!h.enabled, form: h.form && UnitModel.ROOF_TYPES[h.form.type] ? Object.assign({}, h.form) : null, cover: !!h.cover };
|
||||
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
|
||||
return s;
|
||||
}
|
||||
@@ -930,7 +887,7 @@ window.RoofModel = (() => {
|
||||
return {
|
||||
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,
|
||||
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, buildUnit3D, capRegions,
|
||||
computeEdge, buildEdge3D, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user