v0.6.0: ruimtes (verfemmer: functie, netto m², vloerafwerking, FIBO-wandpanelen, eigen verfkleur, vals plafond, ruimtestaat op de PDF), inrichting (sanitair, keuken, kookeiland, kasten, bed, zithoek, boiler; slepen met snappen op het wandvlak, draaien, kosten), kozijnen met deurrichting, glasdeur en schuifdeuren (inbouw/opbouw, deuren open in de loopmodus) plus dagkanten, gevel stucwerk en wandstukken splitsen met eigen gevel, dak over de hele woning met overkapping, geschilderd plafond, model Amstel als nieuw ontwerp, kosten en prijzen uit kosten/intern.csv en kosten/leveranciers.csv (CostCsv); easter egg verwijderd; overbodige comments opgeschoond (alleen copyright, uitzonderingen en valkuilen blijven)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b5510d56c7
commit
ceda0b554d
+77
-145
@@ -4,61 +4,26 @@
|
||||
* 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
|
||||
mode: 'spots',
|
||||
direction: 'both',
|
||||
lampHeight: 1950,
|
||||
lampMin: 1500,
|
||||
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).
|
||||
minWallWidth: 2000,
|
||||
endOffset: 300,
|
||||
openingOffset: 500,
|
||||
indoor: true,
|
||||
indoorMin: 800, // mm h.o.h., in beide richtingen
|
||||
indoorMin: 800,
|
||||
indoorMax: 1500,
|
||||
indoorOffset: 600 // min. afstand tot wand (binnenzijde) of unitrand (naad)
|
||||
indoorOffset: 600
|
||||
};
|
||||
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: {} };
|
||||
}
|
||||
@@ -67,7 +32,6 @@ window.LightingModel = (() => {
|
||||
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 });
|
||||
@@ -75,8 +39,6 @@ window.LightingModel = (() => {
|
||||
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;
|
||||
@@ -93,7 +55,6 @@ window.LightingModel = (() => {
|
||||
return pts;
|
||||
}
|
||||
|
||||
// Aftrekken van intervallen: [lo, hi] min blocked -> vrije intervallen.
|
||||
function freeIntervals(lo, hi, blocked) {
|
||||
const free = [];
|
||||
let cursor = lo;
|
||||
@@ -105,40 +66,26 @@ window.LightingModel = (() => {
|
||||
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 = [];
|
||||
if (p.baseY) return; // opbouw boven een lager dak: geen gevellampen
|
||||
const W = p.place.width;
|
||||
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
|
||||
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal, p.stone);
|
||||
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc);
|
||||
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';
|
||||
// Binnenhoek-doorloop (negatieve skinExt): de gevel begint pas daarna.
|
||||
const tL = Math.max(0, -((p.skinExt || {}).left || 0)), tR = Math.max(0, -((p.skinExt || {}).right || 0));
|
||||
const q0 = UnitModel.localToWorld(p.place, -W / 2 + tL, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2 - tR, 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, p.stone), e = p.skinExt || {};
|
||||
// Gevel loopt tot de afgewerkte hoek, zodat endOffset vanaf de zichtbare hoek telt.
|
||||
const c = WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc), 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({
|
||||
@@ -152,10 +99,8 @@ window.LightingModel = (() => {
|
||||
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
|
||||
const runs = groupFacadeRuns(segs);
|
||||
|
||||
// 3. Lampen per gevel.
|
||||
const lamps = [];
|
||||
const H = s.lampHeight;
|
||||
const addLamp = (run, v) => {
|
||||
@@ -169,11 +114,8 @@ window.LightingModel = (() => {
|
||||
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.
|
||||
const pos = [];
|
||||
// Delen achter een andere wand (bijv. een stuk dat de naad in loopt) zijn geen zichtbare gevel.
|
||||
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;
|
||||
@@ -185,7 +127,6 @@ window.LightingModel = (() => {
|
||||
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))));
|
||||
@@ -197,35 +138,31 @@ window.LightingModel = (() => {
|
||||
});
|
||||
});
|
||||
freeIntervals(run.s0, run.s1, blocked).forEach(([a, b]) => {
|
||||
if (b - a < s.minWallWidth) return; // te smal: geen lampen
|
||||
if (b - a < s.minWallWidth) return;
|
||||
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).
|
||||
// Min. h.o.h. geldt over alle lampen van de gevel samen (ook 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 && !sg.overRoof).forEach(sg => { // niet boven een lager dak
|
||||
edge.segments.filter(sg => sg.fascia && !sg.overRoof).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 };
|
||||
spots[k].y = sg.topAt(spots[k]) - edge.fasciaHeight; // onderkant boeideel (volgt de dakvorm)
|
||||
spots[k].y = sg.topAt(spots[k]) - edge.fasciaHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 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);
|
||||
@@ -236,24 +173,18 @@ window.LightingModel = (() => {
|
||||
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));
|
||||
}
|
||||
|
||||
// Binnenruimte van een unit (woning-mm): de rect min de buitenwanden, en
|
||||
// waar een naadwanddeel van deze unit tegen de naad staat, tot diens
|
||||
// binnenzijde (net als bij een buitenwand). Ook gebruikt door ElektraModel.
|
||||
function unitInnerRect(u, interior) {
|
||||
const T = {};
|
||||
u.walls.forEach(w => { T[w.side] = w.removed ? 0 : w.thickness; });
|
||||
@@ -268,9 +199,6 @@ window.LightingModel = (() => {
|
||||
return { x0: r.x0 + (T.links || 0), x1: r.x1 - (T.rechts || 0), z0: r.z0 + (T.voor || 0), z1: r.z1 - (T.achter || 0) };
|
||||
}
|
||||
|
||||
// Binnenspots per unit. units = HouseModel-parts.units (rect, walls, roof.ceilingAt),
|
||||
// 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 [];
|
||||
@@ -292,6 +220,36 @@ window.LightingModel = (() => {
|
||||
return out;
|
||||
}
|
||||
|
||||
const ROOM_SPOT_MIN_M2 = 0.6;
|
||||
function roomSpots(settings, indoor, rooms, units) {
|
||||
const s = normalizeSettings(settings);
|
||||
if (!s.indoor || !rooms) return indoor;
|
||||
rooms.rooms.concat(rooms.free).forEach(r => {
|
||||
if (r.areaM2 < ROOM_SPOT_MIN_M2) return;
|
||||
if (indoor.some(p => { const g = rooms.regionAt(p.x, p.z); return g && g.idx === r.key; })) return;
|
||||
const c = r.label, u = units.find(q => c.x >= q.rect.x0 && c.x <= q.rect.x1 && c.z >= q.rect.z0 && c.z <= q.rect.z1);
|
||||
if (!u) return;
|
||||
indoor.push({ unitId: u.id, x: c.x, z: c.z, y: u.roof.ceilingAt(c.x, c.z), floorY: u.layout.floorTop, room: true });
|
||||
});
|
||||
return indoor;
|
||||
}
|
||||
|
||||
let offMaterial = null;
|
||||
function applySwitches(group, isOff) {
|
||||
if (!group) return;
|
||||
offMaterial = offMaterial || new THREE.MeshStandardMaterial({ color: 0xcfcbc0, roughness: 0.5 });
|
||||
group.traverse(o => {
|
||||
const ref = o.userData.ref;
|
||||
if (!ref) return;
|
||||
const off = !!isOff(ref);
|
||||
if (o.userData.glow) {
|
||||
if (!o.userData.onMat) o.userData.onMat = o.material;
|
||||
o.material = off ? offMaterial : o.userData.onMat;
|
||||
} else if (o.isLight || o.userData.wash) o.visible = !off;
|
||||
else if (o.userData.beam) { if (off) o.visible = false; }
|
||||
});
|
||||
}
|
||||
|
||||
function createMaterials() {
|
||||
return {
|
||||
housingMaterial: new THREE.MeshStandardMaterial({ color: 0x2b2f33, roughness: 0.5, metalness: 0.3 }),
|
||||
@@ -301,8 +259,6 @@ window.LightingModel = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -316,10 +272,11 @@ window.LightingModel = (() => {
|
||||
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);
|
||||
g.userData = { ref: l, glow: true };
|
||||
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.userData = { beam: true, ref: l };
|
||||
if (sign > 0) cone.rotation.x = Math.PI;
|
||||
cone.position.set(cx, cy + sign * (h / 2 + beamH / 2), cz);
|
||||
group.add(cone);
|
||||
};
|
||||
@@ -329,25 +286,15 @@ window.LightingModel = (() => {
|
||||
(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);
|
||||
spot.userData = { ref: p, glow: true };
|
||||
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, y }] (mm, boeideel; y = onderkant boeideel, anders spotY),
|
||||
// indoor = [{ x, z, y, floorY }] (mm). o.outdoorFactor schaalt de buiten-
|
||||
// lampen (overdag lager: dan vallen ze nauwelijks op).
|
||||
// Zacht en breed i.p.v. fel en smal: lagere intensiteit, grotere bundel
|
||||
// en een brede zachte rand (penumbra), zodat naast elkaar liggende spots
|
||||
// niet optellen tot felle vlekken.
|
||||
// Geen schaduw per lamp (één schaduwkaart per lamp, de browser kan er ~16), dus licht door muren
|
||||
// geometrisch voorkomen: buitenlampen en boeideel-spots half hun bundelhoek naar buiten gekanteld,
|
||||
// binnenspots met een harde reikwijdte net voorbij de vloer (INDOOR_REACH).
|
||||
const OUTDOOR = { lamp: 1.1, spot: 0.9, angle: 0.62, penumbra: 0.85 };
|
||||
const INDOOR = { intensity: 1.4, angle: 1.3, penumbra: 0.9, reach: 1.12, decay: 1.0 };
|
||||
const DAY_OUTDOOR_FACTOR = 0.35;
|
||||
@@ -355,44 +302,34 @@ window.LightingModel = (() => {
|
||||
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, penumbra) => {
|
||||
const add = (x, y, z, tx, ty, tz, intensity, angle, distance, decay, penumbra, ref) => {
|
||||
const light = new THREE.SpotLight(0xffd9a0, intensity, distance, angle, penumbra, decay);
|
||||
light.userData.ref = ref;
|
||||
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
|
||||
const tilt = Math.tan(OUTDOOR.angle);
|
||||
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, OUTDOOR.penumbra);
|
||||
if (l.direction !== 'down') add(x, y + 0.09, z, nx, 1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5, OUTDOOR.penumbra);
|
||||
if (l.direction !== 'up') add(x, y - 0.09, z, nx, -1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5, OUTDOOR.penumbra, l);
|
||||
if (l.direction !== 'down') add(x, y + 0.09, z, nx, 1, nz, OUTDOOR.lamp * fo, OUTDOOR.angle, 4.5, 1.5, OUTDOOR.penumbra, l);
|
||||
});
|
||||
// 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, (p.y !== undefined ? p.y : 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, OUTDOOR.penumbra);
|
||||
add(p.x * toM, (p.y !== undefined ? p.y : 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, OUTDOOR.penumbra, p);
|
||||
});
|
||||
(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, INDOOR.penumbra);
|
||||
const drop = (p.y - p.floorY) * toM;
|
||||
add(p.x * toM, p.y * toM - 0.02, p.z * toM, 0, -1, 0, INDOOR.intensity, INDOOR.angle, drop * INDOOR.reach, INDOOR.decay, INDOOR.penumbra, p);
|
||||
});
|
||||
if (o && o.walls) group.add(buildWash3D(lamps, spots, spotY, o.walls, o.groundY || 0, fo));
|
||||
return group;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Nep-lichtvlekken (wall-wash) voor de render. Echte spots mogen niet op
|
||||
// de gevel schijnen (geen schaduw: het licht zou door de muur naar binnen
|
||||
// gaan, zie buildLights3D). In plaats daarvan komt op het gevelvlak een
|
||||
// additief, zacht kegelvormig verloop: smal en fel bij de lamp, breder en
|
||||
// zwakker naar beneden (of naar boven bij een up-light). Geen licht-
|
||||
// berekening, dus geen grens aan het aantal.
|
||||
// walls = buitenwandstukken (place, buitenMateriaal, stone); groundY = mm.
|
||||
// ------------------------------------------------------------
|
||||
// Nep-lichtvlekken op de gevel: echte spots zonder schaduw zouden door de muur naar binnen schijnen.
|
||||
const WASH = { color: 0xffd9a0, spot: 0.55, lamp: 0.45, spread: 0.5, lampReach: 1800 };
|
||||
let washTexture = null;
|
||||
function getWashTexture() {
|
||||
@@ -403,9 +340,9 @@ window.LightingModel = (() => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const img = ctx.createImageData(W, H);
|
||||
for (let j = 0; j < H; j++) {
|
||||
const t = j / (H - 1); // 0 = bij de lamp, 1 = einde bundel
|
||||
const half = 0.12 + 0.88 * Math.pow(t, 0.6); // bundelbreedte (fractie van de halve breedte)
|
||||
const fade = Math.min(1, t * 12) * Math.pow(1 - t, 1.6); // zacht begin, uitdoven
|
||||
const t = j / (H - 1);
|
||||
const half = 0.12 + 0.88 * Math.pow(t, 0.6);
|
||||
const fade = Math.min(1, t * 12) * Math.pow(1 - t, 1.6);
|
||||
for (let i = 0; i < W; i++) {
|
||||
const x = Math.abs((i + 0.5) / W * 2 - 1) / half;
|
||||
const v = x >= 1 ? 0 : fade * Math.pow(1 - x * x, 2);
|
||||
@@ -424,9 +361,8 @@ window.LightingModel = (() => {
|
||||
blending: THREE.AdditiveBlending, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2
|
||||
});
|
||||
const geo = new THREE.PlaneGeometry(widthM, heightM);
|
||||
// Lamp aan de bovenrand (down) resp. onderrand (up).
|
||||
geo.translate(0, up ? heightM / 2 : -heightM / 2, 0);
|
||||
if (up) { // textuur gespiegeld: lamp onderaan
|
||||
if (up) {
|
||||
const uv = geo.attributes.uv;
|
||||
for (let i = 0; i < uv.count; i++) uv.setY(i, 1 - uv.getY(i));
|
||||
}
|
||||
@@ -437,8 +373,6 @@ window.LightingModel = (() => {
|
||||
function buildWash3D(lamps, spots, spotY, walls, groundY, fo) {
|
||||
const toM = 0.001;
|
||||
const group = new THREE.Group();
|
||||
// Gevelvlak achter een punt (mm) met buitenrichting n: dichtstbijzijnde
|
||||
// wandstuk met dezelfde normaal waar het punt vóór ligt.
|
||||
const facadeBehind = (q, n) => {
|
||||
let best = null;
|
||||
walls.forEach(p => {
|
||||
@@ -447,21 +381,21 @@ window.LightingModel = (() => {
|
||||
const c = Math.cos(pl.rotY), sn = Math.sin(pl.rotY);
|
||||
const dx = q.x - pl.cx, dz = q.z - pl.cz;
|
||||
const lx = dx * c - dz * sn, lz = dx * sn + dz * c;
|
||||
const face = pl.thickness / 2 + WallModel.claddingOffset(p.buitenMateriaal, p.stone);
|
||||
const face = pl.thickness / 2 + WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc);
|
||||
const d = lz - face;
|
||||
if (Math.abs(lx) > pl.width / 2 + 50 || d < -5 || d > 1500) return;
|
||||
if (!best || d < best.d) best = { d, piece: p, lx, face };
|
||||
});
|
||||
return best;
|
||||
};
|
||||
const place = (mesh, pl, lx, lz, yMM) => {
|
||||
const place = (mesh, pl, lx, lz, yMM, ref) => {
|
||||
mesh.userData.ref = ref;
|
||||
mesh.userData.wash = true;
|
||||
const w = UnitModel.localToWorld(pl, lx, lz);
|
||||
mesh.position.set(w.x * toM, yMM * toM, w.z * toM);
|
||||
mesh.rotation.y = pl.rotY;
|
||||
group.add(mesh);
|
||||
};
|
||||
// Boeideel-spots: van de spot omlaag tot het maaiveld (op een hoek,
|
||||
// diagonale normaal, geen vlek).
|
||||
(spots || []).forEach(sp => {
|
||||
const n = sp.normal || { x: 0, z: 0 }, len = Math.hypot(n.x, n.z) || 1;
|
||||
const nn = { x: n.x / len, z: n.z / len };
|
||||
@@ -470,9 +404,8 @@ window.LightingModel = (() => {
|
||||
const hit = facadeBehind(sp, nn);
|
||||
if (!hit || top - groundY < 300) return;
|
||||
const H = (top - groundY) * toM;
|
||||
place(washMesh(2 * (0.1 + H * WASH.spread), H, WASH.spot * fo), hit.piece.place, hit.lx, hit.face + 6, top);
|
||||
place(washMesh(2 * (0.1 + H * WASH.spread), H, WASH.spot * fo), hit.piece.place, hit.lx, hit.face + 6, top, sp);
|
||||
});
|
||||
// Gevellampen (op de gevel): bundel omlaag en/of omhoog.
|
||||
(lamps || []).forEach(l => {
|
||||
const n = { x: l.normal.x, z: l.normal.z };
|
||||
const hit = facadeBehind({ x: l.x + n.x * 20, z: l.z + n.z * 20 }, n);
|
||||
@@ -480,17 +413,16 @@ window.LightingModel = (() => {
|
||||
const pl = hit.piece.place;
|
||||
if (l.direction !== 'up') {
|
||||
const H = Math.min(WASH.lampReach, l.y - 90 - groundY) * toM;
|
||||
if (H > 0.2) place(washMesh(2 * (0.06 + H * WASH.spread), H, WASH.lamp * fo), pl, hit.lx, hit.face + 6, l.y - 90);
|
||||
if (H > 0.2) place(washMesh(2 * (0.06 + H * WASH.spread), H, WASH.lamp * fo), pl, hit.lx, hit.face + 6, l.y - 90, l);
|
||||
}
|
||||
if (l.direction !== 'down') {
|
||||
// Niet boven de gevel uit (tot de bovenkant van het dak).
|
||||
const wallTop = pl.floorTop + WallModel.topLine(hit.piece.config).maxY + (hit.piece.skinAbove || 0);
|
||||
const H = Math.min(WASH.lampReach, wallTop - l.y - 90) * toM;
|
||||
if (H > 0.2) place(washMesh(2 * (0.06 + H * WASH.spread), H, WASH.lamp * fo, true), pl, hit.lx, hit.face + 6, l.y + 90);
|
||||
if (H > 0.2) place(washMesh(2 * (0.06 + H * WASH.spread), H, WASH.lamp * fo, true), pl, hit.lx, hit.face + 6, l.y + 90, l);
|
||||
}
|
||||
});
|
||||
return group;
|
||||
}
|
||||
|
||||
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, DAY_OUTDOOR_FACTOR, normalizeSettings, compute, computeIndoor, unitInnerRect, freeIntervals, createMaterials, build3D, buildLights3D };
|
||||
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, DAY_OUTDOOR_FACTOR, normalizeSettings, compute, computeIndoor, roomSpots, applySwitches, unitInnerRect, freeIntervals, createMaterials, build3D, buildLights3D };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user