/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // Palen onder een overkapping: steekt een dak minstens minExt buiten de vloeren uit (veranda), dan komen er // palen onder de dakrand. Hoekpalen staan met hun buitenkant gelijk met de dakhoek; een eind dat op een gevel // uitkomt rust op die wand. Daartussen zo weinig palen dat de h.o.h.-afstand onder maxSpan blijft. window.PostModel = (() => { const EPS = 0.5; const SHOE_MM = 100; // verzinkte paalvoet tussen maaiveld en paal const WOODS = { eiken: { label: 'Eiken', hex: 0xa07c54, kgM3: 750 }, douglas: { label: 'Douglas', hex: 0xb98a5e, kgM3: 530 } }; const DEFAULT_SETTINGS = { enabled: true, size: 200, wood: 'eiken', maxSpan: 3600, minExt: 1000 }; function normalizeSettings(settings) { const s = Object.assign({}, DEFAULT_SETTINGS, settings || {}); s.enabled = s.enabled !== false; if (!WOODS[s.wood]) s.wood = DEFAULT_SETTINGS.wood; const num = (v, def, min, max) => (isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : def); s.size = num(s.size, DEFAULT_SETTINGS.size, 100, 400); s.maxSpan = num(s.maxSpan, DEFAULT_SETTINGS.maxSpan, 1000, 8000); s.minExt = num(s.minExt, DEFAULT_SETTINGS.minExt, 400, 5000); return s; } // units: [{ rect (vloer), roof: { rect, ceilingAt } }] in wereld-mm; groundY = maaiveld (absoluut). function compute(settings, units, groundY) { const s = normalizeSettings(settings); const out = { posts: [], count: 0, size: s.size, wood: s.wood, lengthMM: 0, m3: 0, kg: 0 }; if (!s.enabled || !units.length) return out; const floors = units.map(u => u.rect); const onFloor = (x, z, m) => floors.some(f => x > f.x0 - m && x < f.x1 + m && z > f.z0 - m && z < f.z1 + m); const underRoof = (x, z) => units.some(u => { const r = u.roof.rect; return x > r.x0 && x < r.x1 && z > r.z0 && z < r.z1; }); // Vrije stukken dakrand: buitenrand van de daken samen, recht tegenover een gevel op minstens minExt (loodrecht // op de rand gemeten); de zijkanten van een veranda krijgen alleen de hoekpalen. In een binnenhoek (veranda // tussen twee units) loopt het stuk door tot de andere gevel en rust daar op. const runs = []; units.forEach(u => { const R = u.roof.rect; [ { axis: 'z', at: R.x1, n: 1, s0: R.z0, s1: R.z1 }, { axis: 'z', at: R.x0, n: -1, s0: R.z0, s1: R.z1 }, { axis: 'x', at: R.z1, n: 1, s0: R.x0, s1: R.x1 }, { axis: 'x', at: R.z0, n: -1, s0: R.x0, s1: R.x1 } ].forEach(e => { const pt = (t, d) => (e.axis === 'z' ? { x: e.at - e.n * d, z: t } : { x: t, z: e.at - e.n * d }); const facing = (p) => Math.min(...floors.map(f => { const [lo, hi, c0, c1, v, c] = e.axis === 'z' ? [f.z0, f.z1, f.x0, f.x1, p.z, p.x] : [f.x0, f.x1, f.z0, f.z1, p.x, p.z]; if (!(v > lo + EPS && v < hi - EPS)) return Infinity; const d = e.n > 0 ? c - c1 : c0 - c; return d > -EPS ? d : Infinity; })); const free = (t) => { const p = pt(t, 0), o = pt(t, -1), d = facing(p); return !underRoof(o.x, o.z) && d >= s.minExt - EPS && d < Infinity; }; const cuts = [e.s0, e.s1].concat(floors.flatMap(f => (e.axis === 'z' ? [f.z0, f.z1] : [f.x0, f.x1])) .filter(t => t > e.s0 + EPS && t < e.s1 - EPS)).sort((a, b) => a - b); for (let i = 0; i < cuts.length - 1; i++) { const a = cuts[i], b = cuts[i + 1]; if (b - a < EPS || !free((a + b) / 2)) continue; const last = runs[runs.length - 1]; if (last && last.axis === e.axis && last.n === e.n && Math.abs(last.at - e.at) < EPS && Math.abs(last.b - a) < EPS) last.b = b; else runs.push({ axis: e.axis, at: e.at, n: e.n, a, b }); } }); }); // Aansluitende stukken van verschillende units (bijv. twee units met dezelfde veranda) samenvoegen. runs.sort((p, q) => p.axis.localeCompare(q.axis) || p.n - q.n || Math.round(p.at) - Math.round(q.at) || p.a - q.a); const merged = []; runs.forEach(r => { const m = merged.find(o => o.axis === r.axis && o.n === r.n && Math.abs(o.at - r.at) < EPS && Math.abs(o.b - r.a) < EPS); if (m) m.b = r.b; else merged.push(Object.assign({}, r)); }); const half = s.size / 2; const pts = []; merged.forEach(r => { const pos = (t) => (r.axis === 'z' ? { x: r.at - r.n * half, z: t } : { x: t, z: r.at - r.n * half }); // Een eind dat op of tegen een vloer uitkomt rust op die gevel. const endFree = (t) => { const p = r.axis === 'z' ? { x: r.at, z: t } : { x: t, z: r.at }; return !onFloor(p.x, p.z, s.size); }; const fa = endFree(r.a), fb = endFree(r.b); const pa = fa ? r.a + half : r.a, pb = fb ? r.b - half : r.b; if (pb - pa < EPS) return; const n = Math.max(1, Math.ceil((pb - pa) / s.maxSpan - 1e-9)); for (let k = 0; k <= n; k++) { if ((k === 0 && !fa) || (k === n && !fb)) continue; pts.push(pos(pa + (pb - pa) * k / n)); } }); pts.forEach(p => { if (out.posts.some(q => Math.abs(q.x - p.x) < s.size && Math.abs(q.z - p.z) < s.size)) return; const roof = units.map(u => u.roof).find(rf => p.x > rf.rect.x0 - EPS && p.x < rf.rect.x1 + EPS && p.z > rf.rect.z0 - EPS && p.z < rf.rect.z1 + EPS); if (!roof) return; // Hoogste hoek: bij een hellend dak verdwijnt de paal aan de lage kant in het dakpakket, zonder kier. const top = Math.max(...[[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([a, b]) => roof.ceilingAt(p.x + a * (half - 1), p.z + b * (half - 1)))); if (!(top > groundY + SHOE_MM)) return; out.posts.push({ x: Math.round(p.x), z: Math.round(p.z), y0: groundY, y1: Math.round(top), size: s.size }); }); out.count = out.posts.length; out.lengthMM = out.posts.reduce((t, p) => t + p.y1 - p.y0 - SHOE_MM, 0); out.m3 = out.lengthMM * s.size * s.size / 1e9; out.kg = out.m3 * WOODS[s.wood].kgM3; return out; } function createMaterials() { return { wood: Object.fromEntries(Object.entries(WOODS).map(([k, w]) => [k, new THREE.MeshStandardMaterial({ color: w.hex, roughness: 0.78 })])), shoe: new THREE.MeshStandardMaterial({ color: 0x9ba0a4, roughness: 0.45, metalness: 0.6 }) }; } function build3D(data, materials) { const toM = 0.001, group = new THREE.Group(); if (!data || !data.posts.length) return group; const m = materials || createMaterials(); const woodMat = m.wood[data.wood] || m.wood.eiken; data.posts.forEach(p => { const h = p.y1 - p.y0 - SHOE_MM; const post = new THREE.Mesh(new THREE.BoxGeometry(p.size * toM, h * toM, p.size * toM), woodMat); post.position.set(p.x * toM, (p.y0 + SHOE_MM + h / 2) * toM, p.z * toM); group.add(post); const plate = new THREE.Mesh(new THREE.BoxGeometry((p.size + 40) * toM, 10 * toM, (p.size + 40) * toM), m.shoe); plate.position.set(p.x * toM, (p.y0 + 5) * toM, p.z * toM); group.add(plate); const stem = new THREE.Mesh(new THREE.BoxGeometry(p.size * 0.35 * toM, (SHOE_MM - 10) * toM, p.size * 0.35 * toM), m.shoe); stem.position.set(p.x * toM, (p.y0 + 10 + (SHOE_MM - 10) / 2) * toM, p.z * toM); group.add(stem); }); return group; } return { WOODS, SHOE_MM, DEFAULT_SETTINGS, normalizeSettings, compute, createMaterials, build3D }; })();