v0.4.6: binnenspots per unit, omgeving als keuzemenu (Ontwerp/Dag/Nacht), lampen 15% zachter

- LightingModel.computeIndoor: plafondraster per unit (800-1500 h.o.h., 600 van wand/naad)
- 3D, plattegrond, render, totalen en PDF
- tegelvloer 20% groter; gras volledig verwijderd

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 12:44:06 +02:00
co-authored by Claude Opus 5.5
parent 7ae76fe390
commit 5ecf3e0c2a
9 changed files with 142 additions and 38 deletions
+72 -9
View File
@@ -27,6 +27,14 @@
// (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.
// ============================================================
@@ -39,7 +47,12 @@ window.LightingModel = (() => {
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
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' };
@@ -182,16 +195,58 @@ 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));
}
// Binnenspots per unit. units = HouseModel-parts.units (rect, walls, roof.base),
// interior = parts.interior (binnenwanden met place). Geeft
// [{ unitId, x, z, y }] (mm, y = onderkant plafond).
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;
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 = interior.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;
});
if (!tooClose) out.push({ unitId: u.id, x, z, y: u.roof.base });
}));
});
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.
function build3D(lamps, materials) {
// 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;
@@ -214,14 +269,21 @@ window.LightingModel = (() => {
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;
}
// Volle render: elke gevellamp (per lichtrichting) en elke boeideel-spot
// wordt een echte SpotLight. Gevellampen staan net buiten het gevelvlak en
// schijnen langs de gevel omhoog/omlaag; spots schijnen recht naar beneden.
// spots = [{ x, z }] (mm), spotY = hoogte onderkant boeideel (mm).
function buildLights3D(lamps, spots, spotY) {
// spots = [{ x, z }] (mm), spotY = hoogte onderkant boeideel (mm);
// indoor = binnenspots [{ x, z, y }] (mm). Buitenlampen 15% minder fel dan
// eerst (was overdag te heftig).
function buildLights3D(lamps, spots, spotY, indoor) {
const toM = 0.001;
const group = new THREE.Group();
const add = (x, y, z, dirY, intensity, angle, distance) => {
@@ -233,12 +295,13 @@ window.LightingModel = (() => {
};
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;
if (l.direction !== 'up') add(x, y - 0.09, z, -1, 3, 0.55, 4.5);
if (l.direction !== 'down') add(x, y + 0.09, z, 1, 3, 0.55, 4.5);
if (l.direction !== 'up') add(x, y - 0.09, z, -1, 2.55, 0.55, 4.5);
if (l.direction !== 'down') add(x, y + 0.09, z, 1, 2.55, 0.55, 4.5);
});
(spots || []).forEach(p => add(p.x * toM, spotY * toM - 0.02, p.z * toM, -1, 2.5, 0.6, 5.5));
(spots || []).forEach(p => add(p.x * toM, spotY * toM - 0.02, p.z * toM, -1, 2.1, 0.6, 5.5));
(indoor || []).forEach(p => add(p.x * toM, p.y * toM - 0.02, p.z * toM, -1, 1.2, 0.7, 4));
return group;
}
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, normalizeSettings, compute, createMaterials, build3D, buildLights3D };
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, normalizeSettings, compute, computeIndoor, createMaterials, build3D, buildLights3D };
})();