v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
+130
-15
@@ -251,6 +251,23 @@ window.LightingModel = (() => {
|
||||
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; });
|
||||
const r = u.rect;
|
||||
interior.filter(iw => iw.seamUnit === u.id).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);
|
||||
});
|
||||
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).
|
||||
@@ -259,21 +276,10 @@ window.LightingModel = (() => {
|
||||
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 off = s.indoorOffset, inner = unitInnerRect(u, interior);
|
||||
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);
|
||||
const xs = gridPositions(inner.x0 + off, inner.x1 - off, s.indoorMin, s.indoorMax);
|
||||
const zs = gridPositions(inner.z0 + off, inner.z1 - 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);
|
||||
@@ -374,8 +380,117 @@ window.LightingModel = (() => {
|
||||
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);
|
||||
});
|
||||
if (o && o.walls) group.add(buildWash3D(lamps, spots, spotY, o.walls, o.groundY || 0, fo));
|
||||
return group;
|
||||
}
|
||||
|
||||
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, DAY_OUTDOOR_FACTOR, normalizeSettings, compute, computeIndoor, createMaterials, build3D, buildLights3D };
|
||||
// ------------------------------------------------------------
|
||||
// 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.
|
||||
// ------------------------------------------------------------
|
||||
const WASH = { color: 0xffd9a0, spot: 0.55, lamp: 0.45, spread: 0.5, lampReach: 1800 };
|
||||
let washTexture = null;
|
||||
function getWashTexture() {
|
||||
if (washTexture) return washTexture;
|
||||
const W = 128, H = 256;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = W; canvas.height = H;
|
||||
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
|
||||
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);
|
||||
const k = (j * W + i) * 4;
|
||||
img.data[k] = img.data[k + 1] = img.data[k + 2] = Math.round(255 * v);
|
||||
img.data[k + 3] = 255;
|
||||
}
|
||||
}
|
||||
ctx.putImageData(img, 0, 0);
|
||||
washTexture = new THREE.CanvasTexture(canvas);
|
||||
return washTexture;
|
||||
}
|
||||
function washMesh(widthM, heightM, opacity, up) {
|
||||
const mat = new THREE.MeshBasicMaterial({
|
||||
map: getWashTexture(), color: WASH.color, transparent: true, opacity,
|
||||
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
|
||||
const uv = geo.attributes.uv;
|
||||
for (let i = 0; i < uv.count; i++) uv.setY(i, 1 - uv.getY(i));
|
||||
}
|
||||
const m = new THREE.Mesh(geo, mat);
|
||||
m.renderOrder = 2;
|
||||
return m;
|
||||
}
|
||||
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 => {
|
||||
const pl = p.place;
|
||||
if (pl.normal.x * n.x + pl.normal.y * n.z < 0.9) return;
|
||||
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 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 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 };
|
||||
if (Math.abs(nn.x) > 0.1 && Math.abs(nn.z) > 0.1) return;
|
||||
const top = (sp.y !== undefined ? sp.y : spotY) - 30;
|
||||
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);
|
||||
});
|
||||
// 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);
|
||||
if (!hit) return;
|
||||
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 (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);
|
||||
}
|
||||
});
|
||||
return group;
|
||||
}
|
||||
|
||||
return { DEFAULT_SETTINGS, MODES, DIRECTIONS, WALL_OVERRIDES, createDefaultUnitLighting, DAY_OUTDOOR_FACTOR, normalizeSettings, compute, computeIndoor, unitInnerRect, freeIntervals, createMaterials, build3D, buildLights3D };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user