v0.4.12: dak volgt wandvorm (plat/lessenaar/zadel), dakrand per hoogte, naadwand midden of geheel in unit A/B
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
001206a108
commit
373c2c2fb7
+126
-51
@@ -303,15 +303,16 @@ window.HouseModel = (() => {
|
||||
// { u0, extStart, isEnd } (zie WallModel.computeBattens): u0 = afstand
|
||||
// langs het wandvlak van het gevelbegin (links, van buitenaf gezien, incl.
|
||||
// hoekdoorloop van het eerste stuk) tot de linkerrand van het stuk.
|
||||
function assignFacades(units) {
|
||||
// pieces: buitenwandstukken + naadwanddelen met een blootliggend buitendeel.
|
||||
function assignFacades(pieces) {
|
||||
const segs = [];
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
||||
pieces.forEach(p => {
|
||||
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]) });
|
||||
})));
|
||||
});
|
||||
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;
|
||||
@@ -333,10 +334,10 @@ 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) {
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow) {
|
||||
if (!build) return { group: null, stats: null, specs: null };
|
||||
const built = WallModel.build3D(config, frame, {
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade,
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow,
|
||||
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
|
||||
});
|
||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
||||
@@ -372,21 +373,33 @@ window.HouseModel = (() => {
|
||||
return Object.assign({}, wl, { pieces, dropped: wp.dropped, removed: pieces.length === 0 });
|
||||
});
|
||||
|
||||
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), op de hoogste
|
||||
// wandbovenkant van deze unit.
|
||||
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 => UnitModel.wallTopOf(ws)));
|
||||
const roofW = roofData.calcData.width, roofD = roofData.calcData.depth;
|
||||
const roofCx = rect.cx + (u.roof.offsetX || 0), roofCz = rect.cz + (u.roof.offsetZ || 0);
|
||||
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
|
||||
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape).
|
||||
const shape = RoofModel.computeShape(u.unit, layout);
|
||||
const deckFrame = FloorModel.computeFrame(u.roof.deck.floor);
|
||||
const roofW = deckFrame.width, roofD = deckFrame.depth;
|
||||
const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0;
|
||||
const ur = RoofModel.computeUnitRoof(u.roof, shape, { x0: ox - roofW / 2, x1: ox + roofW / 2, z0: oz - roofD / 2, z1: oz + roofD / 2 }, rims);
|
||||
const roofCx = rect.cx + ox, roofCz = rect.cz + oz;
|
||||
const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel' }[shape.type];
|
||||
const multi = ur.planes.length > 1;
|
||||
ur.planes.forEach((pl, i) => {
|
||||
pl.element = `${u.id}-D${i + 1}`;
|
||||
const deg = Math.round(Math.abs(pl.theta) * 180 / Math.PI * 10) / 10;
|
||||
pl.label = `Unit ${u.id} dak${multi ? ' deel ' + (i + 1) : ''} (${shapeLabel}${deg ? ', ' + String(deg).replace('.', ',') + '°' : ''})`;
|
||||
});
|
||||
const roof = {
|
||||
element: `${u.id}-D1`, data: roofData, specs: roofSpecs,
|
||||
element: `${u.id}-D1`, shape, 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 + wallTop,
|
||||
top: layout.floorTop + wallTop + RoofModel.thickness(roofData.calcData),
|
||||
base: layout.floorTop, // 3D: group-Y = 0 op het loopvlak
|
||||
// Onderkant plafond resp. bovenkant dakleer op (x, z) (woning-mm, absoluut).
|
||||
ceilingAt: (x, z) => layout.floorTop + ur.undersideAt(x - rect.cx, z - rect.cz),
|
||||
topAt: (x, z) => layout.floorTop + ur.topAt(x - rect.cx, z - rect.cz),
|
||||
ridges: ur.ridges.map(r => ({ axis: r.axis, value: r.value + (r.axis === 'x' ? rect.cx : rect.cz) })),
|
||||
top: layout.floorTop + ur.maxTop,
|
||||
// Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM)
|
||||
// horen bij "Beplating".
|
||||
group: build ? RoofModel.build3D(roofData.calcData, roofData.resolvedOpenings, {
|
||||
group: build ? RoofModel.buildUnit3D(ur, {
|
||||
materials: o.roofMaterials,
|
||||
osbVisible: o.beplatingVisible !== false,
|
||||
dakleerVisible: o.beplatingVisible !== false
|
||||
@@ -399,12 +412,6 @@ window.HouseModel = (() => {
|
||||
};
|
||||
});
|
||||
|
||||
// Doek en latwerk volgen de hele gevel (niet per unit/wandstuk).
|
||||
assignFacades(units);
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
||||
Object.assign(p, buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade));
|
||||
})));
|
||||
|
||||
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
|
||||
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
|
||||
const interior = [];
|
||||
@@ -418,23 +425,36 @@ window.HouseModel = (() => {
|
||||
const config = WallModel.configFromState(iw.wall);
|
||||
const frame = WallModel.computeFrame(config);
|
||||
const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat';
|
||||
interior.push(Object.assign({
|
||||
interior.push({
|
||||
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)));
|
||||
});
|
||||
});
|
||||
|
||||
// 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).
|
||||
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit. Een deel loopt van wand tot wand: waar een buitenwandstuk de hartlijn
|
||||
// van het deel kruist, stopt het (voetafdrukken, zoals bij de
|
||||
// gevellampen). De hoogte volgt de daklijn van de buitenwand van de unit
|
||||
// aan de naadzijde (RoofModel.wallProfile), zodat het deel onder het dak
|
||||
// van zijn eigen unit past. Steekt een deel boven het dak van de andere
|
||||
// unit uit (bijv. hoge unit tegen een lage), dan krijgt dat stuk van de
|
||||
// naadzijde buitenafwerking (vochtdoek + latwerk, skinFrom) die met de
|
||||
// gevel van de woning meeloopt; het regelwerk blijft dat van de naadwand.
|
||||
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;
|
||||
// Buitenwand van de unit aan de naadzijde (side +1: unit a, lage kant).
|
||||
const seamSide = (side) => horiz ? (side > 0 ? 'achter' : 'voor') : (side > 0 ? 'rechts' : 'links');
|
||||
const unitState = (uid) => state.units.find(u => u.id === uid).unit;
|
||||
const baseConfig = WallModel.configFromState(iw.wall);
|
||||
const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth);
|
||||
const T1 = WallModel.thicknessOf(baseConfig, 'geen');
|
||||
// Plaats (iw.seam.in): leeg = midden op de naad (twee delen), 'a'/'b' =
|
||||
// geheel in die unit: één binnenwand (beplating aan beide zijden) tegen
|
||||
// de naad aan, met de hoogte van de daklijn van die unit.
|
||||
const inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
|
||||
const T2 = WallModel.thicknessOf(baseConfig, 'spaanplaat');
|
||||
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;
|
||||
@@ -444,7 +464,9 @@ window.HouseModel = (() => {
|
||||
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;
|
||||
if (inUnit && uid !== inUnit) return;
|
||||
const T = inUnit ? T2 : T1;
|
||||
const line = seamPerp - side * T / 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));
|
||||
@@ -461,18 +483,49 @@ window.HouseModel = (() => {
|
||||
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,
|
||||
cx: horiz ? mid : line, cz: horiz ? line : mid, rotY, width, thickness: T,
|
||||
normal: { x: Math.round(Math.sin(rotY)), y: Math.round(Math.cos(rotY)) }, // naadzijde
|
||||
floorTop: units.find(u => u.id === uid).layout.floorTop
|
||||
};
|
||||
leaves.push({ uid, side, place });
|
||||
});
|
||||
if (!leaves.length) return [];
|
||||
|
||||
// Hoogtes van een deel: daklijn van de buitenwand aan de naadzijde, op
|
||||
// de uiteinden van het deel (incl. nok als die erin valt).
|
||||
const heightsOf = (lf) => {
|
||||
const u = units.find(x => x.id === lf.uid);
|
||||
const prof = RoofModel.wallProfile(unitState(lf.uid), u.layout, seamSide(lf.side));
|
||||
const axisOf = (q) => horiz ? q.x - u.rect.cx : q.z - u.rect.cz;
|
||||
const W = lf.place.width;
|
||||
const pL = axisOf(UnitModel.localToWorld(lf.place, -W / 2, 0)), pR = axisOf(UnitModel.localToWorld(lf.place, W / 2, 0));
|
||||
const peak = prof.length > 2 ? prof[1] : null;
|
||||
const hasPeak = !!peak && peak.p > Math.min(pL, pR) + 1 && peak.p < Math.max(pL, pR) - 1;
|
||||
return {
|
||||
heightLeft: Math.round(RoofModel.profileHeight(prof, pL)), heightRight: Math.round(RoofModel.profileHeight(prof, pR)),
|
||||
heightMid: hasPeak ? Math.round(peak.h) : null, peakX: hasPeak ? Math.abs(peak.p - pL) : undefined
|
||||
};
|
||||
};
|
||||
const paramsOf = (lf) => Object.assign({}, iw.wall.wall, { width: lf.place.width }, heightsOf(lf));
|
||||
// Laagste bovenkant dak van de andere unit langs dit deel (mm vanaf het
|
||||
// loopvlak van het deel), of null als het deel er niet bovenuit steekt.
|
||||
const exposedFrom = (lf, config) => {
|
||||
const other = units.find(u => u.id === (lf.uid === sm.a ? sm.b : sm.a));
|
||||
const W = lf.place.width;
|
||||
const ends = [-W / 2, W / 2].map(lx => along(UnitModel.localToWorld(lf.place, lx, 0)));
|
||||
const lo = Math.min(...ends), hi = Math.max(...ends);
|
||||
const ts = ends.concat(other.roof.ridges.filter(r => r.axis === (horiz ? 'x' : 'z') && r.value > lo && r.value < hi).map(r => r.value));
|
||||
const from = Math.min(...ts.map(t => horiz ? other.roof.topAt(t, seamPerp) : other.roof.topAt(seamPerp, t))) - lf.place.floorTop;
|
||||
return from < WallModel.topLine(config).maxY - 50 ? Math.max(0, Math.round(from)) : null;
|
||||
};
|
||||
|
||||
// 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 firstParams = paramsOf(first);
|
||||
// Voor de wand-editor: breedte en daklijn van het eerste deel.
|
||||
Object.assign(iw.wall.wall, { width: first.place.width, heightLeft: firstParams.heightLeft, heightRight: firstParams.heightRight, heightMid: firstParams.heightMid });
|
||||
const firstConfig = WallModel.buildConfig(firstParams, 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;
|
||||
@@ -486,32 +539,52 @@ window.HouseModel = (() => {
|
||||
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);
|
||||
config = WallModel.buildConfig(paramsOf(lf), 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({
|
||||
const skinFrom = exposedFrom(lf, config);
|
||||
return {
|
||||
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);
|
||||
config, frame, place: lf.place,
|
||||
// Het regelwerk staat gecentreerd op lokaal z = 0, de beplating
|
||||
// alleen aan de achterzijde: bij het bouwen schuiven zodat het
|
||||
// midden van de totale dikte op place.cx/cz ligt (regelwerk op de naad).
|
||||
// Geheel in één unit: beplating aan beide zijden, dus symmetrisch.
|
||||
shiftZ: inUnit ? 0 : (T1 - maxD) / 2,
|
||||
...(skinFrom === null
|
||||
? { buitenMateriaal: inUnit ? 'spaanplaat' : 'geen' }
|
||||
: { buitenMateriaal: 'vochtdoek', skinFrom, spaanplaatBelow: !!inUnit, cladding: state.settings.cladding || 'horizontaal', trasraam: null })
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Doek en latwerk volgen de hele gevel (niet per unit/wandstuk), ook
|
||||
// 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))));
|
||||
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);
|
||||
if (built.group && p.shiftZ) {
|
||||
const outer = new THREE.Group();
|
||||
built.group.position.z = p.shiftZ * 0.001;
|
||||
outer.add(built.group);
|
||||
built.group = outer;
|
||||
}
|
||||
Object.assign(p, built);
|
||||
});
|
||||
|
||||
// Dakrand (boeidelen/daktrim) als één doorlopende rand om alle daken;
|
||||
// boeidelen aan/uit per unit.
|
||||
// boeidelen aan/uit per unit. De rand volgt de dakvorm van de unit waar
|
||||
// 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;
|
||||
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 })),
|
||||
Math.max(...units.map(u => u.roof.top))) : null;
|
||||
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;
|
||||
// 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".
|
||||
@@ -561,9 +634,11 @@ window.HouseModel = (() => {
|
||||
const fc = u.floorData.calcData;
|
||||
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.joistLen));
|
||||
['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width));
|
||||
u.roof.planes.forEach(pl => {
|
||||
const rc = pl.data.calcData;
|
||||
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); }));
|
||||
|
||||
Reference in New Issue
Block a user