- WallModel: optioneel middenpunt (heightMid, topLine), bovenregels in verstek; stijlen, openingen, beplating, doek, latwerk, BOM, 2D en PDF volgen de knik - wand-editor: schakelaar "Middenpunt (nok)" + nokhoogte, links/rechts = goothoogte - UnitModel.syncWallHeights: tegenoverliggende wand gespiegeld, haakse wanden vlak op de hoekhoogtes; wallPieces met nok per stuk - LightingModel.computeIndoor: raster vanaf binnenzijde eigen naadwanddeel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
373 lines
19 KiB
JavaScript
373 lines
19 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// ============================================================
|
|
// lighting/lighting-model.js — LightingModel: ENIGE bron van de
|
|
// buitenverlichting van een woning. Geen DOM.
|
|
//
|
|
// Woning-breed (settings.lighting): type, lichtrichting en alle afstanden.
|
|
// mode: 'geen' | 'spots' (boeideel-spots, alleen waar boeidelen zijn) |
|
|
// 'gevel' (gevellampen), direction: 'both' | 'down' | 'up'.
|
|
//
|
|
// Gevellampen worden per GEVEL verdeeld, niet per wand: wandstukken in
|
|
// hetzelfde gevelvlak die op elkaar aansluiten (ook over een naad tussen
|
|
// units) vormen samen één gevel van hoek tot hoek. De balklogica per wand
|
|
// blijft ongewijzigd; alleen de lampen lopen uniform door.
|
|
// Op lamphoogte (standaard 1950 mm) worden openingen incl. raamwerk
|
|
// (koningsstijl/dorpels) van de gevel afgetrokken. Op elk vrij stuk van
|
|
// minstens minWallWidth (2000 mm) komen lampen: de eerste en laatste op
|
|
// endOffset (300 mm) van het uiteinde (hoek of opening), daartussen
|
|
// verdeeld met min/max h.o.h. (zelfde verdeling als stijlen). Smallere vrije
|
|
// stukken krijgen geen lampen. Gevellampen en boeideel-spots sluiten elkaar
|
|
// uit: bij 'gevel' komen er nooit spots.
|
|
// Per wand(zijde) blijft een harde override mogelijk
|
|
// (unit.lighting.walls[side]): 'auto' (standaard), 'openingen' (lampen op
|
|
// openingOffset = 500 mm naast elke opening, bijv. slaapkamers) of 'geen'.
|
|
//
|
|
// Binnenspots (computeIndoor) worden PER UNIT berekend, net als de spots in
|
|
// het boeideel verdeeld tussen min/max h.o.h.: een raster in het plafond
|
|
// binnen de unit, minstens indoorOffset van de binnenzijde van elke wand (of
|
|
// van de unitrand waar een naad met een andere unit is), met in beide
|
|
// richtingen indoorMin..indoorMax h.o.h. Past er in een richting geen
|
|
// indoorMin tussen, dan één spot in het midden. Spots te dicht bij een
|
|
// binnenwand vervallen.
|
|
//
|
|
// Plain script, geladen na core/geometry.js en roof-model.js.
|
|
// ============================================================
|
|
|
|
window.LightingModel = (() => {
|
|
const DEFAULT_SETTINGS = {
|
|
mode: 'spots', // 'geen' | 'spots' | 'gevel'
|
|
direction: 'both', // gevellampen: 'both' | 'down' | 'up'
|
|
lampHeight: 1950, // mm boven vloer
|
|
lampMin: 1500, // mm h.o.h. gevellampen
|
|
lampMax: 3000,
|
|
minWallWidth: 2000, // kleinste vrije gevelbreedte voor gevellampen
|
|
endOffset: 300, // eerste/laatste lamp vanaf hoek/uiteinde
|
|
openingOffset: 500, // override: lampen zoveel naast elke opening
|
|
// Binnenspots in het plafond, per unit (computeIndoor).
|
|
indoor: true,
|
|
indoorMin: 800, // mm h.o.h., in beide richtingen
|
|
indoorMax: 1500,
|
|
indoorOffset: 600 // min. afstand tot wand (binnenzijde) of unitrand (naad)
|
|
};
|
|
const MODES = { geen: 'Geen', spots: 'Boeideel-spots', gevel: 'Gevellampen' };
|
|
const DIRECTIONS = { both: 'Boven én onder', down: 'Alleen onder', up: 'Alleen boven' };
|
|
const WALL_OVERRIDES = { auto: 'Automatisch', openingen: 'Naast openingen (override)', geen: 'Geen lampen' };
|
|
|
|
// Per unit alleen nog de override per wandzijde.
|
|
function createDefaultUnitLighting() {
|
|
return { walls: {} };
|
|
}
|
|
|
|
function normalizeSettings(settings) {
|
|
return Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
|
}
|
|
|
|
// Spots op een lijnstuk A -> B: op beide uiteinden + daartussen verdeeld.
|
|
function spotsBetween(A, B, min, max, out) {
|
|
const len = Math.hypot(B.x - A.x, B.z - A.z);
|
|
out.push({ x: A.x, z: A.z }, { x: B.x, z: B.z });
|
|
if (len < 1) return;
|
|
calculateSegmentPositions(len, min, max).forEach(p => out.push({ x: A.x + (B.x - A.x) * p / len, z: A.z + (B.z - A.z) * p / len }));
|
|
}
|
|
|
|
// Lampen die dichter dan `min` bij elkaar staan worden samengevoegd tot één
|
|
// lamp in het midden, tot alle onderlinge afstanden >= min zijn.
|
|
function enforceMinSpacing(positions, min) {
|
|
let pts = positions.slice().sort((a, b) => a - b);
|
|
let changed = true;
|
|
while (changed && pts.length > 1) {
|
|
changed = false;
|
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
if (pts[i + 1] - pts[i] < min - 0.5) {
|
|
pts.splice(i, 2, (pts[i] + pts[i + 1]) / 2);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return pts;
|
|
}
|
|
|
|
// Aftrekken van intervallen: [lo, hi] min blocked -> vrije intervallen.
|
|
function freeIntervals(lo, hi, blocked) {
|
|
const free = [];
|
|
let cursor = lo;
|
|
blocked.slice().sort((p, q) => p[0] - q[0]).forEach(([a, b]) => {
|
|
if (a > cursor) free.push([cursor, Math.min(a, hi)]);
|
|
cursor = Math.max(cursor, b);
|
|
});
|
|
if (cursor < hi) free.push([cursor, hi]);
|
|
return free.filter(([a, b]) => b - a > 1);
|
|
}
|
|
|
|
// Alles voor de hele woning. units = HouseModel-parts.units (met
|
|
// walls[].pieces[].config/place), stateUnits = state.units (overrides per
|
|
// wand), edge = RoofModel.computeEdge-resultaat (edge.spots wordt hier
|
|
// gevuld). Zet per wandstuk p.lamps = [{ u, y }] (voor editor/PDF).
|
|
// Geeft { lamps: [{ x, y, z, normal, direction, element, u, yLocal }] }.
|
|
function compute(settings, roofSettings, units, stateUnits, edge) {
|
|
const s = normalizeSettings(settings);
|
|
const overrides = {};
|
|
stateUnits.forEach(u => { overrides[u.id] = (u.lighting && u.lighting.walls) || {}; });
|
|
|
|
// 1. Buiten-wandstukken als lijnstukken in hun gevelvlak (wereld-mm).
|
|
const segs = [];
|
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
|
p.lamps = [];
|
|
const W = p.place.width;
|
|
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
|
|
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal);
|
|
const normal = { x: p.place.normal.x, z: p.place.normal.y }; // place.normal is {x, y} (plattegrond)
|
|
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z';
|
|
const perp = axis === 'x' ? 'z' : 'x';
|
|
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
|
// Gevel loopt tot de afgewerkte hoek, zodat endOffset altijd vanaf de
|
|
// zichtbare hoek telt: elk uiteinde + afwerking (latwerk + bekleding van
|
|
// de haakse wand die eroverheen loopt), en waar dit stuk de kopse kant
|
|
// van een doorlopende wand bedekt (skinExt) ook nog die wanddikte.
|
|
// Binnenhoeken en naden: zie de voetafdrukken in 3 en de overlap in 2.
|
|
const c = WallModel.claddingOffset(p.buitenMateriaal), e = p.skinExt || {};
|
|
const extL = (e.left > 0 ? e.left : 0) + c, extR = (e.right > 0 ? e.right : 0) + c;
|
|
const up = q1[axis] >= q0[axis];
|
|
// Voetafdruk van dit stuk incl. afwerking (om bedekte delen van andere
|
|
// gevels af te trekken, zie 3).
|
|
const corners = [[-W / 2, -p.place.thickness / 2], [W / 2, -p.place.thickness / 2], [-W / 2, T / 2], [W / 2, T / 2]]
|
|
.map(([lx, lz]) => UnitModel.localToWorld(p.place, lx, lz));
|
|
segs.push({
|
|
piece: p, normal, axis, perp, line: q0[perp],
|
|
sAt0: q0[axis], sAtW: q1[axis],
|
|
s0: up ? q0[axis] - extL : q1[axis] - extR, s1: up ? q1[axis] + extR : q0[axis] + extL,
|
|
foot: { x0: Math.min(...corners.map(q => q.x)), x1: Math.max(...corners.map(q => q.x)), z0: Math.min(...corners.map(q => q.z)), z1: Math.max(...corners.map(q => q.z)) },
|
|
override: overrides[u.id][w.side] || 'auto'
|
|
});
|
|
})));
|
|
const uToS = (sg, u) => sg.sAt0 + (sg.sAtW - sg.sAt0) * (u / sg.piece.place.width);
|
|
const sToU = (sg, v) => (v - sg.sAt0) / (sg.sAtW - sg.sAt0) * sg.piece.place.width;
|
|
|
|
// 2. Gevels: zelfde normaal + zelfde gevelvlak + aansluitend.
|
|
const runs = groupFacadeRuns(segs); // core/geometry.js
|
|
|
|
// 3. Lampen per gevel.
|
|
const lamps = [];
|
|
const H = s.lampHeight;
|
|
const addLamp = (run, v) => {
|
|
const sg = run.segs.find(x => v >= x.s0 - 0.5 && v <= x.s1 + 0.5);
|
|
if (!sg) return;
|
|
const u = sToU(sg, v);
|
|
const pt = run.axis === 'x' ? { x: v, z: run.line } : { x: run.line, z: v };
|
|
sg.piece.lamps.push({ u, y: H });
|
|
lamps.push({ x: pt.x, z: pt.z, y: sg.piece.place.floorTop + H, normal: run.normal, direction: s.direction, element: sg.piece.element, u, yLocal: H });
|
|
};
|
|
if (s.mode === 'gevel') {
|
|
runs.forEach(run => {
|
|
const blocked = [];
|
|
const pos = []; // lampposities langs deze gevel (wereld-mm)
|
|
// Delen van deze gevel die achter een andere wand liggen (bijv. het
|
|
// stuk van een wand dat de naad in loopt om een binnenhoek te sluiten)
|
|
// zijn geen zichtbare gevel: de gevel begint pas bij de afgewerkte
|
|
// buitenkant van die andere wand.
|
|
segs.filter(o => !run.segs.includes(o)).forEach(o => {
|
|
const f = o.foot;
|
|
const pMin = run.axis === 'x' ? f.z0 : f.x0, pMax = run.axis === 'x' ? f.z1 : f.x1;
|
|
const aMin = run.axis === 'x' ? f.x0 : f.z0, aMax = run.axis === 'x' ? f.x1 : f.z1;
|
|
if (run.line > pMin + 1 && run.line < pMax - 1 && aMax > run.s0 && aMin < run.s1) blocked.push([Math.max(run.s0, aMin), Math.min(run.s1, aMax)]);
|
|
});
|
|
run.segs.forEach(sg => {
|
|
const cfg = sg.piece.config, sW = cfg.studWidth;
|
|
const low = Math.min(cfg.wallHeightLeft, cfg.wallHeightRight) < H + 100;
|
|
if (sg.override === 'geen' || low) { blocked.push([sg.s0, sg.s1]); return; }
|
|
if (sg.override === 'openingen') {
|
|
// Harde override: lampen naast elke opening, dit stuk doet verder niet mee.
|
|
blocked.push([sg.s0, sg.s1]);
|
|
cfg.openings.forEach(op => [op.x - s.openingOffset, op.x + op.w + s.openingOffset]
|
|
.filter(u => u >= 0 && u <= cfg.wallWidth).forEach(u => pos.push(uToS(sg, u))));
|
|
return;
|
|
}
|
|
cfg.openings.filter(op => op.y - sW <= H && op.y + op.h + sW >= H).forEach(op => {
|
|
const a = uToS(sg, op.x - sW), b = uToS(sg, op.x + op.w + sW);
|
|
blocked.push([Math.min(a, b), Math.max(a, b)]);
|
|
});
|
|
});
|
|
freeIntervals(run.s0, run.s1, blocked).forEach(([a, b]) => {
|
|
if (b - a < s.minWallWidth) return; // te smal: geen lampen
|
|
const first = a + s.endOffset, last = b - s.endOffset;
|
|
pos.push(first);
|
|
calculateSegmentPositions(last - first, s.lampMin, s.lampMax).forEach(p => pos.push(first + p));
|
|
pos.push(last);
|
|
});
|
|
// Min. h.o.h. geldt over ALLE lampen van de gevel samen (ook bij
|
|
// afstand hoek/opening, overrides en de grens tussen wanden).
|
|
enforceMinSpacing(pos, s.lampMin).forEach(v => addLamp(run, v));
|
|
});
|
|
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => p.lamps.sort((a, b) => a.u - b.u))));
|
|
}
|
|
|
|
// 4. Boeideel-spots per dakrand-ribbe (alleen bij type 'spots').
|
|
if (edge) {
|
|
const spots = [];
|
|
if (s.mode === 'spots') {
|
|
const rs = RoofModel.normalizeSettings(roofSettings);
|
|
edge.segments.filter(sg => sg.fascia).forEach(sg => {
|
|
const from = spots.length;
|
|
spotsBetween(sg.spotA, sg.spotB, rs.spotMin, rs.spotMax, spots);
|
|
for (let k = from; k < spots.length; k++) spots[k].normal = { x: sg.normal.x, z: sg.normal.z };
|
|
});
|
|
}
|
|
// Dubbele spots (hoeken) samenvoegen; de buitenrichting wordt dan de
|
|
// som van beide gevels (diagonaal naar buiten).
|
|
edge.spots = [];
|
|
spots.forEach(p => {
|
|
const q = edge.spots.find(e => Math.hypot(e.x - p.x, e.z - p.z) < 5);
|
|
if (!q) edge.spots.push({ x: p.x, z: p.z, normal: p.normal });
|
|
else if (p.normal && q.normal) q.normal = { x: q.normal.x + p.normal.x, z: q.normal.z + p.normal.z };
|
|
});
|
|
}
|
|
return { lamps };
|
|
}
|
|
|
|
// Posities op [a, b]: beide uiteinden + verdeeld (calculateSegmentPositions);
|
|
// korter dan min (of negatief) = één positie in het midden.
|
|
function gridPositions(a, b, min, max) {
|
|
const L = b - a;
|
|
if (L < min) return [(a + b) / 2];
|
|
return [a, ...calculateSegmentPositions(L, min, max).map(p => a + p), b];
|
|
}
|
|
|
|
// Afstand van punt p tot lijnstuk a-b (mm).
|
|
function distToSegment(p, a, b) {
|
|
const dx = b.x - a.x, dz = b.z - a.z, len2 = dx * dx + dz * dz || 1;
|
|
const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.z - a.z) * dz) / len2));
|
|
return Math.hypot(p.x - (a.x + t * dx), p.z - (a.z + t * dz));
|
|
}
|
|
|
|
// Binnenspots per unit. units = HouseModel-parts.units (rect, walls, roof.base),
|
|
// interior = parts.interior (binnenwanden met place). Geeft
|
|
// [{ unitId, x, z, y, floorY }] (mm, y = onderkant plafond, floorY = loopvlak).
|
|
function computeIndoor(settings, units, interior) {
|
|
const s = normalizeSettings(settings);
|
|
if (!s.indoor) return [];
|
|
const out = [];
|
|
units.forEach(u => {
|
|
const T = {};
|
|
u.walls.forEach(w => { T[w.side] = w.removed ? 0 : w.thickness; });
|
|
const r = u.rect, off = s.indoorOffset;
|
|
// Naadwanddelen van deze unit staan binnen de rect tegen de naad: het
|
|
// raster begint dan vanaf hun binnenzijde (net als bij een buitenwand).
|
|
const own = interior.filter(iw => !iw.seamUnit || iw.seamUnit === u.id);
|
|
own.filter(iw => iw.seamUnit).forEach(iw => {
|
|
const p = iw.place, horiz = Math.abs(Math.sin(p.rotY)) < 0.5;
|
|
const [side, d] = horiz
|
|
? (Math.abs(p.cz - r.z0) < Math.abs(p.cz - r.z1) ? ['voor', p.cz - r.z0] : ['achter', r.z1 - p.cz])
|
|
: (Math.abs(p.cx - r.x0) < Math.abs(p.cx - r.x1) ? ['links', p.cx - r.x0] : ['rechts', r.x1 - p.cx]);
|
|
T[side] = Math.max(T[side] || 0, d + p.thickness / 2);
|
|
});
|
|
const xs = gridPositions(r.x0 + (T.links || 0) + off, r.x1 - (T.rechts || 0) - off, s.indoorMin, s.indoorMax);
|
|
const zs = gridPositions(r.z0 + (T.voor || 0) + off, r.z1 - (T.achter || 0) - off, s.indoorMin, s.indoorMax);
|
|
xs.forEach(x => zs.forEach(z => {
|
|
const tooClose = own.some(iw => {
|
|
const half = iw.place.width / 2, c = Math.cos(iw.place.rotY), sn = Math.sin(iw.place.rotY);
|
|
const a = { x: iw.place.cx - half * c, z: iw.place.cz + half * sn }, b = { x: iw.place.cx + half * c, z: iw.place.cz - half * sn };
|
|
return distToSegment({ x, z }, a, b) < iw.place.thickness / 2 + off - 1;
|
|
});
|
|
if (!tooClose) out.push({ unitId: u.id, x, z, y: u.roof.base, floorY: u.layout.floorTop });
|
|
}));
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function createMaterials() {
|
|
return {
|
|
housingMaterial: new THREE.MeshStandardMaterial({ color: 0x2b2f33, roughness: 0.5, metalness: 0.3 }),
|
|
glowMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe2a0, emissiveIntensity: 1 }),
|
|
indoorMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe2a0, emissiveIntensity: 0.8 }),
|
|
beamMaterial: new THREE.MeshBasicMaterial({ color: 0xffe9a8, transparent: true, opacity: 0.16, depthWrite: false, side: THREE.DoubleSide })
|
|
};
|
|
}
|
|
|
|
// 3D: behuizing op het gevelvlak + lichtbundel(s) omhoog/omlaag, en de
|
|
// binnenspots (indoor, zie computeIndoor) als schijfje in het plafond.
|
|
function build3D(lamps, materials, indoor) {
|
|
const toM = 0.001;
|
|
const group = new THREE.Group();
|
|
const w = 0.08, h = 0.16, d = 0.07, beamH = 0.7, beamR = 0.22;
|
|
lamps.forEach(l => {
|
|
const cx = l.x * toM + l.normal.x * d / 2, cz = l.z * toM + l.normal.z * d / 2, cy = l.y * toM;
|
|
const alongX = Math.abs(l.normal.z) > 0.5;
|
|
const box = new THREE.Mesh(new THREE.BoxGeometry(alongX ? w : d, h, alongX ? d : w), materials.housingMaterial);
|
|
box.position.set(cx, cy, cz);
|
|
group.add(box);
|
|
const addGlow = (sign) => {
|
|
const g = new THREE.Mesh(new THREE.BoxGeometry(alongX ? w * 0.8 : d * 0.8, 0.006, alongX ? d * 0.8 : w * 0.8), materials.glowMaterial);
|
|
g.position.set(cx, cy + sign * h / 2, cz);
|
|
group.add(g);
|
|
const cone = new THREE.Mesh(new THREE.ConeGeometry(beamR, beamH, 20, 1, true), materials.beamMaterial);
|
|
cone.userData.beam = true; // verborgen in de volle render (dan schijnt echt licht)
|
|
if (sign > 0) cone.rotation.x = Math.PI; // punt bij de lamp, breed naar boven
|
|
cone.position.set(cx, cy + sign * (h / 2 + beamH / 2), cz);
|
|
group.add(cone);
|
|
};
|
|
if (l.direction !== 'up') addGlow(-1);
|
|
if (l.direction !== 'down') addGlow(1);
|
|
});
|
|
(indoor || []).forEach(p => {
|
|
const spot = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.01, 16), materials.indoorMaterial);
|
|
spot.position.set(p.x * toM, p.y * toM - 0.006, p.z * toM);
|
|
group.add(spot);
|
|
});
|
|
return group;
|
|
}
|
|
|
|
// Render: elke gevellamp (per lichtrichting), boeideel-spot en binnenspot
|
|
// wordt een echte SpotLight. Echte schaduw per lamp is te zwaar (één
|
|
// schaduwkaart per lamp; de browser kan er maar ~16), dus licht door muren
|
|
// wordt geometrisch voorkomen:
|
|
// - buitenlampen en boeideel-spots zijn half hun bundelhoek naar buiten
|
|
// gekanteld: de binnenrand van de bundel loopt verticaal langs de gevel;
|
|
// - binnenspots hebben een harde reikwijdte net voorbij de vloer
|
|
// (INDOOR_REACH), zodat ze niet tot de grond buiten reiken.
|
|
// spots = [{ x, z }] (mm, boeideel), spotY = onderkant boeideel (mm),
|
|
// indoor = [{ x, z, y, floorY }] (mm). o.outdoorFactor schaalt de buiten-
|
|
// lampen (overdag lager: dan vallen ze nauwelijks op).
|
|
const OUTDOOR = { lamp: 2.0, spot: 1.6, angle: 0.5 };
|
|
const INDOOR = { intensity: 4.0, angle: 1.0, reach: 1.12, decay: 0.6 };
|
|
const DAY_OUTDOOR_FACTOR = 0.35;
|
|
function buildLights3D(lamps, spots, spotY, indoor, o) {
|
|
const toM = 0.001;
|
|
const fo = (o && o.outdoorFactor !== undefined) ? o.outdoorFactor : 1;
|
|
const group = new THREE.Group();
|
|
// Spot op (x, y, z) die naar (x + tx, y + ty, z + tz) schijnt.
|
|
const add = (x, y, z, tx, ty, tz, intensity, angle, distance, decay) => {
|
|
const light = new THREE.SpotLight(0xffd9a0, intensity, distance, angle, 0.6, decay);
|
|
light.position.set(x, y, z);
|
|
light.target.position.set(x + tx, y + ty, z + tz);
|
|
group.add(light);
|
|
group.add(light.target);
|
|
};
|
|
const tilt = Math.tan(OUTDOOR.angle); // bundelas half de bundelhoek naar buiten
|
|
lamps.forEach(l => {
|
|
const x = l.x * toM + l.normal.x * 0.12, z = l.z * toM + l.normal.z * 0.12, y = l.y * toM;
|
|
const nx = l.normal.x * tilt, nz = l.normal.z * tilt;
|
|
if (l.direction !== 'up') add(x, y - 0.09, z, nx, -1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5);
|
|
if (l.direction !== 'down') add(x, y + 0.09, z, nx, 1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5);
|
|
});
|
|
// Boeideel-spots: naar buiten gekanteld (normaal van de dakrand; op een
|
|
// hoek diagonaal).
|
|
(spots || []).forEach(p => {
|
|
const n = p.normal || { x: 0, z: 0 }, len = Math.hypot(n.x, n.z) || 1;
|
|
add(p.x * toM, spotY * toM - 0.02, p.z * toM, n.x / len * tilt, -1, n.z / len * tilt, OUTDOOR.spot * fo, OUTDOOR.angle, 5.5, 1.5);
|
|
});
|
|
(indoor || []).forEach(p => {
|
|
const drop = (p.y - p.floorY) * toM; // plafond -> loopvlak
|
|
add(p.x * toM, p.y * toM - 0.02, p.z * toM, 0, -1, 0, INDOOR.intensity, INDOOR.angle, drop * INDOOR.reach, INDOOR.decay);
|
|
});
|
|
return group;
|
|
}
|
|
|
|
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, DAY_OUTDOOR_FACTOR, normalizeSettings, compute, computeIndoor, createMaterials, build3D, buildLights3D };
|
|
})();
|