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:
co-authored by
Claude Opus 5.5
parent
7ae76fe390
commit
5ecf3e0c2a
@@ -5,5 +5,3 @@ examples/
|
|||||||
# Lokale exports
|
# Lokale exports
|
||||||
*.pdf
|
*.pdf
|
||||||
.DS_Store
|
.DS_Store
|
||||||
# Oude grastextuur (niet meer gebruikt), lokaal bewaard
|
|
||||||
assets/grass*.jpg
|
|
||||||
|
|||||||
@@ -157,16 +157,22 @@ assets/ logo (preloader) + bestemmingsmap voor toekomstige
|
|||||||
Koppeling, Dak). Labels: zelfstandig naamwoord, geen dubbele punt, uitleg
|
Koppeling, Dak). Labels: zelfstandig naamwoord, geen dubbele punt, uitleg
|
||||||
in een hint (i-icoon) i.p.v. tussen haakjes. Sneltoets T wisselt in drie
|
in een hint (i-icoon) i.p.v. tussen haakjes. Sneltoets T wisselt in drie
|
||||||
stappen: standaard -> 3D groot -> alleen de render -> standaard.
|
stappen: standaard -> 3D groot -> alleen de render -> standaard.
|
||||||
- **Omgeving en render (v0.4.5).** Knop "Lucht" in elke 3D-werkbalk wisselt
|
- **Omgeving en render (v0.4.6).** Keuzemenu in elke 3D-werkbalk: Ontwerp
|
||||||
geen -> dag -> nacht (core/skybox.js, keuze onthouden). Het panorama
|
(geen omgeving) / Dag / Nacht (core/skybox.js, keuze onthouden). Het
|
||||||
(assets/skybox.webp / skybox-night.webp) wordt op de grond geprojecteerd
|
panorama (assets/skybox.webp / skybox-night.webp) wordt op de grond
|
||||||
(port van three.js GroundedSkybox); onder het object ligt een ronde
|
geprojecteerd (port van three.js GroundedSkybox); onder het object ligt een
|
||||||
tegelvloer (assets/tiles.jpg), groot genoeg voor het hele object, met een
|
ronde tegelvloer (assets/tiles.jpg, 20% ruimer dan object + 1,5 m) met een
|
||||||
witte opstaande rand van 100 x 25 mm. Vloer en rand vangen licht en
|
witte opstaande rand van 100 x 25 mm. Vloer en rand vangen licht en
|
||||||
schaduw. Woning: de tegelvloer ligt op het maaiveld (woning-instelling,
|
schaduw. Woning: de tegelvloer ligt op het maaiveld (woning-instelling,
|
||||||
standaard 350 mm onder bovenkant vloer). Knop "Render" (camera): lampen en
|
standaard 350 mm onder bovenkant vloer). Knop "Render" (camera): lampen en
|
||||||
spots als echte lichtbronnen in het gewone daglicht; Nacht: omgevingslicht
|
spots als echte lichtbronnen in het gewone daglicht; Nacht: omgevingslicht
|
||||||
15%, lampen altijd aan (100%).
|
15%, lampen altijd aan. Buitenlampen 15% minder fel dan in v0.4.5.
|
||||||
|
- **Binnenspots (v0.4.6).** LightingModel.computeIndoor: per unit een raster
|
||||||
|
spots in het plafond (onderkant dak), min. 800 / max. 1500 mm h.o.h. in
|
||||||
|
beide richtingen, min. 600 mm van de binnenzijde van elke wand (of van de
|
||||||
|
unitrand bij een naad); spots te dicht bij een binnenwand vervallen.
|
||||||
|
Woning-instellingen › Verlichting › Binnenspots. In 3D (filter Lampen),
|
||||||
|
plattegrond (open cirkeltje), render en totalen.
|
||||||
- **Mijlpaal v0.4 — gevelopbouw.** Samenvatting van de v0.3.11–0.3.19-iteraties:
|
- **Mijlpaal v0.4 — gevelopbouw.** Samenvatting van de v0.3.11–0.3.19-iteraties:
|
||||||
kruislat-latwerk op vochtdoek (richting gevelbekleding globaal), lampen op de
|
kruislat-latwerk op vochtdoek (richting gevelbekleding globaal), lampen op de
|
||||||
afgewerkte gevel (+52 mm), logo-watermerk op het doek, trasraam (cement-
|
afgewerkte gevel (+52 mm), logo-watermerk op het doek, trasraam (cement-
|
||||||
|
|||||||
@@ -638,3 +638,6 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight:
|
|||||||
.hn-title { display: flex; align-items: center; gap: 6px; }
|
.hn-title { display: flex; align-items: center; gap: 6px; }
|
||||||
.file-protocol-warning { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 9000; max-width: 640px; box-shadow: 0 8px 20px rgba(33,33,33,.2); }
|
.file-protocol-warning { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 9000; max-width: 640px; box-shadow: 0 8px 20px rgba(33,33,33,.2); }
|
||||||
.file-protocol-warning code { background: rgba(0,0,0,.06); padding: 0 4px; border-radius: 3px; }
|
.file-protocol-warning code { background: rgba(0,0,0,.06); padding: 0 4px; border-radius: 3px; }
|
||||||
|
.vf-option { gap: 9px; }
|
||||||
|
.vf-option.active { color: var(--primary-on-dark); font-weight: 600; }
|
||||||
|
.vf-menu .vf-option:hover { color: var(--primary-on-dark); }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
const APP_VERSION = '0.4.5';
|
const APP_VERSION = '0.4.6';
|
||||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||||
|
|
||||||
let tabs = [];
|
let tabs = [];
|
||||||
@@ -294,7 +294,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (location.protocol === 'file:') {
|
if (location.protocol === 'file:') {
|
||||||
const note = document.createElement('div');
|
const note = document.createElement('div');
|
||||||
note.className = 'ui-warn file-protocol-warning';
|
note.className = 'ui-warn file-protocol-warning';
|
||||||
note.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>Tectum is geopend als bestand. Start het via een webserver: <code>./start.sh</code> of <code>python3 -m http.server</code>, en open <code>http://localhost:8000</code>. Lucht en gras werken anders niet.</span>';
|
note.innerHTML = '<i class="bi bi-exclamation-triangle"></i><span>Tectum is geopend als bestand. Start het via een webserver: <code>./start.sh</code> of <code>python3 -m http.server</code>, en open <code>http://localhost:8000</code>. De omgeving (Dag/Nacht) werkt anders niet.</span>';
|
||||||
document.body.appendChild(note);
|
document.body.appendChild(note);
|
||||||
}
|
}
|
||||||
openNew('woning', 'L');
|
openNew('woning', 'L');
|
||||||
|
|||||||
+27
-13
@@ -5,7 +5,7 @@
|
|||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// core/skybox.js — omgeving voor de 3D-weergaven ("Lucht": geen/dag/nacht).
|
// core/skybox.js — omgeving voor de 3D-weergaven (Ontwerp / Dag / Nacht).
|
||||||
// Een panorama (assets/skybox.webp of skybox-night.webp) wordt op de grond
|
// Een panorama (assets/skybox.webp of skybox-night.webp) wordt op de grond
|
||||||
// geprojecteerd, zodat het object op de grond staat; onder het object ligt
|
// geprojecteerd, zodat het object op de grond staat; onder het object ligt
|
||||||
// een ronde tegelvloer (assets/tiles.jpg) met een witte opstaande rand, groot
|
// een ronde tegelvloer (assets/tiles.jpg) met een witte opstaande rand, groot
|
||||||
@@ -28,6 +28,7 @@ window.Skybox = (() => {
|
|||||||
const RADIUS_M = 60; // straal van de omgevingsbol
|
const RADIUS_M = 60; // straal van de omgevingsbol
|
||||||
const TILE_M = 2.4; // één tegeltextuur (1024 px) beslaat zoveel meter
|
const TILE_M = 2.4; // één tegeltextuur (1024 px) beslaat zoveel meter
|
||||||
const MARGIN_M = 1.5; // vrije rand tussen object en opstaande rand
|
const MARGIN_M = 1.5; // vrije rand tussen object en opstaande rand
|
||||||
|
const SIZE_FACTOR = 1.2; // tegelvloer 20% ruimer dan object + marge
|
||||||
const RIM_HEIGHT_M = 0.10, RIM_THICK_M = 0.025;
|
const RIM_HEIGHT_M = 0.10, RIM_THICK_M = 0.025;
|
||||||
const NIGHT_FACTOR = 0.15; // omgevingslicht 's nachts
|
const NIGHT_FACTOR = 0.15; // omgevingslicht 's nachts
|
||||||
const SUN_OFFSET = { x: 8, y: 14, z: 6 };
|
const SUN_OFFSET = { x: 8, y: 14, z: 6 };
|
||||||
@@ -130,7 +131,7 @@ window.Skybox = (() => {
|
|||||||
if (box.isEmpty()) return;
|
if (box.isEmpty()) return;
|
||||||
const c = box.getCenter(new THREE.Vector3());
|
const c = box.getCenter(new THREE.Vector3());
|
||||||
group.position.set(c.x, groundY, c.z);
|
group.position.set(c.x, groundY, c.z);
|
||||||
setTerrace(Math.hypot(box.max.x - box.min.x, box.max.z - box.min.z) / 2 + MARGIN_M);
|
setTerrace((Math.hypot(box.max.x - box.min.x, box.max.z - box.min.z) / 2 + MARGIN_M) * SIZE_FACTOR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -176,26 +177,39 @@ window.Skybox = (() => {
|
|||||||
if (o.shadowRoot) o.shadowRoot.traverse(obj => { if (obj.isMesh) obj.castShadow = on; });
|
if (o.shadowRoot) o.shadowRoot.traverse(obj => { if (obj.isMesh) obj.castShadow = on; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Knop in een 3D-werkbalk die door geen -> dag -> nacht wisselt.
|
// Keuzemenu in een 3D-werkbalk: Ontwerp (geen omgeving) / Dag / Nacht.
|
||||||
const LABELS = { none: ['cloud', 'Lucht'], day: ['sun', 'Dag'], night: ['moon-stars', 'Nacht'] };
|
const LABELS = { none: ['pencil-square', 'Ontwerp'], day: ['sun', 'Dag'], night: ['moon-stars', 'Nacht'] };
|
||||||
function createToggle(toolbar, onChange) {
|
function createToggle(toolbar, onChange) {
|
||||||
const btn = document.createElement('button');
|
|
||||||
btn.type = 'button';
|
|
||||||
btn.title = 'Omgeving: geen / dag / nacht';
|
|
||||||
let m = getMode();
|
let m = getMode();
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'view-filter';
|
||||||
|
wrap.innerHTML = `<button type="button" class="btn-3d-toggle vf-btn" title="Omgeving"></button>
|
||||||
|
<div class="vf-menu">${MODES.map(k => `<div class="vf-item vf-option" data-mode="${k}"><i class="bi bi-${LABELS[k][0]}"></i>${LABELS[k][1]}</div>`).join('')}</div>`;
|
||||||
|
toolbar.appendChild(wrap);
|
||||||
|
const btn = wrap.querySelector('.vf-btn'), menu = wrap.querySelector('.vf-menu');
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
btn.className = 'btn-3d-toggle' + (m !== 'none' ? ' active' : '');
|
btn.innerHTML = `<i class="bi bi-${LABELS[m][0]}"></i>${LABELS[m][1]}<i class="bi bi-chevron-down"></i>`;
|
||||||
btn.innerHTML = `<i class="bi bi-${LABELS[m][0]}"></i>${LABELS[m][1]}`;
|
menu.querySelectorAll('.vf-option').forEach(o => o.classList.toggle('active', o.dataset.mode === m));
|
||||||
};
|
};
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', (e) => { e.stopPropagation(); menu.classList.toggle('open'); });
|
||||||
m = MODES[(MODES.indexOf(m) + 1) % MODES.length];
|
menu.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const opt = e.target.closest('.vf-option');
|
||||||
|
if (!opt) return;
|
||||||
|
menu.classList.remove('open');
|
||||||
|
if (opt.dataset.mode === m) return;
|
||||||
|
m = opt.dataset.mode;
|
||||||
remember(m);
|
remember(m);
|
||||||
sync();
|
sync();
|
||||||
onChange(m);
|
onChange(m);
|
||||||
});
|
});
|
||||||
|
const closeOnOutside = () => {
|
||||||
|
if (!document.body.contains(wrap)) { document.removeEventListener('click', closeOnOutside); return; }
|
||||||
|
menu.classList.remove('open');
|
||||||
|
};
|
||||||
|
document.addEventListener('click', closeOnOutside);
|
||||||
sync();
|
sync();
|
||||||
toolbar.appendChild(btn);
|
return wrap;
|
||||||
return btn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { CAMERA_HEIGHT_M, NIGHT_FACTOR, createEnvironment, apply, getMode, lightFactor, createToggle };
|
return { CAMERA_HEIGHT_M, NIGHT_FACTOR, createEnvironment, apply, getMode, lightFactor, createToggle };
|
||||||
|
|||||||
@@ -387,7 +387,9 @@ window.HouseModel = (() => {
|
|||||||
const roofEdgeGroup = build && roofEdge ? RoofModel.buildEdge3D(roofEdge, o.roofMaterials, {
|
const roofEdgeGroup = build && roofEdge ? RoofModel.buildEdge3D(roofEdge, o.roofMaterials, {
|
||||||
edgeVisible: o.beplatingVisible !== false, spotsVisible: o.lampenVisible !== false
|
edgeVisible: o.beplatingVisible !== false, spotsVisible: o.lampenVisible !== false
|
||||||
}) : null;
|
}) : null;
|
||||||
const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials()) : null;
|
// Binnenspots in het plafond, per unit (LightingModel.computeIndoor).
|
||||||
|
lighting.indoor = LightingModel.computeIndoor(state.settings.lighting, units, interior);
|
||||||
|
const lightingGroup = build ? LightingModel.build3D(lighting.lamps, o.lightingMaterials || LightingModel.createMaterials(), lighting.indoor) : null;
|
||||||
if (lightingGroup) lightingGroup.visible = o.lampenVisible !== false;
|
if (lightingGroup) lightingGroup.visible = o.lampenVisible !== false;
|
||||||
|
|
||||||
// Sandwichpanelen rondom de vloeren, vanaf het loopvlak naar beneden.
|
// Sandwichpanelen rondom de vloeren, vanaf het loopvlak naar beneden.
|
||||||
@@ -502,6 +504,7 @@ window.HouseModel = (() => {
|
|||||||
} : null,
|
} : null,
|
||||||
lamps: {
|
lamps: {
|
||||||
total: parts.lighting.lamps.length,
|
total: parts.lighting.lamps.length,
|
||||||
|
indoor: (parts.lighting.indoor || []).length,
|
||||||
both: parts.lighting.lamps.filter(l => l.direction === 'both').length,
|
both: parts.lighting.lamps.filter(l => l.direction === 'both').length,
|
||||||
down: parts.lighting.lamps.filter(l => l.direction === 'down').length,
|
down: parts.lighting.lamps.filter(l => l.direction === 'down').length,
|
||||||
up: parts.lighting.lamps.filter(l => l.direction === 'up').length
|
up: parts.lighting.lamps.filter(l => l.direction === 'up').length
|
||||||
|
|||||||
@@ -224,7 +224,8 @@ window.HousePdf = (() => {
|
|||||||
if (e.trimLengthM > 0) rows.push(['Daktrim', `${fmt(e.trimLengthM, 2)} m, ${e.trimHeight} mm hoog`]);
|
if (e.trimLengthM > 0) rows.push(['Daktrim', `${fmt(e.trimLengthM, 2)} m, ${e.trimHeight} mm hoog`]);
|
||||||
rows.push(['Boeideel-spots', e.spots + ' stuks']);
|
rows.push(['Boeideel-spots', e.spots + ' stuks']);
|
||||||
rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]);
|
rows.push(['Gevellampen', `${L.total} stuks (boven+onder ${L.both}, onder ${L.down}, boven ${L.up})`]);
|
||||||
PdfSheet.table(doc, 215, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & buitenverlichting'));
|
rows.push(['Binnenspots', `${L.indoor} stuks (plafond)`]);
|
||||||
|
PdfSheet.table(doc, 215, y, [0, 40], ['Onderdeel', 'Hoeveelheid'], rows, tableOpts('Dakrand & verlichting'));
|
||||||
}
|
}
|
||||||
|
|
||||||
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
|
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
|
||||||
|
|||||||
+20
-4
@@ -336,7 +336,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !lampsOn; });
|
if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !lampsOn; });
|
||||||
if (lampsOn && parts && lampenVisible) {
|
if (lampsOn && parts && lampenVisible) {
|
||||||
const edge = dakVisible ? parts.roofEdge : null;
|
const edge = dakVisible ? parts.roofEdge : null;
|
||||||
lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0);
|
lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0, parts.lighting.indoor);
|
||||||
scene.add(lightsGroup);
|
scene.add(lightsGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,6 +584,13 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
// Binnenspots (plafond): open cirkeltje
|
||||||
|
ctx.strokeStyle = '#ffe9a8'; ctx.lineWidth = 1.5;
|
||||||
|
(parts.lighting.indoor || []).forEach(sp => {
|
||||||
|
const q = toPx(sp.x, sp.z);
|
||||||
|
ctx.beginPath(); ctx.arc(q.x, q.y, 3.5, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
});
|
||||||
|
|
||||||
// Gevellampen: blokje op het gevelvlak
|
// Gevellampen: blokje op het gevelvlak
|
||||||
ctx.fillStyle = '#ffd36b'; ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 1;
|
ctx.fillStyle = '#ffd36b'; ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 1;
|
||||||
parts.lighting.lamps.forEach(l => {
|
parts.lighting.lamps.forEach(l => {
|
||||||
@@ -758,7 +765,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) : '')),
|
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) : '')),
|
||||||
section('Sandwichpanelen', 'bounding-box', toggle('hs-sandwich', 'Rondom woning', st.sandwich,
|
section('Sandwichpanelen', 'bounding-box', toggle('hs-sandwich', 'Rondom woning', st.sandwich,
|
||||||
{ hint: `${SandwichModel.PANEL_WIDTH_MM} × ${SandwichModel.PANEL_LENGTH_MM} × ${SandwichModel.THICK_MM} mm, vanaf bovenkant vloer ${SandwichModel.DEPTH_MM} mm naar beneden` })) +
|
{ hint: `${SandwichModel.PANEL_WIDTH_MM} × ${SandwichModel.PANEL_LENGTH_MM} × ${SandwichModel.THICK_MM} mm, vanaf bovenkant vloer ${SandwichModel.DEPTH_MM} mm naar beneden` })) +
|
||||||
section('Maaiveld', 'tree', num('hs-ground', 'Onder bovenkant vloer', st.groundLevel, { step: 10, hint: 'Hoogte van het gras in de 3D-weergave met Lucht aan' }))) },
|
section('Maaiveld', 'tree', num('hs-ground', 'Onder bovenkant vloer', st.groundLevel, { step: 10, hint: 'Hoogte van de tegelvloer in de 3D-weergave bij Dag/Nacht' }))) },
|
||||||
{ key: 'constructie', label: 'Regelwerk', icon: 'columns-gap', html: cols(
|
{ key: 'constructie', label: 'Regelwerk', icon: 'columns-gap', html: cols(
|
||||||
section('Buitenwanden', 'bricks', num('hs-ext-min', 'Afstand min.', sp.exterior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-ext-max', 'Afstand max.', sp.exterior.max, { step: 10, hint: 'Hart-op-hart stijlen' })),
|
section('Buitenwanden', 'bricks', num('hs-ext-min', 'Afstand min.', sp.exterior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-ext-max', 'Afstand max.', sp.exterior.max, { step: 10, hint: 'Hart-op-hart stijlen' })),
|
||||||
section('Binnenwanden', 'distribute-horizontal', num('hs-int-min', 'Afstand min.', sp.interior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-int-max', 'Afstand max.', sp.interior.max, { step: 10, hint: 'Hart-op-hart stijlen' }))) },
|
section('Binnenwanden', 'distribute-horizontal', num('hs-int-min', 'Afstand min.', sp.interior.min, { step: 10, hint: 'Hart-op-hart stijlen' }) + num('hs-int-max', 'Afstand max.', sp.interior.max, { step: 10, hint: 'Hart-op-hart stijlen' }))) },
|
||||||
@@ -766,7 +773,12 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
section('Boeideel', 'border-outer', num('hs-overhang', 'Overstek', r.overhang, { step: 10, hint: `${RoofModel.OVERHANG_MIN} – ${RoofModel.OVERHANG_MAX} mm` }) + num('hs-fascia-h', 'Hoogte', r.fasciaHeight, { step: 10 })),
|
section('Boeideel', 'border-outer', num('hs-overhang', 'Overstek', r.overhang, { step: 10, hint: `${RoofModel.OVERHANG_MIN} – ${RoofModel.OVERHANG_MAX} mm` }) + num('hs-fascia-h', 'Hoogte', r.fasciaHeight, { step: 10 })),
|
||||||
section('Daktrim', 'border-top', num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }))) },
|
section('Daktrim', 'border-top', num('hs-trim-h', 'Hoogte', r.trimHeight, { step: 5, hint: 'Bij units zonder boeidelen' }))) },
|
||||||
{ key: 'verlichting', label: 'Verlichting', icon: 'lightbulb', html:
|
{ key: 'verlichting', label: 'Verlichting', icon: 'lightbulb', html:
|
||||||
cols(sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode), '') + verlichting }
|
cols(section('Buiten', 'sun', sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode)),
|
||||||
|
section('Binnenspots', 'circle', toggle('hs-indoor', 'Spots in plafond', lt.indoor, { hint: 'Per unit verdeeld in het plafond' }) +
|
||||||
|
(lt.indoor ? num('hs-indoor-min', 'Afstand min.', lt.indoorMin, { hint: 'Hart-op-hart, in beide richtingen' }) +
|
||||||
|
num('hs-indoor-max', 'Afstand max.', lt.indoorMax, { hint: 'Hart-op-hart, in beide richtingen' }) +
|
||||||
|
num('hs-indoor-off', 'Afstand tot wand', lt.indoorOffset, { hint: 'Tot de binnenzijde van een wand, of tot de unitrand bij een naad' }) : ''))) +
|
||||||
|
verlichting }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
UI.onSel('hs-cladding', v => { st.cladding = v; }, rerender);
|
UI.onSel('hs-cladding', v => { st.cladding = v; }, rerender);
|
||||||
@@ -791,6 +803,10 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
UI.onNum('hs-lamp-w', v => { lt.minWallWidth = v; }, after, pos);
|
UI.onNum('hs-lamp-w', v => { lt.minWallWidth = v; }, after, pos);
|
||||||
UI.onNum('hs-lamp-end', v => { lt.endOffset = v; }, after, pos);
|
UI.onNum('hs-lamp-end', v => { lt.endOffset = v; }, after, pos);
|
||||||
UI.onNum('hs-lamp-open', v => { lt.openingOffset = v; }, after, pos);
|
UI.onNum('hs-lamp-open', v => { lt.openingOffset = v; }, after, pos);
|
||||||
|
UI.onToggle('hs-indoor', v => { lt.indoor = v; }, rerender);
|
||||||
|
UI.onNum('hs-indoor-min', v => { lt.indoorMin = v; }, after, pos);
|
||||||
|
UI.onNum('hs-indoor-max', v => { lt.indoorMax = v; }, after, pos);
|
||||||
|
UI.onNum('hs-indoor-off', v => { lt.indoorOffset = v; }, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
propsEl.addEventListener('click', (e) => {
|
propsEl.addEventListener('click', (e) => {
|
||||||
@@ -831,7 +847,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
const e = totals.roofEdge, rows = [];
|
const e = totals.roofEdge, rows = [];
|
||||||
if (e.fasciaLengthM > 0) rows.push([`Boeidelen · ${e.fasciaHeight} mm`, f(e.fasciaLengthM, 2), 'm'], ['Onderplaat boeideel', f(e.soffitM2, 2), 'm²']);
|
if (e.fasciaLengthM > 0) rows.push([`Boeidelen · ${e.fasciaHeight} mm`, f(e.fasciaLengthM, 2), 'm'], ['Onderplaat boeideel', f(e.soffitM2, 2), 'm²']);
|
||||||
if (e.trimLengthM > 0) rows.push([`Daktrim · ${e.trimHeight} mm`, f(e.trimLengthM, 2), 'm']);
|
if (e.trimLengthM > 0) rows.push([`Daktrim · ${e.trimHeight} mm`, f(e.trimLengthM, 2), 'm']);
|
||||||
rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.']);
|
rows.push(['Boeideel-spots', e.spots, 'st.'], ['Gevellampen', totals.lamps.total, 'st.'], ['Binnenspots', totals.lamps.indoor, 'st.']);
|
||||||
cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows));
|
cards.push(UI.specCard('Dakrand & verlichting', 'lightbulb', rows));
|
||||||
}
|
}
|
||||||
document.getElementById('ho-specs').innerHTML = `<div class="ui-spec-grid">${cards.join('')}</div>`;
|
document.getElementById('ho-specs').innerHTML = `<div class="ui-spec-grid">${cards.join('')}</div>`;
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
// (unit.lighting.walls[side]): 'auto' (standaard), 'openingen' (lampen op
|
// (unit.lighting.walls[side]): 'auto' (standaard), 'openingen' (lampen op
|
||||||
// openingOffset = 500 mm naast elke opening, bijv. slaapkamers) of 'geen'.
|
// 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.
|
// Plain script, geladen na core/geometry.js en roof-model.js.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
@@ -39,7 +47,12 @@ window.LightingModel = (() => {
|
|||||||
lampMax: 3000,
|
lampMax: 3000,
|
||||||
minWallWidth: 2000, // kleinste vrije gevelbreedte voor gevellampen
|
minWallWidth: 2000, // kleinste vrije gevelbreedte voor gevellampen
|
||||||
endOffset: 300, // eerste/laatste lamp vanaf hoek/uiteinde
|
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 MODES = { geen: 'Geen', spots: 'Boeideel-spots', gevel: 'Gevellampen' };
|
||||||
const DIRECTIONS = { both: 'Boven én onder', down: 'Alleen onder', up: 'Alleen boven' };
|
const DIRECTIONS = { both: 'Boven én onder', down: 'Alleen onder', up: 'Alleen boven' };
|
||||||
@@ -182,16 +195,58 @@ window.LightingModel = (() => {
|
|||||||
return { lamps };
|
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() {
|
function createMaterials() {
|
||||||
return {
|
return {
|
||||||
housingMaterial: new THREE.MeshStandardMaterial({ color: 0x2b2f33, roughness: 0.5, metalness: 0.3 }),
|
housingMaterial: new THREE.MeshStandardMaterial({ color: 0x2b2f33, roughness: 0.5, metalness: 0.3 }),
|
||||||
glowMaterial: new THREE.MeshStandardMaterial({ color: 0xfff4d0, emissive: 0xffe2a0, emissiveIntensity: 1 }),
|
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 })
|
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.
|
// 3D: behuizing op het gevelvlak + lichtbundel(s) omhoog/omlaag, en de
|
||||||
function build3D(lamps, materials) {
|
// binnenspots (indoor, zie computeIndoor) als schijfje in het plafond.
|
||||||
|
function build3D(lamps, materials, indoor) {
|
||||||
const toM = 0.001;
|
const toM = 0.001;
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
const w = 0.08, h = 0.16, d = 0.07, beamH = 0.7, beamR = 0.22;
|
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 !== 'up') addGlow(-1);
|
||||||
if (l.direction !== 'down') 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;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volle render: elke gevellamp (per lichtrichting) en elke boeideel-spot
|
// Volle render: elke gevellamp (per lichtrichting) en elke boeideel-spot
|
||||||
// wordt een echte SpotLight. Gevellampen staan net buiten het gevelvlak en
|
// wordt een echte SpotLight. Gevellampen staan net buiten het gevelvlak en
|
||||||
// schijnen langs de gevel omhoog/omlaag; spots schijnen recht naar beneden.
|
// schijnen langs de gevel omhoog/omlaag; spots schijnen recht naar beneden.
|
||||||
// spots = [{ x, z }] (mm), spotY = hoogte onderkant boeideel (mm).
|
// spots = [{ x, z }] (mm), spotY = hoogte onderkant boeideel (mm);
|
||||||
function buildLights3D(lamps, spots, spotY) {
|
// 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 toM = 0.001;
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
const add = (x, y, z, dirY, intensity, angle, distance) => {
|
const add = (x, y, z, dirY, intensity, angle, distance) => {
|
||||||
@@ -233,12 +295,13 @@ window.LightingModel = (() => {
|
|||||||
};
|
};
|
||||||
lamps.forEach(l => {
|
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 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 !== '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, 3, 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 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 };
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user