v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
+315
-24
@@ -13,8 +13,9 @@
|
||||
// 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 middenpunt (nok) zijn de kopgevels, het dak
|
||||
// volgt hun daklijn (plat, lessenaar of zadel). UnitModel.syncWallHeights
|
||||
// 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.
|
||||
@@ -139,8 +140,10 @@ window.RoofModel = (() => {
|
||||
const m = opts.materials;
|
||||
const toM = 0.001;
|
||||
const group = new THREE.Group();
|
||||
// Plafondplaat én bedekking zichtbaar: de sporen ertussen zijn ingesloten.
|
||||
const deck = FloorModel.build3D(calcData, resolvedOpenings, { wheels: [] }, {
|
||||
materials: m, osbVisible: opts.osbVisible !== false
|
||||
materials: m, osbVisible: opts.osbVisible !== false,
|
||||
outerJoistsOnly: opts.osbVisible !== false && opts.dakleerVisible !== false
|
||||
});
|
||||
const deckH = (calcData.joistH + calcData.underlayThick) * toM;
|
||||
deck.scale.y = -1;
|
||||
@@ -169,8 +172,7 @@ window.RoofModel = (() => {
|
||||
leer.rotation.x = Math.PI / 2;
|
||||
leer.position.y = deckH + cov.thickMM * toM;
|
||||
leer.castShadow = cov.thickMM > DAKLEER_MM;
|
||||
leer.visible = opts.dakleerVisible !== false;
|
||||
group.add(leer);
|
||||
if (opts.dakleerVisible !== false) group.add(leer);
|
||||
return group;
|
||||
}
|
||||
|
||||
@@ -202,22 +204,41 @@ window.RoofModel = (() => {
|
||||
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 }];
|
||||
if (w.heightMid > 0) pts.push({ p: c, h: Math.max(w.heightMid, w.heightLeft, 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 }));
|
||||
return pts.sort((a, b) => a.p - b.p);
|
||||
}
|
||||
|
||||
// Dakvorm van een unit. layout = UnitModel.computeLayout-resultaat.
|
||||
// Geeft { type: 'plat'|'lessenaar'|'zadel', axis: 'x'|'z', profile }:
|
||||
// de hoogte van de dakonderkant (vanaf het loopvlak) hangt alleen af van
|
||||
// de positie langs axis (profileHeight). 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.
|
||||
// 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)) {
|
||||
return { type: profile.length > 2 ? 'zadel' : 'lessenaar', axis, profile };
|
||||
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 };
|
||||
}
|
||||
const h = Math.max(...Object.values(unitState.walls).map(ws => UnitModel.wallTopOf(ws)));
|
||||
return { type: 'plat', axis: 'z', profile: [{ p: 0, h }] };
|
||||
@@ -297,22 +318,200 @@ window.RoofModel = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
const planeAt = (q) => planes.find((pl, i) => q < pl.p1 || i === planes.length - 1);
|
||||
const undersideAt = (x, z) => H(alongX ? x : z);
|
||||
const topAt = (x, z) => {
|
||||
// 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).
|
||||
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);
|
||||
if (Math.abs(ai - aj) < 1e-4) return;
|
||||
const dp = (pj.vThick - pi.vThick) / (ai - aj);
|
||||
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;
|
||||
const onKink = planes.filter(pl => Math.abs(q - pl.p0) < 0.5 || Math.abs(q - pl.p1) < 0.5);
|
||||
const t = onKink.length > 1 ? Math.max(...onKink.map(pl => pl.vThick)) : planeAt(q).vThick;
|
||||
return H(q) + t;
|
||||
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)
|
||||
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));
|
||||
}
|
||||
return {
|
||||
shape, covering, planes, specs: sumSpecs(planes.map(pl => pl.specs)),
|
||||
undersideAt, topAt,
|
||||
shape, covering, planes: outPlanes, specs: sumSpecs(outPlanes.map(pl => pl.specs).concat(cap ? [cap.specs] : [])),
|
||||
undersideAt, topAt, cap, rect, alongX,
|
||||
ridges: kinks.map(value => ({ axis: shape.axis, value })),
|
||||
maxTop: Math.max(...planes.map(pl => Math.max(pl.h0, pl.h1) + pl.vThick))
|
||||
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) {
|
||||
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 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 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 };
|
||||
};
|
||||
const fns = [
|
||||
plane('m1', sA, (p) => H(pA) + (p.x - pA) * sA, 'p'),
|
||||
plane('m2', sB, (p) => H(pB) + (pB - p.x) * sB, 'p'),
|
||||
plane('h1', tH, (p) => hipH + (p.z - q0) * tH, 'q'),
|
||||
plane('h2', tH, (p) => hipH + (q1 - p.z) * tH, 'q')
|
||||
];
|
||||
const band = [{ x: pA, z: q0 }, { x: pB, z: q0 }, { x: pB, z: q1 }, { x: pA, z: q1 }];
|
||||
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.covV });
|
||||
}).filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
|
||||
|
||||
// Snijden van een lijn (vaste p of vaste q) met een convexe veelhoek.
|
||||
const cutLine = (poly, fixed, v) => {
|
||||
const ts = [];
|
||||
poly.forEach((a, k) => {
|
||||
const b = poly[(k + 1) % poly.length], av = a[fixed], bv = b[fixed];
|
||||
if ((av - v) * (bv - v) > 0 || Math.abs(bv - av) < 1e-9) return;
|
||||
const t = (v - av) / (bv - av), o = fixed === 'x' ? 'z' : 'x';
|
||||
ts.push(a[o] + (b[o] - a[o]) * t);
|
||||
});
|
||||
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;
|
||||
const sgn = d(from) >= 0 ? 1 : -1;
|
||||
return (p) => sgn * d(p) - hw;
|
||||
};
|
||||
const distToSeg = (p, m) => {
|
||||
const dx = m.B.x - m.A.x, dz = m.B.z - m.A.z, L2 = dx * dx + dz * dz;
|
||||
const t = Math.max(0, Math.min(1, ((p.x - m.A.x) * dx + (p.z - m.A.z) * dz) / L2));
|
||||
return Math.hypot(p.x - (m.A.x + dx * t), p.z - (m.A.z + dz * t));
|
||||
};
|
||||
const beams = [];
|
||||
const addBeam = (kind, rg, A, B, clips, lineHeight) => {
|
||||
if (Math.hypot(B.x - A.x, B.z - A.z) < 100) return null;
|
||||
let poly = rectAround(A, B);
|
||||
(clips || []).forEach(f => { if (poly.length >= 3) poly = clipPoly(poly, f); });
|
||||
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;
|
||||
const ts = poly.map(p => ((p.x - A.x) * dx + (p.z - A.z) * dz) / L);
|
||||
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,
|
||||
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) }];
|
||||
const hasRidge = ends[1].z - ends[0].z >= 100;
|
||||
const kops = [q0 + hw, q1 - hw].map(qv => addBeam('kopbalk', hOf(qv), { x: pA, z: qv }, { x: pB, z: qv }, [
|
||||
(p) => p.z - q0 - 0.01, (p) => q1 - p.z - 0.01])).filter(Boolean);
|
||||
const hips = band.map(c => {
|
||||
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);
|
||||
let ridgeBeam = null;
|
||||
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);
|
||||
}
|
||||
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 => {
|
||||
const fixed = rg.along === 'p' ? 'z' : 'x';
|
||||
(rg.along === 'p' ? qs : ps).forEach(v => {
|
||||
const span = cutLine(rg.poly, fixed, v);
|
||||
if (!span) return;
|
||||
const pt = (t) => fixed === 'z' ? { x: t, z: v } : { x: v, z: t };
|
||||
const A = pt(span[0]), B = pt(span[1]), mid = pt((span[0] + span[1]) / 2);
|
||||
const clips = members.filter(m => distToSeg(A, m) < hw + 1 || distToSeg(B, m) < hw + 1).map(m => awayFrom(m, mid));
|
||||
addBeam('spoor', rg, A, B, clips);
|
||||
});
|
||||
});
|
||||
|
||||
// Specificaties (zelfde sleutels als computeSpecs).
|
||||
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 = {
|
||||
joistWoodM3: woodM3, joistWoodKg: woodM3 * DENSITY_WOOD_KGM3,
|
||||
osbAreaM2: areaM2, osbVolM3: osbVol, osbKg: osbVol * 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
|
||||
};
|
||||
const maxTop = top + Math.max(...regions.map(rg => rg.vThick));
|
||||
return { pA, pR, pB, q0, q1, hipH, joistW, joistH, regions, beams, 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.
|
||||
@@ -327,6 +526,7 @@ window.RoofModel = (() => {
|
||||
function buildUnit3D(unitRoof, opts) {
|
||||
const toM = 0.001;
|
||||
const group = new THREE.Group();
|
||||
if (unitRoof.cap) group.add(buildCap3D(unitRoof, opts));
|
||||
unitRoof.planes.forEach(pl => {
|
||||
const g = new THREE.Group();
|
||||
g.position.set(pl.cx * toM, pl.y * toM, pl.cz * toM);
|
||||
@@ -339,13 +539,104 @@ window.RoofModel = (() => {
|
||||
0, 1 / c, sn, 0,
|
||||
0, 0, c, 0,
|
||||
0, 0, 0, 1);
|
||||
shear.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, Object.assign({ covering: unitRoof.covering }, opts)));
|
||||
const deck = build3D(pl.data.calcData, pl.data.resolvedOpenings, Object.assign({ covering: unitRoof.covering }, opts));
|
||||
if (pl.miter0 || pl.miter1) miterDeck(deck, pl, c);
|
||||
shear.add(deck);
|
||||
g.add(shear);
|
||||
group.add(g);
|
||||
});
|
||||
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;
|
||||
const d0 = pl.miter0 * toM / cos, d1 = pl.miter1 * toM / cos;
|
||||
const v = new THREE.Vector3(), rel = new THREE.Matrix4(), inv = new THREE.Matrix4();
|
||||
deck.updateMatrixWorld(true);
|
||||
const base = new THREE.Matrix4().copy(deck.matrixWorld).invert();
|
||||
deck.traverse(o => {
|
||||
const pos = o.geometry && o.geometry.attributes && o.geometry.attributes.position;
|
||||
if (!pos) return;
|
||||
rel.multiplyMatrices(base, o.matrixWorld);
|
||||
inv.copy(rel).invert();
|
||||
for (let k = 0; k < pos.count; k++) {
|
||||
v.fromBufferAttribute(pos, k).applyMatrix4(rel);
|
||||
const t = Math.max(0, Math.min(1, v.y / T));
|
||||
if (d1 && v.z > L / 2 - 1e-4) v.z = L / 2 + d1 * t;
|
||||
else if (d0 && v.z < -L / 2 + 1e-4) v.z = -L / 2 + d0 * t;
|
||||
else continue;
|
||||
v.applyMatrix4(inv);
|
||||
pos.setXYZ(k, v.x, v.y, v.z);
|
||||
}
|
||||
pos.needsUpdate = true;
|
||||
o.geometry.computeBoundingSphere();
|
||||
if (o.geometry.attributes.normal) o.geometry.computeVertexNormals();
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
const xc = (r.x0 + r.x1) / 2, zc = (r.z0 + r.z1) / 2;
|
||||
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;
|
||||
for (let k = 1; k < n - 1; k++) up ? tri(B[0], B[k + 1], B[k], z2, z2, z2) : tri(B[0], B[k], B[k + 1], z2, z2, z2);
|
||||
for (let k = 0; k < n; k++) {
|
||||
const j = (k + 1) % n;
|
||||
up ? (tri(T[k], B[k], B[j], z2, z2, z2), tri(T[k], B[j], T[j], z2, z2, z2))
|
||||
: (tri(T[k], B[j], B[k], z2, z2, z2), tri(T[k], T[j], B[j], z2, z2, z2));
|
||||
}
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||
geo.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
|
||||
geo.addGroup(0, topCount, 0);
|
||||
geo.addGroup(topCount, pos.length / 3 - topCount, 1);
|
||||
geo.computeVertexNormals();
|
||||
const mesh = new THREE.Mesh(geo, [topMat, sideMat]);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
group.add(mesh);
|
||||
}
|
||||
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,
|
||||
(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;
|
||||
slab(bm.poly, bm.yAt, bm.jV, m.woodMaterial, m.woodMaterial);
|
||||
});
|
||||
return group;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Dakrand over de hele woning
|
||||
// ------------------------------------------------------------
|
||||
@@ -593,7 +884,7 @@ window.RoofModel = (() => {
|
||||
return {
|
||||
DAKLEER_MM, EDGE_ABOVE_MM, RIET, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor, coveringThumbUrl,
|
||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D,
|
||||
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D, capRegions,
|
||||
computeEdge, buildEdge3D, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user