v0.4.16: PDF-aanzichten (vlakke render) met legenda afwerking en afmetingen, zaaglijst/zaagmachine eigen blad, infopaneel afmetingen, lopen (Shift+T) met camera-menu, zachter licht, naadwanden volgen koppeling, boeideel over de gevel (gevel tot bovenkant dak), thumbnails riet/vloer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
12c2579471
commit
23bfe153a2
+125
-13
@@ -738,9 +738,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
const covOpts = Object.fromEntries(Object.entries(RoofModel.COVERINGS)
|
||||
.filter(([, c]) => !c.sloped || shapeType !== 'plat').map(([k, c]) => [k, c.label]));
|
||||
const covInfo = (k) => { const c = RoofModel.COVERINGS[k]; return `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²`; };
|
||||
const bedekking = sel('hp-roof-cover', 'Bedekking', covOpts, RoofModel.coveringFor(u.roof.covering, shapeType), {
|
||||
hint: shapeType === 'plat' ? 'Riet kan alleen bij een hellend dak (lessenaar/zadel)' : covInfo(RoofModel.coveringFor(u.roof.covering, shapeType)) + '; boeideel schuift mee omhoog'
|
||||
});
|
||||
const covNow = RoofModel.coveringFor(u.roof.covering, shapeType);
|
||||
const bedekking = thumbPicker('hp-roof-cover', 'Bedekking', covOpts, covNow, RoofModel.coveringThumbUrl(covNow, 128),
|
||||
shapeType === 'plat' ? 'Riet kan alleen bij een hellend dak (lessenaar/zadel)' : covInfo(covNow) + '; boeideel schuift mee omhoog');
|
||||
const dak = toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) +
|
||||
(u.roof.followFloor ? '' :
|
||||
num('hp-roof-w', 'Breedte', u.roof.deck.floor.width, { hint: 'West–oost' }) +
|
||||
@@ -750,12 +750,16 @@ function mountHouseEditor(container, callbacks) {
|
||||
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' }) +
|
||||
bedekking;
|
||||
|
||||
const missing = HouseModel.missingSeamWalls(state, u.id);
|
||||
propsEl.innerHTML = warn + head('box', `Unit ${u.id}`, `${f.width} × ${f.depth} mm`) +
|
||||
UI.tabs('unit', [
|
||||
{ key: 'maat', label: 'Afmetingen', icon: 'rulers', html: cols(
|
||||
num('hp-width', 'Breedte', f.width, { hint: 'West–oost' }) + num('hp-depth', 'Diepte', f.depth, { hint: 'Noord–zuid' }),
|
||||
sel('hp-corners', 'Hoekaansluiting', UnitModel.CORNER_MODES, u.unit.layout.corners)) },
|
||||
{ key: 'koppeling', label: 'Koppeling', icon: 'link-45deg', html: cols(koppeling, '') },
|
||||
{ key: 'koppeling', label: 'Koppeling', icon: 'link-45deg', html: cols(koppeling, missing.length
|
||||
? UI.warn(`Zonder naadwand: ${missing.map(m => `unit ${m.a} | ${m.b}`).join(', ')}. De naad staat daar open.`) +
|
||||
actions(button('Naadwand toevoegen', 'plus-lg', `data-action="add-seams" data-unit="${u.id}"`))
|
||||
: '') },
|
||||
{ key: 'dak', label: 'Dak', icon: 'house', html: cols(dak, u.roof.followFloor ? info('Het dak heeft de maat van de vloer. Zet "Maat volgt vloer" uit voor een eigen maat of verschuiving.') : '') }
|
||||
]) +
|
||||
actions(
|
||||
@@ -767,9 +771,11 @@ function mountHouseEditor(container, callbacks) {
|
||||
UI.onNum('hp-depth', v => { if (v >= 1000) f.depth = v; }, after);
|
||||
UI.onNum('hp-att-offset', v => { u.attach.offset = v; }, after);
|
||||
UI.onSel('hp-corners', v => { u.unit.layout.corners = v; }, rerender);
|
||||
UI.onSel('hp-att-to', v => { u.attach.to = v; }, rerender);
|
||||
UI.onSel('hp-att-side', v => { u.attach.side = v; }, rerender);
|
||||
UI.onSel('hp-att-align', v => { u.attach.align = v; }, rerender);
|
||||
// Andere koppeling: naadwanden van deze unit gaan mee (oude vervallen,
|
||||
// nieuwe naden krijgen er een).
|
||||
UI.onSel('hp-att-to', v => { u.attach.to = v; HouseModel.syncSeamWalls(state, u.id); }, rerender);
|
||||
UI.onSel('hp-att-side', v => { u.attach.side = v; HouseModel.syncSeamWalls(state, u.id); }, rerender);
|
||||
UI.onSel('hp-att-align', v => { u.attach.align = v; HouseModel.syncSeamWalls(state, u.id); }, rerender);
|
||||
UI.onToggle('hp-roof-follow', v => { u.roof.followFloor = v; }, rerender);
|
||||
UI.onToggle('hp-fascia', v => { u.roof.fascia = v; }, rerender);
|
||||
UI.onSel('hp-roof-cover', v => { u.roof.covering = v; }, rerender);
|
||||
@@ -869,9 +875,10 @@ function mountHouseEditor(container, callbacks) {
|
||||
// Keuzes voor de vormgeving van het trasraam: plaat of een steen.
|
||||
const stoneOptions = Object.assign({}, ...WallModel.TRASRAAM_STONES.map(c => ({ [c]: `Steen ${c}` })));
|
||||
const trasraamFinishes = Object.assign({ cementplaat: 'Cementplaat' }, stoneOptions);
|
||||
// Steenkeuze met voorvertoning.
|
||||
const stonePicker = (id, label, value, hint) => sel(id, label, stoneOptions, value, { hint }) +
|
||||
`<img class="hs-stone-preview" src="${WallModel.stoneThumbUrl(value, 128)}" alt="${value}">`;
|
||||
// Keuzelijst met voorvertoning (textuur-thumbnail), als die er is.
|
||||
const thumbPicker = (id, label, options, value, thumbUrl, hint) => sel(id, label, options, value, { hint }) +
|
||||
(thumbUrl ? `<img class="hs-stone-preview" src="${thumbUrl}" alt="${value}">` : '');
|
||||
const stonePicker = (id, label, value, hint) => thumbPicker(id, label, stoneOptions, value, WallModel.stoneThumbUrl(value, 128), hint);
|
||||
|
||||
function renderHouseProps(warn) {
|
||||
const st = state.settings, sp = st.spacing, r = st.roof, lt = st.lighting;
|
||||
@@ -916,7 +923,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
{ key: 'binnen', label: 'Binnen', icon: 'palette', html: cols(
|
||||
section('Wanden', 'palette', toggle('hs-int-paint', 'Geschilderd', st.interiorPaint, { hint: 'Alle wanden aan de kamerzijde; egaal, zonder plaatnaden' }) +
|
||||
(st.interiorPaint ? UI.color('hs-int-color', 'Verfkleur', st.interiorColor, { hint: 'Voorlopig één kleur voor de hele woning' }) : '')),
|
||||
section('Vloer', 'grid-3x3', sel('hs-floor-finish', 'Afwerking', Object.fromEntries(Object.entries(FloorModel.FINISHES).map(([k, f]) => [k, f.label])), st.floorFinish, { hint: 'Alle vloeren (voorlopig één keuze)' }) +
|
||||
section('Vloer', 'grid-3x3', thumbPicker('hs-floor-finish', 'Afwerking', Object.fromEntries(Object.entries(FloorModel.FINISHES).map(([k, f]) => [k, f.label])), st.floorFinish,
|
||||
FloorModel.finishThumbUrl(st.floorFinish, 128), 'Alle vloeren (voorlopig één keuze)') +
|
||||
info('Wanden uit: kale spaanplaat met plaatnaden.'))) },
|
||||
{ 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' })),
|
||||
@@ -986,12 +994,15 @@ function mountHouseEditor(container, callbacks) {
|
||||
if (act.dataset.action === 'add-unit') addUnit();
|
||||
else if (act.dataset.action === 'draw-iw') setTool('iw');
|
||||
else if (act.dataset.action === 'project') openProjectProperties();
|
||||
else if (act.dataset.action === 'add-seams') { HouseModel.ensureSeamWalls(state, act.dataset.unit); refresh(); }
|
||||
});
|
||||
|
||||
// --- Specificaties/totalen (onder de 3D) ---
|
||||
function renderSpecs(totals) {
|
||||
const f = houseFmt;
|
||||
const dims = HouseModel.computeDimensions(parts, state.settings.groundLevel);
|
||||
const cards = [
|
||||
UI.specCard('Afmetingen', 'rulers', HouseModel.dimensionRows(dims, f)),
|
||||
UI.specCard('Gewicht', 'speedometer2', totals.weights.map(r => [r.label, f(r.kg), 'kg']), ['Totaal woning', f(totals.totalKg), 'kg']),
|
||||
UI.specCard('Hout & isolatie', 'tree', [
|
||||
['Balken', totals.beamCount, 'st.'],
|
||||
@@ -1010,11 +1021,22 @@ function mountHouseEditor(container, callbacks) {
|
||||
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));
|
||||
}
|
||||
// TIJDELIJK (verwijderen voor productie): zaagmachine-overzicht.
|
||||
const sw = totals.saw, hm = (h) => `${Math.floor(h)}:${String(Math.round((h % 1) * 60)).padStart(2, '0')}`;
|
||||
cards.push(UI.specCard('Zaagmachine (tijdelijk)', 'scissors', [
|
||||
['Zaagmeters', f(sw.meters, 1), 'm'],
|
||||
[`Zaagtijd (${HouseModel.SAW_M_PER_H} m/u)`, hm(sw.hours), 'u:mm'],
|
||||
['Zaagsneden', sw.cuts, 'st.'],
|
||||
['Verschillende hoeken', sw.angles.length, '']
|
||||
].concat(sw.angles.map(a => [a.deg === 0 ? 'Haaks (0°)' : `Schuin ${f(a.deg, 1)}°`, a.count, 'sneden']))));
|
||||
document.getElementById('ho-specs').innerHTML = `<div class="ui-spec-grid">${cards.join('')}</div>`;
|
||||
}
|
||||
|
||||
// --- Alles opnieuw doorrekenen ---
|
||||
function refresh(opts) {
|
||||
// Voorvertoningen van de gekozen materialen alvast laden voor de PDF
|
||||
// (legenda op het aanzichtenblad; HousePdf.materialThumbs).
|
||||
HousePdf.materialThumbUrls(state).forEach(PdfSheet.preloadImage);
|
||||
// Binnenzijde geschilderd: de (gedeelde) spaanplaat krijgt de verfkleur.
|
||||
wallMaterials.spaanplaatMaterial.color.set(state.settings.interiorPaint ? state.settings.interiorColor : SPAANPLAAT_COLOR);
|
||||
parts = HouseModel.computeParts(state, { wallMaterials, floorMaterials, roofMaterials, lightingMaterials, sandwichMaterials, beplatingVisible, lampenVisible });
|
||||
@@ -1080,9 +1102,93 @@ function mountHouseEditor(container, callbacks) {
|
||||
renderNav();
|
||||
}
|
||||
|
||||
let rafId = null;
|
||||
// --- Lopen (first person), eigen sneltoets Shift+T ---
|
||||
// Start 3 m voor de voordeur, buiten; botsen tegen wanden (deuren zijn
|
||||
// doorgangen), ondergrond = vloer binnen een unit, anders het maaiveld.
|
||||
const WALK_RADIUS = 250; // mm, "lichaam"
|
||||
function wallBlocked(xm, zm) {
|
||||
if (!parts) return false;
|
||||
const x = xm * 1000, z = zm * 1000;
|
||||
return HouseModel.allWallPieces(parts).some(p => {
|
||||
if (p.place.baseY) return false; // opbouw boven een lager dak
|
||||
const c = Math.cos(p.place.rotY), s = Math.sin(p.place.rotY);
|
||||
const dx = x - p.place.cx, dz = z - p.place.cz;
|
||||
const lx = dx * c - dz * s, lz = dx * s + dz * c;
|
||||
const W = p.place.width, T = p.place.thickness;
|
||||
const out = p.place.normal ? WallModel.claddingOffset(p.buitenMateriaal, p.stone) : 0;
|
||||
if (lz < -T / 2 - WALK_RADIUS || lz > T / 2 + out + WALK_RADIUS || Math.abs(lx) > W / 2 + WALK_RADIUS) return false;
|
||||
// Door een deur(opening) mag je lopen.
|
||||
return !(p.config.openings || []).some(op => op.y < 100 && op.y + op.h > 1800 &&
|
||||
lx > -W / 2 + op.x + WALK_RADIUS * 0.6 && lx < -W / 2 + op.x + op.w - WALK_RADIUS * 0.6);
|
||||
});
|
||||
}
|
||||
function groundAt(xm, zm) {
|
||||
const x = xm * 1000, z = zm * 1000;
|
||||
const u = parts && parts.units.find(q => x >= q.rect.x0 && x <= q.rect.x1 && z >= q.rect.z0 && z <= q.rect.z1);
|
||||
const top = parts ? Math.max(...parts.units.map(q => q.layout.floorTop)) : 0;
|
||||
return (u ? u.layout.floorTop : top - (state.settings.groundLevel || 0)) / 1000;
|
||||
}
|
||||
const walk = createWalkControls(camera, renderer.domElement, { blocked: wallBlocked, floorAt: groundAt });
|
||||
let orbitSaved = null;
|
||||
// Startpositie: 3 m voor de voordeur (anders een buitendeur, anders midden
|
||||
// voor de noordgevel), kijkend naar de deur.
|
||||
function walkStartPose() {
|
||||
const ext = [];
|
||||
parts.units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => ext.push(p))));
|
||||
let best = null;
|
||||
['voordeur', 'deur'].some(type => ext.some(p => (p.config.openings || []).some(op => {
|
||||
if (op.type !== type) return false;
|
||||
best = { p, lx: -p.place.width / 2 + op.x + op.w / 2 };
|
||||
return true;
|
||||
})));
|
||||
if (!best) {
|
||||
const p = ext.find(q => q.place.normal && q.place.normal.y < -0.5) || ext[0];
|
||||
best = { p, lx: 0 };
|
||||
}
|
||||
const d = UnitModel.localToWorld(best.p.place, best.lx, best.p.place.thickness / 2);
|
||||
const n = { x: best.p.place.normal.x, z: best.p.place.normal.y };
|
||||
const x = (d.x + n.x * 3000) / 1000, z = (d.z + n.z * 3000) / 1000;
|
||||
return { x, z, yaw: Math.atan2(-n.x, -n.z) };
|
||||
}
|
||||
const walkHint = document.createElement('div');
|
||||
walkHint.className = 'walk-hint';
|
||||
walkHint.textContent = 'Lopen: W/S of pijltjes = vooruit/achteruit · A/D = opzij · pijl links/rechts = draaien · Shift = rennen · slepen = rondkijken · Shift+T = terug';
|
||||
pane3D.appendChild(walkHint);
|
||||
function setWalk(on) {
|
||||
if (on === walk.active || !parts) return;
|
||||
const view = document.getElementById('house-overview');
|
||||
if (on) {
|
||||
// Lopen gebeurt in de volle 3D (alleen render); vorige stand onthouden.
|
||||
orbitSaved = { pos: camera.position.clone(), target: controls.target.clone(), fov: camera.fov, cls: view.className };
|
||||
view.classList.remove('fullscreen-2d');
|
||||
view.classList.add('fullscreen-3d', 'render-only');
|
||||
requestAnimationFrame(onResize);
|
||||
controls.enableRotate = controls.enablePan = controls.enableZoom = false;
|
||||
camera.fov = 65;
|
||||
camera.updateProjectionMatrix();
|
||||
walk.start(walkStartPose());
|
||||
} else {
|
||||
walk.stop();
|
||||
controls.enableRotate = controls.enablePan = controls.enableZoom = true;
|
||||
if (orbitSaved) {
|
||||
view.className = orbitSaved.cls;
|
||||
requestAnimationFrame(onResize);
|
||||
camera.fov = orbitSaved.fov;
|
||||
camera.updateProjectionMatrix();
|
||||
camera.position.copy(orbitSaved.pos);
|
||||
controls.target.copy(orbitSaved.target);
|
||||
controls.update();
|
||||
}
|
||||
}
|
||||
walkHint.classList.toggle('show', on);
|
||||
}
|
||||
|
||||
let rafId = null, lastT = performance.now();
|
||||
function animate() {
|
||||
rafId = requestAnimationFrame(animate);
|
||||
const now = performance.now();
|
||||
walk.update((now - lastT) / 1000);
|
||||
lastT = now;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
function onResize() {
|
||||
@@ -1094,7 +1200,9 @@ function mountHouseEditor(container, callbacks) {
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
function toggleView(fn) {
|
||||
fn(document.getElementById('house-overview'));
|
||||
const el = document.getElementById('house-overview');
|
||||
setWalk(false); // weergave wisselen beëindigt het lopen
|
||||
fn(el);
|
||||
requestAnimationFrame(onResize);
|
||||
}
|
||||
|
||||
@@ -1108,12 +1216,15 @@ function mountHouseEditor(container, callbacks) {
|
||||
window.removeEventListener('resize', onResize);
|
||||
window.removeEventListener('keydown', onKey);
|
||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||
setWalk(false); // orbitcamera terug vóór het opslaan
|
||||
walk.dispose();
|
||||
CameraMemory.save(overviewCamKey, camera, controls);
|
||||
renderer.dispose();
|
||||
},
|
||||
refresh,
|
||||
setTool,
|
||||
toggleFullscreen3D: () => toggleView(UI.cycle3D),
|
||||
toggleWalk: () => setWalk(!walk.active),
|
||||
toggleFullscreen2D: () => toggleView(UI.toggle2D)
|
||||
};
|
||||
}
|
||||
@@ -1229,6 +1340,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
triggerImport,
|
||||
mirrorHorizontal: forward('mirrorHorizontal'),
|
||||
toggleFullscreen3D: forward('toggleFullscreen3D'),
|
||||
toggleWalk: forward('toggleWalk'),
|
||||
toggleFullscreen2D: forward('toggleFullscreen2D'),
|
||||
openProjectProperties,
|
||||
getStateJson,
|
||||
|
||||
Reference in New Issue
Block a user