323 lines
18 KiB
JavaScript
323 lines
18 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// Schoorsteen tegen een buitenwand: hol, binnen vlak met de wand, buiten (iets) voor de gevel. Hij maakt in elke
|
|
// buitenwand in hetzelfde vlak die hij raakt een sparing zoals een raam (ook over een naad tussen units heen) en
|
|
// onderbreekt de dakrand; aan de voorkant steekt hij minstens tot voorbij de dakoverstek uit.
|
|
// Lokaal assenstelsel per schoorsteen: t langs de wand (wereld-x of -z), s loodrecht erop, 0 = buitenkant regelwerk
|
|
// van de unit (rand van de vloer), positief naar buiten.
|
|
|
|
window.ChimneyModel = (() => {
|
|
const EPS = 0.5;
|
|
const SHELL_MM = 100; // wand rond het rookkanaal
|
|
const CAP_MM = 60, CAP_OVER_MM = 30;
|
|
const FLUE_SHOW_MM = 500; // zichtbare diepte van het rookkanaal onder de kap
|
|
const INSET_MM = 2; // sparingen in wand, dak en dakrand net binnen de schoorsteen (geen z-fighting)
|
|
const BEAR_MM = -4; // nokbalk stopt net vóór de binnenkant van de schoorsteen
|
|
const MIN_CUT_MM = 100;
|
|
const CORNER_CLEAR_MM = 200; // slepen: zoveel vrij van een buitenhoek (gevelbekleding van de haakse wand)
|
|
const INSIDE_TYPES = { wand: 'Zoals de wanden', stuc: 'Stucwerk', steen: 'Steenstrip' };
|
|
const OUTSIDE_TYPES = { steen: 'Steenstrip', stuc: 'Stucwerk' };
|
|
const DEFAULTS = { w: 800, out: 180, above: 600, inside: { type: 'wand', stone: 'R777', stuc: 'wit' }, outside: { type: 'steen', stone: 'R777', stuc: 'wit' } };
|
|
const SIDES = {
|
|
voor: { axis: 'x', n: { x: 0, z: -1 } }, achter: { axis: 'x', n: { x: 0, z: 1 } },
|
|
links: { axis: 'z', n: { x: -1, z: 0 } }, rechts: { axis: 'z', n: { x: 1, z: 0 } }
|
|
};
|
|
|
|
const skinOf = (finish) => finish.type === 'steen' ? WallModel.STONE.STRIP_MM : finish.type === 'stuc' ? WallModel.STUC.THICK_MM : 0;
|
|
|
|
function normalizeFinish(f, types, def) {
|
|
const o = Object.assign({}, def, f || {});
|
|
if (!types[o.type]) o.type = def.type;
|
|
if (!WallModel.isStoneFinish(o.stone)) o.stone = def.stone;
|
|
if (!WallModel.isStucColor(o.stuc)) o.stuc = def.stuc;
|
|
return { type: o.type, stone: o.stone, stuc: o.stuc };
|
|
}
|
|
|
|
function normalize(list, unitIds) {
|
|
const used = new Set();
|
|
const num = (v, def, min, max) => (isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : def);
|
|
return (Array.isArray(list) ? list : []).filter(c => c && unitIds.includes(c.unitId) && SIDES[c.side]).map(c => {
|
|
let id = Number(c.id) > 0 ? Math.round(c.id) : 1;
|
|
while (used.has(id)) id++;
|
|
used.add(id);
|
|
return {
|
|
id, unitId: c.unitId, side: c.side, u: num(c.u, 0, -20000, 40000),
|
|
w: num(c.w, DEFAULTS.w, 400, 2000), out: num(c.out, DEFAULTS.out, 0, 1000), above: num(c.above, DEFAULTS.above, 0, 3000),
|
|
inside: normalizeFinish(c.inside, INSIDE_TYPES, DEFAULTS.inside),
|
|
outside: normalizeFinish(c.outside, OUTSIDE_TYPES, DEFAULTS.outside)
|
|
};
|
|
});
|
|
}
|
|
|
|
function create(list, unitId, side, u) {
|
|
const id = (list || []).reduce((m, c) => Math.max(m, c.id || 0), 0) + 1;
|
|
return normalize([{ id, unitId, side, u }], [unitId])[0];
|
|
}
|
|
|
|
// Vlak van een zijde van een unit; u = hart vanaf de linker vloerhoek, van buitenaf gezien.
|
|
const planeOf = (r, side) => ({ voor: r.z0, achter: r.z1, links: r.x0, rechts: r.x1 })[side];
|
|
// opts: { wallThickness(unitId, side), faceDepth(unitId, side) (gevelbekleding), overhang } voor de voetafdruk
|
|
// (sIn..sFront), die het dak al nodig heeft voordat de gevels gebouwd zijn.
|
|
function place(list, rects, opts) {
|
|
const o = opts || {};
|
|
return list.map(c => {
|
|
const r = rects.find(q => q.id === c.unitId);
|
|
if (!r || r.error) return null;
|
|
const S = SIDES[c.side];
|
|
const t = { voor: r.x1 - c.u, achter: r.x0 + c.u, links: r.z0 + c.u, rechts: r.z1 - c.u }[c.side];
|
|
const T = o.wallThickness ? o.wallThickness(c.unitId, c.side) : 260, face = o.faceDepth ? o.faceDepth(c.unitId, c.side) : 0;
|
|
return Object.assign({}, c, {
|
|
axis: S.axis, n: S.n, plane: planeOf(r, c.side), t0: t - c.w / 2, t1: t + c.w / 2,
|
|
sIn: -T, sFront: Math.max(face + c.out, (o.overhang || 0) + 20)
|
|
});
|
|
}).filter(Boolean);
|
|
}
|
|
|
|
// Sparingen in het dak van een unit (plattegrondmaten t.o.v. het unitmidden, zoals RoofModel.computeUnitRoof):
|
|
// de hele voetafdruk binnen het dak; een nokbalk stopt ervoor (BEAR_MM).
|
|
function roofCuts(placed, rect, roofRect) {
|
|
const out = [];
|
|
placed.forEach(c => {
|
|
const S = SIDES[c.side];
|
|
const k = S.axis === 'z' ? S.n.x : S.n.z, sA = c.plane + k * (c.sIn + INSET_MM), sB = c.plane + k * c.sFront;
|
|
const w = S.axis === 'z'
|
|
? { x0: Math.min(sA, sB), x1: Math.max(sA, sB), z0: c.t0 + INSET_MM, z1: c.t1 - INSET_MM }
|
|
: { x0: c.t0 + INSET_MM, x1: c.t1 - INSET_MM, z0: Math.min(sA, sB), z1: Math.max(sA, sB) };
|
|
const x0 = Math.max(w.x0, roofRect.x0), x1 = Math.min(w.x1, roofRect.x1), z0 = Math.max(w.z0, roofRect.z0), z1 = Math.min(w.z1, roofRect.z1);
|
|
if (x1 - x0 < 1 || z1 - z0 < 1) return;
|
|
out.push({ x0: x0 - rect.cx, x1: x1 - rect.cx, z0: z0 - rect.cz, z1: z1 - rect.cz, bear: BEAR_MM, chimney: c.id });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
// Ruwe sparingen voor één unitwand (wl uit UnitModel.computeLayout; rect en rects in wereld-mm).
|
|
// Loopt de schoorsteen over het wandeinde heen en gaat de wand daar over in die van een andere unit in hetzelfde
|
|
// vlak (naad), dan loopt de sparing tot het einde door: geen hoekstijlen in de schoorsteen.
|
|
function wallCuts(placed, rect, wl, studWidth, rects) {
|
|
const S = SIDES[wl.side], sw = studWidth, plane = planeOf(rect, wl.side);
|
|
const dir = UnitModel.WALL_SIDES.find(x => x.key === wl.side).dir;
|
|
const mid = S.axis === 'x' ? wl.cx : wl.cz, c = S.axis === 'x' ? rect.cx : rect.cz;
|
|
const s0 = mid - wl.width / 2, s1 = mid + wl.width / 2;
|
|
const toU = (t) => { const p = t - c; return dir > 0 ? p - s0 : s1 - p; };
|
|
const toT = (u) => c + (dir > 0 ? u + s0 : s1 - u);
|
|
const range = (r) => S.axis === 'x' ? [r.x0, r.x1] : [r.z0, r.z1];
|
|
const seamEnd = (u) => {
|
|
const t = toT(u);
|
|
return (rects || []).some(r => r !== rect && r.id !== rect.id && !r.error && Math.abs(planeOf(r, wl.side) - plane) < EPS &&
|
|
range(r)[0] <= t + EPS && range(r)[1] >= t - EPS);
|
|
};
|
|
const edge = sw * WallModel.CORNER_STUDS;
|
|
const out = [];
|
|
placed.filter(ch => ch.side === wl.side && Math.abs(ch.plane - plane) < EPS).forEach(ch => {
|
|
const ua = toU(ch.t0 + INSET_MM), ub = toU(ch.t1 - INSET_MM);
|
|
const atStart = Math.min(ua, ub) < edge && Math.max(toU(ch.t0), toU(ch.t1)) > 0 && seamEnd(0);
|
|
const atEnd = Math.max(ua, ub) > wl.width - edge && Math.min(toU(ch.t0), toU(ch.t1)) < wl.width && seamEnd(wl.width);
|
|
const a = atStart ? 0 : Math.max(edge, Math.min(ua, ub)), b = atEnd ? wl.width : Math.min(wl.width - edge, Math.max(ua, ub));
|
|
if (b - a < MIN_CUT_MM) return;
|
|
out.push({ id: 1000 + ch.id, type: 'schoorsteen', w: Math.round(b - a), h: 99999, y: 0, anchor: 'left', offset: Math.round(a), fill: null,
|
|
chimney: { id: ch.id, inside: ch.inside.type, atStart, atEnd } });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
// units: parts.units (met walls/pieces en roof), roofEdge: RoofModel.computeEdge (krijgt gaps), groundY absoluut.
|
|
function resolve(placed, units, roofEdge, groundY, overhang) {
|
|
return placed.map(ch => {
|
|
const S = SIDES[ch.side];
|
|
const toW = (t, s) => S.axis === 'z' ? { x: ch.plane + S.n.x * s, z: t } : { x: t, z: ch.plane + S.n.z * s };
|
|
const sOf = (p) => S.axis === 'z' ? (p.x - ch.plane) * S.n.x : (p.z - ch.plane) * S.n.z;
|
|
const tOf = (p) => S.axis === 'z' ? p.z : p.x;
|
|
let T = 0, face = 0, floorTop = null;
|
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
|
if (p.baseY || !p.place.normal || p.place.normal.x !== S.n.x || p.place.normal.y !== S.n.z) return;
|
|
const m = UnitModel.localToWorld(p.place, 0, p.place.thickness / 2);
|
|
if (Math.abs(sOf(m)) > 2) return;
|
|
const a = UnitModel.localToWorld(p.place, -p.place.width / 2, 0), b = UnitModel.localToWorld(p.place, p.place.width / 2, 0);
|
|
if (Math.min(ch.t1, Math.max(tOf(a), tOf(b))) - Math.max(ch.t0, Math.min(tOf(a), tOf(b))) < EPS) return;
|
|
T = Math.max(T, p.place.thickness);
|
|
face = Math.max(face, WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc));
|
|
if (floorTop === null) floorTop = p.place.floorTop;
|
|
})));
|
|
if (floorTop === null) {
|
|
const u0 = units.find(u => u.id === ch.unitId);
|
|
floorTop = u0 ? u0.layout.floorTop : 0;
|
|
T = T || (u0 && u0.walls.find(w => w.side === ch.side) || { thickness: 260 }).thickness;
|
|
}
|
|
// De dakrand onderbreken en de voorkant tot voorbij de overstek.
|
|
let roofOut = 0;
|
|
if (roofEdge) roofEdge.segments.forEach(sg => {
|
|
if (Math.abs(sg.normal.x - S.n.x) > 0.01 || Math.abs(sg.normal.z - S.n.z) > 0.01) return;
|
|
if (Math.abs(sOf(sg.a)) > Math.max(overhang, 50) + face + 5) return;
|
|
const ta = tOf(sg.outerA), tb = tOf(sg.outerB);
|
|
if (Math.min(ch.t1, Math.max(ta, tb)) - Math.max(ch.t0, Math.min(ta, tb)) < EPS) return;
|
|
sg.gaps = (sg.gaps || []).concat([[ch.t0 + INSET_MM, ch.t1 - INSET_MM]]);
|
|
roofOut = Math.max(roofOut, sOf(sg.outerA) + 20);
|
|
});
|
|
const sIn = -T, sFace = face, sFront = Math.max(face + ch.out, roofOut);
|
|
|
|
const roofs = units.map(u => u.roof);
|
|
const roofAt = (p) => roofs.filter(r => p.x > r.rect.x0 - EPS && p.x < r.rect.x1 + EPS && p.z > r.rect.z0 - EPS && p.z < r.rect.z1 + EPS);
|
|
const ridgeTs = roofs.flatMap(r => r.ridges.filter(rd => rd.axis === (S.axis === 'x' ? 'x' : 'z') && rd.value > ch.t0 && rd.value < ch.t1).map(rd => rd.value));
|
|
// Ook de randen van de daken: twee lessenaars die op een naad de nok vormen hebben geen eigen nok.
|
|
const edgeTs = roofs.flatMap(r => (S.axis === 'x' ? [r.rect.x0, r.rect.x1] : [r.rect.z0, r.rect.z1])).filter(t => t > ch.t0 + 1 && t < ch.t1 - 1);
|
|
const ts = [...new Set([ch.t0, ch.t1, ...ridgeTs, ...edgeTs].map(Math.round))].sort((a, b) => a - b);
|
|
let roofTop = -Infinity;
|
|
ts.forEach(t => [sIn, 0, sFront].forEach(s => roofAt(toW(t, s)).forEach(r => { roofTop = Math.max(roofTop, r.topAt(toW(t, s).x, toW(t, s).z)); })));
|
|
// Binnenkant tot de onderkant van het dak (vlak achter de wand gemeten).
|
|
const insideTop = ts.map(t => {
|
|
const p = toW(t, sIn - 1), r = roofAt(p);
|
|
return { t, y: r.length ? Math.min(...r.map(q => q.ceilingAt(p.x, p.z))) : floorTop + 2600 };
|
|
});
|
|
const under = Math.max(...insideTop.map(q => q.y));
|
|
if (!isFinite(roofTop)) roofTop = under + 300;
|
|
const yTop = Math.round(roofTop + ch.above);
|
|
const flue = {
|
|
t0: ch.t0 + SHELL_MM, t1: Math.max(ch.t0 + SHELL_MM + MIN_CUT_MM, ch.t1 - SHELL_MM),
|
|
s0: sIn + SHELL_MM, s1: Math.max(sIn + SHELL_MM + MIN_CUT_MM, sFront - SHELL_MM)
|
|
};
|
|
const corners = (q) => [toW(q.t0, q.s0), toW(q.t1, q.s0), toW(q.t1, q.s1), toW(q.t0, q.s1)];
|
|
return Object.assign({}, ch, {
|
|
T, sIn, sFace, sFront, yGround: groundY, yFloor: floorTop, yTop, yUnder: under, insideTop, flue,
|
|
footprint: corners({ t0: ch.t0, t1: ch.t1, s0: sIn, s1: sFront }), flueFootprint: corners(flue),
|
|
protrudes: Math.round(sFront - sFace), height: yTop - groundY
|
|
});
|
|
});
|
|
}
|
|
|
|
// Ligt een punt (wereld-mm) in of vlak voor een schoorsteen?
|
|
function covers(list, x, z, margin) {
|
|
const m = margin || 0;
|
|
return list.some(c => {
|
|
const S = SIDES[c.side];
|
|
const t = S.axis === 'z' ? z : x, s = S.axis === 'z' ? (x - c.plane) * S.n.x : (z - c.plane) * S.n.z;
|
|
return t > c.t0 - m && t < c.t1 + m && s > c.sIn - m && s < c.sFront + m;
|
|
});
|
|
}
|
|
|
|
function hit(list, x, z, margin) {
|
|
return list.find(c => covers([c], x, z, margin)) || null;
|
|
}
|
|
|
|
// Slepen: dichtstbijzijnde stuk buitengevel (omtrek van de units samen), hart van de schoorsteen erlangs zodat hij
|
|
// er helemaal tegen staat; ook over een naad tussen units heen. Geeft { unitId, side, u, t, plane, n } of null.
|
|
function snapToFacade(rects, x, z, w) {
|
|
const rs = rects.filter(r => !r.error);
|
|
if (!rs.length) return null;
|
|
const loop = rectUnionLoops(rs)[0];
|
|
if (!loop) return null;
|
|
const pts = loop.pts, n = pts.length;
|
|
let best = null;
|
|
for (let i = 0; i < n; i++) {
|
|
const a = pts[i], b = pts[(i + 1) % n], nrm = rectilinearNormal(a, b);
|
|
const side = Object.keys(SIDES).find(k => Math.abs(SIDES[k].n.x - nrm.x) < 1e-6 && Math.abs(SIDES[k].n.z - nrm.z) < 1e-6);
|
|
if (!side) continue;
|
|
const axis = SIDES[side].axis, lo = Math.min(axis === 'z' ? a.z : a.x, axis === 'z' ? b.z : b.x), hi = Math.max(axis === 'z' ? a.z : a.x, axis === 'z' ? b.z : b.x);
|
|
if (hi - lo < w + 2 * CORNER_CLEAR_MM) continue;
|
|
const plane = axis === 'z' ? a.x : a.z, along = axis === 'z' ? z : x, across = axis === 'z' ? x : z;
|
|
const t = Math.max(lo + w / 2 + CORNER_CLEAR_MM, Math.min(hi - w / 2 - CORNER_CLEAR_MM, along));
|
|
const d = Math.hypot(along - t, across - plane);
|
|
if (best && d >= best.d) continue;
|
|
const owner = rs.find(r => Math.abs(planeOf(r, side) - plane) < EPS && (axis === 'z' ? r.z0 <= t + EPS && r.z1 >= t - EPS : r.x0 <= t + EPS && r.x1 >= t - EPS));
|
|
if (!owner) continue;
|
|
const u = { voor: owner.x1 - t, achter: t - owner.x0, links: t - owner.z0, rechts: owner.z1 - t }[side];
|
|
best = { d, unitId: owner.id, side, u: Math.round(u), t, plane, n: SIDES[side].n };
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function createMaterials() {
|
|
return {
|
|
cap: new THREE.MeshStandardMaterial({ color: 0x77736d, roughness: 0.85 }),
|
|
flue: new THREE.MeshStandardMaterial({ color: 0x1b1a19, roughness: 1, side: THREE.DoubleSide }),
|
|
plain: new THREE.MeshStandardMaterial({ color: 0xf2efe9, roughness: 0.9 })
|
|
};
|
|
}
|
|
|
|
const finishMaterial = (f, fallback) => f.type === 'steen' ? WallModel.stoneMaterial(f.stone) : f.type === 'stuc' ? WallModel.stucMaterial(f.stuc) : fallback;
|
|
|
|
// opts: { materials, wallMaterial (binnenkant 'wand': hetzelfde materiaal als de wanden) }
|
|
function build3D(list, opts) {
|
|
const o = opts || {}, m = o.materials || createMaterials(), toM = 0.001;
|
|
const group = new THREE.Group();
|
|
list.forEach(c => {
|
|
const S = SIDES[c.side];
|
|
const W = (t, s) => S.axis === 'z' ? { x: c.plane + S.n.x * s, z: t } : { x: t, z: c.plane + S.n.z * s };
|
|
const buckets = new Map();
|
|
const add = (mat, tri) => { if (!buckets.has(mat)) buckets.set(mat, { pos: [], uv: [] }); const b = buckets.get(mat); tri.forEach(v => { b.pos.push(v.x * toM, v.y * toM, v.z * toM); b.uv.push(v.u, v.v); }); };
|
|
// Verticaal vlak tussen p0 en p1 (wereld), buitennormaal nrm; hoogtes per kant. UV in meters (steen/stuc).
|
|
const quad = (mat, p0, p1, y0a, y1a, y0b, y1b, nrm) => {
|
|
const right = { x: nrm.z, z: -nrm.x };
|
|
if ((p1.x - p0.x) * right.x + (p1.z - p0.z) * right.z < 0) { [p0, p1] = [p1, p0]; [y0a, y0b] = [y0b, y0a]; [y1a, y1b] = [y1b, y1a]; }
|
|
const v = (p, y) => ({ x: p.x, y, z: p.z, u: (p.x * right.x + p.z * right.z) * toM, v: (y - c.yFloor) * toM });
|
|
const BL = v(p0, y0a), BR = v(p1, y0b), TR = v(p1, y1b), TL = v(p0, y1a);
|
|
add(mat, [BL, BR, TR]); add(mat, [BL, TR, TL]);
|
|
};
|
|
const nOut = S.n, nIn = { x: -S.n.x, z: -S.n.z };
|
|
const tDir = S.axis === 'z' ? { x: 0, z: 1 } : { x: 1, z: 0 }, tNeg = { x: -tDir.x, z: -tDir.z };
|
|
const outMat = finishMaterial(c.outside, m.plain);
|
|
const yG = c.yGround, yT = c.yTop;
|
|
quad(outMat, W(c.t0, c.sFront), W(c.t1, c.sFront), yG, yT, yG, yT, nOut);
|
|
quad(outMat, W(c.t0, c.sIn), W(c.t0, c.sFront), yG, yT, yG, yT, tNeg);
|
|
quad(outMat, W(c.t1, c.sIn), W(c.t1, c.sFront), yG, yT, yG, yT, tDir);
|
|
quad(outMat, W(c.t0, c.sIn), W(c.t1, c.sIn), c.yUnder, yT, c.yUnder, yT, nIn); // achterkant boven het dak
|
|
|
|
// Binnenkant: van de vloer tot de onderkant van het dak.
|
|
const k = skinOf(c.inside), inMat = finishMaterial(c.inside, o.wallMaterial || m.plain), sI = c.sIn - k;
|
|
for (let i = 0; i < c.insideTop.length - 1; i++) {
|
|
const a = c.insideTop[i], b = c.insideTop[i + 1];
|
|
quad(inMat, W(a.t, sI), W(b.t, sI), c.yFloor, a.y, c.yFloor, b.y, nIn);
|
|
}
|
|
if (k > 0) {
|
|
const first = c.insideTop[0], last = c.insideTop[c.insideTop.length - 1];
|
|
quad(inMat, W(c.t0, sI), W(c.t0, c.sIn), c.yFloor, first.y, c.yFloor, first.y, tNeg);
|
|
quad(inMat, W(c.t1, sI), W(c.t1, c.sIn), c.yFloor, last.y, c.yFloor, last.y, tDir);
|
|
}
|
|
|
|
// Rookkanaal: zwarte binnenwanden en bodem tot FLUE_SHOW_MM onder de kap.
|
|
const f = c.flue, yF = yT - Math.min(FLUE_SHOW_MM, c.above + 100);
|
|
quad(m.flue, W(f.t0, f.s0), W(f.t1, f.s0), yF, yT, yF, yT, nOut);
|
|
quad(m.flue, W(f.t0, f.s1), W(f.t1, f.s1), yF, yT, yF, yT, nIn);
|
|
quad(m.flue, W(f.t0, f.s0), W(f.t0, f.s1), yF, yT, yF, yT, tDir);
|
|
quad(m.flue, W(f.t1, f.s0), W(f.t1, f.s1), yF, yT, yF, yT, tNeg);
|
|
const fp = [W(f.t0, f.s0), W(f.t1, f.s0), W(f.t1, f.s1), W(f.t0, f.s1)].map(p => ({ x: p.x, y: yF, z: p.z, u: 0, v: 0 }));
|
|
add(m.flue, [fp[0], fp[1], fp[2]]); add(m.flue, [fp[0], fp[2], fp[3]]);
|
|
|
|
buckets.forEach((b, mat) => {
|
|
const geo = new THREE.BufferGeometry();
|
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(b.pos, 3));
|
|
geo.setAttribute('uv', new THREE.Float32BufferAttribute(b.uv, 2));
|
|
geo.computeVertexNormals();
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.castShadow = mat !== m.flue;
|
|
mesh.receiveShadow = true;
|
|
group.add(mesh);
|
|
});
|
|
|
|
// Afdekplaat rond het rookkanaal (vier stroken).
|
|
const capY = (yT + CAP_MM / 2) * toM;
|
|
const strips = [
|
|
[c.t0 - CAP_OVER_MM, c.t1 + CAP_OVER_MM, c.sIn - CAP_OVER_MM, f.s0],
|
|
[c.t0 - CAP_OVER_MM, c.t1 + CAP_OVER_MM, f.s1, c.sFront + CAP_OVER_MM],
|
|
[c.t0 - CAP_OVER_MM, f.t0, f.s0, f.s1],
|
|
[f.t1, c.t1 + CAP_OVER_MM, f.s0, f.s1]
|
|
];
|
|
strips.forEach(([ta, tb, sa, sb]) => {
|
|
if (tb - ta < 1 || sb - sa < 1) return;
|
|
const p = W((ta + tb) / 2, (sa + sb) / 2);
|
|
const [dx, dz] = S.axis === 'z' ? [sb - sa, tb - ta] : [tb - ta, sb - sa];
|
|
const box = new THREE.Mesh(new THREE.BoxGeometry(dx * toM, CAP_MM * toM, dz * toM), m.cap);
|
|
box.position.set(p.x * toM, capY, p.z * toM);
|
|
box.castShadow = box.receiveShadow = true;
|
|
group.add(box);
|
|
});
|
|
});
|
|
return group;
|
|
}
|
|
|
|
return { SHELL_MM, INSIDE_TYPES, OUTSIDE_TYPES, DEFAULTS, SIDES, normalize, create, place, wallCuts, roofCuts, resolve, covers, hit, snapToFacade, createMaterials, build3D };
|
|
})();
|