v0.4.15: gevelbekleding per woning (rabat/steen, override per wand), stenen trasraam (110 mm, 181 texturen), riet per unit, vloerafwerking, verfkleur binnen, boeideelkleur, gevel tot onder dakrand, binnenhoek-fixes; testontwerp als standaard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7d91416004
commit
12c2579471
+109
-7
@@ -169,6 +169,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
lockWidth: 'Binnen een woning volgt de wandbreedte uit de vloer van de unit.',
|
||||
lockSpacing: SPACING_LOCK_MSG,
|
||||
lockCladding: 'De richting van de gevelbekleding is (voorlopig) globaal: Overzicht, niets geselecteerd.',
|
||||
lockExterior: 'De gevelbekleding volgt de woning (Overzicht › Gevel); per wand aan te passen via de wand in de plattegrond.',
|
||||
seamCuts: wl.cutsU,
|
||||
lamps: [].concat(...wl.pieces.map(p => p.lamps.map(l => ({ u: p.u0 + l.u, y: l.y }))))
|
||||
});
|
||||
@@ -313,6 +314,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
ground.visible = false;
|
||||
scene.add(ground);
|
||||
const wallMaterials = WallModel.createMaterials();
|
||||
const SPAANPLAAT_COLOR = wallMaterials.spaanplaatMaterial.color.getHex(); // kaal (niet geschilderd)
|
||||
const floorMaterials = FloorModel.createMaterials();
|
||||
const roofMaterials = RoofModel.createMaterials();
|
||||
const lightingMaterials = LightingModel.createMaterials();
|
||||
@@ -328,6 +330,46 @@ function mountHouseEditor(container, callbacks) {
|
||||
let vloerplan = false;
|
||||
const cutPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
|
||||
renderer.localClippingEnabled = true;
|
||||
const capMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||||
// Egaal witte afdekking van de snede: per wandstuk een vlak op de
|
||||
// snijhoogte over de volle dikte (incl. doek/latwerk of steen aan de
|
||||
// buitenzijde en de hoekdoorloop), met de openingen die door de snede
|
||||
// lopen uitgespaard. cutAbs = snijhoogte (mm, absoluut).
|
||||
function buildPlanCaps(cutAbs) {
|
||||
const toM = 0.001, g = new THREE.Group();
|
||||
const S = WallModel.STONE;
|
||||
HouseModel.allWallPieces(parts).forEach(p => {
|
||||
const y = cutAbs - p.place.floorTop; // snijhoogte in het stuk
|
||||
if (y <= 0 || y >= WallModel.topLine(p.config).maxY) return;
|
||||
const W = p.place.width, T = p.place.thickness;
|
||||
// Buitenlagen op de snijhoogte (alleen buitenwanden: skinExt/normal).
|
||||
let out = 0;
|
||||
if (p.place.normal && p.buitenMateriaal !== 'geen') {
|
||||
const tr = p.trasraam;
|
||||
out = tr && y < tr.top ? (WallModel.isStoneFinish(tr.finish) ? S.THICK_MM : 0) : WallModel.claddingOffset(p.buitenMateriaal, p.stone);
|
||||
}
|
||||
const e = p.skinExt || {};
|
||||
const xL = -W / 2 - (e.left > 0 ? e.left + out : 0), xR = W / 2 + (e.right > 0 ? e.right + out : 0);
|
||||
const gaps = p.config.openings.filter(op => op.y < y && op.y + op.h > y)
|
||||
.map(op => [-W / 2 + op.x, -W / 2 + op.x + op.w]).sort((a, b) => a[0] - b[0]);
|
||||
const spans = [];
|
||||
let cur = xL;
|
||||
gaps.forEach(([a, b]) => { if (a > cur) spans.push([cur, Math.min(a, xR)]); cur = Math.max(cur, b); });
|
||||
if (xR > cur) spans.push([cur, xR]);
|
||||
const z0 = -T / 2, z1 = T / 2 + out;
|
||||
spans.filter(([a, b]) => b - a > 1).forEach(([a, b]) => {
|
||||
const m = new THREE.Mesh(new THREE.PlaneGeometry((b - a) * toM, (z1 - z0) * toM), capMaterial);
|
||||
m.rotation.x = -Math.PI / 2;
|
||||
m.position.set((a + b) / 2 * toM, (y - 0.5) * toM, (z0 + z1) / 2 * toM);
|
||||
const holder = new THREE.Group();
|
||||
holder.rotation.y = p.place.rotY;
|
||||
holder.position.set(p.place.cx * toM, p.place.floorTop * toM, p.place.cz * toM);
|
||||
holder.add(m);
|
||||
g.add(holder);
|
||||
});
|
||||
});
|
||||
return g;
|
||||
}
|
||||
let fullRender = false;
|
||||
let skyMode = Skybox.getMode();
|
||||
const skyEnv = Skybox.createEnvironment(scene);
|
||||
@@ -690,13 +732,23 @@ function mountHouseEditor(container, callbacks) {
|
||||
sel('hp-att-side', 'Zijde', HouseModel.SIDE_LABELS, a.side) +
|
||||
sel('hp-att-align', 'Uitlijning', HouseModel.ALIGN_LABELS[axis], a.align) +
|
||||
num('hp-att-offset', 'Verschuiving', a.offset || 0);
|
||||
// Dakbedekking per unit; riet alleen bij een hellend dak.
|
||||
const pu = parts && parts.units.find(x => x.id === u.id);
|
||||
const shapeType = pu ? pu.roof.shape.type : 'plat';
|
||||
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 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' }) +
|
||||
num('hp-roof-d', 'Diepte', u.roof.deck.floor.depth, { hint: 'Noord–zuid' }) +
|
||||
num('hp-roof-ox', 'Verschuiving oost', u.roof.offsetX || 0) +
|
||||
num('hp-roof-oz', 'Verschuiving zuid', u.roof.offsetZ || 0)) +
|
||||
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' });
|
||||
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim van 80 mm' }) +
|
||||
bedekking;
|
||||
|
||||
propsEl.innerHTML = warn + head('box', `Unit ${u.id}`, `${f.width} × ${f.depth} mm`) +
|
||||
UI.tabs('unit', [
|
||||
@@ -720,6 +772,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
UI.onSel('hp-att-align', v => { u.attach.align = v; }, 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);
|
||||
UI.onNum('hp-roof-w', v => { if (v >= 500) u.roof.deck.floor.width = v; }, after);
|
||||
UI.onNum('hp-roof-d', v => { if (v >= 500) u.roof.deck.floor.depth = v; }, after);
|
||||
UI.onNum('hp-roof-ox', v => { u.roof.offsetX = v; }, after);
|
||||
@@ -744,11 +797,25 @@ function mountHouseEditor(container, callbacks) {
|
||||
{ hint: `Automatisch: ${lt.endOffset} mm uit de hoek, alleen op vrije gevel van min. ${lt.minWallWidth} mm. Naast openingen: ${lt.openingOffset} mm naast elke opening.` }) +
|
||||
UI.specCard('Op dit wandstuk', 'lightbulb', [['Gevellampen', piece.lamps.length, 'st.']])
|
||||
: info('Gevellampen staan uit. Kies bij Woning › Verlichting het type "Gevellampen".');
|
||||
const su = unitById(pu.id);
|
||||
const fc = HouseModel.facadeFor(state, su, wl.side);
|
||||
const ov = su.facade.walls[wl.side] || null;
|
||||
const f0 = state.settings.facade;
|
||||
const woningLabel = `Woning (${HouseModel.FACADE_TYPES[f0.type]}${f0.type === 'steen' ? ' ' + f0.stone : ''})`;
|
||||
const gevel = sel('hw-facade', 'Type', Object.assign({ woning: woningLabel }, HouseModel.FACADE_TYPES), ov ? ov.type : 'woning',
|
||||
{ hint: 'Geldt voor de hele zijde van deze unit' }) +
|
||||
(ov && ov.type === 'steen' ? stonePicker('hw-facade-stone', 'Steen', fc.stone, `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik`) : '');
|
||||
propsEl.innerHTML = warn + head('bricks', piece.element, piece.label) +
|
||||
cols(UI.specCard('Wandstuk', 'rulers', rows), section('Gevellampen', 'lightbulb', lampen)) +
|
||||
cols(UI.specCard('Wandstuk', 'rulers', rows), section('Gevelbekleding', 'grip-horizontal', gevel)) +
|
||||
section('Gevellampen', 'lightbulb', lampen) +
|
||||
(wl.dropped.length ? UI.warn(`${wl.dropped.length} opening(en) van deze zijde vallen in een naad en vervallen.`) : '') +
|
||||
actions(button('Wand bewerken', 'pencil-square', `data-go="wall:${pu.id}:${wl.side}"`));
|
||||
UI.onSel('hw-light', v => { unitById(pu.id).lighting.walls[wl.side] = v; }, rerender);
|
||||
UI.onSel('hw-facade', v => {
|
||||
if (v === 'woning') delete su.facade.walls[wl.side];
|
||||
else su.facade.walls[wl.side] = v === 'steen' ? { type: v, stone: fc.stone } : { type: v };
|
||||
}, rerender);
|
||||
UI.onSel('hw-facade-stone', v => { su.facade.walls[wl.side].stone = v; }, rerender);
|
||||
}
|
||||
|
||||
function renderIwProps(iw, warn) {
|
||||
@@ -799,6 +866,13 @@ function mountHouseEditor(container, callbacks) {
|
||||
|
||||
// Woning-instellingen (niets geselecteerd): alles wat voor de hele woning
|
||||
// geldt, in tabbladen.
|
||||
// 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}">`;
|
||||
|
||||
function renderHouseProps(warn) {
|
||||
const st = state.settings, sp = st.spacing, r = st.roof, lt = st.lighting;
|
||||
const pos = { positive: true };
|
||||
@@ -822,17 +896,34 @@ function mountHouseEditor(container, callbacks) {
|
||||
actions(button('Unit toevoegen', 'plus-lg', 'data-action="add-unit"'), button('Binnenwand tekenen', 'pencil', 'data-action="draw-iw"'),
|
||||
button('Projectgegevens', 'card-text', 'data-action="project"')) },
|
||||
{ key: 'gevel', label: 'Gevel', icon: 'bricks', html: cols(
|
||||
section('Gevelbekleding', 'grip-horizontal', sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'WEO/Novalit, voorlopig voor alle buitenwanden' })) +
|
||||
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' }) : '')),
|
||||
section('Gevelbekleding', 'grip-horizontal',
|
||||
sel('hs-facade', 'Type', HouseModel.FACADE_TYPES, st.facade.type, { hint: 'Voor alle buitenwanden; per wand aan te passen (klik een wand in de plattegrond)' }) +
|
||||
(st.facade.type === 'steen'
|
||||
? stonePicker('hs-facade-stone', 'Steen', st.facade.stone, `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik`)
|
||||
: sel('hs-cladding', 'Richting', WallModel.CLADDING_DIRECTIONS, st.cladding, { hint: 'Rabat op vochtdoek en kruislat' }))) +
|
||||
section('Trasraam', 'bricks', toggle('hs-trasraam', 'Trasraam', st.trasraam.enabled, { hint: 'Strook onderaan de gevel, onder rabat én steen (eigen steen mogelijk)' }) +
|
||||
(st.trasraam.enabled ? num('hs-trasraam-h', 'Hoogte', st.trasraam.height, { step: 10, hint: 'Vanaf bovenkant vloer' }) +
|
||||
sel('hs-trasraam-finish', 'Vormgeving', trasraamFinishes, st.trasraam.finish, {
|
||||
hint: WallModel.isStoneFinish(st.trasraam.finish)
|
||||
? `Steenstrip op isolatieblok, samen ${WallModel.STONE.THICK_MM} mm dik`
|
||||
: 'Cement-gebonden plaat'
|
||||
}) +
|
||||
(WallModel.isStoneFinish(st.trasraam.finish)
|
||||
? `<img class="hs-stone-preview" src="${WallModel.stoneThumbUrl(st.trasraam.finish, 128)}" alt="${st.trasraam.finish}">` : '') : '')),
|
||||
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` })) +
|
||||
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: '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)' }) +
|
||||
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' })),
|
||||
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' }))) },
|
||||
{ key: 'dak', label: 'Dakrand', icon: 'house', html: cols(
|
||||
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 }) +
|
||||
sel('hs-fascia-color', 'Kleur', Object.fromEntries(Object.entries(RoofModel.FASCIA_COLORS).map(([k, c]) => [k, c.label])), r.fasciaColor, { hint: 'Egale kleur' })),
|
||||
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:
|
||||
cols(section('Buiten', 'sun', sel('hs-light-mode', 'Type', LightingModel.MODES, lt.mode)),
|
||||
@@ -844,9 +935,15 @@ function mountHouseEditor(container, callbacks) {
|
||||
]);
|
||||
|
||||
UI.onSel('hs-cladding', v => { st.cladding = v; }, rerender);
|
||||
UI.onSel('hs-facade', v => { st.facade.type = v; }, rerender);
|
||||
UI.onColor('hs-int-color', v => { st.interiorColor = v; }, after);
|
||||
UI.onToggle('hs-int-paint', v => { st.interiorPaint = v; }, rerender);
|
||||
UI.onSel('hs-floor-finish', v => { st.floorFinish = v; }, rerender);
|
||||
UI.onSel('hs-facade-stone', v => { st.facade.stone = v; }, rerender);
|
||||
UI.onToggle('hs-trasraam', v => { st.trasraam.enabled = v; }, rerender);
|
||||
UI.onToggle('hs-sandwich', v => { st.sandwich = v; }, rerender);
|
||||
UI.onNum('hs-trasraam-h', v => { st.trasraam.height = v; }, after, pos);
|
||||
UI.onSel('hs-trasraam-finish', v => { st.trasraam.finish = v; }, rerender);
|
||||
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-max', v => { sp.exterior.max = v; }, after, pos);
|
||||
@@ -854,6 +951,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
UI.onNum('hs-int-max', v => { sp.interior.max = v; }, after, pos);
|
||||
UI.onNum('hs-overhang', v => { r.overhang = Math.min(RoofModel.OVERHANG_MAX, Math.max(RoofModel.OVERHANG_MIN, v)); }, after, pos);
|
||||
UI.onNum('hs-fascia-h', v => { r.fasciaHeight = v; }, after, pos);
|
||||
UI.onSel('hs-fascia-color', v => { r.fasciaColor = v; }, rerender);
|
||||
UI.onNum('hs-trim-h', v => { r.trimHeight = v; }, after, pos);
|
||||
UI.onNum('hs-spot-min', v => { r.spotMin = v; }, after, pos);
|
||||
UI.onNum('hs-spot-max', v => { r.spotMax = v; }, after, pos);
|
||||
@@ -917,6 +1015,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
|
||||
// --- Alles opnieuw doorrekenen ---
|
||||
function refresh(opts) {
|
||||
// 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 });
|
||||
const totals = HouseModel.computeTotals(parts);
|
||||
|
||||
@@ -944,6 +1044,7 @@ function mountHouseEditor(container, callbacks) {
|
||||
});
|
||||
houseGroup.add(p.group);
|
||||
});
|
||||
if (vloerplan) houseGroup.add(buildPlanCaps(cutPlane.constant / toM));
|
||||
if (dakVisible && !vloerplan) {
|
||||
parts.units.forEach(u => {
|
||||
u.roof.group.position.set(u.roof.rect.cx * toM, u.roof.base * toM, u.roof.rect.cz * toM);
|
||||
@@ -960,7 +1061,8 @@ function mountHouseEditor(container, callbacks) {
|
||||
const cx = (bx0 + bx1) / 2 * toM, cz = (bz0 + bz1) / 2 * toM;
|
||||
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;
|
||||
// 2 mm extra lager: bij maaiveld 0 niet in hetzelfde vlak als de OSB (flikkeren).
|
||||
const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel - 2) * toM;
|
||||
skyEnv.fitTo(houseGroup, groundY);
|
||||
ground.position.set(cx, groundY - 0.002, cz);
|
||||
applyRenderMode();
|
||||
|
||||
Reference in New Issue
Block a user