v0.4.9: naadwanden (2 delen, deur), kopbalken op naden, isolatie niet meer getekend

- HouseModel: naadwand per naad (ensureSeamWalls bij nieuwe woning en unit toevoegen),
  twee delen met beplating alleen aan de kamerzijde, openingen gespiegeld
- WallModel: buitenzijde 'geen', thicknessOf eenzijdig
- FloorModel: kopbalken (rims) voor vloer en dak op naden, ook in PDF en totalen
- isolatie: 3D-lagen en filter verwijderd, berekening blijft

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 13:13:07 +02:00
co-authored by Claude Opus 5.5
parent f163f4deb1
commit 9bd7faeaa6
11 changed files with 271 additions and 124 deletions
+152 -19
View File
@@ -92,6 +92,7 @@ window.HouseModel = (() => {
{ id: 3, type: 'raam', w: 500, h: 1900, y: 250, anchor: 'right', offset: 450 }
];
}
ensureSeamWalls(state);
computeParts(state, { skip3D: true });
return state;
}
@@ -208,15 +209,55 @@ window.HouseModel = (() => {
return wall;
}
// Dikte = regelwerk + 2x beplating (WallModel.thicknessOf); de diepte van
// stijlen en regels wordt daarop afgestemd.
// Dikte = regelwerk + beplating (2x, of 1x bij een naadwand-deel met
// buitenzijde 'geen'); de diepte van stijlen en regels wordt daarop afgestemd.
const skinSides = (wallState) => ((wallState.materials || {}).exterior === 'geen' ? 1 : 2);
function setThickness(wallState, thickness) {
const depth = Math.max(20, Math.round(thickness - 2 * wallState.wall.sheathingThick));
const depth = Math.max(20, Math.round(thickness - skinSides(wallState) * wallState.wall.sheathingThick));
wallState.wall.studDepth = depth;
wallState.wall.plateDepth = depth;
}
function thicknessOf(wallState) {
return Math.max(wallState.wall.studDepth, wallState.wall.plateDepth) + 2 * wallState.wall.sheathingThick;
return Math.max(wallState.wall.studDepth, wallState.wall.plateDepth) + skinSides(wallState) * wallState.wall.sheathingThick;
}
// ------------------------------------------------------------
// Naadwanden: op elke naad tussen twee units staat standaard een tussenwand
// met in het midden een deur. Hij bestaat uit twee delen, één per unit
// (de modules worden los vervoerd), die met hun regelwerk tegen elkaar aan
// staan: balken en isolatie in beide delen, beplating alleen aan de
// kamerzijde (buitenMateriaal 'geen' aan de naadzijde). Eén wand-state
// (hoogte, openingen) voor beide delen; de openingen gelden voor het deel
// in unit `seam.a` en worden in het andere deel gespiegeld, zodat de deur
// recht door beide delen gaat. De gebruiker kan hem aanpassen of
// verwijderen; alleen bij een nieuwe woning en bij "unit toevoegen" worden
// ontbrekende naadwanden aangemaakt (ensureSeamWalls).
// state.interiorWalls: { id, seam: { a, b }, wall }
// ------------------------------------------------------------
const DEFAULT_SEAM_LEAF_MM = 100; // dikte per deel (regelwerk + 1x beplating)
function seamWallDefaults(height) {
const wall = WallEditor.getDefaultState();
wall.wall.heightAll = wall.wall.heightLeft = wall.wall.heightRight = height || wall.wall.heightAll;
wall.materials = { interior: 'spaanplaat', exterior: 'geen' };
const d = WallModel.OPENING_TYPES.deur;
wall.openings = [{ id: 1, type: 'deur', w: d.w, h: d.h, y: d.y, anchor: 'middle', offset: 0 }];
setThickness(wall, DEFAULT_SEAM_LEAF_MM);
return wall;
}
const sameSeam = (s, a, b) => (s.a === a && s.b === b) || (s.a === b && s.b === a);
// Maakt ontbrekende naadwanden aan (alleen naden met onlyUnitId, indien gegeven).
function ensureSeamWalls(state, onlyUnitId) {
const { seams } = computeSeams(unitRects(state));
seams.forEach(sm => {
if (onlyUnitId && sm.a !== onlyUnitId && sm.b !== onlyUnitId) return;
if (state.interiorWalls.some(iw => iw.seam && sameSeam(iw.seam, sm.a, sm.b))) return;
const id = state.interiorWalls.reduce((m, w) => Math.max(m, w.id), 0) + 1;
const height = Math.min(...[sm.a, sm.b].map(uid => {
const w = state.units.find(u => u.id === uid).unit.walls.voor.wall;
return Math.min(w.heightLeft, w.heightRight);
}));
state.interiorWalls.push({ id, seam: { a: sm.a, b: sm.b }, wall: seamWallDefaults(height) });
});
}
function interiorLayout(iw) {
@@ -233,7 +274,7 @@ window.HouseModel = (() => {
// ------------------------------------------------------------
// Alle onderdelen doorgerekend (+ 3D, tenzij opts.skip3D).
// opts: { wallMaterials, floorMaterials, beplatingVisible, isolatieVisible, skip3D }
// opts: { wallMaterials, floorMaterials, beplatingVisible, skip3D }
// ------------------------------------------------------------
// Past de woning-brede instellingen toe op de onderdeel-states (muteert
// state): h.o.h. van alle wanden, en de dakmaat als die de vloer volgt.
@@ -296,8 +337,7 @@ window.HouseModel = (() => {
if (!build) return { group: null, stats: null, specs: null };
const built = WallModel.build3D(config, frame, {
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade,
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false,
isolatieVisible: o.isolatieVisible !== false
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
});
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
}
@@ -305,10 +345,13 @@ window.HouseModel = (() => {
const units = state.units.map(u => {
const rect = rectById[u.id];
const layout = UnitModel.computeLayout(u.unit, cuts[u.id]);
const floorData = FloorModel.computeFromState(u.unit.floor);
// Kopbalk op een naad langs voor-/achterrand (haaks op de balken): daar
// worden de modules weer aan elkaar gemonteerd. Zelfde voor het dak.
const rims = { front: cuts[u.id].voor.length > 0, back: cuts[u.id].achter.length > 0 };
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, isolatieVisible: o.isolatieVisible !== false
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false
}) : null;
const walls = layout.walls.map(wl => {
@@ -331,7 +374,7 @@ window.HouseModel = (() => {
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), op de hoogste
// wandbovenkant van deze unit.
const roofData = FloorModel.computeFromState(u.roof.deck);
const roofData = FloorModel.computeFromState(u.roof.deck, rims);
const roofSpecs = RoofModel.computeSpecs(roofData.calcData, roofData.resolvedOpenings);
const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => Math.max(ws.wall.heightLeft, ws.wall.heightRight)));
const roofW = roofData.calcData.width, roofD = roofData.calcData.depth;
@@ -342,9 +385,9 @@ window.HouseModel = (() => {
base: layout.floorTop + wallTop,
top: layout.floorTop + wallTop + RoofModel.thickness(roofData.calcData),
// Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM)
// horen bij "Beplating", de dakisolatie bij "Isolatie".
// horen bij "Beplating".
group: build ? RoofModel.build3D(roofData.calcData, roofData.resolvedOpenings, {
materials: o.roofMaterials, isolatieVisible: o.isolatieVisible !== false,
materials: o.roofMaterials,
osbVisible: o.beplatingVisible !== false,
dakleerVisible: o.beplatingVisible !== false
}) : null
@@ -363,19 +406,107 @@ window.HouseModel = (() => {
})));
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
const interior = state.interiorWalls.map((iw, i) => {
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
const interior = [];
let bwNr = 0, nwNr = 0;
state.interiorWalls.forEach(iw => {
if (iw.seam) { nwNr++; seamLeaves(iw, nwNr).forEach(l => interior.push(l)); return; }
bwNr++;
const lay = interiorLayout(iw);
const host = units.find(u => lay.cx >= u.rect.x0 && lay.cx <= u.rect.x1 && lay.cz >= u.rect.z0 && lay.cz <= u.rect.z1);
const floorTop = host ? host.layout.floorTop : Math.max(...units.map(u => u.layout.floorTop));
const config = WallModel.configFromState(iw.wall);
const frame = WallModel.computeFrame(config);
const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat';
return Object.assign({
id: iw.id, element: `BW-${i + 1}`, label: `Binnenwand ${i + 1}`, config, frame, buitenMateriaal,
interior.push(Object.assign({
id: iw.id, element: `BW-${bwNr}`, label: `Binnenwand ${bwNr}`, config, frame, buitenMateriaal,
place: { cx: lay.cx, cz: lay.cz, rotY: lay.rotY, width: lay.width, thickness: WallModel.thicknessOf(config), floorTop }
}, buildWall(config, frame, buitenMateriaal));
}, buildWall(config, frame, buitenMateriaal)));
});
// Twee delen van een naadwand. Elk deel staat met zijn regelwerk op de
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit, en loopt van
// wand tot wand: waar een buitenwandstuk de hartlijn van het deel kruist,
// stopt het (voetafdrukken, zoals bij de gevellampen).
function seamLeaves(iw, nr) {
const sm = seams.find(x => sameSeam(x, iw.seam.a, iw.seam.b));
if (!sm) return []; // units niet (meer) tegen elkaar: naadwand vervalt
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
const baseConfig = WallModel.configFromState(iw.wall);
const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth);
const T1 = WallModel.thicknessOf(baseConfig, 'geen');
const extPieces = [];
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
const seamPerp = horiz ? sm.z0 : sm.x0;
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1;
const leaves = [];
[[sm.a, 1], [sm.b, -1]].forEach(([uid, side]) => {
// side +1: unit a ligt aan de lage kant, de naad aan zijn +kant.
const line = seamPerp - side * T1 / 2;
const blocked = [];
extPieces.forEach(p => {
const cs = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([i, j]) => UnitModel.localToWorld(p.place, i * p.place.width / 2, j * p.place.thickness / 2));
const pMin = Math.min(...cs.map(perpOf)), pMax = Math.max(...cs.map(perpOf));
if (line > pMin + 0.5 && line < pMax - 0.5) blocked.push([Math.min(...cs.map(along)), Math.max(...cs.map(along))]);
});
let cursor = s0, best = null;
blocked.sort((p, q) => p[0] - q[0]).concat([[s1, s1]]).forEach(([a, b]) => {
const hi = Math.min(a, s1);
if (hi - cursor > (best ? best[1] - best[0] : 0)) best = [cursor, hi];
cursor = Math.max(cursor, b);
});
if (!best || best[1] - best[0] < 300) return;
const width = Math.round(best[1] - best[0]), mid = (best[0] + best[1]) / 2;
const rotY = horiz ? (side > 0 ? 0 : Math.PI) : (side > 0 ? Math.PI / 2 : -Math.PI / 2);
const place = {
cx: horiz ? mid : line, cz: horiz ? line : mid, rotY, width, thickness: T1,
floorTop: units.find(u => u.id === uid).layout.floorTop
};
leaves.push({ uid, side, place });
});
if (!leaves.length) return [];
// Openingen: opgeslagen voor het eerste deel; het andere deel krijgt
// dezelfde openingen op dezelfde plek in de wereld (gespiegeld).
const first = leaves[0];
iw.wall.wall.width = first.place.width; // voor de wand-editor
const firstConfig = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: first.place.width }), iw.wall.openings || []);
const toLocalU = (pl, q) => {
const c = Math.cos(pl.rotY), sn = Math.sin(pl.rotY);
return (q.x - pl.cx) * c - (q.z - pl.cz) * sn + pl.width / 2;
};
return leaves.map((lf, k) => {
let config = firstConfig;
if (k > 0) {
const ops = firstConfig.openings.map(op => {
const wa = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x, 0);
const wb = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x + op.w, 0);
const u = Math.min(toLocalU(lf.place, wa), toLocalU(lf.place, wb));
return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: Math.round(u) };
});
config = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: lf.place.width }), ops);
}
const frame = WallModel.computeFrame(config);
const built = buildWall(config, frame, 'geen');
if (built.group) {
// Het regelwerk staat gecentreerd op lokaal z = 0, de beplating
// alleen aan de achterzijde: schuif zodat het midden van de totale
// dikte op place.cx/cz ligt (regelwerk op de naad).
const outer = new THREE.Group();
built.group.position.z = (T1 - maxD) / 2 * 0.001;
outer.add(built.group);
built.group = outer;
}
return Object.assign({
id: iw.id, seam: iw.seam, seamUnit: lf.uid,
element: `NW-${nr}${lf.uid}`, label: `Naadwand ${nr} (unit ${lf.uid})`,
config, frame, buitenMateriaal: 'geen', place: lf.place
}, built);
});
}
// Dakrand (boeidelen/daktrim) als één doorlopende rand om alle daken;
// boeidelen aan/uit per unit.
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
@@ -428,9 +559,11 @@ window.HouseModel = (() => {
}
parts.units.forEach(u => {
const fc = u.floorData.calcData;
fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.depth));
fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.joistLen));
['front', 'back'].filter(k => fc.rims[k]).forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.width));
const rc = u.roof.data.calcData;
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.depth));
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen));
['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width));
});
const wallBom = [];
pieces.forEach(p => WallModel.computeBOM(p.config, p.frame).forEach(b => { wallBom.push(b); addPiece(b.kind, b.width, b.depth, b.length); }));
@@ -568,7 +701,7 @@ window.HouseModel = (() => {
return {
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS,
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
interiorWallDefaults, setThickness, thicknessOf, interiorLayout,
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls,
computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize
};
})();