v0.4.16: PDF-aanzichten (vlakke render) met legenda afwerking en afmetingen, zaaglijst/zaagmachine eigen blad, infopaneel afmetingen, lopen (Shift+T) met camera-menu, zachter licht, naadwanden volgen koppeling, boeideel over de gevel (gevel tot bovenkant dak), thumbnails riet/vloer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
12c2579471
commit
23bfe153a2
+133
-19
@@ -296,6 +296,21 @@ window.HouseModel = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
// Naadwanden van één unit bijwerken na een gewijzigde koppeling: naadwanden
|
||||
// van die unit waarvan de naad niet meer bestaat vervallen, ontbrekende
|
||||
// naden krijgen er een.
|
||||
function syncSeamWalls(state, unitId) {
|
||||
const { seams } = computeSeams(unitRects(state));
|
||||
state.interiorWalls = state.interiorWalls.filter(iw => !iw.seam || (iw.seam.a !== unitId && iw.seam.b !== unitId) ||
|
||||
seams.some(sm => sameSeam(iw.seam, sm.a, sm.b)));
|
||||
ensureSeamWalls(state, unitId);
|
||||
}
|
||||
// Naden van een unit zonder naadwand (bijv. verwijderd): [{ a, b }].
|
||||
function missingSeamWalls(state, unitId) {
|
||||
const { seams } = computeSeams(unitRects(state));
|
||||
return seams.filter(sm => (sm.a === unitId || sm.b === unitId) && !state.interiorWalls.some(iw => iw.seam && sameSeam(iw.seam, sm.a, sm.b)));
|
||||
}
|
||||
|
||||
function interiorLayout(iw) {
|
||||
const dx = iw.x2 - iw.x1, dz = iw.z2 - iw.z1;
|
||||
const alongX = Math.abs(dx) >= Math.abs(dz);
|
||||
@@ -465,7 +480,7 @@ window.HouseModel = (() => {
|
||||
pl.label = `Unit ${u.id} dak${multi ? ' deel ' + (i + 1) : ''} (${shapeLabel}${deg ? ', ' + String(deg).replace('.', ',') + '°' : ''})`;
|
||||
});
|
||||
const roof = {
|
||||
element: `${u.id}-D1`, shape, planes: ur.planes, specs: ur.specs,
|
||||
element: `${u.id}-D1`, shape, covering: ur.covering, planes: ur.planes, specs: ur.specs,
|
||||
rect: { x0: roofCx - roofW / 2, x1: roofCx + roofW / 2, z0: roofCz - roofD / 2, z1: roofCz + roofD / 2, cx: roofCx, cz: roofCz, W: roofW, D: roofD },
|
||||
base: layout.floorTop, // 3D: group-Y = 0 op het loopvlak
|
||||
// Onderkant plafond resp. bovenkant dakleer op (x, z) (woning-mm, absoluut).
|
||||
@@ -693,20 +708,15 @@ 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;
|
||||
// Gevel tot de bovenkant van het dak: tussen wandbovenkant en bovenkant
|
||||
// dak zit het dakpakket (dek + bedekking, verschilt per dak). De
|
||||
// buitenlagen van de wand lopen daar door (skinAbove, mm). Boeideel en
|
||||
// daktrim komen daar bovenop (computeEdge, facadeDepth); de houtconstructie
|
||||
// en de gevel zijn gelijk met of zonder boeideel.
|
||||
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));
|
||||
p.skinAbove = Math.max(0, Math.round(u.roof.topAt(m.x, m.z) - u.roof.ceilingAt(m.x, m.z)));
|
||||
// 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;
|
||||
@@ -736,11 +746,31 @@ window.HouseModel = (() => {
|
||||
// de ribbe bij hoort; platte daken delen één randhoogte (de hoogste).
|
||||
const flatTops = units.filter(u => u.roof.shape.type === 'plat').map(u => u.roof.top);
|
||||
const flatTop = flatTops.length ? Math.max(...flatTops) : 0;
|
||||
// Dikte van de gevelafwerking langs een dakrand-ribbe (boeideel komt
|
||||
// erbovenop): gevelstuk op die lijn met dezelfde buitenrichting (+), of
|
||||
// een gevel van een ander deel die naar dit dak toe kijkt (-).
|
||||
const facadeDepth = (a, b, nrm) => {
|
||||
const horiz = Math.abs(nrm.z) > 0.5;
|
||||
const line = horiz ? a.z : a.x, s0 = Math.min(horiz ? a.x : a.z, horiz ? b.x : b.z), s1 = Math.max(horiz ? a.x : a.z, horiz ? b.x : b.z);
|
||||
let best = 0, bestOv = 1;
|
||||
extPieces.forEach(p => {
|
||||
const pn = { x: p.place.normal.x, z: p.place.normal.y };
|
||||
const dot = pn.x * nrm.x + pn.z * nrm.z;
|
||||
if (Math.abs(dot) < 0.5) return;
|
||||
const q0 = UnitModel.localToWorld(p.place, -p.place.width / 2, p.place.thickness / 2), q1 = UnitModel.localToWorld(p.place, p.place.width / 2, p.place.thickness / 2);
|
||||
if (Math.abs((horiz ? q0.z : q0.x) - line) > 2) return;
|
||||
const ov = Math.min(s1, Math.max(horiz ? q0.x : q0.z, horiz ? q1.x : q1.z)) - Math.max(s0, Math.min(horiz ? q0.x : q0.z, horiz ? q1.x : q1.z));
|
||||
if (ov <= bestOv) return;
|
||||
bestOv = ov;
|
||||
best = Math.sign(dot) * WallModel.claddingOffset(p.buitenMateriaal, p.stone);
|
||||
});
|
||||
return best;
|
||||
};
|
||||
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
|
||||
units.map(u => ({
|
||||
unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false,
|
||||
topAt: u.roof.shape.type === 'plat' ? () => flatTop : u.roof.topAt, ridges: u.roof.ridges
|
||||
}))) : null;
|
||||
})), facadeDepth) : null;
|
||||
// Buitenverlichting: gevellampen per wandstuk + boeideel-spots (vult roofEdge.spots).
|
||||
const lighting = LightingModel.compute(state.settings.lighting, state.settings.roof, units, state.units, roofEdge);
|
||||
// Zichtbaarheid: boeideel/daktrim volgt "Beplating", spots en lampen volgen "Lampen".
|
||||
@@ -772,11 +802,88 @@ window.HouseModel = (() => {
|
||||
// ------------------------------------------------------------
|
||||
// Totalen over de hele woning (overzicht + laatste PDF-blad).
|
||||
// ------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// Afmetingen (infopaneel 3D + blad 1 PDF). Oppervlakken in m2, maten in mm.
|
||||
// bruto vloer = vloer-rechthoeken (tot buitenkant constructie)
|
||||
// leefbaar = bruto vloer min de voetafdruk van alle wanden
|
||||
// gevel = buitenvlak van alle gevelstukken (incl. doorloop tot
|
||||
// onder de dakrand), bruto en netto (min openingen)
|
||||
// binnenwanden = geplaatst oppervlak aan de kamerzijde (alle wanden)
|
||||
// dak = hellend dakoppervlak (bedekking) en in plattegrond
|
||||
// groundLevel: maaiveld onder het loopvlak (mm), voor de bouwhoogte.
|
||||
// ------------------------------------------------------------
|
||||
function computeDimensions(parts, groundLevel) {
|
||||
const m2 = (mm2) => mm2 / 1e6;
|
||||
const rects = parts.rects;
|
||||
const bx0 = Math.min(...rects.map(r => r.x0)), bx1 = Math.max(...rects.map(r => r.x1));
|
||||
const bz0 = Math.min(...rects.map(r => r.z0)), bz1 = Math.max(...rects.map(r => r.z1));
|
||||
const floorMM2 = rects.reduce((a, r) => a + r.W * r.D, 0);
|
||||
const ext = [], all = allWallPieces(parts);
|
||||
parts.units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => ext.push(p))));
|
||||
// Voetafdruk: gewone wanden (geen opbouw boven een lager dak).
|
||||
const footprintMM2 = all.filter(p => !p.place.baseY).reduce((a, p) => a + p.place.width * p.place.thickness, 0);
|
||||
const wallArea = (p, y0) => {
|
||||
const tl = WallModel.topLine(p.config), W = p.config.wallWidth;
|
||||
const poly = [{ x: -W / 2, y: y0 }, { x: W / 2, y: y0 }, ...tl.pts.slice().reverse()];
|
||||
return Math.max(0, polygonArea(poly)) + (p.skinAbove || 0) * W;
|
||||
};
|
||||
const openingsMM2 = (p) => (p.config.openings || []).reduce((a, op) => a + op.w * op.h, 0);
|
||||
const facadePieces = ext.concat(parts.interior.filter(p => p.skinFrom !== undefined));
|
||||
const gevelBruto = facadePieces.reduce((a, p) => a + wallArea(p, p.skinFrom || 0), 0);
|
||||
const gevelOpen = ext.reduce((a, p) => a + openingsMM2(p), 0);
|
||||
const glas = ext.reduce((a, p) => a + (p.config.openings || []).filter(op => op.type === 'raam').reduce((b, op) => b + op.w * op.h, 0), 0);
|
||||
const binnen = all.reduce((a, p) => a + ((p.stats && p.stats.spaanplaatAreaM2) || 0) + ((p.stats && p.stats.spaanplaatBuitenAreaM2) || 0), 0);
|
||||
const dakM2 = parts.units.reduce((a, u) => a + (u.roof.specs.dakleerM2 || 0) + (u.roof.specs.rietM2 || 0), 0);
|
||||
const dakPlanMM2 = parts.units.reduce((a, u) => a + u.roof.rect.W * u.roof.rect.D, 0);
|
||||
// Inhoud (bruto): per unit gemiddelde hoogte bovenkant dak boven de
|
||||
// onderkant vloer, bemonsterd op een raster.
|
||||
const inhoudMM3 = parts.units.reduce((a, u) => {
|
||||
const r = u.rect, n = 8;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) sum += u.roof.topAt(r.x0 + (i + 0.5) * r.W / n, r.z0 + (j + 0.5) * r.D / n);
|
||||
return a + r.W * r.D * (sum / (n * n));
|
||||
}, 0);
|
||||
const floorTop = Math.max(...parts.units.map(u => u.layout.floorTop));
|
||||
const top = parts.roofEdge ? parts.roofEdge.top : Math.max(...parts.units.map(u => u.roof.top));
|
||||
return {
|
||||
lengthX: bx1 - bx0, lengthZ: bz1 - bz0, height: top - (floorTop - (groundLevel || 0)),
|
||||
brutoM2: m2(floorMM2), leefbaarM2: m2(floorMM2 - footprintMM2),
|
||||
gevelBrutoM2: m2(gevelBruto), gevelOpeningM2: m2(gevelOpen), gevelNettoM2: m2(gevelBruto - gevelOpen), glasM2: m2(glas),
|
||||
binnenwandM2: binnen, dakM2, dakPlanM2: m2(dakPlanMM2), inhoudM3: inhoudMM3 / 1e9
|
||||
};
|
||||
}
|
||||
// Als rijen [label, waarde, eenheid] (UI-kaart en PDF-tabel).
|
||||
function dimensionRows(d, fmt) {
|
||||
return [
|
||||
['Buitenmaten (b × d)', `${Math.round(d.lengthX)} × ${Math.round(d.lengthZ)}`, 'mm'],
|
||||
['Bouwhoogte (vanaf maaiveld)', Math.round(d.height), 'mm'],
|
||||
['Bruto vloeroppervlak (buitenwerks)', fmt(d.brutoM2, 2), 'm²'],
|
||||
['Leefbare ruimte (netto vloer)', fmt(d.leefbaarM2, 2), 'm²'],
|
||||
['Gevel buitenkant (bruto)', fmt(d.gevelBrutoM2, 2), 'm²'],
|
||||
['Openingen in gevel', fmt(d.gevelOpeningM2, 2), 'm²'],
|
||||
['Gevel buitenkant (netto)', fmt(d.gevelNettoM2, 2), 'm²'],
|
||||
['Glas (ramen)', fmt(d.glasM2, 2), 'm²'],
|
||||
['Wanden binnenkant (alle)', fmt(d.binnenwandM2, 2), 'm²'],
|
||||
['Dak (dakoppervlak)', fmt(d.dakM2, 2), 'm²'],
|
||||
['Dak (plattegrond)', fmt(d.dakPlanM2, 2), 'm²'],
|
||||
['Inhoud (bruto)', fmt(d.inhoudM3, 1), 'm³']
|
||||
];
|
||||
}
|
||||
|
||||
// TIJDELIJK (verwijderen voor productie): capaciteit zaagmachine.
|
||||
const SAW_M_PER_H = 2400;
|
||||
|
||||
function computeTotals(parts) {
|
||||
const pieces = allWallPieces(parts);
|
||||
const kinds = new Map();
|
||||
const sawList = new Map();
|
||||
function addPiece(kind, width, depth, length) {
|
||||
// TIJDELIJK (verwijderen voor productie): zaagmachine-overzicht.
|
||||
const saw = { lengthMM: 0, cuts: 0, angles: new Map() };
|
||||
function addPiece(kind, width, depth, length, cutAngles) {
|
||||
const ca = cutAngles || [0, 0];
|
||||
saw.lengthMM += length;
|
||||
saw.cuts += ca.length;
|
||||
ca.forEach(a => saw.angles.set(a, (saw.angles.get(a) || 0) + 1));
|
||||
const k = kinds.get(kind) || { kind, count: 0, lengthMM: 0 };
|
||||
k.count++; k.lengthMM += length;
|
||||
kinds.set(kind, k);
|
||||
@@ -792,12 +899,14 @@ window.HouseModel = (() => {
|
||||
['front', 'back'].filter(k => fc.rims[k]).forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.width));
|
||||
u.roof.planes.forEach(pl => {
|
||||
const rc = pl.data.calcData;
|
||||
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen));
|
||||
// Sporen in een hellend dakvlak: loodrechte snede aan beide koppen.
|
||||
const ang = Math.round(Math.abs(pl.theta || 0) * 180 / Math.PI * 10) / 10;
|
||||
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen, [ang, ang]));
|
||||
['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); }));
|
||||
pieces.forEach(p => WallModel.computeBOM(p.config, p.frame).forEach(b => { wallBom.push(b); addPiece(b.kind, b.width, b.depth, b.length, b.cutAngles); }));
|
||||
const purchase = WallModel.planPurchase(wallBom);
|
||||
|
||||
const sumP = (fn) => pieces.reduce((acc, p) => acc + fn(p), 0);
|
||||
@@ -823,6 +932,11 @@ window.HouseModel = (() => {
|
||||
return {
|
||||
kinds: kindList,
|
||||
sawList: Array.from(sawList.values()).sort((a, b) => b.length - a.length || a.width - b.width),
|
||||
// TIJDELIJK (verwijderen voor productie): zaagmachine, SAW_M_PER_H meter per uur.
|
||||
saw: {
|
||||
meters: saw.lengthMM / 1000, hours: saw.lengthMM / 1000 / SAW_M_PER_H, cuts: saw.cuts,
|
||||
angles: Array.from(saw.angles.entries()).map(([deg, count]) => ({ deg, count })).sort((a, b) => a.deg - b.deg)
|
||||
},
|
||||
beamCount: kindList.reduce((a, k) => a + k.count, 0),
|
||||
purchase,
|
||||
purchaseWasteMM: purchase.reduce((a, s) => a + s.wasteMM, 0),
|
||||
@@ -970,9 +1084,9 @@ window.HouseModel = (() => {
|
||||
}
|
||||
|
||||
return {
|
||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor,
|
||||
ALIGN_LABELS, SIDE_LABELS, DEFAULT_INTERIOR_THICKNESS, FACADE_TYPES, facadeFor, SAW_M_PER_H, // SAW_M_PER_H: tijdelijk
|
||||
clone, createEmptyProject, createDefaultSettings, createUnitEntry, applyGlobalSettings, nextUnitId, createDefaultState, unitRects, computeSeams, overlappingPairs,
|
||||
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls,
|
||||
computeParts, allWallPieces, computeTotals, validateState, fromUnitDesign, normalize
|
||||
interiorWallDefaults, setThickness, thicknessOf, interiorLayout, ensureSeamWalls, syncSeamWalls, missingSeamWalls,
|
||||
computeParts, allWallPieces, computeTotals, computeDimensions, dimensionRows, validateState, fromUnitDesign, normalize
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user