v0.6.2: dakaansluiting L/T (vleugel met nokdak loopt het hoofddak in met kilkepers, of de kop wordt een schild met hoekkeper; eigen dakdeel en PDF-blad per unit, ook in de kosten), model IJssel (L-vorm met zadeldaken in riet), unit max 4,7 m breed; mansarde vervallen (oude ontwerpen worden een zadeldak met dezelfde nok); gevelbekleding volgt voorbij het wandeinde de daklijn (geen blok meer boven een steil dakvlak) en bij een bolle daklijn per punt de bovenkant van het dak; gevellampen standaard min. 500 mm h.o.h. (geen samengevoegde lamp meer boven een smal raam)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bd084e4a96
commit
b137a458d4
+264
-70
@@ -212,7 +212,7 @@ window.RoofModel = (() => {
|
||||
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';
|
||||
const type = profile.length === 2 ? 'lessenaar' : 'zadel';
|
||||
return { type, axis, profile };
|
||||
}
|
||||
|
||||
@@ -267,8 +267,9 @@ window.RoofModel = (() => {
|
||||
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) {
|
||||
const ridgeTypes = ['zadel', 'wolf'];
|
||||
// span: [q0, q1] waar de nok echt loopt (dakaansluiting: tot de hoekkeper).
|
||||
function computeRidgeBeam(roofState, shape, rect, alongX, cd, kgM2, span) {
|
||||
if (roofState.steelRidge === false || !ridgeTypes.includes(shape.type)) return null;
|
||||
const pr = shape.profile, inner = pr.slice(1, -1);
|
||||
if (!inner.length) return null;
|
||||
@@ -283,6 +284,7 @@ window.RoofModel = (() => {
|
||||
q0 = Math.max(uq0, Math.min(g.qM, g.q0 + g.inset));
|
||||
q1 = Math.min(uq1, Math.max(g.qM, g.q1 - g.inset));
|
||||
}
|
||||
if (span) { q0 = Math.max(q0, span[0]); q1 = Math.min(q1, span[1]); }
|
||||
const L = q1 - q0;
|
||||
if (L < 100) return null;
|
||||
// q in N/mm = kN/m.
|
||||
@@ -393,7 +395,7 @@ window.RoofModel = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
// Mansardeknik: het steilere vlak is verticaal dikker, dus verstek: onderkant op de knik,
|
||||
// Knik in de daklijn: 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) => {
|
||||
@@ -418,11 +420,7 @@ window.RoofModel = (() => {
|
||||
}
|
||||
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;
|
||||
addRidgeSpecs(specs, ridgeBeam);
|
||||
return {
|
||||
shape, covering, planes: outPlanes, specs, ridgeBeam,
|
||||
undersideAt, topAt, cap, rect, alongX,
|
||||
@@ -431,6 +429,185 @@ window.RoofModel = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
function addRidgeSpecs(specs, ridgeBeam) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Dakaansluiting (HouseModel.roofJoins): vlakken h = a + bx·x + bz·z, samengevoegd volgens expr
|
||||
// (vlaknummer of ['min' | 'max', ...]). extra(k): ophoging per vlak (bovenkant dak).
|
||||
const planeAt = (pl, p) => pl.a + pl.bx * p.x + pl.bz * p.z;
|
||||
function joinValue(join, p, extra) {
|
||||
const at = (e) => Array.isArray(e) ? Math[e[0]](...e.slice(1).map(at)) : planeAt(join.planes[e], p) + (extra ? extra(e) : 0);
|
||||
return at(join.expr);
|
||||
}
|
||||
function convexHull(pts) {
|
||||
const sorted = pts.slice().sort((a, b) => a.x - b.x || a.z - b.z);
|
||||
const cross = (o, a, b) => (a.x - o.x) * (b.z - o.z) - (a.z - o.z) * (b.x - o.x);
|
||||
const half = (list) => list.reduce((h, q) => {
|
||||
while (h.length >= 2 && cross(h[h.length - 2], h[h.length - 1], q) <= 0.01 * Math.hypot(q.x - h[h.length - 2].x, q.z - h[h.length - 2].z)) h.pop();
|
||||
h.push(q);
|
||||
return h;
|
||||
}, []);
|
||||
const lo = half(sorted), up = half(sorted.slice().reverse());
|
||||
const out = lo.slice(0, -1).concat(up.slice(0, -1));
|
||||
return out.filter((p, k) => { const q = out[(k + 1) % out.length]; return Math.hypot(q.x - p.x, q.z - p.z) > 0.5; });
|
||||
}
|
||||
|
||||
// Dak van één unit in een dakaansluiting (join in unit-lokale maten): per vlak de convexe stukken binnen het dek,
|
||||
// sporen haaks op de goot (kreupel tegen kil- en hoekkepers), kepers en nokken op de grenzen tussen de vlakken.
|
||||
// Alles als houtwerk zoals het bovenste deel van een wolfsdak (cap); de eigen nok wordt de stalen nokbalk.
|
||||
function computeJoinedRoof(roofState, join, rect, shape) {
|
||||
const fp = roofState.deck.floor;
|
||||
const joistW = fp.joistWidth || 70, joistH = fp.joistHeight || 220, underlay = fp.underlayThick || 22;
|
||||
const covering = coveringFor(roofState.covering, shape.type);
|
||||
const cov = COVERINGS[covering] || COVERINGS.dakleer;
|
||||
const pls = join.planes.map((pl, k) => {
|
||||
const slope = Math.hypot(pl.bx, pl.bz), cos = 1 / Math.sqrt(1 + slope * slope);
|
||||
const tV = underlay / cos, jV = joistH / cos, bV = BESCHOT_MM / cos, covV = cov.thickMM / cos;
|
||||
const under = (p) => planeAt(pl, p);
|
||||
return { key: k, pl, slope, cos, tV, jV, bV, covV, vThick: tV + jV + bV + covV, under, f: under, along: Math.abs(pl.bx) >= Math.abs(pl.bz) ? 'p' : 'q' };
|
||||
});
|
||||
const activeAt = (p) => {
|
||||
const h = joinValue(join, p);
|
||||
return pls.reduce((a, b) => Math.abs(b.under(p) - h) < Math.abs(a.under(p) - h) ? b : a);
|
||||
};
|
||||
|
||||
const box = [{ x: rect.x0, z: rect.z0 }, { x: rect.x1, z: rect.z0 }, { x: rect.x1, z: rect.z1 }, { x: rect.x0, z: rect.z1 }];
|
||||
const clipBox = (poly) => [(p) => p.x - rect.x0, (p) => rect.x1 - p.x, (p) => p.z - rect.z0, (p) => rect.z1 - p.z]
|
||||
.reduce((pl, f) => pl.length >= 3 ? clipPoly(pl, f) : pl, poly);
|
||||
const pairs = [];
|
||||
pls.forEach((A, i) => pls.slice(i + 1).forEach(B => {
|
||||
if (Math.hypot(A.pl.bx - B.pl.bx, A.pl.bz - B.pl.bz) > 1e-9) pairs.push({ A, B, f: (p) => A.under(p) - B.under(p) });
|
||||
}));
|
||||
// Afgeronde wandhoogtes maken bijna samenvallende lijnen: splinters (gemiddeld < 0,5 mm breed) vallen weg.
|
||||
const perim = (poly) => poly.reduce((t, p, k) => { const q = poly[(k + 1) % poly.length]; return t + Math.hypot(q.x - p.x, q.z - p.z); }, 0);
|
||||
const sliver = (poly) => quadArea(poly) < Math.max(100, 0.25 * perim(poly));
|
||||
let cells = [box];
|
||||
pairs.forEach(({ f }) => {
|
||||
cells = cells.flatMap(c => [clipPoly(c, f), clipPoly(c, p => -f(p))]).filter(c => c.length >= 3 && !sliver(c));
|
||||
});
|
||||
const centroid = (poly) => ({ x: poly.reduce((t, p) => t + p.x, 0) / poly.length, z: poly.reduce((t, p) => t + p.z, 0) / poly.length });
|
||||
const regions = [];
|
||||
pls.forEach(pl => {
|
||||
const list = cells.filter(c => activeAt(centroid(c)) === pl).map(convexHull);
|
||||
// Aangrenzende cellen samenvoegen zolang het geheel convex blijft.
|
||||
let merged = true;
|
||||
while (merged) {
|
||||
merged = false;
|
||||
for (let i = 0; i < list.length && !merged; i++) {
|
||||
for (let j = i + 1; j < list.length && !merged; j++) {
|
||||
const h = convexHull(list[i].concat(list[j]));
|
||||
if (quadArea(h) - quadArea(list[i]) - quadArea(list[j]) < 0.5 * perim(h)) { list[i] = h; list.splice(j, 1); merged = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
list.filter(poly => poly.length >= 3).forEach(poly => regions.push(Object.assign({}, pl, { poly })));
|
||||
});
|
||||
|
||||
// Grenzen tussen twee vlakken: bol = nok (waterpas) of hoekkeper, hol = kilkeper.
|
||||
const members = [];
|
||||
pairs.forEach(({ A, B, f }) => {
|
||||
const gx = A.pl.bx - B.pl.bx, gz = A.pl.bz - B.pl.bz, g = Math.hypot(gx, gz), c0 = f({ x: 0, z: 0 });
|
||||
const o = { x: -c0 * gx / (g * g), z: -c0 * gz / (g * g) }, d = { x: -gz / g, z: gx / g }, n = { x: gx / g, z: gz / g };
|
||||
let t0 = -Infinity, t1 = Infinity;
|
||||
[['x', rect.x0, rect.x1], ['z', rect.z0, rect.z1]].forEach(([k, lo, hi]) => {
|
||||
if (Math.abs(d[k]) < 1e-9) { if (o[k] < lo || o[k] > hi) t1 = -Infinity; return; }
|
||||
const a = (lo - o[k]) / d[k], b = (hi - o[k]) / d[k];
|
||||
t0 = Math.max(t0, Math.min(a, b));
|
||||
t1 = Math.min(t1, Math.max(a, b));
|
||||
});
|
||||
if (!(t1 - t0 >= 100)) return;
|
||||
const pt = (t) => ({ x: o.x + d.x * t, z: o.z + d.z * t });
|
||||
const ts = [t0, t1];
|
||||
pairs.forEach(q => {
|
||||
const dn = q.f(pt(1)) - q.f(pt(0));
|
||||
if (Math.abs(dn) < 1e-9) return;
|
||||
const t = -q.f(pt(0)) / dn;
|
||||
if (t > t0 && t < t1) ts.push(t);
|
||||
});
|
||||
ts.sort((a, b) => a - b);
|
||||
const segs = [];
|
||||
let cur = null;
|
||||
ts.slice(0, -1).forEach((ta, k) => {
|
||||
const tb = ts[k + 1];
|
||||
if (tb - ta < 1) return;
|
||||
const m = pt((ta + tb) / 2), off = (s) => ({ x: m.x + n.x * s, z: m.z + n.z * s });
|
||||
const s1 = activeAt(off(2)), s2 = activeAt(off(-2));
|
||||
if (Math.abs(joinValue(join, m) - A.under(m)) > 0.5 || !((s1 === A && s2 === B) || (s1 === B && s2 === A))) { cur = null; return; }
|
||||
const onA = off(s1 === A ? 2 : -2), convex = B.under(onA) > A.under(onA);
|
||||
if (cur && cur.convex === convex) { cur.tb = tb; return; }
|
||||
cur = { ta, tb, convex };
|
||||
segs.push(cur);
|
||||
});
|
||||
// Waterpas en bol: nok als de vlakken van elkaar af aflopen, anders een knik.
|
||||
const ridge = A.pl.bx * B.pl.bx + A.pl.bz * B.pl.bz < 0;
|
||||
segs.forEach(sg => {
|
||||
const P = pt(sg.ta), Q = pt(sg.tb), level = Math.abs(A.under(P) - A.under(Q)) < 1;
|
||||
members.push({ kind: !sg.convex ? 'kilkeper' : !level ? 'hoekkeper' : ridge ? 'nokbalk' : 'knikbalk', A: P, B: Q, rg: A });
|
||||
});
|
||||
});
|
||||
|
||||
const alongX = shape.axis === 'x';
|
||||
const own = members.find(m => m.kind === 'nokbalk' && (alongX ? Math.abs(m.A.x - m.B.x) < 1 : Math.abs(m.A.z - m.B.z) < 1));
|
||||
const steel = own && roofState.steelRidge !== false;
|
||||
const { hw, beams, awayFrom, addBeam } = beamTools(joistW);
|
||||
members.forEach(m => { if (!(steel && m === own)) addBeam(m.kind, m.rg, m.A, m.B, [], true); });
|
||||
pls.forEach(pl => {
|
||||
const polys = regions.filter(rg => rg.key === pl.key).map(rg => rg.poly);
|
||||
if (!polys.length) return;
|
||||
const fixed = pl.along === 'p' ? 'z' : 'x';
|
||||
const [lo, hi] = fixed === 'z' ? [rect.z0, rect.z1] : [rect.x0, rect.x1];
|
||||
joistSpread(fp, hi - lo).map(v => lo + v).forEach(v => {
|
||||
const spans = polys.map(poly => cutLine(poly, fixed, v)).filter(Boolean).sort((a, b) => a[0] - b[0]);
|
||||
const runs = [];
|
||||
spans.forEach(sp => {
|
||||
const last = runs[runs.length - 1];
|
||||
if (last && sp[0] <= last[1] + 1) last[1] = Math.max(last[1], sp[1]);
|
||||
else runs.push(sp.slice());
|
||||
});
|
||||
runs.forEach(span => {
|
||||
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', pl, A, B, clips);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const ownBeams = clipBeams(beams, clipBox);
|
||||
const capSpecs = regionSpecs(ownBeams, regions, joistW, joistH, underlay, covering);
|
||||
const kgM2 = capSpecs.totalKg / Math.max(0.1, (rect.x1 - rect.x0) * (rect.z1 - rect.z0) / 1e6);
|
||||
const q = own ? (alongX ? [own.A.z, own.B.z] : [own.A.x, own.B.x]) : null;
|
||||
// Wolfsdak: de nok loopt hier tot de aansluiting, de inkorting van de wolfseinden volgt uit span.
|
||||
const ridgeShape = shape.type === 'wolf' ? Object.assign({}, shape, { type: 'zadel' }) : shape;
|
||||
const ridgeBeam = steel ? computeRidgeBeam(roofState, ridgeShape, rect, alongX, { underlayThick: underlay, joistH }, kgM2, [Math.min(...q), Math.max(...q)]) : null;
|
||||
if (ridgeBeam) ridgeBeam.alongX = alongX;
|
||||
const specs = Object.assign({}, capSpecs);
|
||||
addRidgeSpecs(specs, ridgeBeam);
|
||||
const heights = regions.flatMap(rg => rg.poly.map(p => rg.under(p)));
|
||||
const maxTop = Math.max(...regions.map(rg => Math.max(...rg.poly.map(p => rg.under(p) + rg.vThick))));
|
||||
const cap = {
|
||||
kind: 'join', pA: rect.x0, pB: rect.x1, q0: rect.z0, q1: rect.z1, eave: Math.min(...heights), ridgeH: Math.max(...heights),
|
||||
slope: Math.max(...regions.map(rg => rg.slope)), joistW, joistH, regions, beams: ownBeams, specs: capSpecs, maxTop
|
||||
};
|
||||
// Knikpunten van de daklijn op de randen (nok, knik, wolfseind, kepers): daar knikt ook het boeideel.
|
||||
const ridges = [];
|
||||
members.forEach(m => [m.A, m.B].forEach(p => {
|
||||
const onX = Math.abs(p.x - rect.x0) < 1 || Math.abs(p.x - rect.x1) < 1, onZ = Math.abs(p.z - rect.z0) < 1 || Math.abs(p.z - rect.z1) < 1;
|
||||
const r = onX && !onZ ? { axis: 'z', value: p.z } : onZ && !onX ? { axis: 'x', value: p.x } : null;
|
||||
if (r && !ridges.some(q => q.axis === r.axis && Math.abs(q.value - r.value) < 1)) ridges.push(r);
|
||||
}));
|
||||
return {
|
||||
shape, covering, planes: [], specs, ridgeBeam, cap, rect, alongX: true, ridges, maxTop,
|
||||
undersideAt: (x, z) => joinValue(join, { x, z }),
|
||||
topAt: (x, z) => joinValue(join, { x, z }, k => pls[k].vThick)
|
||||
};
|
||||
}
|
||||
|
||||
const DENSITY_WOOD_KGM3 = 450, DENSITY_OSB_KGM3 = 650; // zelfde als FloorModel
|
||||
function computeCap(roofState, shape, rect, alongX, covering) {
|
||||
const H = (p) => profileHeight(shape.profile, p);
|
||||
@@ -465,53 +642,7 @@ window.RoofModel = (() => {
|
||||
.filter(rg => rg.poly.length >= 3 && quadArea(rg.poly) > 100);
|
||||
if (!clipped.some(rg => rg.along === 'q')) return null;
|
||||
|
||||
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;
|
||||
};
|
||||
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 }];
|
||||
};
|
||||
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;
|
||||
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, 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;
|
||||
};
|
||||
const { hw, beams, awayFrom, addBeam } = beamTools(joistW);
|
||||
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) }];
|
||||
@@ -534,8 +665,7 @@ window.RoofModel = (() => {
|
||||
if (ridgeBeam && roofState.steelRidge !== false) beams.splice(beams.indexOf(ridgeBeam), 1);
|
||||
}
|
||||
const members = kops.concat(hips, ridgeBeam ? [ridgeBeam] : []);
|
||||
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);
|
||||
const qs = joistSpread(fp, q1 - q0).map(v => q0 + v), ps = joistSpread(fp, pB - pA).map(v => pA + v);
|
||||
regions.forEach(rg => {
|
||||
const fixed = rg.along === 'p' ? 'z' : 'x';
|
||||
(rg.along === 'p' ? qs : ps).forEach(v => {
|
||||
@@ -548,8 +678,69 @@ window.RoofModel = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
const ownBeams = beams.map(bm => {
|
||||
const poly = clipBox(bm.poly);
|
||||
const ownBeams = clipBeams(beams, clipBox);
|
||||
const specs = regionSpecs(ownBeams, clipped, joistW, joistH, underlay, covering);
|
||||
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 };
|
||||
}
|
||||
|
||||
// Houtwerk op dakvlakken met een eigen omtrek (wolfseinden, dakaansluiting); balken als rechthoek in bovenaanzicht.
|
||||
function 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;
|
||||
}
|
||||
function 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));
|
||||
}
|
||||
function joistSpread(fp, len) {
|
||||
return FloorModel.computeFrame({ width: len, depth: 1000, joistWidth: fp.joistWidth || 70, joistHeight: fp.joistHeight || 220, minSpacing: fp.minSpacing, maxSpacing: fp.maxSpacing, underlayThick: fp.underlayThick || 22 })
|
||||
.joists.map(x => x + len / 2);
|
||||
}
|
||||
function beamTools(joistW) {
|
||||
const hw = joistW / 2, beams = [];
|
||||
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 }];
|
||||
};
|
||||
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 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;
|
||||
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, 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;
|
||||
};
|
||||
return { hw, beams, awayFrom, addBeam };
|
||||
}
|
||||
function clipBeams(beams, clip) {
|
||||
return beams.map(bm => {
|
||||
const poly = clip(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);
|
||||
@@ -557,20 +748,19 @@ window.RoofModel = (() => {
|
||||
if (plan < 50) return null;
|
||||
return Object.assign({}, bm, { poly, length: Math.round(plan * Math.sqrt(1 + slope * slope)) });
|
||||
}).filter(Boolean);
|
||||
|
||||
}
|
||||
function regionSpecs(beams, regions, joistW, joistH, underlay, covering) {
|
||||
const W = joistW * 1e-3, Hh = joistH * 1e-3;
|
||||
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 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 cs = coverSpecs(covering, areaM2), osbVol = areaM2 * underlay * 1e-3, bVol = areaM2 * BESCHOT_MM * 1e-3;
|
||||
const specs = Object.assign({
|
||||
return 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),
|
||||
totalKg: woodM3 * DENSITY_WOOD_KGM3 + (osbVol + bVol) * DENSITY_OSB_KGM3 + cs.coverKg
|
||||
}, cs);
|
||||
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 };
|
||||
}
|
||||
|
||||
// Afschuiving i.p.v. kanteling (lokaal +Z = oplopende as): plan = z·cos θ, Y = y/cos θ + z·sin θ.
|
||||
@@ -581,7 +771,8 @@ window.RoofModel = (() => {
|
||||
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));
|
||||
const rb = unitRoof.ridgeBeam;
|
||||
group.add(buildRidgeBeam3D(rb, (r.x0 + r.x1) / 2, (r.z0 + r.z1) / 2, rb.alongX !== undefined ? rb.alongX : unitRoof.alongX, opts.materials));
|
||||
}
|
||||
unitRoof.planes.forEach(pl => {
|
||||
const g = new THREE.Group();
|
||||
@@ -647,7 +838,7 @@ window.RoofModel = (() => {
|
||||
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); };
|
||||
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;
|
||||
const up = T.reduce((t, a, k) => { const b = T[(k + 1) % n]; return t + a[0] * b[2] - b[0] * a[2]; }, 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);
|
||||
@@ -879,7 +1070,10 @@ window.RoofModel = (() => {
|
||||
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 };
|
||||
const form = h.form ? Object.assign({}, h.form) : null;
|
||||
// v0.6.2: mansarde vervallen, wordt een zadeldak met dezelfde nok.
|
||||
if (form && form.type === 'mansarde') { form.type = 'zadel'; delete form.kneeH; delete form.kneeIn; delete form.kneeRatio; }
|
||||
s.house = { enabled: !!h.enabled, form: form && UnitModel.ROOF_TYPES[form.type] ? form : null, cover: !!h.cover };
|
||||
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
|
||||
return s;
|
||||
}
|
||||
@@ -887,7 +1081,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, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, buildUnit3D, capRegions,
|
||||
computeShape, shapeFromProfile, shapeLines, wallProfile, profileHeight, computeUnitRoof, computeJoinedRoof, joinValue, buildUnit3D, capRegions,
|
||||
computeEdge, buildEdge3D, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user