2 Commits
9 changed files with 261 additions and 58 deletions
+11 -6
View File
@@ -59,9 +59,12 @@ assets/ logo, texturen, skybox
- **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op
naden vervallen de buitenwanden en komt een naadwand met deur: midden op
de naad in twee delen (één per unit), of als één binnenwand geheel in unit
A of B. De hoogte volgt per deel de daklijn van zijn unit; steekt een deel
boven het dak van de andere unit uit, dan krijgt dat stuk buitenafwerking
(vochtdoek + latwerk) die met de gevel meeloopt.
A of B. De hoogte volgt per deel de daklijn van zijn unit. Staat een unit
met een hogere daklijn tegen een lager dak, dan krijgt zijn buitenwand aan
de naadzijde een opbouw vanaf de bovenkant van het lage dak (ramen daar
teken je op die buitenwand) en stopt de naadwand op die hoogte. Loopt het
lage dak langs de naad schuin, dan loopt de naadwand door en krijgt het
uitstekende stuk buitenafwerking (vochtdoek + latwerk).
Binnenwanden teken je in de plattegrond.
- **Dak:** per unit opgebouwd als gespiegelde vloer + dakleer, in de vorm
die de wanden aangeven: plat, lessenaar (schuine kopgevels) of zadel
@@ -70,13 +73,15 @@ assets/ logo, texturen, skybox
(boeidelen/daktrim, spots) loopt als één rand door en volgt de helling.
Is een dak langs de naad minstens een boeideelhoogte hoger dan het
aangrenzende, dan krijgt het een eigen rand rondom (boven het lage dak
zonder spots) en loopt de rand van het lage dak door tot de gevel.
zonder spots). De rand van het lage dak loopt dan door tot de gevel van
het hoge, of langs een zijgevel tot de volle overstek (onder de hoge
rand). Boeideel en daktrim steken 80 mm boven het dakleer uit.
- **Gevel:** latwerk en vochtdoek lopen per gevel door over unit-naden;
sandwichpanelen rondom, 600 mm de grond in.
- **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld,
override per wand) en binnenspots in een raster per unit.
- **3D:** weergavefilters, omgeving Ontwerp/Dag/Nacht, Render met lampen als
echte lichtbronnen.
- **3D:** weergavefilters (o.a. Vloerplan: wanden afgesneden op 500 mm),
omgeving Ontwerp/Dag/Nacht, Render met lampen als echte lichtbronnen.
- **Uitvoer:** JSON opslaan/openen en een samenhangende PDF (plattegrond,
vloeren, wandstukken, binnenwanden, totalen).
+1 -1
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
const APP_VERSION = '0.4.12';
const APP_VERSION = '0.4.14';
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
let tabs = [];
+109 -23
View File
@@ -318,8 +318,18 @@ window.HouseModel = (() => {
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;
// 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);
const gridAbs = ref ? ref.piece.place.floorTop + WallModel.skinStartY(ref.piece.trasraam) : undefined;
const floorAbs = ref ? ref.piece.place.floorTop : undefined;
run.segs.forEach(sg => {
sg.piece.facade = { u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5 };
const p = sg.piece;
sg.piece.facade = {
u0: Math.abs(sg.sAt0 - sLeft) + extLeft, 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
};
});
});
}
@@ -343,6 +353,38 @@ window.HouseModel = (() => {
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
}
// Wandstukken van één zijde (UnitModel.wallPieces), met plaats in de
// woning. raised: opbouw-zones (zie raiseSeamWalls). Een opbouwstuk
// (baseY) staat op die hoogte boven het loopvlak.
function wallOf(u, rect, layout, wl, raised) {
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 wp = UnitModel.wallPieces(wl, ws, raised);
const pieces = wp.pieces.map(p => {
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
const base = p.baseY || 0;
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,
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)
});
// Naad-zones met opbouw: in de wand-editor alleen gearceerd tot base.
// Naast de opbouw (haakse wand in de hoek) blijft het over de volle hoogte.
const cutsU = [];
wl.cutsU.forEach(([a, b]) => {
const r = pieces.find(p => p.baseY && p.u0 < b - EPS && p.u1 > a + EPS);
if (!r) { cutsU.push([a, b]); return; }
if (r.u0 > a + EPS) cutsU.push([a, r.u0]);
cutsU.push([Math.max(a, r.u0), Math.min(b, r.u1), r.baseY]);
if (r.u1 < b - EPS) cutsU.push([r.u1, b]);
});
return Object.assign({}, wl, { cutsU, pieces, dropped: wp.dropped, removed: pieces.length === 0 });
}
const units = state.units.map(u => {
const rect = rectById[u.id];
const layout = UnitModel.computeLayout(u.unit, cuts[u.id]);
@@ -355,23 +397,7 @@ window.HouseModel = (() => {
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false
}) : null;
const walls = layout.walls.map(wl => {
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 wp = UnitModel.wallPieces(wl, ws);
const pieces = wp.pieces.map(p => {
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
return Object.assign(p, {
element: `${u.id}-W${wl.nr}${p.suffix}`,
label: `Unit ${u.id} ${wl.label.toLowerCase()}${p.suffix ? ' (' + p.suffix + ')' : ''}`,
buitenMateriaal, cladding, trasraam,
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 }
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
});
return Object.assign({}, wl, { pieces, dropped: wp.dropped, removed: pieces.length === 0 });
});
const walls = layout.walls.map(wl => wallOf(u, rect, layout, wl));
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape).
@@ -412,6 +438,55 @@ window.HouseModel = (() => {
};
});
// Opbouw op een naad: staat een unit met een hogere daklijn tegen een
// unit met een lager dak, dan loopt zijn buitenwand aan de naadzijde door
// vanaf de bovenkant van het lage dak tot zijn eigen daklijn (zoals een
// buitenwand die bij een schuin dak omhoog groeit). Ramen daar teken je
// op die buitenwand. De naadwand eronder stopt op die hoogte (raisedCap).
// Alleen bij een rechte voet: het lage dak ligt langs de hele naad even
// hoog; anders blijft de naadwand doorlopen met buitenafwerking.
const raisedCap = {}; // `${uid}|${andere uid}` -> base (mm vanaf loopvlak uid)
(function raiseSeamWalls() {
const zones = {}; // uid -> side -> [{ u0, u1, base, other }]
seams.forEach(sm => {
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
const seamPerp = horiz ? sm.z0 : sm.x0;
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1;
[[sm.a, sm.b, horiz ? 'achter' : 'rechts'], [sm.b, sm.a, horiz ? 'voor' : 'links']].forEach(([uid, oid, side]) => {
const u = units.find(x => x.id === uid), other = units.find(x => x.id === oid);
const ts = [s0, s1].concat(other.roof.ridges.filter(r => r.axis === (horiz ? 'x' : 'z') && r.value > s0 && r.value < s1).map(r => r.value));
const tops = ts.map(t => horiz ? other.roof.topAt(t, seamPerp) : other.roof.topAt(seamPerp, t));
if (Math.max(...tops) - Math.min(...tops) > 2) return; // schuine voet: terugval
const base = Math.round(tops[0] - u.layout.floorTop);
const wl = u.walls.find(w => w.side === side);
const sd = UnitModel.WALL_SIDES.find(x => x.key === side);
const c0 = sd.axis === 'x' ? u.rect.cx : u.rect.cz;
const start = (sd.axis === 'x' ? wl.cx : wl.cz) - wl.width / 2;
const toU = (t) => sd.dir > 0 ? t - c0 - start : start + wl.width - (t - c0);
const z0 = Math.min(toU(s0), toU(s1)), z1 = Math.max(toU(s0), toU(s1));
wl.cutsU.forEach(([a, b]) => {
let lo = Math.max(a, z0), hi = Math.min(b, z1);
if (lo < EPS) lo = wl.endInset.left;
if (hi > wl.width - EPS) hi = wl.width - wl.endInset.right;
if (hi - lo > EPS) ((zones[uid] = zones[uid] || {})[side] = zones[uid][side] || []).push({ u0: lo, u1: hi, base, other: oid });
});
});
});
Object.keys(zones).forEach(uid => {
const u = units.find(x => x.id === uid);
const su = state.units.find(x => x.id === uid);
Object.keys(zones[uid]).forEach(side => {
const i = u.walls.findIndex(w => w.side === side);
const wl = u.layout.walls.find(w => w.side === side);
const next = wallOf(su, u.rect, u.layout, wl, zones[uid][side]);
u.walls[i] = next;
zones[uid][side].forEach(z => {
if (next.pieces.some(p => p.baseY && Math.abs(p.u0 - z.u0) < EPS)) raisedCap[`${uid}|${z.other}`] = z.base;
});
});
});
})();
// 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 = [];
@@ -455,8 +530,9 @@ window.HouseModel = (() => {
// 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');
// Opbouwstukken (baseY) staan boven de naadwand, niet ernaast.
const extPieces = [];
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => { if (!p.baseY) 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;
@@ -500,10 +576,14 @@ window.HouseModel = (() => {
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;
// Opbouw erboven (raiseSeamWalls): het deel stopt op de voet daarvan.
const cap = raisedCap[`${lf.uid}|${lf.uid === sm.a ? sm.b : sm.a}`];
const lim = (h) => cap !== undefined ? Math.min(h, cap) : h;
const hL = lim(RoofModel.profileHeight(prof, pL)), hR = lim(RoofModel.profileHeight(prof, pR));
const hasPeak = !!peak && peak.p > Math.min(pL, pR) + 1 && peak.p < Math.max(pL, pR) - 1 && lim(peak.h) > Math.max(hL, hR) + 0.5;
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
heightLeft: Math.round(hL), heightRight: Math.round(hR),
heightMid: hasPeak ? Math.round(lim(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));
@@ -733,7 +813,13 @@ window.HouseModel = (() => {
try { UnitModel.validateState(u.unit || {}); } catch (err) { throw new Error(`unit ${u.id}: ${err.message}`); }
});
(design.interiorWalls || []).forEach((iw, i) => {
['x1', 'z1', 'x2', 'z2'].forEach(f => { if (typeof iw[f] !== 'number') throw new Error(`binnenwand #${i + 1}: ${f} ontbreekt`); });
if (iw.seam) {
// Naadwand: ligt op de naad tussen twee units, geen eigen coördinaten.
if (!ids.has(iw.seam.a) || !ids.has(iw.seam.b)) throw new Error(`naadwand #${i + 1}: unit ${iw.seam.a} of ${iw.seam.b} bestaat niet`);
if (iw.seam.in !== undefined && !['a', 'b'].includes(iw.seam.in)) throw new Error(`naadwand #${i + 1}: ongeldige plaats`);
} else {
['x1', 'z1', 'x2', 'z2'].forEach(f => { if (typeof iw[f] !== 'number') throw new Error(`binnenwand #${i + 1}: ${f} ontbreekt`); });
}
try { WallModel.validateState(iw.wall || {}); } catch (err) { throw new Error(`binnenwand #${i + 1}: ${err.message}`); }
});
}
+24 -4
View File
@@ -322,6 +322,12 @@ function mountHouseEditor(container, callbacks) {
// Opgeslagen camerastand gaat voor; anders eenmalig passend maken.
let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls);
let beplatingVisible = true, lampenVisible = true;
// Vloerplan: alle wanden afgesneden op VLOERPLAN_CUT_MM boven het loopvlak
// (clipping-vlak, doorsnede blijft open); dak en lampen uit.
const VLOERPLAN_CUT_MM = 500;
let vloerplan = false;
const cutPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
renderer.localClippingEnabled = true;
let fullRender = false;
let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene);
@@ -331,11 +337,13 @@ function mountHouseEditor(container, callbacks) {
createViewFilter(document.getElementById('ho-toolbar-3d'), [
{ key: 'beplating', label: 'Beplating' },
{ key: 'dak', label: 'Dak' },
{ key: 'lampen', label: 'Lampen' }
{ key: 'lampen', label: 'Lampen' },
{ key: 'vloerplan', label: 'Vloerplan', checked: false }
], (v) => {
beplatingVisible = v.beplating;
dakVisible = v.dak;
lampenVisible = v.lampen;
vloerplan = v.vloerplan;
refresh({ keepProps: true });
});
@@ -354,7 +362,7 @@ function mountHouseEditor(container, callbacks) {
if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; }
// De nep-lichtbundels (kegels) alleen zonder echte lampen.
if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !lampsOn; });
if (lampsOn && parts && lampenVisible) {
if (lampsOn && parts && lampenVisible && !vloerplan) {
const edge = dakVisible ? parts.roofEdge : null;
lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0, parts.lighting.indoor, {
outdoorFactor: skyMode === 'night' ? 1 : LightingModel.DAY_OUTDOOR_FACTOR
@@ -919,12 +927,24 @@ function mountHouseEditor(container, callbacks) {
u.floorGroup.position.set(u.rect.cx * toM, 0, u.rect.cz * toM);
houseGroup.add(u.floorGroup);
});
// Vloerplan: snijvlak op het (hoogste) loopvlak + VLOERPLAN_CUT_MM. De
// wandmaterialen worden gedeeld, dus altijd zetten (ook weer uit).
cutPlane.constant = (Math.max(...parts.units.map(u => u.layout.floorTop)) + VLOERPLAN_CUT_MM) * toM;
const clip = vloerplan ? [cutPlane] : [];
HouseModel.allWallPieces(parts).forEach(p => {
p.group.rotation.y = p.place.rotY;
p.group.position.set(p.place.cx * toM, p.place.floorTop * toM, p.place.cz * toM);
p.group.traverse(o => {
if (!o.material) return;
(Array.isArray(o.material) ? o.material : [o.material]).forEach(m => {
if (m.clippingPlanes && m.clippingPlanes.length === clip.length) return;
m.clippingPlanes = clip;
m.needsUpdate = true;
});
});
houseGroup.add(p.group);
});
if (dakVisible) {
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);
houseGroup.add(u.roof.group);
@@ -932,7 +952,7 @@ function mountHouseEditor(container, callbacks) {
if (parts.roofEdgeGroup) houseGroup.add(parts.roofEdgeGroup);
}
if (parts.sandwichGroup) houseGroup.add(parts.sandwichGroup);
if (parts.lightingGroup) houseGroup.add(parts.lightingGroup);
if (parts.lightingGroup && !vloerplan) houseGroup.add(parts.lightingGroup);
scene.add(houseGroup);
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bx1 = Math.max(...parts.rects.map(r => r.x1));
+1
View File
@@ -119,6 +119,7 @@ window.LightingModel = (() => {
const segs = [];
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
p.lamps = [];
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);
+53 -16
View File
@@ -28,7 +28,9 @@
// dakrand = standaard de buitenkant gevel), standaard 300 mm hoog.
// Spots in de onderplaat staan op 40% van de overstek (spotLine); welke
// spots er komen bepaalt LightingModel.
// - zonder boeidelen: een daktrim van 80 mm hoog.
// - zonder boeidelen: een daktrim van 80 mm hoog (onder het dakleer).
// De bovenkant van boeideel en daktrim ligt EDGE_ABOVE_MM boven het dakleer
// (opstand rondom), dus het dakleer ligt altijd lager dan de rand.
//
// Plain script, geladen na floor-model.js en core/geometry.js.
// ============================================================
@@ -37,7 +39,7 @@ window.RoofModel = (() => {
const DAKLEER_MM = 2; // visuele dikte van het dakleer
const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2
const TRIM_THICK_MM = 25; // daktrim-profiel, uitstekend buiten de dakrand
const TRIM_ABOVE_MM = 30; // daktrim steekt zoveel boven het dakleer uit
const EDGE_ABOVE_MM = 80; // bovenkant boeideel/daktrim ligt zoveel boven het dakleer
const SPOT_RATIO = 0.4; // spots op 40% van de overstek, gemeten vanaf de gevel
const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
@@ -245,17 +247,33 @@ window.RoofModel = (() => {
}
// 3D van het dak van één unit (meters, gecentreerd op de dakrechthoek,
// Y = 0 op het loopvlak): per dakvlak build3D, gekanteld om de onderkant
// van het plafond zodat die precies op de daklijn van de wanden ligt.
// Y = 0 op het loopvlak): per dakvlak build3D, zo vervormd dat de
// onderkant van het plafond precies op de daklijn van de wanden ligt.
// Geen kanteling maar een afschuiving (lokaal +Z = oplopende as):
// plan = z·cos θ, Y = y/cos θ + z·sin θ
// De onderkant ligt dan op de helling, de dikte haaks op de helling blijft
// gelijk (verticaal = dikte/cos θ, zoals vThick), de lengte langs de
// helling ook (balklengtes), en de kopse kanten staan loodrecht (zoals een
// loodrechte zaagsnede aan sporen). Bij kantelen zouden de kopse kanten
// haaks op de helling staan en ligt de bovenkant aan de hoge kant
// dikte·sin θ terug van de dakrand.
function buildUnit3D(unitRoof, opts) {
const toM = 0.001;
const group = new THREE.Group();
unitRoof.planes.forEach(pl => {
const g = new THREE.Group();
g.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, opts));
g.position.set(pl.cx * toM, pl.y * toM, pl.cz * toM);
// Eerst kantelen om lokaal X (lokaal +Z = oplopende as), dan draaien.
g.rotation.set(-pl.theta, pl.yaw, 0, 'YXZ');
g.rotation.set(0, pl.yaw, 0);
const shear = new THREE.Group();
const c = Math.cos(pl.theta), sn = Math.sin(pl.theta);
shear.matrixAutoUpdate = false;
shear.matrix.set(
1, 0, 0, 0,
0, 1 / c, sn, 0,
0, 0, c, 0,
0, 0, 0, 1);
shear.add(build3D(pl.data.calcData, pl.data.resolvedOpenings, opts));
g.add(shear);
group.add(g);
});
return group;
@@ -357,17 +375,35 @@ window.RoofModel = (() => {
return {
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
topAt: (p) => owner.topAt(p.x, p.z), ridges: owner.ridges || [],
// Bovenkant van de rand (dakleer + opstand).
topAt: (p) => owner.topAt(p.x, p.z) + EDGE_ABOVE_MM, ridges: owner.ridges || [],
overRoof: !!nb && !dropped, dropped
};
});
// Naast een vervallen ribbe loopt de rand recht door tot de gevel.
// Naast een vervallen ribbe (tegen een hoger dak):
// - ligt het verlengde van de rand in het hoge deel, dan loopt hij
// recht door tot die gevel (spot op spotafstand van de gevel);
// - ligt het verlengde ernaast (de rand loopt langs de zijgevel van
// het hoge deel), dan loopt hij recht door tot de overstek, onder
// de rand van het hoge dak (van bovenaf overlappen ze).
const insideOther = (p) => others.some(r => p.x > r.rect.x0 + 0.5 && p.x < r.rect.x1 - 0.5 && p.z > r.rect.z0 + 0.5 && p.z < r.rect.z1 - 0.5);
segs.forEach((sg, i) => {
if (sg.dropped) return;
const prev = segs[(i - 1 + n) % n], next = segs[(i + 1) % n];
const alongZ = Math.abs(sg.a.x - sg.b.x) < 0.5;
const square = (key, v) => { sg[key] = alongZ ? { x: sg[key].x, z: v.z } : { x: v.x, z: sg[key].z }; };
if (!sg.dropped && prev.dropped) square('outerA', sg.a);
if (!sg.dropped && next.dropped) square('outerB', sg.b);
const at = (q, v) => alongZ ? { x: q.x, z: v.z } : { x: v.x, z: q.z };
[['a', 'outerA', 'spotA', 'b', prev], ['b', 'outerB', 'spotB', 'a', next]].forEach(([k, outerK, spotK, otherK, nb]) => {
if (!nb.dropped) return;
const v = sg[k], corner = sg[outerK];
if (insideOther({ x: (v.x + corner.x) / 2, z: (v.z + corner.z) / 2 })) {
sg[outerK] = at(corner, v);
const dir = Math.sign(alongZ ? sg[otherK].z - v.z : sg[otherK].x - v.x);
const inset = dir * s.overhang * SPOT_RATIO;
sg[spotK] = alongZ ? { x: sg[spotK].x, z: v.z + inset } : { x: v.x + inset, z: sg[spotK].z };
} else {
sg[k] = at(v, corner);
}
});
});
segs.filter(sg => !sg.dropped).forEach(sg => segments.push(sg));
});
@@ -377,7 +413,7 @@ window.RoofModel = (() => {
return {
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
top, trimAbove: TRIM_ABOVE_MM,
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),
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
@@ -431,10 +467,11 @@ window.RoofModel = (() => {
const f = (p) => r.axis === 'x' ? p.x - r.value : p.z - r.value;
polys = polys.flatMap(pl => [clipPoly(pl, f), clipPoly(pl, p => -f(p))]).filter(pl => pl.length >= 3);
});
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight;
const topFn = sg.fascia ? sg.topAt : (p) => sg.topAt(p) + edge.trimAbove;
// Boeideel: totale hoogte fasciaHeight vanaf de bovenkant van de rand;
// daktrim: trimHeight onder het dakleer + de opstand erboven.
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
polys.forEach(pl => {
const mesh = new THREE.Mesh(prismGeometry(pl, topFn, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
mesh.visible = o.edgeVisible !== false;
group.add(mesh);
});
+44 -2
View File
@@ -157,9 +157,18 @@ window.UnitModel = (() => {
const full = s.axis === 'x' ? W / 2 : D / 2;
const ext0 = Math.max(0, Math.round(s0 + full)), ext1 = Math.max(0, Math.round(full - s1));
const skinExt = s.dir > 0 ? { left: ext0, right: ext1 } : { left: ext1, right: ext0 };
// Doorlopende wand die op een hoek vervalt: de haakse wand loopt daar
// tot de vloerrand door en staat dus op deze wandlijn (endInset, u-mm
// vanaf links/rechts). Een opbouw in die naad-zone begint erna.
const through = state.layout.corners === 'zijkanten' ? s.axis === 'z' : s.axis === 'x';
const pL = s.dir > 0 ? s0 : s1, pR = s.dir > 0 ? s1 : s0;
const endInset = {
left: through && cutAt(s.key, pL) ? T[WALL_ENDS[s.key].left] : 0,
right: through && cutAt(s.key, pR) ? T[WALL_ENDS[s.key].right] : 0
};
return {
side: s.key, label: s.label, nr: s.nr, rotY: s.rotY, normal: s.normal, axis: s.axis,
thickness: T[s.key], width, cx, cz, cutsU, pieces, skinExt
thickness: T[s.key], width, cx, cz, cutsU, pieces, skinExt, endInset
};
});
@@ -170,7 +179,13 @@ window.UnitModel = (() => {
// op de volle zijde opgelost (ankers gelden voor de hele zijde) en daarna
// aan het stuk toegewezen waar ze volledig in vallen; openingen die in een
// naad vallen vervallen (dropped).
function wallPieces(wallLayout, wallState) {
// raised (optioneel): [{ u0, u1, base }] = opbouw in een naad-zone: daar
// staat de wand pas vanaf hoogte `base` (mm vanaf het loopvlak, bijv. boven
// het lagere dak van de buur-unit) tot de daklijn. Zo'n stuk heeft
// baseY = base; openingen die er volledig boven base in vallen horen erbij
// (y t.o.v. de onderkant van het stuk). Te lage opbouw vervalt.
const MIN_RAISE_MM = 150;
function wallPieces(wallLayout, wallState, raised) {
const fullConfig = WallModel.configFromState(wallState);
const W = wallLayout.width;
const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight;
@@ -200,6 +215,33 @@ window.UnitModel = (() => {
config, frame: WallModel.computeFrame(config)
};
});
const raisedList = (raised || []).filter(r => r.u1 - r.u0 >= MIN_PIECE_MM);
const ins = wallLayout.endInset || { left: 0, right: 0 };
raisedList.forEach((r, i) => {
const { u0: a, u1: b, base } = r;
const hasPeak = hM !== null && a < pU - 1 && b > pU + 1;
const hA = heightAt(a) - base, hB = heightAt(b) - base;
if (Math.min(hA, hB) < MIN_RAISE_MM) return;
const openings = fullConfig.openings
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS && op.y >= base - EPS)
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y - base, anchor: 'left', offset: op.x - a }; });
const params = Object.assign({}, wallState.wall, {
width: b - a, heightLeft: Math.round(hA), heightRight: Math.round(hB),
heightMid: hasPeak ? hM - base : null, peakX: hasPeak ? pU - a : undefined
});
const config = WallModel.buildConfig(params, openings);
pieces.push({
suffix: 'o' + (raisedList.length > 1 ? i + 1 : ''), // opbouw
u0: a, u1: b, lxCenter: -W / 2 + (a + b) / 2, baseY: Math.round(base),
// Naast de haakse wand in de hoek (endInset) bedekken de buitenlagen
// ook diens kopse kant, die boven het lage dak bloot ligt.
skinExt: {
left: ins.left > 0 && a <= ins.left + EPS ? ins.left : 0,
right: ins.right > 0 && b >= W - ins.right - EPS ? ins.right : 0
},
config, frame: WallModel.computeFrame(config)
});
});
const dropped = fullConfig.openings.filter(op => !used.has(op.id));
return { pieces, dropped, fullConfig };
}
+15 -4
View File
@@ -508,6 +508,9 @@ window.WallModel = (() => {
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
// alleen aan het einde van de gevel.
// facade.gridY0 (optioneel, mm, wand-coördinaten): hoogte waarop het
// horizontale latten-raster van de hele gevel begint. Een wandstuk dat
// 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) {
@@ -524,7 +527,12 @@ window.WallModel = (() => {
// Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat).
const horizontaal = [];
for (let y = y0 + w / 2; y < tl.maxY - w / 2; y += sp) {
let yStart = y0 + w / 2;
if (facade && facade.gridY0 !== undefined) {
const g = facade.gridY0 + w / 2;
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]) => {
if (x0 <= -halfW + 0.5) x0 = xL;
if (x1 >= halfW - 0.5) x1 = xR;
@@ -646,10 +654,13 @@ window.WallModel = (() => {
// Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
// Verticaal: een hoger beginnend stuk (facade.yOff, mm boven het
// loopvlak van de gevel) schuift het watermerk mee omhoog.
const uvShiftYM = facade && facade.yOff ? facade.yOff * 0.001 : 0;
const shiftUV = (geo) => {
if (!uvShiftM) return geo;
if (!uvShiftM && !uvShiftYM) return geo;
const uv = geo.attributes.uv;
for (let k = 0; k < uv.count; k++) uv.setX(k, uv.getX(k) + uvShiftM);
for (let k = 0; k < uv.count; k++) { uv.setX(k, uv.getX(k) + uvShiftM); uv.setY(k, uv.getY(k) + uvShiftYM); }
return geo;
};
@@ -1335,7 +1346,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, 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, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
createMaterials, build3D, computeSpecs
+3 -2
View File
@@ -1028,10 +1028,11 @@ function mountWallEditor(container, callbacks) {
}
// Naad-zones (wand vervalt daar, zie woning) als gearceerde overlay.
// Met upTo (mm): opbouw boven het lagere dak, alleen gearceerd tot daar.
function drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM) {
seamCuts.forEach(([u0, u1]) => {
seamCuts.forEach(([u0, u1, upTo]) => {
const x0 = toCanvasX(-halfWMM + u0), x1 = toCanvasX(-halfWMM + u1);
const yTop = toCanvasY(maxRoofMM), yBot = toCanvasY(0);
const yTop = toCanvasY(upTo > 0 ? Math.min(upTo, maxRoofMM) : maxRoofMM), yBot = toCanvasY(0);
ctxFront.save();
ctxFront.fillStyle = 'rgba(17, 17, 17, 0.8)';
ctxFront.fillRect(x0, yTop, x1 - x0, yBot - yTop);