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
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
const APP_VERSION = '0.4.14';
|
||||
const APP_VERSION = '0.4.15';
|
||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||
|
||||
let tabs = [];
|
||||
|
||||
+10
-1
@@ -40,6 +40,15 @@ window.UI = (() => {
|
||||
return field(id, label, `<select id="${id}">${opts}</select><span class="ui-unit"></span>`, o);
|
||||
}
|
||||
|
||||
// Kleurkiezer (waarde '#rrggbb'); onColor reageert pas bij loslaten.
|
||||
function color(id, label, value, o) {
|
||||
return field(id, label, `<input type="color" id="${id}" value="${value}"><span class="ui-unit"></span>`, o);
|
||||
}
|
||||
function onColor(id, fn, after) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('change', (e) => { fn(e.target.value); after && after(); });
|
||||
}
|
||||
|
||||
// Aan/uit-schakelaar.
|
||||
function toggle(id, label, checked, o) {
|
||||
return field(id, label,
|
||||
@@ -117,5 +126,5 @@ window.UI = (() => {
|
||||
el.classList.toggle('fullscreen-2d');
|
||||
}
|
||||
|
||||
return { esc, icon, hint, num, sel, toggle, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
|
||||
return { esc, icon, hint, num, sel, toggle, color, onColor, section, info, warn, grid, button, actions, tabs, wireTabs, specCard, onNum, onSel, onToggle, cycle3D, toggle2D };
|
||||
})();
|
||||
|
||||
+57
-2
@@ -213,6 +213,32 @@ window.FloorModel = (() => {
|
||||
return { calcData, resolvedOpenings, chassisData };
|
||||
}
|
||||
|
||||
// Vloerafwerking (bovenop de OSB). Texturen in assets/textures/vloer/,
|
||||
// naadloos herhalend; tileMM = maat van één tegel van de textuur.
|
||||
const FINISHES = {
|
||||
geen: { label: 'Geen (OSB)' },
|
||||
visgraat_001: { label: 'Visgraat (Hongaarse punt)', file: 'assets/textures/vloer/visgraat_001.jpg', tileMM: 3000 }
|
||||
};
|
||||
const finishMaterials = {};
|
||||
function finishMaterial(key) {
|
||||
const f = FINISHES[key];
|
||||
if (!f || !f.file) return null;
|
||||
if (finishMaterials[key]) return finishMaterials[key];
|
||||
const mat = new THREE.MeshStandardMaterial({ color: 0xb89a7c, roughness: 0.7 });
|
||||
if (typeof document !== 'undefined' && THREE.TextureLoader) {
|
||||
new THREE.TextureLoader().load(f.file, (tex) => {
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
tex.repeat.set(1000 / f.tileMM, 1000 / f.tileMM);
|
||||
tex.anisotropy = 8;
|
||||
mat.map = tex;
|
||||
mat.color.set(0xffffff);
|
||||
mat.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
finishMaterials[key] = mat;
|
||||
return mat;
|
||||
}
|
||||
|
||||
function createMaterials() {
|
||||
return {
|
||||
woodMaterial: new THREE.MeshStandardMaterial({ color: 0xc29b62, roughness: 0.6 }),
|
||||
@@ -226,7 +252,10 @@ window.FloorModel = (() => {
|
||||
// Z = -diepte/2), Y = dikte (balkhoogte + OSB). Balken en isolatie zijn
|
||||
// simpele dozen; de OSB-onderlaag is een geëxtrudeerd Shape met ronde
|
||||
// uitsparingen voor de doorvoeren.
|
||||
// opts: { materials, osbVisible }
|
||||
// opts: { materials, osbVisible, finish }
|
||||
// finish (optioneel): { key (FINISHES), originX, originZ } — afwerking als
|
||||
// dun vlak op de OSB; UV's in meters vanaf origin (m, woning-coördinaten
|
||||
// van het vloermidden), zodat het patroon over units doorloopt.
|
||||
// ----------------------------------------------------------
|
||||
function build3D(calcData, resolvedOpenings, chassisData, opts) {
|
||||
const { woodMaterial, osbMaterial, wheelMaterial } = opts.materials;
|
||||
@@ -311,6 +340,32 @@ window.FloorModel = (() => {
|
||||
floorGroup3D.add(mesh);
|
||||
});
|
||||
|
||||
// Vloerafwerking: vlak net boven de OSB, over de hele vloer (onder de
|
||||
// wanden verdwijnt het), met de doorvoeren uitgespaard.
|
||||
const fin = opts.finish && finishMaterial(opts.finish.key);
|
||||
if (fin) {
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(-widthM / 2, -depthM / 2);
|
||||
shape.lineTo(widthM / 2, -depthM / 2);
|
||||
shape.lineTo(widthM / 2, depthM / 2);
|
||||
shape.lineTo(-widthM / 2, depthM / 2);
|
||||
shape.closePath();
|
||||
resolvedOpenings.filter(o => !o.removed && o.r > 0).forEach(o => {
|
||||
const hole = new THREE.Path();
|
||||
hole.absarc(o.x * toM, o.y * toM - depthM / 2, o.r * toM, 0, Math.PI * 2, false);
|
||||
shape.holes.push(hole);
|
||||
});
|
||||
const geo = new THREE.ShapeGeometry(shape);
|
||||
const ox = opts.finish.originX || 0, oz = opts.finish.originZ || 0;
|
||||
const pos = geo.attributes.position, uv = geo.attributes.uv;
|
||||
// Na het kantelen (-90° om X) wordt vorm-y wereld -z.
|
||||
for (let k = 0; k < pos.count; k++) uv.setXY(k, pos.getX(k) + ox, -pos.getY(k) + oz);
|
||||
const mesh = new THREE.Mesh(geo, fin);
|
||||
mesh.rotation.x = -Math.PI / 2;
|
||||
mesh.position.y = joistHM + underlayM + 0.0005;
|
||||
mesh.receiveShadow = true;
|
||||
floorGroup3D.add(mesh);
|
||||
}
|
||||
return floorGroup3D;
|
||||
}
|
||||
|
||||
@@ -338,7 +393,7 @@ window.FloorModel = (() => {
|
||||
}
|
||||
|
||||
return {
|
||||
validateState, computeFrame, topHeight, resolveOpenings: resolveFloorOpenings, computeChassisWheels,
|
||||
FINISHES, finishMaterial, validateState, computeFrame, topHeight, resolveOpenings: resolveFloorOpenings, computeChassisWheels,
|
||||
computeSpecs, computeFromState, createMaterials, build3D
|
||||
};
|
||||
})();
|
||||
|
||||
+129
-19
@@ -60,12 +60,23 @@ window.HouseModel = (() => {
|
||||
function createDefaultSettings() {
|
||||
return {
|
||||
spacing: { exterior: { min: 240, max: 480 }, interior: { min: 300, max: 600 } },
|
||||
cladding: 'horizontaal', // richting gevelbekleding, voorlopig voor alle buitenwanden
|
||||
// Gevelbekleding van alle buitenwanden (override per wand: unit.facade):
|
||||
// rabat = vochtdoek + kruislat + rabat (richting: cladding);
|
||||
// steen = cement-gebonden plaat + steenblok (stone = steencode, WallModel.STONE).
|
||||
// Het trasraam (eigen afwerking/steen) komt onder beide.
|
||||
facade: { type: 'rabat', stone: 'K490' },
|
||||
cladding: 'horizontaal', // richting rabat, voorlopig voor alle buitenwanden
|
||||
// Trasraam: cement-gebonden plaat onderaan alle vochtdoek-gevels, hoogte
|
||||
// vanaf het beloopbare vloervlak (WallModel.trasraamFor).
|
||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM },
|
||||
// finish: 'cementplaat' of een steencode (WallModel.TRASRAAM_STONES).
|
||||
trasraam: { enabled: true, height: WallModel.TRASRAAM_DEFAULT_MM, finish: 'cementplaat' },
|
||||
// Binnenzijde van alle wanden (voorlopig globaal): geschilderd in
|
||||
// interiorColor (egaal, zonder plaatnaden) of kale spaanplaat.
|
||||
interiorPaint: true,
|
||||
interiorColor: '#f2efe9',
|
||||
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
|
||||
groundLevel: 0, // maaiveld: zoveel mm onder het loopvlak (bovenkant vloer); voor de 3D-omgeving
|
||||
floorFinish: 'visgraat_001', // vloerafwerking (FloorModel.FINISHES), voorlopig globaal
|
||||
roof: Object.assign({}, RoofModel.DEFAULT_SETTINGS),
|
||||
lighting: Object.assign({}, LightingModel.DEFAULT_SETTINGS)
|
||||
};
|
||||
@@ -73,10 +84,35 @@ window.HouseModel = (() => {
|
||||
|
||||
function createUnitEntry(id, attach, unitOpts) {
|
||||
const unit = UnitModel.createDefaultState(unitOpts);
|
||||
return { id, attach, unit, roof: RoofModel.createDefaultRoof(unit.floor), lighting: LightingModel.createDefaultUnitLighting() };
|
||||
return { id, attach, unit, roof: RoofModel.createDefaultRoof(unit.floor), lighting: LightingModel.createDefaultUnitLighting(), facade: { walls: {} } };
|
||||
}
|
||||
|
||||
// Gevelbekleding: keuze voor de woning, override per wand.
|
||||
const FACADE_TYPES = { rabat: 'Rabat', steen: 'Steen' };
|
||||
// { type, stone } voor één wand van een unit (u = entry uit state.units).
|
||||
function facadeFor(state, u, side) {
|
||||
const f = state.settings.facade;
|
||||
const o = (u.facade && u.facade.walls && u.facade.walls[side]) || null;
|
||||
const type = o && FACADE_TYPES[o.type] ? o.type : f.type;
|
||||
const stone = o && o.type === 'steen' && WallModel.isStoneFinish(o.stone) ? o.stone : f.stone;
|
||||
return { type, stone, override: !!o };
|
||||
}
|
||||
|
||||
// TIJDELIJK (testen): een nieuwe woning is standaard het testontwerp:
|
||||
// L-vorm, unit A lessenaar (noord 2600, zuid 3200) met riet. Op false
|
||||
// zetten om terug te gaan naar de gewone standaard (één unit).
|
||||
const TEST_DEFAULT = true;
|
||||
|
||||
function createDefaultState(template) {
|
||||
if (TEST_DEFAULT && (!template || template === 'single')) {
|
||||
const st = createDefaultState('L');
|
||||
const A = st.units[0], W = A.unit.walls;
|
||||
const h = (k, l, r) => Object.assign(W[k].wall, { heightAll: Math.min(l, r), heightLeft: l, heightRight: r, heightMid: null });
|
||||
h('achter', 3200, 3200); h('links', 2600, 3200); h('rechts', 3200, 2600);
|
||||
A.roof.covering = 'riet_001';
|
||||
computeParts(st, { skip3D: true });
|
||||
return st;
|
||||
}
|
||||
const state = { project: createEmptyProject(), settings: createDefaultSettings(), units: [], interiorWalls: [] };
|
||||
state.units.push(createUnitEntry('A', null, { width: 6000, depth: 3000 }));
|
||||
if (template === 'L') {
|
||||
@@ -282,11 +318,14 @@ window.HouseModel = (() => {
|
||||
const sp = state.settings.spacing;
|
||||
const tr = state.settings.trasraam;
|
||||
state.units.forEach(u => {
|
||||
const trasraam = tr.enabled ? WallModel.trasraamFor(tr.height) : null;
|
||||
Object.values(u.unit.walls).forEach(ws => {
|
||||
const trasraam = tr.enabled ? WallModel.trasraamFor(tr.height, tr.finish) : null;
|
||||
Object.entries(u.unit.walls).forEach(([side, ws]) => {
|
||||
ws.wall.minSpacing = sp.exterior.min;
|
||||
ws.wall.maxSpacing = sp.exterior.max;
|
||||
ws.materials = Object.assign({}, ws.materials, { cladding: state.settings.cladding, trasraam });
|
||||
const fc = facadeFor(state, u, side);
|
||||
ws.materials = Object.assign({}, ws.materials, fc.type === 'steen'
|
||||
? { exterior: 'cementplaat', stone: fc.stone, cladding: state.settings.cladding, trasraam }
|
||||
: { exterior: 'vochtdoek', stone: null, cladding: state.settings.cladding, trasraam });
|
||||
});
|
||||
if (u.roof.followFloor) {
|
||||
u.roof.deck.floor.width = u.unit.floor.floor.width;
|
||||
@@ -310,14 +349,22 @@ window.HouseModel = (() => {
|
||||
const W = p.place.width, T = p.place.thickness;
|
||||
const normal = { x: p.place.normal.x, z: p.place.normal.y };
|
||||
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z', perp = axis === 'x' ? 'z' : 'x';
|
||||
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
||||
segs.push({ piece: p, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
|
||||
// Binnenhoek-doorloop (negatieve skinExt, zie trimTop): zit dat stuk
|
||||
// over de hele hoogte in de buur-unit, dan begint de gevel pas daarna.
|
||||
// Loopt de gevel erboven door (lager dak van de buur), dan telt het
|
||||
// hele stuk mee, zodat de gevel één geheel blijft met de rest.
|
||||
const e = p.skinExt || {}, tt = p.trimTop || {};
|
||||
const maxY = WallModel.topLine(p.config).maxY + (p.skinAbove || 0);
|
||||
const trim = (k) => e[k] < 0 && !(tt[k] > 0 && tt[k] < maxY - 1) ? -e[k] : 0;
|
||||
const tL = trim('left'), tR = trim('right');
|
||||
const q0 = UnitModel.localToWorld(p.place, -W / 2 + tL, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2 - tR, T / 2);
|
||||
segs.push({ piece: p, trimL: tL, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
|
||||
});
|
||||
groupFacadeRuns(segs).forEach(run => {
|
||||
const dir = Math.sign(run.segs[0].sAtW - run.segs[0].sAt0) || 1;
|
||||
const sLeft = dir > 0 ? run.s0 : run.s1, sRight = dir > 0 ? run.s1 : run.s0;
|
||||
const first = run.segs.reduce((best, sg) => Math.abs(sg.sAt0 - sLeft) < Math.abs(best.sAt0 - sLeft) ? sg : best);
|
||||
const extLeft = (first.piece.skinExt && first.piece.skinExt.left) || 0;
|
||||
const extLeft = Math.max(0, (first.piece.skinExt && first.piece.skinExt.left) || 0);
|
||||
// Horizontaal latten-raster: vanaf het trasraam van een gewoon stuk
|
||||
// (absolute hoogte), zodat opbouw/naadwand-stukken erop aansluiten.
|
||||
const ref = run.segs.find(sg => !sg.piece.place.baseY && sg.piece.skinFrom === undefined);
|
||||
@@ -326,7 +373,7 @@ window.HouseModel = (() => {
|
||||
run.segs.forEach(sg => {
|
||||
const p = sg.piece;
|
||||
sg.piece.facade = {
|
||||
u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5,
|
||||
u0: Math.abs(sg.sAt0 - sLeft) + extLeft - sg.trimL, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5,
|
||||
gridY0: gridAbs === undefined ? undefined : gridAbs - p.place.floorTop,
|
||||
yOff: floorAbs === undefined ? 0 : p.place.floorTop - floorAbs
|
||||
};
|
||||
@@ -344,10 +391,11 @@ window.HouseModel = (() => {
|
||||
const wallThickness = (id, side) => WallModel.thicknessOf(WallModel.configFromState(state.units.find(u => u.id === id).unit.walls[side]));
|
||||
const { cuts, seams } = computeSeams(rects, wallThickness);
|
||||
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow) {
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop) {
|
||||
if (!build) return { group: null, stats: null, specs: null };
|
||||
const built = WallModel.build3D(config, frame, {
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow,
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow, stone, skinAbove, trimTop,
|
||||
painted: state.settings.interiorPaint,
|
||||
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
|
||||
});
|
||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
||||
@@ -360,7 +408,8 @@ window.HouseModel = (() => {
|
||||
const ws = u.unit.walls[wl.side];
|
||||
const buitenMateriaal = (ws.materials || {}).exterior || 'vochtdoek';
|
||||
const cladding = (ws.materials || {}).cladding || 'horizontaal';
|
||||
const trasraam = buitenMateriaal === 'vochtdoek' ? ((ws.materials || {}).trasraam || null) : null;
|
||||
const stone = buitenMateriaal === 'cementplaat' ? ((ws.materials || {}).stone || null) : null;
|
||||
const trasraam = buitenMateriaal === 'vochtdoek' || stone ? ((ws.materials || {}).trasraam || null) : null;
|
||||
const wp = UnitModel.wallPieces(wl, ws, raised);
|
||||
const pieces = wp.pieces.map(p => {
|
||||
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
|
||||
@@ -368,7 +417,7 @@ window.HouseModel = (() => {
|
||||
return Object.assign(p, {
|
||||
element: `${u.id}-W${wl.nr}${p.suffix}`,
|
||||
label: `Unit ${u.id} ${wl.label.toLowerCase()}` + (base ? ` opbouw (vanaf ${base} mm)` : p.suffix ? ` (${p.suffix})` : ''),
|
||||
buitenMateriaal, cladding, trasraam: base ? null : trasraam,
|
||||
buitenMateriaal, cladding, trasraam: base ? null : trasraam, stone,
|
||||
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop + base, baseY: base }
|
||||
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
|
||||
});
|
||||
@@ -394,7 +443,8 @@ window.HouseModel = (() => {
|
||||
const floorData = FloorModel.computeFromState(u.unit.floor, rims);
|
||||
const floorSpecs = FloorModel.computeSpecs(floorData.calcData, floorData.resolvedOpenings);
|
||||
const floorGroup = build ? FloorModel.build3D(floorData.calcData, floorData.resolvedOpenings, floorData.chassisData, {
|
||||
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false
|
||||
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false,
|
||||
finish: { key: state.settings.floorFinish, originX: rect.cx * 0.001, originZ: rect.cz * 0.001 }
|
||||
}) : null;
|
||||
|
||||
const walls = layout.walls.map(wl => wallOf(u, rect, layout, wl));
|
||||
@@ -643,9 +693,35 @@ window.HouseModel = (() => {
|
||||
// over het blootliggende deel van een naadwand. Daarna pas bouwen.
|
||||
const extPieces = [];
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
|
||||
// Gevel tot onder de dakrand: tussen wandbovenkant en onderkant boeideel
|
||||
// resp. daktrim zit het dakpakket (dek + bedekking, verschilt per dak).
|
||||
// De buitenlagen van de wand lopen daar door (skinAbove, mm), zodat er
|
||||
// geen dakrand bloot ligt, bij elke daksoort.
|
||||
const roofSt = RoofModel.normalizeSettings(state.settings.roof);
|
||||
const flatTopsPre = units.filter(u => u.roof.shape.type === 'plat').map(u => u.roof.top);
|
||||
const flatTopPre = flatTopsPre.length ? Math.max(...flatTopsPre) : 0;
|
||||
units.forEach(u => {
|
||||
const su = state.units.find(x => x.id === u.id);
|
||||
const below = su.roof.fascia !== false ? roofSt.fasciaHeight - RoofModel.EDGE_ABOVE_MM : roofSt.trimHeight;
|
||||
u.walls.forEach(w => w.pieces.forEach(p => {
|
||||
const m = UnitModel.localToWorld(p.place, 0, p.place.thickness / 2);
|
||||
const top = u.roof.shape.type === 'plat' ? flatTopPre : u.roof.topAt(m.x, m.z);
|
||||
p.skinAbove = Math.max(0, Math.round(top - u.roof.ceilingAt(m.x, m.z) - below));
|
||||
// Binnenhoek-doorloop (negatieve skinExt): tot de bovenkant van het
|
||||
// dak van de buur-unit die daar staat geen buitenlagen (trimTop).
|
||||
const e = p.skinExt || {}, W = p.place.width;
|
||||
p.trimTop = {};
|
||||
[['left', -1], ['right', 1]].forEach(([k, d]) => {
|
||||
if (!(e[k] < 0)) return;
|
||||
const q = UnitModel.localToWorld(p.place, d * (W / 2 + e[k] / 2), p.place.thickness / 2 + 20);
|
||||
const nb = units.find(o => o.id !== u.id && q.x > o.rect.x0 && q.x < o.rect.x1 && q.z > o.rect.z0 && q.z < o.rect.z1);
|
||||
p.trimTop[k] = nb ? Math.round(nb.roof.topAt(q.x, q.z) - p.place.floorTop) : 0;
|
||||
});
|
||||
}));
|
||||
});
|
||||
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
|
||||
extPieces.concat(interior).forEach(p => {
|
||||
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow);
|
||||
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow, p.stone, p.skinAbove, p.trimTop);
|
||||
if (built.group && p.shiftZ) {
|
||||
const outer = new THREE.Group();
|
||||
built.group.position.z = p.shiftZ * 0.001;
|
||||
@@ -725,6 +801,9 @@ window.HouseModel = (() => {
|
||||
const purchase = WallModel.planPurchase(wallBom);
|
||||
|
||||
const sumP = (fn) => pieces.reduce((acc, p) => acc + fn(p), 0);
|
||||
const stonePiece = pieces.find(p => p.trasraam && WallModel.isStoneFinish(p.trasraam.finish));
|
||||
const stoneCode = stonePiece ? stonePiece.trasraam.finish : null;
|
||||
const stoneCodes = [...new Set(pieces.filter(p => p.stone).map(p => p.stone))].join(', ');
|
||||
const sumU = (fn) => parts.units.reduce((acc, u) => acc + fn(u), 0);
|
||||
const byMat = (mat) => pieces.filter(p => p.buitenMateriaal === mat);
|
||||
|
||||
@@ -762,10 +841,24 @@ window.HouseModel = (() => {
|
||||
},
|
||||
{ naam: 'OSB-plaat (vloeren + plafonds dak)', m2: sumU(u => u.floorSpecs.osbAreaM2 + u.roof.specs.osbAreaM2), m3: sumU(u => u.floorSpecs.osbVolM3 + u.roof.specs.osbVolM3), kg: sumU(u => u.floorSpecs.osbKg + u.roof.specs.osbKg) },
|
||||
{ naam: 'Dakleer (EPDM)', m2: sumU(u => u.roof.specs.dakleerM2), m3: null, kg: sumU(u => u.roof.specs.dakleerKg) },
|
||||
{ naam: `Riet (${RoofModel.RIET.thickMM} mm)`, m2: sumU(u => u.roof.specs.rietM2 || 0), m3: null, kg: sumU(u => u.roof.specs.rietKg || 0) },
|
||||
{
|
||||
naam: 'Cementplaat (buitenzijde)', m2: byMat('cementplaat').reduce((a, p) => a + p.stats.cementplaatAreaM2, 0),
|
||||
m3: byMat('cementplaat').reduce((a, p) => a + p.stats.cementplaatVolumeM3, 0), kg: byMat('cementplaat').reduce((a, p) => a + p.specs.buitenKg, 0)
|
||||
},
|
||||
{
|
||||
naam: `Steenstrips gevel${stoneCodes ? ' (' + stoneCodes + ')' : ''}`,
|
||||
m2: sumP(p => p.stats.stoneAreaM2 || 0), m3: null, kg: 0 // gewicht volgt
|
||||
},
|
||||
{
|
||||
naam: `Steenstrips trasraam${stoneCode ? ' (' + stoneCode + ')' : ''}`,
|
||||
m2: sumP(p => p.stats.trasraamStoneAreaM2 || 0), m3: null, kg: 0 // gewicht volgt
|
||||
},
|
||||
{
|
||||
naam: `Isolatieblok steen (${WallModel.STONE.THICK_MM - WallModel.STONE.STRIP_MM} mm)`,
|
||||
m2: sumP(p => (p.stats.trasraamIsolatieM3 || 0) + (p.stats.stoneIsolatieM3 || 0)) / ((WallModel.STONE.THICK_MM - WallModel.STONE.STRIP_MM) / 1000),
|
||||
m3: sumP(p => (p.stats.trasraamIsolatieM3 || 0) + (p.stats.stoneIsolatieM3 || 0)), kg: 0
|
||||
},
|
||||
{
|
||||
naam: 'Cementplaat (trasraam)', m2: sumP(p => p.stats.trasraamAreaM2 || 0),
|
||||
m3: sumP(p => p.stats.trasraamVolumeM3 || 0), kg: sumP(p => p.specs.trasraamKg || 0)
|
||||
@@ -784,6 +877,7 @@ window.HouseModel = (() => {
|
||||
roofEdge: parts.roofEdge ? {
|
||||
fasciaLengthM: parts.roofEdge.fasciaLengthMM / 1000,
|
||||
fasciaHeight: parts.roofEdge.fasciaHeight,
|
||||
fasciaColor: (RoofModel.FASCIA_COLORS[parts.roofEdge.fasciaColor] || {}).label || '',
|
||||
overhang: parts.roofEdge.overhang,
|
||||
soffitM2: parts.roofEdge.soffitM2,
|
||||
trimLengthM: parts.roofEdge.trimLengthMM / 1000,
|
||||
@@ -837,11 +931,22 @@ window.HouseModel = (() => {
|
||||
state.settings = state.settings || {};
|
||||
state.settings.spacing = Object.assign(def.spacing, state.settings.spacing || {});
|
||||
if (!WallModel.CLADDING_DIRECTIONS[state.settings.cladding]) state.settings.cladding = def.cladding;
|
||||
// v0.4.15: gevelbekleding per woning; een wand met cementplaat wordt een
|
||||
// steen-override (de rest volgt de woning, standaard rabat).
|
||||
const hadFacade = !!state.settings.facade;
|
||||
state.settings.facade = Object.assign(def.facade, state.settings.facade || {});
|
||||
if (!FACADE_TYPES[state.settings.facade.type]) state.settings.facade.type = 'rabat';
|
||||
if (!WallModel.isStoneFinish(state.settings.facade.stone)) state.settings.facade.stone = def.facade.stone;
|
||||
state.settings.trasraam = Object.assign(def.trasraam, state.settings.trasraam || {});
|
||||
state.settings.trasraam.enabled = state.settings.trasraam.enabled !== false;
|
||||
if (!(state.settings.trasraam.height > 0)) state.settings.trasraam.height = WallModel.TRASRAAM_DEFAULT_MM;
|
||||
if (!WallModel.isStoneFinish(state.settings.trasraam.finish)) state.settings.trasraam.finish = 'cementplaat';
|
||||
state.settings.sandwich = state.settings.sandwich !== false;
|
||||
if (!(state.settings.groundLevel >= 0)) state.settings.groundLevel = def.groundLevel;
|
||||
if (!/^#[0-9a-f]{6}$/i.test(state.settings.interiorColor || '')) state.settings.interiorColor = def.interiorColor;
|
||||
state.settings.interiorPaint = state.settings.interiorPaint !== false;
|
||||
// v0.4.15: maaiveld standaard gelijk met de bovenkant vloer (was 350 mm eronder).
|
||||
if (!(state.settings.groundLevel >= 0) || (!state.settings.floorFinish && state.settings.groundLevel === 350)) state.settings.groundLevel = def.groundLevel;
|
||||
if (!FloorModel.FINISHES[state.settings.floorFinish]) state.settings.floorFinish = def.floorFinish;
|
||||
const oldRoof = state.settings.roof || {}; // v0.3.0: edge/spots waren woning-breed
|
||||
state.settings.roof = RoofModel.normalizeSettings(oldRoof);
|
||||
// v0.3.1: type/richting van de verlichting stonden per unit; nu woning-breed.
|
||||
@@ -854,13 +959,18 @@ window.HouseModel = (() => {
|
||||
if (!u.unit.layout) u.unit.layout = { corners: 'voor-achter' };
|
||||
if (!u.roof) { u.roof = RoofModel.createDefaultRoof(u.unit.floor); delete u.roof.fascia; }
|
||||
if (u.roof.fascia === undefined) u.roof.fascia = oldRoof.edge !== 'daktrim';
|
||||
if (!RoofModel.COVERINGS[u.roof.covering]) u.roof.covering = 'dakleer';
|
||||
u.lighting = { walls: (u.lighting && u.lighting.walls) || {} };
|
||||
u.facade = { walls: (u.facade && u.facade.walls) || {} };
|
||||
if (!hadFacade) Object.entries(u.unit.walls).forEach(([side, ws]) => {
|
||||
if ((ws.materials || {}).exterior === 'cementplaat' && !u.facade.walls[side]) u.facade.walls[side] = { type: 'steen' };
|
||||
});
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS,
|
||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor,
|
||||
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
||||
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls,
|
||||
computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize
|
||||
|
||||
@@ -218,7 +218,7 @@ window.HousePdf = (() => {
|
||||
const e = totals.roofEdge, L = totals.lamps;
|
||||
const rows = [];
|
||||
if (e.fasciaLengthM > 0) {
|
||||
rows.push(['Boeidelen', `${fmt(e.fasciaLengthM, 2)} m, ${e.fasciaHeight} mm hoog, overstek ${e.overhang} mm`]);
|
||||
rows.push(['Boeidelen', `${fmt(e.fasciaLengthM, 2)} m, ${e.fasciaHeight} mm hoog, overstek ${e.overhang} mm${e.fasciaColor ? ', ' + e.fasciaColor.toLowerCase() : ''}`]);
|
||||
rows.push(['Onderplaat boeideel', fmt(e.soffitM2, 2) + ' m2']);
|
||||
}
|
||||
if (e.trimLengthM > 0) rows.push(['Daktrim', `${fmt(e.trimLengthM, 2)} m, ${e.trimHeight} mm hoog`]);
|
||||
|
||||
+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();
|
||||
|
||||
@@ -122,17 +122,19 @@ window.LightingModel = (() => {
|
||||
if (p.baseY) return; // opbouw boven een lager dak: geen gevellampen
|
||||
const W = p.place.width;
|
||||
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
|
||||
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal);
|
||||
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal, p.stone);
|
||||
const normal = { x: p.place.normal.x, z: p.place.normal.y }; // place.normal is {x, y} (plattegrond)
|
||||
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z';
|
||||
const perp = axis === 'x' ? 'z' : 'x';
|
||||
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
||||
// Binnenhoek-doorloop (negatieve skinExt): de gevel begint pas daarna.
|
||||
const tL = Math.max(0, -((p.skinExt || {}).left || 0)), tR = Math.max(0, -((p.skinExt || {}).right || 0));
|
||||
const q0 = UnitModel.localToWorld(p.place, -W / 2 + tL, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2 - tR, T / 2);
|
||||
// Gevel loopt tot de afgewerkte hoek, zodat endOffset altijd vanaf de
|
||||
// zichtbare hoek telt: elk uiteinde + afwerking (latwerk + bekleding van
|
||||
// de haakse wand die eroverheen loopt), en waar dit stuk de kopse kant
|
||||
// van een doorlopende wand bedekt (skinExt) ook nog die wanddikte.
|
||||
// Binnenhoeken en naden: zie de voetafdrukken in 3 en de overlap in 2.
|
||||
const c = WallModel.claddingOffset(p.buitenMateriaal), e = p.skinExt || {};
|
||||
const c = WallModel.claddingOffset(p.buitenMateriaal, p.stone), e = p.skinExt || {};
|
||||
const extL = (e.left > 0 ? e.left : 0) + c, extR = (e.right > 0 ? e.right : 0) + c;
|
||||
const up = q1[axis] >= q0[axis];
|
||||
// Voetafdruk van dit stuk incl. afwerking (om bedekte delen van andere
|
||||
|
||||
+82
-18
@@ -49,9 +49,57 @@ window.RoofModel = (() => {
|
||||
fasciaHeight: 300, // mm
|
||||
spotMin: 800, // mm h.o.h. boeideel-spots
|
||||
spotMax: 1200,
|
||||
trimHeight: 80 // mm
|
||||
trimHeight: 80, // mm
|
||||
fasciaColor: 'antraciet' // FASCIA_COLORS
|
||||
};
|
||||
|
||||
// Kleur van het boeideel (egaal).
|
||||
const FASCIA_COLORS = {
|
||||
wit: { label: 'Wit', hex: 0xf2f2f0 },
|
||||
lichtgrijs: { label: 'Lichtgrijs', hex: 0xbfc3c6 },
|
||||
middelgrijs: { label: 'Middelgrijs', hex: 0x7d8286 },
|
||||
antraciet: { label: 'Antraciet', hex: 0x3a3f44 },
|
||||
zwart: { label: 'Zwart', hex: 0x151617 }
|
||||
};
|
||||
|
||||
// Dakbedekking per unit (roof.covering). Riet e.d. alleen bij een hellend
|
||||
// dak (lessenaar/zadel); plat valt terug op dakleer. thickMM telt mee in de
|
||||
// dakdikte (vThick/topAt), dus boeideel/daktrim en een opbouw ernaast
|
||||
// schuiven vanzelf mee omhoog. Texturen: assets/textures/dak/, naadloos;
|
||||
// de stengels lopen langs de helling.
|
||||
// Riet: één keuze per textuur assets/textures/dak/riet_<nr>.jpg (nummer
|
||||
// hier toevoegen); zelfde opbouw (180 mm, 3,8 kg/m2).
|
||||
const RIET = { thickMM: 180, kgM2: 3.8, tileMM: 1000, nrs: ['001', '002', '003'] };
|
||||
const COVERINGS = Object.assign({
|
||||
dakleer: { label: 'Dakleer (EPDM)', thickMM: DAKLEER_MM, kgM2: DENSITY_DAKLEER_KGM2 }
|
||||
}, ...RIET.nrs.map(nr => ({
|
||||
['riet_' + nr]: { label: 'Riet ' + nr, thickMM: RIET.thickMM, kgM2: RIET.kgM2, file: `assets/textures/dak/riet_${nr}.jpg`, tileMM: RIET.tileMM, sloped: true }
|
||||
})));
|
||||
// Werkelijke bedekking voor een dakvorm (riet niet op een plat dak).
|
||||
function coveringFor(key, shapeType) {
|
||||
const c = COVERINGS[key];
|
||||
return c && !(c.sloped && shapeType === 'plat') ? key : 'dakleer';
|
||||
}
|
||||
const coveringMaterials = {};
|
||||
function coveringMaterial(key) {
|
||||
const c = COVERINGS[key];
|
||||
if (!c || !c.file) return null;
|
||||
if (coveringMaterials[key]) return coveringMaterials[key];
|
||||
const mat = new THREE.MeshStandardMaterial({ color: 0x9c8466, roughness: 0.95 });
|
||||
if (typeof document !== 'undefined' && THREE.TextureLoader) {
|
||||
new THREE.TextureLoader().load(c.file, (tex) => {
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
tex.repeat.set(1000 / c.tileMM, 1000 / c.tileMM);
|
||||
tex.anisotropy = 8;
|
||||
mat.map = tex;
|
||||
mat.color.set(0xffffff);
|
||||
mat.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
coveringMaterials[key] = mat;
|
||||
return mat;
|
||||
}
|
||||
|
||||
function clone(obj) { return JSON.parse(JSON.stringify(obj)); }
|
||||
|
||||
// Dak-state per unit: volgt standaard de vloermaat (followFloor), maar mag
|
||||
@@ -60,11 +108,12 @@ window.RoofModel = (() => {
|
||||
const deck = clone(floorState);
|
||||
deck.chassis = { enabled: false, wheelDiameter: 400, trackWidth: 1200, wheelbase: 1200 };
|
||||
deck.openings = [];
|
||||
return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, deck };
|
||||
return { followFloor: true, offsetX: 0, offsetZ: 0, fascia: true, covering: 'dakleer', deck };
|
||||
}
|
||||
|
||||
function thickness(calcData) {
|
||||
return calcData.joistH + calcData.underlayThick + DAKLEER_MM;
|
||||
// Dikte dek + bedekking (mm, haaks op het dakvlak).
|
||||
function thickness(calcData, covering) {
|
||||
return calcData.joistH + calcData.underlayThick + (COVERINGS[covering] || COVERINGS.dakleer).thickMM;
|
||||
}
|
||||
|
||||
function createMaterials() {
|
||||
@@ -106,20 +155,31 @@ window.RoofModel = (() => {
|
||||
hole.absarc(o.x * toM, o.y * toM - dM / 2, o.r * toM, 0, Math.PI * 2, false);
|
||||
shape.holes.push(hole);
|
||||
});
|
||||
const leer = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: DAKLEER_MM * toM, bevelEnabled: false }), m.dakleerMaterial);
|
||||
// Andere bedekking (opts.covering, bijv. riet): een laag van thickMM met
|
||||
// de textuur (vorm-coördinaten in m = UV; langs de helling = v).
|
||||
const cov = COVERINGS[opts.covering] || COVERINGS.dakleer;
|
||||
const covMat = coveringMaterial(opts.covering) || m.dakleerMaterial;
|
||||
const leer = new THREE.Mesh(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: cov.thickMM * toM, bevelEnabled: false }), covMat);
|
||||
leer.rotation.x = Math.PI / 2;
|
||||
leer.position.y = deckH + DAKLEER_MM * toM;
|
||||
leer.position.y = deckH + cov.thickMM * toM;
|
||||
leer.castShadow = cov.thickMM > DAKLEER_MM;
|
||||
leer.visible = opts.dakleerVisible !== false;
|
||||
group.add(leer);
|
||||
return group;
|
||||
}
|
||||
|
||||
// Specificaties: die van het dek (FloorModel) + dakleer.
|
||||
function computeSpecs(calcData, resolvedOpenings) {
|
||||
// Specificaties: die van het dek (FloorModel) + de bedekking (dakleer of
|
||||
// bijv. riet: coveringM2/Kg, dakleer dan 0).
|
||||
function computeSpecs(calcData, resolvedOpenings, covering) {
|
||||
const deck = FloorModel.computeSpecs(calcData, resolvedOpenings);
|
||||
const dakleerM2 = deck.osbAreaM2; // zelfde vlak minus doorvoeren
|
||||
const dakleerKg = dakleerM2 * DENSITY_DAKLEER_KGM2;
|
||||
return Object.assign({}, deck, { dakleerM2, dakleerKg, totalKg: deck.totalKg + dakleerKg });
|
||||
const areaM2 = deck.osbAreaM2; // zelfde vlak minus doorvoeren
|
||||
const isLeer = !COVERINGS[covering] || covering === 'dakleer';
|
||||
const kg = areaM2 * (COVERINGS[covering] || COVERINGS.dakleer).kgM2;
|
||||
return Object.assign({}, deck, {
|
||||
dakleerM2: isLeer ? areaM2 : 0, dakleerKg: isLeer ? kg : 0,
|
||||
rietM2: isLeer ? 0 : areaM2, rietKg: isLeer ? 0 : kg,
|
||||
totalKg: deck.totalKg + kg
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
@@ -193,6 +253,7 @@ window.RoofModel = (() => {
|
||||
const cuts = [a0, ...kinks, a1];
|
||||
const flat = FloorModel.computeFromState(roofState.deck, rims);
|
||||
const H = (p) => profileHeight(shape.profile, p);
|
||||
const covering = coveringFor(roofState.covering, shape.type);
|
||||
|
||||
// Doorvoeren: positie in de plattegrond (zoals in de dak-editor) -> vlak.
|
||||
const holes = flat.resolvedOpenings.filter(o => !o.removed).map(o => {
|
||||
@@ -223,10 +284,10 @@ window.RoofModel = (() => {
|
||||
}
|
||||
const mid = (p0 + p1) / 2;
|
||||
return Object.assign(plane, {
|
||||
data, specs: computeSpecs(data.calcData, data.resolvedOpenings),
|
||||
data, specs: computeSpecs(data.calcData, data.resolvedOpenings, covering),
|
||||
cx: alongX ? mid - xc : 0, cz: alongX ? 0 : mid - zc, y: (h0 + h1) / 2,
|
||||
// Verticale dikte (dek + dakleer) van dit vlak.
|
||||
vThick: thickness(data.calcData) / cos
|
||||
// Verticale dikte (dek + bedekking) van dit vlak.
|
||||
vThick: thickness(data.calcData, covering) / cos
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,7 +300,7 @@ window.RoofModel = (() => {
|
||||
return H(q) + t;
|
||||
};
|
||||
return {
|
||||
shape, planes, specs: sumSpecs(planes.map(pl => pl.specs)),
|
||||
shape, covering, planes, specs: sumSpecs(planes.map(pl => pl.specs)),
|
||||
undersideAt, topAt,
|
||||
ridges: kinks.map(value => ({ axis: shape.axis, value })),
|
||||
maxTop: Math.max(...planes.map(pl => Math.max(pl.h0, pl.h1) + pl.vThick))
|
||||
@@ -272,7 +333,7 @@ window.RoofModel = (() => {
|
||||
0, 1 / c, sn, 0,
|
||||
0, 0, c, 0,
|
||||
0, 0, 0, 1);
|
||||
shear.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, opts));
|
||||
shear.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, Object.assign({ covering: unitRoof.covering }, opts)));
|
||||
g.add(shear);
|
||||
group.add(g);
|
||||
});
|
||||
@@ -412,7 +473,7 @@ window.RoofModel = (() => {
|
||||
const top = Math.max(...segments.map(sg => Math.max(sg.topAt(sg.a), sg.topAt(sg.b))));
|
||||
return {
|
||||
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
||||
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
|
||||
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaColor: s.fasciaColor,
|
||||
top, edgeAbove: EDGE_ABOVE_MM,
|
||||
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
||||
trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
||||
@@ -461,6 +522,8 @@ window.RoofModel = (() => {
|
||||
const toM = 0.001;
|
||||
const group = new THREE.Group();
|
||||
if (!edge) return group;
|
||||
// Het boeidelen-materiaal wordt gedeeld: kleur volgens de instelling.
|
||||
materials.fasciaMaterial.color.setHex((FASCIA_COLORS[edge.fasciaColor] || FASCIA_COLORS.antraciet).hex);
|
||||
edge.segments.forEach(sg => {
|
||||
let polys = [[sg.a, sg.b, sg.outerB, sg.outerA]];
|
||||
sg.ridges.forEach(r => {
|
||||
@@ -491,11 +554,12 @@ window.RoofModel = (() => {
|
||||
const s = Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
||||
s.overhang = Math.min(OVERHANG_MAX, Math.max(OVERHANG_MIN, s.overhang));
|
||||
delete s.edge; delete s.spots; // oud (v0.3.0): nu per unit
|
||||
if (!FASCIA_COLORS[s.fasciaColor]) s.fasciaColor = DEFAULT_SETTINGS.fasciaColor;
|
||||
return s;
|
||||
}
|
||||
|
||||
return {
|
||||
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
|
||||
DAKLEER_MM, EDGE_ABOVE_MM, RIET, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO, FASCIA_COLORS, COVERINGS, coveringFor,
|
||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D,
|
||||
computeEdge, buildEdge3D, normalizeSettings
|
||||
|
||||
+14
-5
@@ -140,14 +140,17 @@ window.UnitModel = (() => {
|
||||
|
||||
// Resterende stukken wand; een stuk dat aan een naad grenst loopt de
|
||||
// naad in door met de doorloop (ext) van die kant.
|
||||
// [u0, u1, inL, inR]: inL/inR = hoever het stuk aan die kant de naad
|
||||
// in loopt (binnenhoek dicht, zie computeSeams).
|
||||
const pieces = [];
|
||||
let cursor = 0;
|
||||
let cursor = 0, inL = 0;
|
||||
rawU.forEach(([a, b, e0, e1]) => {
|
||||
const end = Math.min(b, a + e0);
|
||||
if (end - cursor >= MIN_PIECE_MM) pieces.push([cursor, end]);
|
||||
if (end - cursor >= MIN_PIECE_MM) pieces.push([cursor, end, inL, end - a]);
|
||||
cursor = Math.max(cursor, b - e1);
|
||||
inL = b - cursor;
|
||||
});
|
||||
if (width - cursor >= MIN_PIECE_MM) pieces.push([cursor, width]);
|
||||
if (width - cursor >= MIN_PIECE_MM) pieces.push([cursor, width, inL, 0]);
|
||||
// Werkelijk vervallende zones (voor de weergave in de wand-editor).
|
||||
const cutsU = rawU.map(([a, b, e0, e1]) => [a + e0, b - e1]).filter(([a, b]) => b - a > EPS);
|
||||
|
||||
@@ -198,7 +201,7 @@ window.UnitModel = (() => {
|
||||
const multi = wallLayout.pieces.length > 1;
|
||||
const used = new Set();
|
||||
|
||||
const pieces = wallLayout.pieces.map(([a, b], i) => {
|
||||
const pieces = wallLayout.pieces.map(([a, b, inL, inR], i) => {
|
||||
const openings = fullConfig.openings
|
||||
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS)
|
||||
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: op.x - a }; });
|
||||
@@ -211,7 +214,13 @@ window.UnitModel = (() => {
|
||||
return {
|
||||
suffix: multi ? String.fromCharCode(97 + i) : '', // a, b, ...
|
||||
u0: a, u1: b, lxCenter: -W / 2 + (a + b) / 2,
|
||||
skinExt: { left: a < EPS ? wallLayout.skinExt.left : 0, right: b > W - EPS ? wallLayout.skinExt.right : 0 },
|
||||
// Loopt het stuk een naad in (binnenhoek), dan ligt dat deel binnen
|
||||
// de wand van de buur-unit: buitenlagen daar niet (negatieve
|
||||
// doorloop, zie WallModel.cornerExt) en een binnenplaat op de kopse kant.
|
||||
skinExt: {
|
||||
left: inL > EPS ? -inL : a < EPS ? wallLayout.skinExt.left : 0,
|
||||
right: inR > EPS ? -inR : b > W - EPS ? wallLayout.skinExt.right : 0
|
||||
},
|
||||
config, frame: WallModel.computeFrame(config)
|
||||
};
|
||||
});
|
||||
|
||||
+234
-34
@@ -56,8 +56,10 @@ window.WallModel = (() => {
|
||||
// Hoe ver de afgewerkte gevel buiten het wandvlak ligt (mm): bij vochtdoek
|
||||
// de kruislat (2 lagen) + de gevelbekleding. Gebruikt o.a. voor de
|
||||
// positie van gevellampen.
|
||||
function claddingOffset(buitenMateriaal) {
|
||||
return buitenMateriaal === 'vochtdoek' ? 2 * BATTEN.thick + CLADDING_THICK_MM : 0;
|
||||
// Bij cementplaat met steen (stone): de steenstrip.
|
||||
function claddingOffset(buitenMateriaal, stone) {
|
||||
if (buitenMateriaal === 'vochtdoek') return 2 * BATTEN.thick + CLADDING_THICK_MM;
|
||||
return buitenMateriaal === 'cementplaat' && stone ? STONE.THICK_MM : 0;
|
||||
}
|
||||
|
||||
// Trasraam: strook cement-gebonden plaat (montage-achterkant voor de
|
||||
@@ -69,9 +71,50 @@ window.WallModel = (() => {
|
||||
// Geeft null als hij uit staat. Alleen van toepassing op vochtdoek-wanden
|
||||
// (cement-gebonden buitenzijde is al plaat).
|
||||
const TRASRAAM_DEFAULT_MM = 350;
|
||||
function trasraamFor(heightMM) {
|
||||
// finish: 'cementplaat' of een steencode uit TRASRAAM_STONES (steenstrip
|
||||
// op een isolatieblok, zie STONE).
|
||||
function trasraamFor(heightMM, finish) {
|
||||
if (!(heightMM > 0)) return null;
|
||||
return { bottom: 0, top: heightMM };
|
||||
return { bottom: 0, top: heightMM, finish: finish || 'cementplaat' };
|
||||
}
|
||||
|
||||
// Steen (trasraam en steengevel): isolatieblok met steenstrip ervoor,
|
||||
// samen THICK_MM dik (vóór het doek resp. de cementplaat). Texturen: assets/textures/buitenmuur/
|
||||
// "<code> - Witte voeg.jpg", 2062 x 1125 px op 2 mm/px (steen + voeg
|
||||
// ~62 mm per laag), naadloos herhalend. Voorvertoning in thumb/<px>/ (.png).
|
||||
// JOINT_MM: hart van de onderste voeg boven de onderrand van de textuur
|
||||
// (gemeten: 1-2 px); COURSE_MM: steen + voeg (31 px). De bovenrand van de
|
||||
// textuur snijdt door een steen (1125 px is geen veelvoud van 31).
|
||||
const STONE = { THICK_MM: 110, STRIP_MM: 20, TILE_W_MM: 4124, TILE_H_MM: 2250, JOINT_MM: 3, COURSE_MM: 62, DIR: 'assets/textures/buitenmuur/' };
|
||||
const TRASRAAM_STONES = ('K490 K518 K560 K662 K706 K739 K752 K764 K773 K911P K911 K914 K915 K919 K923P K923 K941P K941 K947 K948 K960 K991 ' +
|
||||
'R100 R116 R140 R200 R206 R214 R216 R220 R227 R228 R240 R268 R286 R287 R303 R307 R332 R335 R343 R356 R381 R382 R384 R385 R386 R391 ' +
|
||||
'R400 R401 R435 R436 R440 R480 R483 R484 R487 R488 R489 R500 R510 R518 R535 R540 R550 R555 R560 R561 R563 R564 R565 R572DF14_64 R572DF ' +
|
||||
'R580 R581 R582 R658 R659 R663 R664 R665 R669 R680 R682 R684 R685 R686 R687 R688 R689 R690 R691 R692 R-693 R694 R695 R696 R697 R698 ' +
|
||||
'R700 R714 R715 R717 R719 R720 R721 R725 R726 R727 R729 R730 R731 R732 R733 R734 R735 R736 R737 R738 R739 R740 R742 R743 R744 R745X737 ' +
|
||||
'R746 R747 R748 R748X764X749 R749 R750 R751 R751X759 R752 R754 R756 R757 R758 R759 R760 R761 R762 R763 R764 R764X756 R766 R767 R768 ' +
|
||||
'R769 R770 R771 R772 R773 R775 R777 R788 R800 R835 R840 R911P R911 R913 R916 R917 R918 R921 R923P R931 R932 R941P R941 R942 R943 R944 ' +
|
||||
'R960DF14_70 R960DF R961 R970 R980 R985 R991').split(' ');
|
||||
const isStoneFinish = (finish) => TRASRAAM_STONES.includes(finish);
|
||||
const stoneTextureUrl = (code) => STONE.DIR + encodeURIComponent(code + ' - Witte voeg.jpg');
|
||||
const stoneThumbUrl = (code, px) => STONE.DIR + 'thumb/' + (px || 64) + '/' + encodeURIComponent(code + ' - Witte voeg.png');
|
||||
// Eén materiaal per steen (gedeeld, pas geladen als hij gebruikt wordt).
|
||||
// UV's van de steenblokken zijn in meters, dus repeat = 1/tegel.
|
||||
const stoneMaterials = {};
|
||||
function stoneMaterial(code) {
|
||||
if (stoneMaterials[code]) return stoneMaterials[code];
|
||||
const mat = new THREE.MeshStandardMaterial({ color: 0xb0a090, roughness: 0.9 });
|
||||
if (typeof document !== 'undefined' && THREE.TextureLoader) {
|
||||
new THREE.TextureLoader().load(stoneTextureUrl(code), (tex) => {
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
tex.repeat.set(1000 / STONE.TILE_W_MM, 1000 / STONE.TILE_H_MM);
|
||||
tex.anisotropy = 8;
|
||||
mat.map = tex;
|
||||
mat.color.set(0xffffff);
|
||||
mat.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
stoneMaterials[code] = mat;
|
||||
return mat;
|
||||
}
|
||||
// Hoekdoorloop van de buitenlagen. Een wand die tussen twee doorlopende
|
||||
// wanden past (unit-hoek) moet met zijn buitenlagen ook de kopse kant
|
||||
@@ -79,9 +122,13 @@ window.WallModel = (() => {
|
||||
// right } = dikte van die wand (mm, 0 = vrij uiteinde). Elke laag loopt
|
||||
// daarbovenop nog door tot zijn eigen buitenvlak, zodat de hoek dicht is.
|
||||
// depth = afstand van het buitenvlak van de laag tot het wandvlak (mm).
|
||||
// Negatief (binnenhoek): het stuk loopt zoveel de naad in, binnen de wand
|
||||
// van de buur-unit. Geen doorloop; de buitenlagen krijgen daar een
|
||||
// uitsparing (build3D: trim, tot opts.trimTop).
|
||||
function cornerExt(skinExt, depth) {
|
||||
const e = skinExt || {};
|
||||
return { left: e.left > 0 ? e.left + depth : 0, right: e.right > 0 ? e.right + depth : 0 };
|
||||
const one = (v) => v > 0 ? v + depth : 0;
|
||||
return { left: one(e.left), right: one(e.right) };
|
||||
}
|
||||
|
||||
// Onderkant van vochtdoek en latwerk (wand-coördinaten, mm).
|
||||
@@ -479,13 +526,13 @@ window.WallModel = (() => {
|
||||
// Stukken van een horizontale lijn op hoogte y (mm) die OP de beplating
|
||||
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
|
||||
// uitsparingen. Geeft [[x0, x1], ...] terug (mm, t.o.v. het wandmidden).
|
||||
function skinSegmentsAtY(CONFIG, y) {
|
||||
function skinSegmentsAtY(CONFIG, y, extraHoles) {
|
||||
if (y <= 0) return [];
|
||||
// Deel van de breedte waar de daklijn boven y ligt.
|
||||
const range = topLine(CONFIG).rangeAbove(y);
|
||||
if (!range) return [];
|
||||
let segments = [range];
|
||||
sheathingHoles(CONFIG).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
|
||||
sheathingHoles(CONFIG).concat(extraHoles || []).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
|
||||
segments = segments.flatMap(([a, b]) => {
|
||||
if (h.hx1 <= a || h.hx0 >= b) return [[a, b]];
|
||||
const out = [];
|
||||
@@ -513,7 +560,9 @@ window.WallModel = (() => {
|
||||
// hoger begint (opbouw, blootliggende naadwand) volgt zo hetzelfde raster.
|
||||
// skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het
|
||||
// blootliggende deel van een naadwand boven het dak van de andere unit).
|
||||
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) {
|
||||
// extraHoles (optioneel, mm, zoals sheathingHoles): extra uitsparingen
|
||||
// (binnenhoek-doorloop, zie build3D).
|
||||
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom, extraHoles) {
|
||||
const y0 = Math.max(skinStartY(trasraam), skinFrom || 0); // boven het trasraam
|
||||
const ext = cornerExt(skinExt, 2 * BATTEN.thick); // over de hoekstijlen van de doorlopende wand
|
||||
const xL = -CONFIG.wallWidth / 2 - ext.left, xR = CONFIG.wallWidth / 2 + ext.right;
|
||||
@@ -521,7 +570,7 @@ window.WallModel = (() => {
|
||||
const isEnd = facade ? facade.isEnd : true;
|
||||
const halfW = CONFIG.wallWidth / 2;
|
||||
const tl = topLine(CONFIG), topYAt = tl.at;
|
||||
const holes = sheathingHoles(CONFIG);
|
||||
const holes = sheathingHoles(CONFIG).concat(extraHoles || []);
|
||||
const w = BATTEN.width, sp = BATTEN.spacing;
|
||||
const top = (cladding === 'verticaal') ? 'horizontaal' : 'verticaal';
|
||||
|
||||
@@ -533,7 +582,7 @@ window.WallModel = (() => {
|
||||
yStart = g + Math.max(0, Math.ceil((yStart - g) / sp - 1e-9)) * sp;
|
||||
}
|
||||
for (let y = yStart; y < tl.maxY - w / 2; y += sp) {
|
||||
skinSegmentsAtY(CONFIG, y).forEach(([x0, x1]) => {
|
||||
skinSegmentsAtY(CONFIG, y, extraHoles).forEach(([x0, x1]) => {
|
||||
if (x0 <= -halfW + 0.5) x0 = xL;
|
||||
if (x1 >= halfW - 0.5) x1 = xR;
|
||||
if (x1 - x0 > w) horizontaal.push({ x0, x1, y });
|
||||
@@ -627,6 +676,7 @@ window.WallModel = (() => {
|
||||
? new THREE.MeshStandardMaterial({ color: 0xffffff, map: doekMap, roughness: 0.7, side: THREE.DoubleSide })
|
||||
: new THREE.MeshStandardMaterial({ color: DOEK_COLOR, roughness: 0.7, side: THREE.DoubleSide }),
|
||||
cementplaatMaterial: new THREE.MeshStandardMaterial({ color: 0x808285, roughness: 0.8, side: THREE.DoubleSide }),
|
||||
isolatieMaterial: new THREE.MeshStandardMaterial({ color: 0xf4f4f2, roughness: 0.95 }),
|
||||
battenMaterial: new THREE.MeshStandardMaterial({ color: 0xd9b77e, roughness: 0.7 })
|
||||
};
|
||||
}
|
||||
@@ -644,10 +694,14 @@ window.WallModel = (() => {
|
||||
const spaanplaatVisible = opts.spaanplaatVisible !== false;
|
||||
const buitenVisible = opts.buitenVisible !== false;
|
||||
const cladding = opts.cladding || 'horizontaal';
|
||||
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
|
||||
// Trasraam: bij rabat (vochtdoek) en bij een steengevel (cementplaat + stone).
|
||||
const trasraam = buitenMateriaal === 'vochtdoek' || (buitenMateriaal === 'cementplaat' && isStoneFinish(opts.stone)) ? (opts.trasraam || null) : null;
|
||||
const skinExt = opts.skinExt || null;
|
||||
const facade = opts.facade || null;
|
||||
const skinFrom = opts.skinFrom > 0 ? opts.skinFrom : 0; // buitenafwerking vanaf (mm), zie computeBattens
|
||||
const painted = !!opts.painted; // spaanplaat geschilderd: zonder plaatnaden
|
||||
// stone: steencode (TRASRAAM_STONES) voor steenstrips op een cementplaat-gevel.
|
||||
const stone = buitenMateriaal === 'cementplaat' && isStoneFinish(opts.stone) ? opts.stone : null;
|
||||
// spaanplaatBelow: onder skinFrom aan de buitenzijde spaanplaat (naadwand
|
||||
// geheel in één unit: onder het dak van de andere unit is het een binnenwand).
|
||||
const spaanplaatBelow = !!opts.spaanplaatBelow && skinFrom > 0 && buitenMateriaal === 'vochtdoek';
|
||||
@@ -680,6 +734,8 @@ window.WallModel = (() => {
|
||||
let spaanplaatBuitenVolumeM3 = 0;
|
||||
let battenLengthM = 0, battenVolumeM3 = 0, battenCount = 0; // kruislat (alleen vochtdoek)
|
||||
let trasraamAreaM2 = 0, trasraamVolumeM3 = 0; // cement-gebonden plaat onderaan (alleen vochtdoek)
|
||||
let stoneAreaM2 = 0, stoneIsolatieM3 = 0; // steengevel (cementplaat + stone): strip + isolatieblok
|
||||
let trasraamStoneAreaM2 = 0, trasraamIsolatieM3 = 0; // stenen trasraam: steenstrip (gevelvlak) + isolatieblok
|
||||
let horizBeamCount = 0; // onderregel-segmenten + topbalk + dorpels per opening
|
||||
|
||||
function createExtrudedBeam(shape, depth, positionZ) {
|
||||
@@ -709,6 +765,16 @@ window.WallModel = (() => {
|
||||
const tl = topLine(CONFIG, toM);
|
||||
const startXM = -halfW;
|
||||
const topYAt = tl.at;
|
||||
// Gevel doorgetrokken tot onder de dakrand (opts.skinAbove, mm): de
|
||||
// buitenlagen (doek, latwerk, steen, cementplaat) volgen een bovenrand
|
||||
// die zoveel hoger ligt, met dezelfde helling; regelwerk en binnenzijde
|
||||
// blijven op de wandhoogte.
|
||||
const skinAbove = opts.skinAbove > 0 ? opts.skinAbove : 0;
|
||||
const CONFIG_OUT = skinAbove ? Object.assign({}, CONFIG, {
|
||||
wallHeightLeft: CONFIG.wallHeightLeft + skinAbove, wallHeightRight: CONFIG.wallHeightRight + skinAbove,
|
||||
wallPeak: CONFIG.wallPeak ? { x: CONFIG.wallPeak.x, h: CONFIG.wallPeak.h + skinAbove } : null
|
||||
}) : CONFIG;
|
||||
const tlOut = skinAbove ? topLine(CONFIG_OUT, toM) : tl;
|
||||
// Onderkant bovenregel van xLeft naar xRight (incl. verstek bij de nok).
|
||||
const underPts = tl.underPts;
|
||||
|
||||
@@ -858,6 +924,89 @@ window.WallModel = (() => {
|
||||
const holeExtents = sheathingHoles(CONFIG).map(hole => ({
|
||||
hx0: hole.hx0 * toM, hx1: hole.hx1 * toM, hy0: hole.hy0 * toM, hy1: hole.hy1 * toM
|
||||
}));
|
||||
// Binnenhoek-doorloop (negatieve skinExt): dat stuk zit binnen de wand
|
||||
// en het dak van de buur-unit; daar geen buitenlagen, tot opts.trimTop
|
||||
// ({ left, right }, mm; bovenkant dak van de buur). Daarboven loopt de
|
||||
// gevel gewoon door. Als uitsparing (zoals een opening) in alle
|
||||
// buitenlagen.
|
||||
const trimL = skinExt && skinExt.left < 0 ? -skinExt.left * toM : 0;
|
||||
const trimR = skinExt && skinExt.right < 0 ? -skinExt.right * toM : 0;
|
||||
const trimTop = opts.trimTop || {};
|
||||
const topOf = (v) => (v > 0 ? v : 1e7) * toM;
|
||||
const trimHoles = [];
|
||||
if (trimL > 0) trimHoles.push({ hx0: -halfW - 1, hx1: -halfW + trimL, hy0: -1, hy1: topOf(trimTop.left) });
|
||||
if (trimR > 0) trimHoles.push({ hx0: halfW - trimR, hx1: halfW + 1, hy0: -1, hy1: topOf(trimTop.right) });
|
||||
const outerHoles = holeExtents.concat(trimHoles);
|
||||
const trimHolesMM = trimHoles.map(h => ({ hx0: h.hx0 / toM, hx1: h.hx1 / toM, hy0: h.hy0 / toM, hy1: h.hy1 / toM }));
|
||||
|
||||
// Steenblok (trasraam en steengevel): isolatieblok met een steenstrip
|
||||
// ervoor, samen STONE.THICK_MM dik, vóór het wandvlak (doek resp.
|
||||
// cementplaat). Van hoogte y0 tot yCap (m) of tot de daklijn, met de
|
||||
// openingen uitgespaard. Hoeken: doorloop zoals de andere buitenlagen
|
||||
// (cornerExt); het laatste stuk van die doorloop is de hoek zelf,
|
||||
// waarvan de kopse kant zichtbaar is: dat stuk is over de volle dikte
|
||||
// steen. UV's in meters in gevel-coördinaten, zodat het metselwerk
|
||||
// over wandstukken en naden doorloopt; per vlakrichting gezet (ook de
|
||||
// kopse kanten horizontaal, niet de standaard van ExtrudeGeometry).
|
||||
// Verticaal: anchor = { y (m), down } — op hoogte y ligt een voeg;
|
||||
// de gevel telt vanaf daar omhoog (hele stenen boven de naad), het
|
||||
// trasraam vanaf zijn bovenkant omlaag (hele stenen onder de naad).
|
||||
// Geeft { areaM2, isoM3 }.
|
||||
function stoneBlock(code, y0, yCap, anchor) {
|
||||
// v (m) op hoogte y: omhoog vanaf de onderste voeg van de textuur;
|
||||
// omlaag vanaf een voeg een aantal hele lagen hoger in de textuur,
|
||||
// zodat de strook eronder nog binnen de textuur valt.
|
||||
const jointM = STONE.JOINT_MM * toM, courseM = STONE.COURSE_MM * toM;
|
||||
const vOff = anchor.down
|
||||
? jointM + Math.ceil(Math.max(0, anchor.y - y0) / courseM) * courseM - anchor.y
|
||||
: jointM - anchor.y;
|
||||
const setUV = (geo) => {
|
||||
const pos = geo.attributes.position, nrm = geo.attributes.normal, uv = geo.attributes.uv;
|
||||
for (let k = 0; k < pos.count; k++) {
|
||||
const x = pos.getX(k) + uvShiftM, y = pos.getY(k) + vOff, z = pos.getZ(k);
|
||||
const nx = Math.abs(nrm.getX(k)), ny = Math.abs(nrm.getY(k));
|
||||
if (nx > 0.5) uv.setXY(k, z, y); else if (ny > 0.5) uv.setXY(k, x, z); else uv.setXY(k, x, y);
|
||||
}
|
||||
uv.needsUpdate = true;
|
||||
return geo;
|
||||
};
|
||||
const S = STONE;
|
||||
const zB = (maxDepth / 2) + skinGapM + sThickM + skinGapM;
|
||||
const tIso = (S.THICK_MM - S.STRIP_MM) * toM, tAll = S.THICK_MM * toM;
|
||||
const ext = extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM + S.THICK_MM);
|
||||
const xL = -halfW - ext.left, xR = halfW + ext.right;
|
||||
const capL = ext.left > 0 ? xL + tAll : xL, capR = ext.right > 0 ? xR - tAll : xR;
|
||||
const topClamped = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x)));
|
||||
const rects = outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 }));
|
||||
const shapesIn = (a, b) => {
|
||||
if (b - a <= edgeEps) return [];
|
||||
const capped = yCap === Infinity;
|
||||
const pieces = columnMinusRects(a, b, y0, capped ? topClamped : (x) => Math.min(yCap, topClamped(x)), rects,
|
||||
capped ? { extraXs: tlOut.kinks } : { yCap, extraXs: tlOut.kinks.concat(tlOut.crossings(yCap)) });
|
||||
return (pieces || []).map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
|
||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(q => new THREE.Vector2(q.x, q.y)))));
|
||||
return shape;
|
||||
});
|
||||
};
|
||||
const add = (shapes, depth, z, mat) => {
|
||||
if (!shapes.length) return;
|
||||
const mesh = new THREE.Mesh(setUV(new THREE.ExtrudeGeometry(shapes, { steps: 1, depth, bevelEnabled: false })), mat);
|
||||
mesh.position.set(0, 0, z);
|
||||
mesh.visible = buitenVisible;
|
||||
mesh.castShadow = true;
|
||||
wallGroup3D.add(mesh);
|
||||
};
|
||||
const stoneMat = stoneMaterial(code);
|
||||
const isoMat = opts.materials.isolatieMaterial || cementplaatMaterial;
|
||||
const mid = shapesIn(capL, capR);
|
||||
const caps = shapesIn(xL, capL).concat(shapesIn(capR, xR));
|
||||
add(mid, tIso, zB, isoMat);
|
||||
add(mid, tAll - tIso, zB + tIso, stoneMat);
|
||||
add(caps, tAll, zB, stoneMat);
|
||||
const area = (list) => list.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
||||
return { areaM2: area(mid) + area(caps), isoM3: area(mid) * tIso };
|
||||
}
|
||||
|
||||
// side: 'front' (buitenzijde/zuid) of 'back' (binnenzijde/noord).
|
||||
// range (optioneel, m): { y0, y1, zExtra } = alleen de strook daartussen
|
||||
@@ -867,11 +1016,15 @@ window.WallModel = (() => {
|
||||
// komt er een losse strook zOut verder naar buiten (laag in de
|
||||
// beplatingsruimte, anders zouden de hoekstijlen erdoor steken).
|
||||
function buildVerticalBoardSkin(material, side, visible, range, ext) {
|
||||
const bounds = boardBoundaries.slice();
|
||||
// Geschilderde spaanplaat (opts.painted): één egaal vlak, geen naden.
|
||||
const seamless = painted && material === spaanplaatMaterial;
|
||||
const bounds = seamless ? [boardBoundaries[0], boardBoundaries[boardBoundaries.length - 1]] : boardBoundaries.slice();
|
||||
if (ext && !ext.zOut) { bounds[0] -= ext.left; bounds[bounds.length - 1] += ext.right; }
|
||||
const yB = range ? range.y0 : 0;
|
||||
const yCap = range ? range.y1 : Infinity;
|
||||
const skinTopAt = (x) => Math.min(yCap, topYAt(x));
|
||||
const TL = side === 'front' && !range ? tlOut : tl; // buitenplaat: tot onder de dakrand
|
||||
const HX = side === 'front' ? outerHoles : holeExtents;
|
||||
const skinTopAt = (x) => Math.min(yCap, TL.at(x));
|
||||
let areaM2 = 0, volumeM3 = 0;
|
||||
const z = (side === 'front' ? (maxDepth / 2) + skinGapM : -(maxDepth / 2) - sThickM - skinGapM) + ((range && range.zExtra) || 0);
|
||||
// Duidelijke naad tussen platen: dunne rand rond elke plaat, 50%
|
||||
@@ -894,8 +1047,8 @@ window.WallModel = (() => {
|
||||
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
|
||||
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
|
||||
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
|
||||
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
|
||||
{ yCap: range ? yCap : undefined, extraXs: range ? tl.kinks.concat(tl.crossings(yCap)) : tl.kinks });
|
||||
HX.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
|
||||
{ yCap: range ? yCap : undefined, extraXs: range ? TL.kinks.concat(TL.crossings(yCap)) : TL.kinks });
|
||||
let boardShapes;
|
||||
if (pieces) {
|
||||
boardShapes = pieces.map(piece => {
|
||||
@@ -910,7 +1063,7 @@ window.WallModel = (() => {
|
||||
boardShape.moveTo(ex0, yB);
|
||||
boardShape.lineTo(ex1, yB);
|
||||
boardShape.lineTo(ex1, skinTopAt(ex1));
|
||||
tl.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
|
||||
TL.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
|
||||
boardShape.lineTo(ex0, skinTopAt(ex0));
|
||||
boardShape.closePath();
|
||||
holeExtents.forEach(h => {
|
||||
@@ -941,11 +1094,13 @@ window.WallModel = (() => {
|
||||
board.visible = visible;
|
||||
wallGroup3D.add(board);
|
||||
|
||||
const edgesGeo = new THREE.EdgesGeometry(boardGeo);
|
||||
const edgeLines = new THREE.LineSegments(edgesGeo, edgeMaterial);
|
||||
edgeLines.position.set(0, 0, z);
|
||||
edgeLines.visible = visible;
|
||||
wallGroup3D.add(edgeLines);
|
||||
if (!seamless) {
|
||||
const edgesGeo = new THREE.EdgesGeometry(boardGeo);
|
||||
const edgeLines = new THREE.LineSegments(edgesGeo, edgeMaterial);
|
||||
edgeLines.position.set(0, 0, z);
|
||||
edgeLines.visible = visible;
|
||||
wallGroup3D.add(edgeLines);
|
||||
}
|
||||
|
||||
areaM2 += boardAreaM2;
|
||||
volumeM3 += boardAreaM2 * sThickM;
|
||||
@@ -975,6 +1130,18 @@ window.WallModel = (() => {
|
||||
|
||||
// Binnenzijde (noord): altijd spaanplaat.
|
||||
const spaanplaatResult = buildVerticalBoardSkin(spaanplaatMaterial, 'back', spaanplaatVisible);
|
||||
// Kopse kant van een binnenhoek-doorloop (trimL/trimR): die staat open
|
||||
// naar de kamer van de buur-unit; binnenplaat erop (over regelwerk +
|
||||
// beplating), net buiten het stijlvlak tegen flikkeren.
|
||||
[[trimL, -halfW, -1], [trimR, halfW, 1]].forEach(([trim, x, dir]) => {
|
||||
if (!(trim > 0)) return;
|
||||
const h = Math.min(topYAt(x), topOf(dir < 0 ? trimTop.left : trimTop.right)), depth = maxDepth + 2 * (skinGapM + sThickM);
|
||||
const cap = new THREE.Mesh(new THREE.PlaneGeometry(depth, h), spaanplaatMaterial);
|
||||
cap.rotation.y = dir * Math.PI / 2;
|
||||
cap.position.set(x + dir * 0.0003, h / 2, 0);
|
||||
cap.visible = spaanplaatVisible;
|
||||
wallGroup3D.add(cap);
|
||||
});
|
||||
spaanplaatAreaM2 = spaanplaatResult.areaM2;
|
||||
spaanplaatVolumeM3 = spaanplaatResult.volumeM3;
|
||||
|
||||
@@ -997,6 +1164,31 @@ window.WallModel = (() => {
|
||||
Object.assign(extM(0), { zOut: sThickM + 2 * skinGapM }));
|
||||
cementplaatAreaM2 = cementResult.areaM2;
|
||||
cementplaatVolumeM3 = cementResult.volumeM3;
|
||||
// Steen (opts.stone): steenblok (isolatie + strip) over de hele gevel,
|
||||
// vóór de cement-gebonden plaat; onderaan het trasraam (eigen steen
|
||||
// of cementplaat), de gevelsteen begint erboven.
|
||||
if (stone) {
|
||||
let y0 = 0;
|
||||
if (trasraam && isStoneFinish(trasraam.finish)) {
|
||||
const r = stoneBlock(trasraam.finish, trasraam.bottom * toM, trasraam.top * toM, { y: trasraam.top * toM, down: true });
|
||||
trasraamStoneAreaM2 = r.areaM2;
|
||||
trasraamIsolatieM3 = r.isoM3;
|
||||
y0 = trasraam.top * toM;
|
||||
} else if (trasraam) {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, {
|
||||
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + skinGapM
|
||||
}, extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM));
|
||||
trasraamAreaM2 = tras.areaM2;
|
||||
trasraamVolumeM3 = tras.volumeM3;
|
||||
y0 = trasraam.top * toM;
|
||||
}
|
||||
// Anker op de gevel-hoogte van de naad (facade.gridY0 = bovenkant
|
||||
// trasraam t.o.v. dit stuk), zodat ook een opbouw aansluit.
|
||||
const anchorY = facade && facade.gridY0 !== undefined ? facade.gridY0 * toM : y0;
|
||||
const r = stoneBlock(stone, y0, Infinity, { y: anchorY, down: false });
|
||||
stoneAreaM2 = r.areaM2;
|
||||
stoneIsolatieM3 = r.isoM3;
|
||||
}
|
||||
} else {
|
||||
// F. VOCHTDOEK BUITENZIJDE (zuid) — horizontale banen van 1500mm
|
||||
// (rolbreedte), "oneindig" strekkend in de breedte (dus geen
|
||||
@@ -1011,9 +1203,10 @@ window.WallModel = (() => {
|
||||
// ligt — zoals bij een echte shingle-overlap — i.p.v. exact co-planair
|
||||
// (wat flikkering/z-fighting zou geven nu de platen niet meer
|
||||
// transparant zijn).
|
||||
const doekZStepM = 0.0002; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
||||
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tl.pts.slice().reverse()];
|
||||
const maxRoofY = tl.maxY;
|
||||
const doekZStepM = 0.0002;
|
||||
const DOEK_OUT_M = 0.001; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
||||
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tlOut.pts.slice().reverse()];
|
||||
const maxRoofY = tlOut.maxY;
|
||||
|
||||
let rowY0 = skinFrom * toM;
|
||||
let rowIndex = 0;
|
||||
@@ -1023,11 +1216,11 @@ window.WallModel = (() => {
|
||||
// Baan = strook [rowY0, min(rowY1, daklijn)] min uitsparingen
|
||||
// (columnMinusRects, zelfde als de platen): een opening over de
|
||||
// baanrand wordt een inkeping, geen reepje doek door de opening.
|
||||
const roofAt = topYAt;
|
||||
const [rx0, rx1] = tl.rangeAbove(rowY0) || [0, 0];
|
||||
const extraXs = tl.crossings(rowY1).concat(tl.kinks);
|
||||
const roofAt = tlOut.at;
|
||||
const [rx0, rx1] = tlOut.rangeAbove(rowY0) || [0, 0];
|
||||
const extraXs = tlOut.crossings(rowY1).concat(tlOut.kinks);
|
||||
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
|
||||
holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
|
||||
outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
|
||||
|
||||
let rowShapes;
|
||||
if (pieces) {
|
||||
@@ -1068,7 +1261,10 @@ window.WallModel = (() => {
|
||||
const rowAreaM2 = rowShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
||||
|
||||
const doekRow = new THREE.Mesh(rowGeo, doekMaterial);
|
||||
doekRow.position.set(0, 0, (maxDepth / 2) + skinGapM + rowIndex * doekZStepM);
|
||||
// 1 mm naar buiten: niet in hetzelfde vlak als de dakrand (dak op
|
||||
// de wand), anders flikkert het dak door het doek heen waar de
|
||||
// gevel tot onder de dakrand doorloopt (skinAbove).
|
||||
doekRow.position.set(0, 0, (maxDepth / 2) + skinGapM + rowIndex * doekZStepM + DOEK_OUT_M);
|
||||
doekRow.visible = buitenVisible;
|
||||
wallGroup3D.add(doekRow);
|
||||
|
||||
@@ -1101,7 +1297,11 @@ window.WallModel = (() => {
|
||||
// uitsparingen als de buitenplaten), vóór het doek
|
||||
// (zelfde vlak als de latten). Het doek loopt
|
||||
// er ongewijzigd achter door; het latwerk begint erboven.
|
||||
if (trasraam) {
|
||||
if (trasraam && isStoneFinish(trasraam.finish)) {
|
||||
const r = stoneBlock(trasraam.finish, trasraam.bottom * toM, trasraam.top * toM, { y: trasraam.top * toM, down: true });
|
||||
trasraamStoneAreaM2 = r.areaM2;
|
||||
trasraamIsolatieM3 = r.isoM3;
|
||||
} else if (trasraam) {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, {
|
||||
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + (rowIndex + 1) * doekZStepM + skinGapM
|
||||
}, extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM));
|
||||
@@ -1111,9 +1311,9 @@ window.WallModel = (() => {
|
||||
|
||||
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
|
||||
// het doek, bovenste laag (haaks op de gevelbekleding) daaroverheen.
|
||||
const bat = computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom);
|
||||
const bat = computeBattens(CONFIG_OUT, cladding, trasraam, skinExt, facade, skinFrom, trimHolesMM);
|
||||
const bT = BATTEN.thick * toM, bW = BATTEN.width * toM;
|
||||
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM;
|
||||
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM + DOEK_OUT_M;
|
||||
const addLayer = (orientation, z) => {
|
||||
bat.layers[orientation].forEach(b => {
|
||||
const horiz = orientation === 'horizontaal';
|
||||
@@ -1136,7 +1336,7 @@ window.WallModel = (() => {
|
||||
stats: {
|
||||
woodVolumeM3, spaanplaatVolumeM3, spaanplaatAreaM2, cementplaatVolumeM3, cementplaatAreaM2, doekAreaM2,
|
||||
spaanplaatBuitenAreaM2, spaanplaatBuitenVolumeM3, horizBeamCount,
|
||||
battenLengthM, battenVolumeM3, battenCount, trasraamAreaM2, trasraamVolumeM3
|
||||
battenLengthM, battenVolumeM3, battenCount, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1346,7 +1546,7 @@ window.WallModel = (() => {
|
||||
}
|
||||
|
||||
return {
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
|
||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||
createMaterials, build3D, computeSpecs
|
||||
|
||||
+4
-1
@@ -88,7 +88,10 @@ window.WallPdf = (() => {
|
||||
doc.text(`Bovenste lat ${bat.top}, gevelbekleding ${opts.cladding || 'horizontaal'} (WEO/Novalit)`, tblX[0], my);
|
||||
if (opts.trasraam) {
|
||||
my += 4;
|
||||
doc.text(`Trasraam: cement-gebonden plaat tot ${opts.trasraam.top} mm boven bovenkant vloer`, tblX[0], my);
|
||||
const finish = WallModel.isStoneFinish(opts.trasraam.finish)
|
||||
? `steenstrip ${opts.trasraam.finish} op isolatieblok (${WallModel.STONE.THICK_MM} mm)`
|
||||
: 'cement-gebonden plaat';
|
||||
doc.text(`Trasraam: ${finish} tot ${opts.trasraam.top} mm boven bovenkant vloer`, tblX[0], my);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -253,6 +253,8 @@ function mountWallEditor(container, callbacks) {
|
||||
}
|
||||
// Binnen een woning is de richting van de gevelbekleding (voorlopig) globaal.
|
||||
const lockCladding = callbacks && callbacks.lockCladding;
|
||||
// ... en de buitenzijde (gevelbekleding per woning, override per wand).
|
||||
const lockExterior = callbacks && callbacks.lockExterior;
|
||||
|
||||
// Binnen een woning is de h.o.h. globaal (woning-instellingen), niet per wand.
|
||||
if (callbacks && callbacks.lockSpacing) {
|
||||
@@ -566,7 +568,7 @@ function mountWallEditor(container, callbacks) {
|
||||
function getPureState() {
|
||||
return {
|
||||
wall: collectWallParams(),
|
||||
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam },
|
||||
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone },
|
||||
openings: openings.map(o => ({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset }))
|
||||
};
|
||||
}
|
||||
@@ -591,6 +593,7 @@ function mountWallEditor(container, callbacks) {
|
||||
buitenMateriaal = (state.materials && state.materials.exterior) || 'vochtdoek';
|
||||
cladding = (state.materials && state.materials.cladding) || 'horizontaal';
|
||||
trasraam = (state.materials && state.materials.trasraam) || null; // woning-instelling
|
||||
stone = (state.materials && state.materials.stone) || null; // woning-instelling (steen)
|
||||
syncMaterialButtons();
|
||||
|
||||
openings = state.openings.map(o => ({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset }));
|
||||
@@ -1244,11 +1247,13 @@ function mountWallEditor(container, callbacks) {
|
||||
let buitenMateriaal = 'vochtdoek';
|
||||
let cladding = 'horizontaal'; // richting gevelbekleding (alleen bij vochtdoek)
|
||||
let trasraam = null; // { bottom, top } mm, komt uit de woning-instellingen (WallModel.trasraamFor)
|
||||
let stone = null; // steencode bij cementplaat, komt uit de woning-instellingen
|
||||
function syncMaterialButtons() {
|
||||
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => b.classList.toggle('active', b.dataset.material === buitenMateriaal));
|
||||
document.querySelectorAll('.material-toggle-btn[data-cladding]').forEach(b => b.classList.toggle('active', b.dataset.cladding === cladding));
|
||||
document.getElementById('cladding-section').style.display = buitenMateriaal === 'vochtdoek' ? '' : 'none';
|
||||
if (lockCladding) document.querySelectorAll('.material-toggle-btn[data-cladding]').forEach(b => { b.disabled = true; b.title = lockCladding; });
|
||||
if (lockExterior) document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => { b.disabled = true; b.title = lockExterior; });
|
||||
}
|
||||
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(btn => {
|
||||
btn.addEventListener('click', () => { buitenMateriaal = btn.dataset.material; syncMaterialButtons(); updateAll(); });
|
||||
@@ -1284,7 +1289,7 @@ function mountWallEditor(container, callbacks) {
|
||||
function update3DScene() {
|
||||
if (wallGroup3D) scene.remove(wallGroup3D);
|
||||
const result = WallModel.build3D(CONFIG, calcData, {
|
||||
materials: wallMaterials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible
|
||||
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, spaanplaatVisible, buitenVisible
|
||||
});
|
||||
wallGroup3D = result.group;
|
||||
lastWallStats = result.stats;
|
||||
|
||||
Reference in New Issue
Block a user