v0.4.3: lucht op de grond (grounded skybox) met gras en schaduw; webserver verplicht
- core/skybox.js: panorama op de grond geprojecteerd (port GroundedSkybox), grastextuur in cirkel rond het object met overvloeiing, zon met schaduw, camera niet onder de grond - woning: maaiveld als instelling (standaard 350 mm onder bovenkant vloer) - assets opgeschoond (alleen grass.jpg + skybox.webp), skybox-data.js weg - start.sh + melding bij file:// Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3a4b9608c0
commit
f8da490640
@@ -9,16 +9,14 @@ voorbehouden; gebruik uitsluitend met schriftelijke toestemming. Zie
|
|||||||
|
|
||||||
## Draaien
|
## Draaien
|
||||||
|
|
||||||
**Optie 1 — direct openen (geen server nodig):**
|
Tectum draait via een (lokale) webserver; direct openen via `file://` wordt
|
||||||
Dubbelklik `index.html`. Alles werkt met gewone `<script src="...">`-tags
|
niet meer ondersteund (texturen/lucht laden dan niet, de app toont een melding).
|
||||||
(geen ES-modules), dus dit werkt ook via `file://`.
|
|
||||||
|
|
||||||
**Optie 2 — lokale server (optioneel):**
|
|
||||||
```
|
```
|
||||||
cd project
|
./start.sh # of: python3 -m http.server 8000
|
||||||
python3 -m http.server 8000
|
|
||||||
```
|
```
|
||||||
en open `http://localhost:8000`.
|
en open `http://localhost:8000`. Elke statische webserver volstaat (geen
|
||||||
|
build-stap; gewone `<script src>`-tags, geen ES-modules).
|
||||||
|
|
||||||
## Structuur
|
## Structuur
|
||||||
|
|
||||||
@@ -159,12 +157,15 @@ 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.
|
||||||
- **Lucht (v0.4.2).** Knop "Lucht" in elke 3D-werkbalk (core/skybox.js):
|
- **Lucht en gras (v0.4.3).** Knop "Lucht" in elke 3D-werkbalk (core/skybox.js,
|
||||||
panorama uit assets/skybox.webp als achtergrond, keuze onthouden. Voor
|
keuze onthouden). Het panorama (assets/skybox.webp) wordt op de grond
|
||||||
file:// staat een verkleinde kopie als data-URI in assets/skybox-data.js
|
geprojecteerd (port van three.js GroundedSkybox), met scherp gras
|
||||||
(pas geladen bij eerste gebruik; opnieuw genereren als skybox.webp
|
(assets/grass.jpg, Poly Haven grass_medium_01, CC0) in een cirkel van 12 m
|
||||||
verandert). In de woning verdwijnen dan het raster en de sandwichpanelen
|
dat daarna overvloeit in het panorama. De zon werpt schaduw op het gras en
|
||||||
(die zitten in de grond); de volle render blijft een avondscène.
|
de camera kan niet onder de grond. Woning: het gras ligt op het maaiveld
|
||||||
|
(woning-instelling, standaard 350 mm onder bovenkant vloer), zodat het
|
||||||
|
ingegraven deel van vloer en sandwichpanelen eronder verdwijnt. Wand/vloer:
|
||||||
|
gras op de onderkant van het element. De volle render blijft een avondscène.
|
||||||
- **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-
|
||||||
|
|||||||
+4
-2
@@ -1,2 +1,4 @@
|
|||||||
Bestemmingsmap voor toekomstige assets: GLB-modellen (ramen/deuren),
|
Assets voor de 3D-weergave en de PDF:
|
||||||
texturen/bumpmaps, e.d. Nog leeg.
|
- logo.svg / logo-animated.svg logo (splash, about); PDF-logo staat als data in js/core/logo-data.js
|
||||||
|
- skybox.webp equirectangulair panorama (6000x3000) voor "Lucht" (core/skybox.js)
|
||||||
|
- grass.jpg naadloze grastextuur (Poly Haven grass_medium_01, diffuse 1k, CC0)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
File diff suppressed because one or more lines are too long
@@ -636,3 +636,5 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight:
|
|||||||
.add-opening-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; text-transform: none; font-size: 12.5px; }
|
.add-opening-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; text-transform: none; font-size: 12.5px; }
|
||||||
.hn-actions .ui-btn { flex: 1; justify-content: center; }
|
.hn-actions .ui-btn { flex: 1; justify-content: center; }
|
||||||
.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 code { background: rgba(0,0,0,.06); padding: 0 4px; border-radius: 3px; }
|
||||||
|
|||||||
@@ -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.2';
|
const APP_VERSION = '0.4.3';
|
||||||
// 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 = [];
|
||||||
@@ -290,6 +290,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
wireNewModal();
|
wireNewModal();
|
||||||
wireHotkeys();
|
wireHotkeys();
|
||||||
wireTabBar();
|
wireTabBar();
|
||||||
|
// Tectum draait via een webserver (texturen, lucht); file:// werkt niet volledig.
|
||||||
|
if (location.protocol === 'file:') {
|
||||||
|
const note = document.createElement('div');
|
||||||
|
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>';
|
||||||
|
document.body.appendChild(note);
|
||||||
|
}
|
||||||
openNew('woning', 'L');
|
openNew('woning', 'L');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+114
-39
@@ -5,41 +5,101 @@
|
|||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// core/skybox.js — lucht-achtergrond (equirectangulair panorama) voor de
|
// core/skybox.js — omgeving voor de 3D-weergaven ("Lucht"-knop): een
|
||||||
// 3D-weergaven, met een "Lucht"-knop in de 3D-werkbalk. De afbeelding
|
// panorama (assets/skybox.webp) dat op de grond geprojecteerd wordt, zodat
|
||||||
// (assets/skybox-data.js, gegenereerd uit assets/skybox.webp) wordt pas bij
|
// het object echt op het gras staat, plus scherp gras (assets/grass.jpg) in
|
||||||
// de eerste keer aanzetten geladen en daarna gedeeld. De keuze aan/uit wordt
|
// een cirkel rond het object dat naar buiten toe overvloeit in het gras van
|
||||||
// onthouden (localStorage, alleen gemak).
|
// het panorama. Het gras vangt ook de schaduw op.
|
||||||
// Plain script (geen ES-module).
|
//
|
||||||
|
// Grondprojectie: port van three.js GroundedSkybox (r157+, MIT; wij draaien
|
||||||
|
// r128): een binnenstebuiten bol waarvan de onderhelft plat gedrukt wordt op
|
||||||
|
// een vloer op CAMERA_HEIGHT_M onder het midden van de bol.
|
||||||
|
//
|
||||||
|
// Vereist een webserver (texturen via TextureLoader). Plain script.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
window.Skybox = (() => {
|
window.Skybox = (() => {
|
||||||
const PREF_KEY = 'tectum.skybox';
|
const PREF_KEY = 'tectum.skybox';
|
||||||
let texturePromise = null;
|
const SKY_URL = 'assets/skybox.webp';
|
||||||
|
const GRASS_URL = 'assets/grass.jpg';
|
||||||
|
const CAMERA_HEIGHT_M = 1.7; // hoogte van de panoramacamera boven het gras
|
||||||
|
const RADIUS_M = 60; // straal van de omgevingsbol
|
||||||
|
const GRASS_RADIUS_M = 12; // tot hier is het eigen gras volledig zichtbaar
|
||||||
|
const GRASS_FADE_M = 6; // daarna vloeit het over in het panorama
|
||||||
|
const GRASS_TILE_M = 2.5; // één grastegel beslaat zoveel meter
|
||||||
|
const GRASS_SHADE = 0x9a9a9a; // compenseert ambient + zon (anders te licht)
|
||||||
|
|
||||||
function load() {
|
let loading = null;
|
||||||
if (texturePromise) return texturePromise;
|
function loadTextures() {
|
||||||
texturePromise = new Promise((resolve, reject) => {
|
if (loading) return loading;
|
||||||
const fromData = () => {
|
const loader = new THREE.TextureLoader();
|
||||||
const img = new Image();
|
const load = (url) => new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject));
|
||||||
img.onload = () => {
|
loading = Promise.all([load(SKY_URL), load(GRASS_URL)]).then(([sky, grass]) => {
|
||||||
const tex = new THREE.Texture(img);
|
grass.wrapS = grass.wrapT = THREE.RepeatWrapping;
|
||||||
tex.mapping = THREE.EquirectangularReflectionMapping;
|
grass.anisotropy = 8;
|
||||||
tex.needsUpdate = true;
|
return { sky, grass };
|
||||||
resolve(tex);
|
|
||||||
};
|
|
||||||
img.onerror = reject;
|
|
||||||
img.src = window.TECTUM_SKYBOX;
|
|
||||||
};
|
|
||||||
if (window.TECTUM_SKYBOX) return fromData();
|
|
||||||
const s = document.createElement('script');
|
|
||||||
s.src = 'assets/skybox-data.js';
|
|
||||||
s.onload = fromData;
|
|
||||||
s.onerror = reject;
|
|
||||||
document.head.appendChild(s);
|
|
||||||
});
|
});
|
||||||
texturePromise.catch(() => { texturePromise = null; });
|
loading.catch(() => { loading = null; });
|
||||||
return texturePromise;
|
return loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSky(map) {
|
||||||
|
const geometry = new THREE.SphereGeometry(RADIUS_M, 256, 128);
|
||||||
|
geometry.scale(1, 1, -1); // binnenkant zichtbaar
|
||||||
|
const pos = geometry.getAttribute('position');
|
||||||
|
const v = new THREE.Vector3();
|
||||||
|
const h = CAMERA_HEIGHT_M, y1 = -h * 3 / 2;
|
||||||
|
for (let i = 0; i < pos.count; i++) {
|
||||||
|
v.fromBufferAttribute(pos, i);
|
||||||
|
if (v.y < 0) {
|
||||||
|
// Onderhelft plat op y = -h; vloeiende overgang naar de bol.
|
||||||
|
const f = v.y < y1 ? -h / v.y : (1 - v.y * v.y / (3 * y1 * y1));
|
||||||
|
v.multiplyScalar(f);
|
||||||
|
pos.setXYZ(i, v.x, v.y, v.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos.needsUpdate = true;
|
||||||
|
const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ map, depthWrite: false }));
|
||||||
|
mesh.position.y = h - 0.01; // vlakke vloer net onder het eigen gras
|
||||||
|
mesh.renderOrder = -1;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGrass(map) {
|
||||||
|
const R = GRASS_RADIUS_M + GRASS_FADE_M;
|
||||||
|
const tex = map.clone();
|
||||||
|
tex.needsUpdate = true;
|
||||||
|
tex.repeat.set(2 * R / GRASS_TILE_M, 2 * R / GRASS_TILE_M);
|
||||||
|
const material = new THREE.MeshLambertMaterial({ map: tex, color: GRASS_SHADE, transparent: true, depthWrite: false });
|
||||||
|
// Randvervaging op afstand tot het midden (eigen varying: de UV's zijn
|
||||||
|
// al herhaald voor de textuur).
|
||||||
|
material.onBeforeCompile = (shader) => {
|
||||||
|
shader.vertexShader = 'varying float vGrassR;\n' + shader.vertexShader.replace(
|
||||||
|
'#include <begin_vertex>', '#include <begin_vertex>\nvGrassR = length(transformed.xy);');
|
||||||
|
shader.fragmentShader = 'varying float vGrassR;\n' + shader.fragmentShader.replace(
|
||||||
|
'#include <dithering_fragment>',
|
||||||
|
`#include <dithering_fragment>\ngl_FragColor.a *= 1.0 - smoothstep(${GRASS_RADIUS_M.toFixed(1)}, ${R.toFixed(1)}, vGrassR);`);
|
||||||
|
};
|
||||||
|
const mesh = new THREE.Mesh(new THREE.CircleGeometry(R, 96), material);
|
||||||
|
mesh.rotation.x = -Math.PI / 2;
|
||||||
|
mesh.receiveShadow = true;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Omgeving voor één scene. Geeft { group, setPlacement(x, groundY, z) }:
|
||||||
|
// het midden (m) waar het object staat en de hoogte van het maaiveld (m).
|
||||||
|
function createEnvironment(scene) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.visible = false;
|
||||||
|
scene.add(group);
|
||||||
|
loadTextures().then(({ sky, grass }) => {
|
||||||
|
group.add(buildSky(sky));
|
||||||
|
group.add(buildGrass(grass));
|
||||||
|
}).catch(() => console.warn('Tectum: lucht/gras niet geladen (draait de app via een webserver?)'));
|
||||||
|
return {
|
||||||
|
group,
|
||||||
|
setPlacement(x, groundY, z) { group.position.set(x, groundY, z); }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOn() {
|
function isOn() {
|
||||||
@@ -49,21 +109,36 @@ window.Skybox = (() => {
|
|||||||
try { localStorage.setItem(PREF_KEY, on ? '1' : '0'); } catch (e) { /* geen opslag: alleen deze sessie */ }
|
try { localStorage.setItem(PREF_KEY, on ? '1' : '0'); } catch (e) { /* geen opslag: alleen deze sessie */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zet de achtergrond van `scene`: lucht als on, anders fallbackColor (hex).
|
// Lucht aan/uit voor een weergave: omgeving tonen, raster verbergen, camera
|
||||||
// wanted() wordt na het laden nog eens gevraagd (knop intussen uitgezet?).
|
// niet onder de grond, zon met schaduw op het gras.
|
||||||
function apply(scene, on, fallbackColor, wanted) {
|
// o: { env, grid, controls, sun, shadowRoot (Object3D die schaduw werpt) }
|
||||||
if (!on) { scene.background = new THREE.Color(fallbackColor); return; }
|
function apply(on, o) {
|
||||||
load().then(tex => { if (!wanted || wanted()) scene.background = tex; })
|
o.env.group.visible = on;
|
||||||
.catch(() => { scene.background = new THREE.Color(fallbackColor); });
|
if (o.grid) o.grid.visible = !on;
|
||||||
|
if (o.controls) o.controls.maxPolarAngle = on ? Math.PI / 2 - 0.03 : Math.PI - 0.001;
|
||||||
|
if (o.sun) {
|
||||||
|
o.sun.castShadow = on;
|
||||||
|
if (on) {
|
||||||
|
const cam = o.sun.shadow.camera;
|
||||||
|
cam.left = cam.bottom = -GRASS_RADIUS_M; cam.right = cam.top = GRASS_RADIUS_M;
|
||||||
|
cam.near = 0.5; cam.far = 60;
|
||||||
|
cam.updateProjectionMatrix();
|
||||||
|
o.sun.shadow.mapSize.set(2048, 2048);
|
||||||
|
o.sun.shadow.bias = -0.0005;
|
||||||
|
o.sun.position.set(o.env.group.position.x + 8, 14, o.env.group.position.z + 6);
|
||||||
|
o.sun.target.position.copy(o.env.group.position);
|
||||||
|
o.sun.target.updateMatrixWorld();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (o.shadowRoot) o.shadowRoot.traverse(m => { if (m.isMesh) m.castShadow = on; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Lucht"-knop in een 3D-werkbalk. onChange(on) bij elke klik (en niet bij
|
// "Lucht"-knop in een 3D-werkbalk. onChange(on) bij elke klik.
|
||||||
// aanmaken: de aanroeper leest de begintoestand met isOn()).
|
|
||||||
function createToggle(toolbar, onChange) {
|
function createToggle(toolbar, onChange) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.className = 'btn-3d-toggle' + (isOn() ? ' active' : '');
|
btn.className = 'btn-3d-toggle' + (isOn() ? ' active' : '');
|
||||||
btn.title = 'Lucht als achtergrond';
|
btn.title = 'Lucht en gras als omgeving';
|
||||||
btn.innerHTML = '<i class="bi bi-cloud-sun"></i>Lucht';
|
btn.innerHTML = '<i class="bi bi-cloud-sun"></i>Lucht';
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const on = !btn.classList.contains('active');
|
const on = !btn.classList.contains('active');
|
||||||
@@ -75,5 +150,5 @@ window.Skybox = (() => {
|
|||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { load, isOn, apply, createToggle };
|
return { CAMERA_HEIGHT_M, createEnvironment, apply, isOn, createToggle };
|
||||||
})();
|
})();
|
||||||
|
|||||||
+4
-3
@@ -727,11 +727,11 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
isolatieVisible = v.isolatie;
|
isolatieVisible = v.isolatie;
|
||||||
update3DScene();
|
update3DScene();
|
||||||
});
|
});
|
||||||
// Lucht-achtergrond (core/skybox.js).
|
// Lucht en gras (core/skybox.js); de vloer staat met zijn onderkant op het gras.
|
||||||
let skyOn = Skybox.isOn();
|
let skyOn = Skybox.isOn();
|
||||||
const applySky = () => { grid.visible = !skyOn; Skybox.apply(scene, skyOn, 0x1a1a1a, () => skyOn); };
|
const skyEnv = Skybox.createEnvironment(scene);
|
||||||
|
const applySky = () => Skybox.apply(skyOn, { env: skyEnv, grid, controls, sun: dirLight, shadowRoot: floorGroup3D });
|
||||||
Skybox.createToggle(document.getElementById('floor-toolbar-3d'), (on) => { skyOn = on; applySky(); });
|
Skybox.createToggle(document.getElementById('floor-toolbar-3d'), (on) => { skyOn = on; applySky(); });
|
||||||
applySky();
|
|
||||||
|
|
||||||
function update3DScene() {
|
function update3DScene() {
|
||||||
if (floorGroup3D) scene.remove(floorGroup3D);
|
if (floorGroup3D) scene.remove(floorGroup3D);
|
||||||
@@ -739,6 +739,7 @@ function mountFloorEditor(container, callbacks) {
|
|||||||
? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, isolatieVisible, dakleerVisible: osbVisible })
|
? RoofModel.build3D(calcData, lastResolvedOpenings, { materials: floorMaterials, osbVisible, isolatieVisible, dakleerVisible: osbVisible })
|
||||||
: FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible, isolatieVisible });
|
: FloorModel.build3D(calcData, lastResolvedOpenings, lastChassisData, { materials: floorMaterials, osbVisible, isolatieVisible });
|
||||||
scene.add(floorGroup3D);
|
scene.add(floorGroup3D);
|
||||||
|
applySky(); // schaduw van de nieuwe vloer
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ window.HouseModel = (() => {
|
|||||||
// vanaf het beloopbare vloervlak (WallModel.trasraamFor).
|
// vanaf het beloopbare vloervlak (WallModel.trasraamFor).
|
||||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM },
|
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM },
|
||||||
sandwich: true, // sandwichpanelen rondom (SandwichModel), vanaf loopvlak 600 mm de grond in
|
sandwich: true, // sandwichpanelen rondom (SandwichModel), vanaf loopvlak 600 mm de grond in
|
||||||
|
groundLevel: 350, // maaiveld: zoveel mm onder het loopvlak (bovenkant vloer); voor de 3D-omgeving
|
||||||
roof: Object.assign({}, RoofModel.DEFAULT_SETTINGS),
|
roof: Object.assign({}, RoofModel.DEFAULT_SETTINGS),
|
||||||
lighting: Object.assign({}, LightingModel.DEFAULT_SETTINGS)
|
lighting: Object.assign({}, LightingModel.DEFAULT_SETTINGS)
|
||||||
};
|
};
|
||||||
@@ -543,6 +544,7 @@ window.HouseModel = (() => {
|
|||||||
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
||||||
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
||||||
state.settings.sandwich = state.settings.sandwich !== false;
|
state.settings.sandwich = state.settings.sandwich !== false;
|
||||||
|
if (!(state.settings.groundLevel >= 0)) state.settings.groundLevel = def.groundLevel;
|
||||||
const oldRoof = state.settings.roof || {}; // v0.3.0: edge/spots waren woning-breed
|
const oldRoof = state.settings.roof || {}; // v0.3.0: edge/spots waren woning-breed
|
||||||
state.settings.roof = RoofModel.normalizeSettings(oldRoof);
|
state.settings.roof = RoofModel.normalizeSettings(oldRoof);
|
||||||
// v0.3.1: type/richting van de verlichting stonden per unit; nu woning-breed.
|
// v0.3.1: type/richting van de verlichting stonden per unit; nu woning-breed.
|
||||||
|
|||||||
+16
-9
@@ -302,6 +302,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
let beplatingVisible = true, isolatieVisible = true, lampenVisible = true;
|
let beplatingVisible = true, isolatieVisible = true, lampenVisible = true;
|
||||||
let fullRender = false;
|
let fullRender = false;
|
||||||
let skyOn = Skybox.isOn();
|
let skyOn = Skybox.isOn();
|
||||||
|
const skyEnv = Skybox.createEnvironment(scene);
|
||||||
|
|
||||||
// Weergave-filter (core/view-filter.js). Beplating = wandplaten, vloer-
|
// Weergave-filter (core/view-filter.js). Beplating = wandplaten, vloer-
|
||||||
// en plafondplaten, dakleer en boeideel/daktrim.
|
// en plafondplaten, dakleer en boeideel/daktrim.
|
||||||
@@ -320,17 +321,17 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
|
|
||||||
// Volle render: avondscène, lampen en spots als echte lichtbronnen.
|
// Volle render: avondscène, lampen en spots als echte lichtbronnen.
|
||||||
let lightsGroup = null;
|
let lightsGroup = null;
|
||||||
// Lucht (core/skybox.js): alleen in de gewone weergave (de volle render is
|
// Lucht en gras (core/skybox.js): alleen in de gewone weergave (de volle
|
||||||
// een avondscène). De sandwichpanelen zitten in de grond: met lucht aan
|
// render is een avondscène). Het gras ligt op het maaiveld, dus het
|
||||||
// zijn ze niet zichtbaar.
|
// ingegraven deel van vloer en sandwichpanelen verdwijnt eronder.
|
||||||
function applyRenderMode() {
|
function applyRenderMode() {
|
||||||
if (fullRender) scene.background = new THREE.Color(0x0b0f18);
|
const sky = skyOn && !fullRender;
|
||||||
else Skybox.apply(scene, skyOn, 0x1a1a1a, () => skyOn && !fullRender);
|
scene.background = new THREE.Color(fullRender ? 0x0b0f18 : 0x1a1a1a);
|
||||||
if (parts && parts.sandwichGroup) parts.sandwichGroup.visible = beplatingVisible && !(skyOn && !fullRender);
|
Skybox.apply(sky, { env: skyEnv, controls, sun: dirLight, shadowRoot: houseGroup });
|
||||||
ambient.intensity = fullRender ? 0.1 : 0.6;
|
ambient.intensity = fullRender ? 0.1 : 0.6;
|
||||||
dirLight.intensity = fullRender ? 0.12 : 0.8;
|
dirLight.intensity = fullRender ? 0.12 : 0.8;
|
||||||
ground.visible = fullRender;
|
ground.visible = fullRender;
|
||||||
grid.visible = !fullRender && !skyOn;
|
grid.visible = !fullRender && !sky;
|
||||||
if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; }
|
if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; }
|
||||||
// De nep-lichtbundels (kegels) alleen in de snelle weergave; in de
|
// De nep-lichtbundels (kegels) alleen in de snelle weergave; in de
|
||||||
// volle render schijnt er echt licht.
|
// volle render schijnt er echt licht.
|
||||||
@@ -758,7 +759,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Cement-gebonden plaat onderaan de gevel' }) +
|
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Cement-gebonden plaat onderaan de gevel' }) +
|
||||||
(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 de grond in` }))) },
|
{ 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' }))) },
|
||||||
{ 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' }))) },
|
||||||
@@ -773,6 +775,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
|
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
|
||||||
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
|
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
|
||||||
UI.onNum('hs-trasraam-h', v => { st.trasraam.height = v; }, after, pos);
|
UI.onNum('hs-trasraam-h', v => { st.trasraam.height = v; }, after, pos);
|
||||||
|
UI.onNum('hs-ground', v => { if (v >= 0) st.groundLevel = v; }, after);
|
||||||
UI.onNum('hs-ext-min', v => { sp.exterior.min = v; }, after, pos);
|
UI.onNum('hs-ext-min', v => { sp.exterior.min = v; }, after, pos);
|
||||||
UI.onNum('hs-ext-max', v => { sp.exterior.max = v; }, after, pos);
|
UI.onNum('hs-ext-max', v => { sp.exterior.max = v; }, after, pos);
|
||||||
UI.onNum('hs-int-min', v => { sp.interior.min = v; }, after, pos);
|
UI.onNum('hs-int-min', v => { sp.interior.min = v; }, after, pos);
|
||||||
@@ -863,12 +866,16 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
|
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
|
||||||
if (parts.lightingGroup) houseGroup.add(parts.lightingGroup);
|
if (parts.lightingGroup) houseGroup.add(parts.lightingGroup);
|
||||||
scene.add(houseGroup);
|
scene.add(houseGroup);
|
||||||
applyRenderMode();
|
|
||||||
|
|
||||||
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1));
|
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1));
|
||||||
const bz0 = Math.min(...parts.rects.map(r => r.z0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
|
const bz0 = Math.min(...parts.rects.map(r => r.z0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
|
||||||
const cx = (bx0 + bx1) / 2 * toM, cz = (bz0 + bz1) / 2 * toM;
|
const cx = (bx0 + bx1) / 2 * toM, cz = (bz0 + bz1) / 2 * toM;
|
||||||
grid.position.set(cx, 0, cz);
|
grid.position.set(cx, 0, cz);
|
||||||
|
// Maaiveld: settings.groundLevel onder het loopvlak (bovenkant vloer).
|
||||||
|
const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel) * toM;
|
||||||
|
skyEnv.setPlacement(cx, groundY, cz);
|
||||||
|
ground.position.set(cx, groundY - 0.002, cz);
|
||||||
|
applyRenderMode();
|
||||||
if (!cameraFitted) {
|
if (!cameraFitted) {
|
||||||
const size = Math.max(bx1 - bx0, bz1 - bz0) * toM;
|
const size = Math.max(bx1 - bx0, bz1 - bz0) * toM;
|
||||||
controls.target.set(cx, 1.2, cz);
|
controls.target.set(cx, 1.2, cz);
|
||||||
|
|||||||
+4
-3
@@ -1242,11 +1242,11 @@ function mountWallEditor(container, callbacks) {
|
|||||||
isolatieVisible = v.isolatie;
|
isolatieVisible = v.isolatie;
|
||||||
update3DScene();
|
update3DScene();
|
||||||
});
|
});
|
||||||
// Lucht-achtergrond (core/skybox.js).
|
// Lucht en gras (core/skybox.js); de wand staat met zijn onderkant op het gras.
|
||||||
let skyOn = Skybox.isOn();
|
let skyOn = Skybox.isOn();
|
||||||
const applySky = () => { grid.visible = !skyOn; Skybox.apply(scene, skyOn, 0x1a1a1a, () => skyOn); };
|
const skyEnv = Skybox.createEnvironment(scene);
|
||||||
|
const applySky = () => Skybox.apply(skyOn, { env: skyEnv, grid, controls, sun: dirLight, shadowRoot: wallGroup3D });
|
||||||
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (on) => { skyOn = on; applySky(); });
|
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (on) => { skyOn = on; applySky(); });
|
||||||
applySky();
|
|
||||||
|
|
||||||
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
|
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
|
||||||
// met de unit-editor).
|
// met de unit-editor).
|
||||||
@@ -1260,6 +1260,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
wallGroup3D = result.group;
|
wallGroup3D = result.group;
|
||||||
lastWallStats = result.stats;
|
lastWallStats = result.stats;
|
||||||
scene.add(wallGroup3D);
|
scene.add(wallGroup3D);
|
||||||
|
applySky(); // schaduw van de nieuwe wand
|
||||||
|
|
||||||
updateSpecificaties();
|
updateSpecificaties();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tectum lokaal starten: statische webserver in deze map (Python 3).
|
||||||
|
# Gebruik: ./start.sh [poort] (standaard 8000), daarna http://localhost:8000
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
PORT="${1:-8000}"
|
||||||
|
echo "Tectum: http://localhost:$PORT (Ctrl+C om te stoppen)"
|
||||||
|
exec python3 -m http.server "$PORT" --bind 127.0.0.1
|
||||||
Reference in New Issue
Block a user