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
@@ -25,7 +25,7 @@ js/core/ gedeelde helpers: geometrie, UI-kit, PDF-bladen, labels,
|
|||||||
camerageheugen, weergavefilter, skybox, licentie, logo
|
camerageheugen, weergavefilter, skybox, licentie, logo
|
||||||
js/wall/ WallModel (logica) + wand-PDF + wand-editor
|
js/wall/ WallModel (logica) + wand-PDF + wand-editor
|
||||||
js/floor/ FloorModel (logica) + vloer-PDF + vloer-editor
|
js/floor/ FloorModel (logica) + vloer-PDF + vloer-editor
|
||||||
js/roof/ RoofModel: plat dak + dakrand (boeidelen/daktrim)
|
js/roof/ RoofModel: dak (plat/lessenaar/zadel) + dakrand (boeidelen/daktrim)
|
||||||
js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots
|
js/lighting/ LightingModel: gevellampen, boeideel-spots, binnenspots
|
||||||
js/sandwich/ SandwichModel: sandwichpanelen rondom
|
js/sandwich/ SandwichModel: sandwichpanelen rondom
|
||||||
js/unit/ UnitModel: 1 vloer + 4 wanden, naden, wandstukken
|
js/unit/ UnitModel: 1 vloer + 4 wanden, naden, wandstukken
|
||||||
@@ -47,19 +47,30 @@ assets/ logo, texturen, skybox
|
|||||||
|
|
||||||
## Functionaliteit
|
## Functionaliteit
|
||||||
|
|
||||||
- **Wand:** openingen (raam/deur/voordeur) met ankers, regelwerk 38x235 met
|
- **Wand:** openingen (raam/deur/voordeur) met ankers, ook boven elkaar
|
||||||
dubbele hoekstijlen, zaagplan op handelslengtes, spaanplaat binnen,
|
(raam boven een deur; stijlen ertussen van dorpel tot dorpel), regelwerk
|
||||||
vochtdoek (met kruislat en trasraam) of cementplaat buiten.
|
38x235 met dubbele hoekstijlen en een dubbele stijl naast deuren, zaagplan
|
||||||
|
op handelslengtes, spaanplaat binnen, vochtdoek (met kruislat en trasraam)
|
||||||
|
of cementplaat buiten.
|
||||||
- **Wandhoogte:** schuine bovenrand of middenpunt (nok) met goothoogtes. In
|
- **Wandhoogte:** schuine bovenrand of middenpunt (nok) met goothoogtes. In
|
||||||
een unit bepaalt de laatst gewijzigde wand de dakvorm: de tegenoverliggende
|
een unit bepaalt de laatst gewijzigde wand de dakvorm: de tegenoverliggende
|
||||||
wand spiegelt mee, de haakse wanden worden vlak op de hoekhoogtes.
|
wand spiegelt mee, de haakse wanden worden vlak op de hoekhoogtes.
|
||||||
- **Vloer:** balken, cirkelvormige doorvoeren, OSB, optioneel chassis.
|
- **Vloer:** balken, cirkelvormige doorvoeren, OSB, optioneel chassis.
|
||||||
- **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op
|
- **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op
|
||||||
naden vervallen de buitenwanden en komt een naadwand in twee delen (één per
|
naden vervallen de buitenwanden en komt een naadwand met deur: midden op
|
||||||
unit, met deur). Binnenwanden teken je in de plattegrond.
|
de naad in twee delen (één per unit), of als één binnenwand geheel in unit
|
||||||
- **Dak:** plat dak per unit (vloer gespiegeld + dakleer), dakrand als één
|
A of B. De hoogte volgt per deel de daklijn van zijn unit; steekt een deel
|
||||||
doorlopende rand met boeidelen of daktrim. Daklogica voor schuine
|
boven het dak van de andere unit uit, dan krijgt dat stuk buitenafwerking
|
||||||
wanden/nok volgt nog.
|
(vochtdoek + latwerk) die met de gevel meeloopt.
|
||||||
|
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
|
||||||
|
(nok). Elk dakvlak is een eigen dek met de balken als sporen en een eigen
|
||||||
|
PDF-blad; doorvoeren gaan naar het vlak waarin ze vallen. De dakrand
|
||||||
|
(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.
|
||||||
- **Gevel:** latwerk en vochtdoek lopen per gevel door over unit-naden;
|
- **Gevel:** latwerk en vochtdoek lopen per gevel door over unit-naden;
|
||||||
sandwichpanelen rondom, 600 mm de grond in.
|
sandwichpanelen rondom, 600 mm de grond in.
|
||||||
- **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld,
|
- **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld,
|
||||||
@@ -72,7 +83,8 @@ assets/ logo, texturen, skybox
|
|||||||
## Bekende beperkingen
|
## Bekende beperkingen
|
||||||
|
|
||||||
- Units kunnen niet gedraaid worden.
|
- Units kunnen niet gedraaid worden.
|
||||||
- Binnen- en naadwanden volgen de dakvorm (nog) niet.
|
- Getekende binnenwanden volgen de dakvorm (nog) niet.
|
||||||
|
- De dak-editor toont het dek in de plattegrond (niet per schuin dakvlak).
|
||||||
- Geen schaduw per lamp in de render (browserlimiet).
|
- Geen schaduw per lamp in de render (browserlimiet).
|
||||||
- Eigen OrbitControls: roteren/zoomen/pannen, geen damping.
|
- Eigen OrbitControls: roteren/zoomen/pannen, geen damping.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
const APP_VERSION = '0.4.10';
|
const APP_VERSION = '0.4.12';
|
||||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||||
|
|
||||||
let tabs = [];
|
let tabs = [];
|
||||||
|
|||||||
+126
-51
@@ -303,15 +303,16 @@ window.HouseModel = (() => {
|
|||||||
// { u0, extStart, isEnd } (zie WallModel.computeBattens): u0 = afstand
|
// { u0, extStart, isEnd } (zie WallModel.computeBattens): u0 = afstand
|
||||||
// langs het wandvlak van het gevelbegin (links, van buitenaf gezien, incl.
|
// langs het wandvlak van het gevelbegin (links, van buitenaf gezien, incl.
|
||||||
// hoekdoorloop van het eerste stuk) tot de linkerrand van het stuk.
|
// 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 = [];
|
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 W = p.place.width, T = p.place.thickness;
|
||||||
const normal = { x: p.place.normal.x, z: p.place.normal.y };
|
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 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);
|
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]) });
|
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 => {
|
groupFacadeRuns(segs).forEach(run => {
|
||||||
const dir = Math.sign(run.segs[0].sAtW - run.segs[0].sAt0) || 1;
|
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 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 wallThickness = (id, side) => WallModel.thicknessOf(WallModel.configFromState(state.units.find(u => u.id === id).unit.walls[side]));
|
||||||
const { cuts, seams } = computeSeams(rects, wallThickness);
|
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 };
|
if (!build) return { group: null, stats: null, specs: null };
|
||||||
const built = WallModel.build3D(config, frame, {
|
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
|
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
|
||||||
});
|
});
|
||||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
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 });
|
return Object.assign({}, wl, { pieces, dropped: wp.dropped, removed: pieces.length === 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), op de hoogste
|
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
|
||||||
// wandbovenkant van deze unit.
|
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape).
|
||||||
const roofData = FloorModel.computeFromState(u.roof.deck, rims);
|
const shape = RoofModel.computeShape(u.unit, layout);
|
||||||
const roofSpecs = RoofModel.computeSpecs(roofData.calcData, roofData.resolvedOpenings);
|
const deckFrame = FloorModel.computeFrame(u.roof.deck.floor);
|
||||||
const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => UnitModel.wallTopOf(ws)));
|
const roofW = deckFrame.width, roofD = deckFrame.depth;
|
||||||
const roofW = roofData.calcData.width, roofD = roofData.calcData.depth;
|
const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0;
|
||||||
const roofCx = rect.cx + (u.roof.offsetX || 0), roofCz = rect.cz + (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 = {
|
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 },
|
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,
|
base: layout.floorTop, // 3D: group-Y = 0 op het loopvlak
|
||||||
top: layout.floorTop + wallTop + RoofModel.thickness(roofData.calcData),
|
// 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)
|
// Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM)
|
||||||
// horen bij "Beplating".
|
// horen bij "Beplating".
|
||||||
group: build ? RoofModel.build3D(roofData.calcData, roofData.resolvedOpenings, {
|
group: build ? RoofModel.buildUnit3D(ur, {
|
||||||
materials: o.roofMaterials,
|
materials: o.roofMaterials,
|
||||||
osbVisible: o.beplatingVisible !== false,
|
osbVisible: o.beplatingVisible !== false,
|
||||||
dakleerVisible: 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.
|
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
|
||||||
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
|
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
|
||||||
const interior = [];
|
const interior = [];
|
||||||
@@ -418,23 +425,36 @@ window.HouseModel = (() => {
|
|||||||
const config = WallModel.configFromState(iw.wall);
|
const config = WallModel.configFromState(iw.wall);
|
||||||
const frame = WallModel.computeFrame(config);
|
const frame = WallModel.computeFrame(config);
|
||||||
const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat';
|
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,
|
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 }
|
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
|
// Twee delen van een naadwand. Elk deel staat met zijn regelwerk op de
|
||||||
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit, en loopt van
|
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit. Een deel loopt van wand tot wand: waar een buitenwandstuk de hartlijn
|
||||||
// wand tot wand: waar een buitenwandstuk de hartlijn van het deel kruist,
|
// van het deel kruist, stopt het (voetafdrukken, zoals bij de
|
||||||
// stopt het (voetafdrukken, zoals bij de gevellampen).
|
// 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) {
|
function seamLeaves(iw, nr) {
|
||||||
const sm = seams.find(x => sameSeam(x, iw.seam.a, iw.seam.b));
|
const sm = seams.find(x => sameSeam(x, iw.seam.a, iw.seam.b));
|
||||||
if (!sm) return []; // units niet (meer) tegen elkaar: naadwand vervalt
|
if (!sm) return []; // units niet (meer) tegen elkaar: naadwand vervalt
|
||||||
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
|
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 baseConfig = WallModel.configFromState(iw.wall);
|
||||||
const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth);
|
const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth);
|
||||||
const T1 = WallModel.thicknessOf(baseConfig, 'geen');
|
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 = [];
|
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 => extPieces.push(p))));
|
||||||
const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
|
const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
|
||||||
@@ -444,7 +464,9 @@ window.HouseModel = (() => {
|
|||||||
const leaves = [];
|
const leaves = [];
|
||||||
[[sm.a, 1], [sm.b, -1]].forEach(([uid, side]) => {
|
[[sm.a, 1], [sm.b, -1]].forEach(([uid, side]) => {
|
||||||
// side +1: unit a ligt aan de lage kant, de naad aan zijn +kant.
|
// 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 = [];
|
const blocked = [];
|
||||||
extPieces.forEach(p => {
|
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 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 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 rotY = horiz ? (side > 0 ? 0 : Math.PI) : (side > 0 ? Math.PI / 2 : -Math.PI / 2);
|
||||||
const place = {
|
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
|
floorTop: units.find(u => u.id === uid).layout.floorTop
|
||||||
};
|
};
|
||||||
leaves.push({ uid, side, place });
|
leaves.push({ uid, side, place });
|
||||||
});
|
});
|
||||||
if (!leaves.length) return [];
|
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
|
// Openingen: opgeslagen voor het eerste deel; het andere deel krijgt
|
||||||
// dezelfde openingen op dezelfde plek in de wereld (gespiegeld).
|
// dezelfde openingen op dezelfde plek in de wereld (gespiegeld).
|
||||||
const first = leaves[0];
|
const first = leaves[0];
|
||||||
iw.wall.wall.width = first.place.width; // voor de wand-editor
|
const firstParams = paramsOf(first);
|
||||||
const firstConfig = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: first.place.width }), iw.wall.openings || []);
|
// 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 toLocalU = (pl, q) => {
|
||||||
const c = Math.cos(pl.rotY), sn = Math.sin(pl.rotY);
|
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 (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));
|
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) };
|
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 frame = WallModel.computeFrame(config);
|
||||||
const built = buildWall(config, frame, 'geen');
|
const skinFrom = exposedFrom(lf, config);
|
||||||
if (built.group) {
|
return {
|
||||||
// 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,
|
id: iw.id, seam: iw.seam, seamUnit: lf.uid,
|
||||||
element: `NW-${nr}${lf.uid}`, label: `Naadwand ${nr} (unit ${lf.uid})`,
|
element: `NW-${nr}${lf.uid}`, label: `Naadwand ${nr} (unit ${lf.uid})`,
|
||||||
config, frame, buitenMateriaal: 'geen', place: lf.place
|
config, frame, place: lf.place,
|
||||||
}, built);
|
// 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;
|
// 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,
|
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 })),
|
units.map(u => ({
|
||||||
Math.max(...units.map(u => u.roof.top))) : null;
|
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).
|
// Buitenverlichting: gevellampen per wandstuk + boeideel-spots (vult roofEdge.spots).
|
||||||
const lighting = LightingModel.compute(state.settings.lighting, state.settings.roof, units, state.units, roofEdge);
|
const lighting = LightingModel.compute(state.settings.lighting, state.settings.roof, units, state.units, roofEdge);
|
||||||
// Zichtbaarheid: boeideel/daktrim volgt "Beplating", spots en lampen volgen "Lampen".
|
// Zichtbaarheid: boeideel/daktrim volgt "Beplating", spots en lampen volgen "Lampen".
|
||||||
@@ -561,9 +634,11 @@ window.HouseModel = (() => {
|
|||||||
const fc = u.floorData.calcData;
|
const fc = u.floorData.calcData;
|
||||||
fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.joistLen));
|
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));
|
['front', 'back'].filter(k => fc.rims[k]).forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.width));
|
||||||
const rc = u.roof.data.calcData;
|
u.roof.planes.forEach(pl => {
|
||||||
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen));
|
const rc = pl.data.calcData;
|
||||||
['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width));
|
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 = [];
|
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); }));
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ window.HousePdf = (() => {
|
|||||||
parts.units.forEach(u => {
|
parts.units.forEach(u => {
|
||||||
list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u });
|
list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u });
|
||||||
u.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })));
|
u.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })));
|
||||||
list.push({ kind: 'roof', element: u.roof.element, label: `Unit ${u.id} dak (plat)`, unit: u });
|
u.roof.planes.forEach(pl => list.push({ kind: 'roof', element: pl.element, label: pl.label, plane: pl }));
|
||||||
});
|
});
|
||||||
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
|
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
|
||||||
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
|
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
|
||||||
@@ -245,8 +245,8 @@ window.HousePdf = (() => {
|
|||||||
const opts = { project, titel: sheet.label, element: sheet.element, blad: `${i + 1} / ${sheets.length}` };
|
const opts = { project, titel: sheet.label, element: sheet.element, blad: `${i + 1} / ${sheets.length}` };
|
||||||
if (sheet.kind === 'plan') drawPlanSheet(doc, parts, sheets, opts);
|
if (sheet.kind === 'plan') drawPlanSheet(doc, parts, sheets, opts);
|
||||||
else if (sheet.kind === 'floor') FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, opts);
|
else if (sheet.kind === 'floor') FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, opts);
|
||||||
else if (sheet.kind === 'roof') FloorPdf.drawSheet(doc, sheet.unit.roof.data.calcData, sheet.unit.roof.data.resolvedOpenings, Object.assign({ roof: true }, opts));
|
else if (sheet.kind === 'roof') FloorPdf.drawSheet(doc, sheet.plane.data.calcData, sheet.plane.data.resolvedOpenings, Object.assign({ roof: true }, opts));
|
||||||
else if (sheet.kind === 'wall') WallPdf.drawSheet(doc, sheet.piece.config, sheet.piece.frame, Object.assign({ buitenMateriaal: sheet.piece.buitenMateriaal, cladding: sheet.piece.cladding, trasraam: sheet.piece.trasraam, skinExt: sheet.piece.skinExt, facade: sheet.piece.facade, lamps: sheet.piece.lamps || [] }, opts));
|
else if (sheet.kind === 'wall') WallPdf.drawSheet(doc, sheet.piece.config, sheet.piece.frame, Object.assign({ buitenMateriaal: sheet.piece.buitenMateriaal, cladding: sheet.piece.cladding, trasraam: sheet.piece.trasraam, skinExt: sheet.piece.skinExt, facade: sheet.piece.facade, skinFrom: sheet.piece.skinFrom, lamps: sheet.piece.lamps || [] }, opts));
|
||||||
else drawTotalsSheet(doc, totals, opts);
|
else drawTotalsSheet(doc, totals, opts);
|
||||||
});
|
});
|
||||||
return doc;
|
return doc;
|
||||||
|
|||||||
+19
-8
@@ -759,23 +759,34 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
UI.onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; }, after);
|
UI.onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; }, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Naadwand: twee delen (één per unit), één set openingen en maten.
|
// Naadwand: midden op de naad (twee delen, één per unit) of geheel in
|
||||||
|
// unit A/B (één wand). Eén set openingen en maten; de hoogte volgt per
|
||||||
|
// deel de daklijn van zijn unit.
|
||||||
function renderSeamProps(iw, warn) {
|
function renderSeamProps(iw, warn) {
|
||||||
const leaves = parts.interior.filter(x => x.id === iw.id);
|
const leaves = parts.interior.filter(x => x.id === iw.id);
|
||||||
const title = `Naadwand ${iw.seam.a} | ${iw.seam.b}`;
|
const title = `Naadwand ${iw.seam.a} | ${iw.seam.b}`;
|
||||||
propsEl.innerHTML = warn + head('layout-split', title, leaves.length ? `${leaves.length} delen` : 'vervalt') +
|
const exposed = leaves.filter(l => l.skinFrom !== undefined);
|
||||||
|
const inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
|
||||||
|
const plaatsen = { midden: 'Midden op de naad (2 delen)', a: `Geheel in unit ${iw.seam.a}`, b: `Geheel in unit ${iw.seam.b}` };
|
||||||
|
const sheet = iw.wall.wall.sheathingThick;
|
||||||
|
const dikte = inUnit
|
||||||
|
? num('hi-thick', 'Dikte', HouseModel.thicknessOf(iw.wall) + sheet, { step: 5, hint: 'Regelwerk + beplating aan beide zijden' })
|
||||||
|
: num('hi-thick', 'Dikte per deel', HouseModel.thicknessOf(iw.wall), { step: 5, hint: 'Regelwerk + beplating aan de kamerzijde; naadzijde zonder plaat' });
|
||||||
|
const uitleg = inUnit
|
||||||
|
? `Binnenwand geheel in unit ${inUnit}, tegen de naad aan, met beplating aan beide zijden. De hoogte volgt de daklijn van unit ${inUnit}.`
|
||||||
|
: 'Tussenwand op de naad, per unit een eigen deel dat tegen het andere aan staat. Openingen gelden voor beide delen (de deur loopt door). De hoogte volgt per deel de daklijn van zijn unit.';
|
||||||
|
propsEl.innerHTML = warn + head('layout-split', title, leaves.length ? (leaves.length === 1 ? '1 deel' : `${leaves.length} delen`) : 'vervalt') +
|
||||||
(leaves.length ? '' : UI.warn(`Unit ${iw.seam.a} en ${iw.seam.b} staan niet (meer) tegen elkaar; deze naadwand wordt niet gebouwd.`)) +
|
(leaves.length ? '' : UI.warn(`Unit ${iw.seam.a} en ${iw.seam.b} staan niet (meer) tegen elkaar; deze naadwand wordt niet gebouwd.`)) +
|
||||||
cols(
|
cols(
|
||||||
section('Opbouw', 'layers',
|
section('Opbouw', 'layers', sel('hi-seam-in', 'Plaats', plaatsen, iw.seam.in || 'midden') + dikte),
|
||||||
num('hi-thick', 'Dikte per deel', HouseModel.thicknessOf(iw.wall), { step: 5, hint: 'Regelwerk + beplating aan de kamerzijde; naadzijde zonder plaat' }) +
|
|
||||||
num('hi-height', 'Hoogte', iw.wall.wall.heightAll)),
|
|
||||||
UI.specCard('Delen', 'layout-split', leaves.map(l => [`${l.element} (unit ${l.seamUnit})`, l.place.width, 'mm']))) +
|
UI.specCard('Delen', 'layout-split', leaves.map(l => [`${l.element} (unit ${l.seamUnit})`, l.place.width, 'mm']))) +
|
||||||
info('Tussenwand op de naad, per unit een eigen deel dat tegen het andere aan staat. Openingen gelden voor beide delen (de deur loopt door).') +
|
info(uitleg +
|
||||||
|
exposed.map(l => ` ${l.element} steekt vanaf ${l.skinFrom} mm boven het dak van de andere unit uit; dat stuk krijgt buitenafwerking die met de gevel meeloopt.`).join('')) +
|
||||||
actions(
|
actions(
|
||||||
button('Openingen bewerken', 'pencil-square', `data-go="iw:${iw.id}"`),
|
button('Openingen bewerken', 'pencil-square', `data-go="iw:${iw.id}"`),
|
||||||
button('Verwijderen', 'trash', `data-remove-iw="${iw.id}"`, 'ui-btn-danger'));
|
button('Verwijderen', 'trash', `data-remove-iw="${iw.id}"`, 'ui-btn-danger'));
|
||||||
UI.onNum('hi-thick', v => { if (v >= 40) HouseModel.setThickness(iw.wall, v); }, after);
|
UI.onSel('hi-seam-in', v => { if (v === 'midden') delete iw.seam.in; else iw.seam.in = v; }, rerender);
|
||||||
UI.onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; }, after);
|
UI.onNum('hi-thick', v => { const t = inUnit ? v - sheet : v; if (t >= 40) HouseModel.setThickness(iw.wall, t); }, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Woning-instellingen (niets geselecteerd): alles wat voor de hele woning
|
// Woning-instellingen (niets geselecteerd): alles wat voor de hele woning
|
||||||
|
|||||||
@@ -212,10 +212,13 @@ window.LightingModel = (() => {
|
|||||||
const spots = [];
|
const spots = [];
|
||||||
if (s.mode === 'spots') {
|
if (s.mode === 'spots') {
|
||||||
const rs = RoofModel.normalizeSettings(roofSettings);
|
const rs = RoofModel.normalizeSettings(roofSettings);
|
||||||
edge.segments.filter(sg => sg.fascia).forEach(sg => {
|
edge.segments.filter(sg => sg.fascia && !sg.overRoof).forEach(sg => { // niet boven een lager dak
|
||||||
const from = spots.length;
|
const from = spots.length;
|
||||||
spotsBetween(sg.spotA, sg.spotB, rs.spotMin, rs.spotMax, spots);
|
spotsBetween(sg.spotA, sg.spotB, rs.spotMin, rs.spotMax, spots);
|
||||||
for (let k = from; k < spots.length; k++) spots[k].normal = { x: sg.normal.x, z: sg.normal.z };
|
for (let k = from; k < spots.length; k++) {
|
||||||
|
spots[k].normal = { x: sg.normal.x, z: sg.normal.z };
|
||||||
|
spots[k].y = sg.topAt(spots[k]) - edge.fasciaHeight; // onderkant boeideel (volgt de dakvorm)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Dubbele spots (hoeken) samenvoegen; de buitenrichting wordt dan de
|
// Dubbele spots (hoeken) samenvoegen; de buitenrichting wordt dan de
|
||||||
@@ -223,7 +226,7 @@ window.LightingModel = (() => {
|
|||||||
edge.spots = [];
|
edge.spots = [];
|
||||||
spots.forEach(p => {
|
spots.forEach(p => {
|
||||||
const q = edge.spots.find(e => Math.hypot(e.x - p.x, e.z - p.z) < 5);
|
const q = edge.spots.find(e => Math.hypot(e.x - p.x, e.z - p.z) < 5);
|
||||||
if (!q) edge.spots.push({ x: p.x, z: p.z, normal: p.normal });
|
if (!q) edge.spots.push({ x: p.x, z: p.z, y: p.y, normal: p.normal });
|
||||||
else if (p.normal && q.normal) q.normal = { x: q.normal.x + p.normal.x, z: q.normal.z + p.normal.z };
|
else if (p.normal && q.normal) q.normal = { x: q.normal.x + p.normal.x, z: q.normal.z + p.normal.z };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -245,7 +248,7 @@ window.LightingModel = (() => {
|
|||||||
return Math.hypot(p.x - (a.x + t * dx), p.z - (a.z + t * dz));
|
return Math.hypot(p.x - (a.x + t * dx), p.z - (a.z + t * dz));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binnenspots per unit. units = HouseModel-parts.units (rect, walls, roof.base),
|
// Binnenspots per unit. units = HouseModel-parts.units (rect, walls, roof.ceilingAt),
|
||||||
// interior = parts.interior (binnenwanden met place). Geeft
|
// interior = parts.interior (binnenwanden met place). Geeft
|
||||||
// [{ unitId, x, z, y, floorY }] (mm, y = onderkant plafond, floorY = loopvlak).
|
// [{ unitId, x, z, y, floorY }] (mm, y = onderkant plafond, floorY = loopvlak).
|
||||||
function computeIndoor(settings, units, interior) {
|
function computeIndoor(settings, units, interior) {
|
||||||
@@ -274,7 +277,7 @@ window.LightingModel = (() => {
|
|||||||
const a = { x: iw.place.cx - half * c, z: iw.place.cz + half * sn }, b = { x: iw.place.cx + half * c, z: iw.place.cz - half * sn };
|
const a = { x: iw.place.cx - half * c, z: iw.place.cz + half * sn }, b = { x: iw.place.cx + half * c, z: iw.place.cz - half * sn };
|
||||||
return distToSegment({ x, z }, a, b) < iw.place.thickness / 2 + off - 1;
|
return distToSegment({ x, z }, a, b) < iw.place.thickness / 2 + off - 1;
|
||||||
});
|
});
|
||||||
if (!tooClose) out.push({ unitId: u.id, x, z, y: u.roof.base, floorY: u.layout.floorTop });
|
if (!tooClose) out.push({ unitId: u.id, x, z, y: u.roof.ceilingAt(x, z), floorY: u.layout.floorTop });
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
return out;
|
return out;
|
||||||
@@ -330,7 +333,7 @@ window.LightingModel = (() => {
|
|||||||
// gekanteld: de binnenrand van de bundel loopt verticaal langs de gevel;
|
// gekanteld: de binnenrand van de bundel loopt verticaal langs de gevel;
|
||||||
// - binnenspots hebben een harde reikwijdte net voorbij de vloer
|
// - binnenspots hebben een harde reikwijdte net voorbij de vloer
|
||||||
// (INDOOR_REACH), zodat ze niet tot de grond buiten reiken.
|
// (INDOOR_REACH), zodat ze niet tot de grond buiten reiken.
|
||||||
// spots = [{ x, z }] (mm, boeideel), spotY = onderkant boeideel (mm),
|
// spots = [{ x, z, y }] (mm, boeideel; y = onderkant boeideel, anders spotY),
|
||||||
// indoor = [{ x, z, y, floorY }] (mm). o.outdoorFactor schaalt de buiten-
|
// indoor = [{ x, z, y, floorY }] (mm). o.outdoorFactor schaalt de buiten-
|
||||||
// lampen (overdag lager: dan vallen ze nauwelijks op).
|
// lampen (overdag lager: dan vallen ze nauwelijks op).
|
||||||
const OUTDOOR = { lamp: 2.0, spot: 1.6, angle: 0.5 };
|
const OUTDOOR = { lamp: 2.0, spot: 1.6, angle: 0.5 };
|
||||||
@@ -359,7 +362,7 @@ window.LightingModel = (() => {
|
|||||||
// hoek diagonaal).
|
// hoek diagonaal).
|
||||||
(spots || []).forEach(p => {
|
(spots || []).forEach(p => {
|
||||||
const n = p.normal || { x: 0, z: 0 }, len = Math.hypot(n.x, n.z) || 1;
|
const n = p.normal || { x: 0, z: 0 }, len = Math.hypot(n.x, n.z) || 1;
|
||||||
add(p.x * toM, spotY * toM - 0.02, p.z * toM, n.x / len * tilt, -1, n.z / len * tilt, OUTDOOR.spot * fo, OUTDOOR.angle, 5.5, 1.5);
|
add(p.x * toM, (p.y !== undefined ? p.y : spotY) * toM - 0.02, p.z * toM, n.x / len * tilt, -1, n.z / len * tilt, OUTDOOR.spot * fo, OUTDOOR.angle, 5.5, 1.5);
|
||||||
});
|
});
|
||||||
(indoor || []).forEach(p => {
|
(indoor || []).forEach(p => {
|
||||||
const drop = (p.y - p.floorY) * toM; // plafond -> loopvlak
|
const drop = (p.y - p.floorY) * toM; // plafond -> loopvlak
|
||||||
|
|||||||
+272
-40
@@ -5,14 +5,20 @@
|
|||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica (voorlopig
|
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica. Geen DOM.
|
||||||
// alleen plat dak). Geen DOM.
|
|
||||||
//
|
//
|
||||||
// Een plat dak is opgebouwd als een vloer (FloorModel: balken, isolatie,
|
// Een dakvlak is opgebouwd als een vloer (FloorModel: balken, isolatie,
|
||||||
// doorvoeren, OSB-plaat), maar 180° gespiegeld: de OSB-plaat zit aan de
|
// doorvoeren, OSB-plaat), maar 180° gespiegeld: de OSB-plaat zit aan de
|
||||||
// onderzijde (plafond), de balken erboven, met dakleer als buitenlaag
|
// onderzijde (plafond), de balken erboven, met dakleer als buitenlaag
|
||||||
// bovenop. Het "dek" van een dak is dus gewoon een vloer-state.
|
// bovenop. Het "dek" van een dak is dus gewoon een vloer-state.
|
||||||
//
|
//
|
||||||
|
// Dakvorm volgt de wanden (computeShape): het paar tegenoverliggende wanden
|
||||||
|
// met een schuine bovenrand of middenpunt (nok) zijn de kopgevels, het dak
|
||||||
|
// volgt hun daklijn (plat, lessenaar of zadel). UnitModel.syncWallHeights
|
||||||
|
// houdt de wanden daarvoor consistent. Het dek (in de plattegrond) wordt per
|
||||||
|
// dakvlak omgerekend naar een schuin vlak met de balken als sporen (van
|
||||||
|
// laag naar hoog); doorvoeren gaan mee naar het vlak waarin ze vallen.
|
||||||
|
//
|
||||||
// Dakrand (over de hele woning, doorlopend over naden tussen units): de
|
// Dakrand (over de hele woning, doorlopend over naden tussen units): de
|
||||||
// omtrek van alle daken samen, opgeknipt per ribbe (en op elke unit-hoek).
|
// omtrek van alle daken samen, opgeknipt per ribbe (en op elke unit-hoek).
|
||||||
// Per ribbe bepaalt de unit waar die ribbe bij hoort of het een boeideel of
|
// Per ribbe bepaalt de unit waar die ribbe bij hoort of het een boeideel of
|
||||||
@@ -114,6 +120,147 @@ window.RoofModel = (() => {
|
|||||||
return Object.assign({}, deck, { dakleerM2, dakleerKg, totalKg: deck.totalKg + dakleerKg });
|
return Object.assign({}, deck, { dakleerM2, dakleerKg, totalKg: deck.totalKg + dakleerKg });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Dakvorm per unit (volgt de wanden)
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
// Daklijn van één wand als profiel langs zijn as (unit-lokaal mm):
|
||||||
|
// [{ p, h }] gesorteerd op p, h vanaf het loopvlak. Zelfde definitie als
|
||||||
|
// UnitModel.wallPieces: links/rechts op de uiteinden van de wand (van
|
||||||
|
// buitenaf gezien), het middenpunt (nok) midden op de wand.
|
||||||
|
function wallProfile(unitState, layout, side) {
|
||||||
|
const w = unitState.walls[side].wall;
|
||||||
|
const wl = layout.walls.find(x => x.side === side);
|
||||||
|
const s = UnitModel.WALL_SIDES.find(x => x.key === side);
|
||||||
|
const c = s.axis === 'x' ? wl.cx : wl.cz;
|
||||||
|
const pts = [{ p: c - s.dir * wl.width / 2, h: w.heightLeft }, { p: c + s.dir * wl.width / 2, h: w.heightRight }];
|
||||||
|
if (w.heightMid > 0) pts.push({ p: c, h: Math.max(w.heightMid, w.heightLeft, w.heightRight) });
|
||||||
|
return pts.sort((a, b) => a.p - b.p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dakvorm van een unit. layout = UnitModel.computeLayout-resultaat.
|
||||||
|
// Geeft { type: 'plat'|'lessenaar'|'zadel', axis: 'x'|'z', profile }:
|
||||||
|
// de hoogte van de dakonderkant (vanaf het loopvlak) hangt alleen af van
|
||||||
|
// de positie langs axis (profileHeight). Kopgevels: de eerste wand met
|
||||||
|
// een schuine daklijn (noord/zuid -> dak helt langs X, west/oost -> langs Z).
|
||||||
|
// Zonder schuine wanden: plat op de hoogste wand.
|
||||||
|
function computeShape(unitState, layout) {
|
||||||
|
for (const [side, axis] of [['achter', 'x'], ['voor', 'x'], ['links', 'z'], ['rechts', 'z']]) {
|
||||||
|
const profile = wallProfile(unitState, layout, side);
|
||||||
|
if (profile.some(q => Math.abs(q.h - profile[0].h) > 0.5)) {
|
||||||
|
return { type: profile.length > 2 ? 'zadel' : 'lessenaar', axis, profile };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const h = Math.max(...Object.values(unitState.walls).map(ws => UnitModel.wallTopOf(ws)));
|
||||||
|
return { type: 'plat', axis: 'z', profile: [{ p: 0, h }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoogte van het profiel op positie p; buiten de wand loopt de laatste
|
||||||
|
// helling door (overstek, boeideel).
|
||||||
|
function profileHeight(profile, p) {
|
||||||
|
if (profile.length === 1) return profile[0].h;
|
||||||
|
let i = 0;
|
||||||
|
while (i < profile.length - 2 && p > profile[i + 1].p) i++;
|
||||||
|
const a = profile[i], b = profile[i + 1];
|
||||||
|
return a.h + (p - a.p) / (b.p - a.p) * (b.h - a.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tel alle getallen van een lijst specificaties op.
|
||||||
|
function sumSpecs(list) {
|
||||||
|
const out = Object.assign({}, list[0]);
|
||||||
|
list.slice(1).forEach(sp => Object.keys(sp).forEach(k => {
|
||||||
|
if (typeof sp[k] === 'number' && typeof out[k] === 'number') out[k] += sp[k];
|
||||||
|
}));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Het dak van één unit: de dakrechthoek (unit-lokaal mm { x0, x1, z0, z1 },
|
||||||
|
// = maat van roofState.deck) geknipt op de nok in dakvlakken. Per vlak een
|
||||||
|
// eigen dek (vloer-state) met breedte = maat dwars op de helling en diepte
|
||||||
|
// = lengte langs de helling, zodat de balken als sporen van laag naar hoog
|
||||||
|
// lopen. rims: zie FloorModel.computeFrame (alleen bij helling langs Z).
|
||||||
|
// Geeft { shape, planes, specs, undersideAt(x, z), topAt(x, z), ridges, maxTop }
|
||||||
|
// (unit-lokaal, hoogtes vanaf het loopvlak). Vlak-posities (cx, cz) zijn
|
||||||
|
// t.o.v. het midden van de dakrechthoek.
|
||||||
|
function computeUnitRoof(roofState, shape, rect, rims) {
|
||||||
|
const alongX = shape.axis === 'x';
|
||||||
|
const W = rect.x1 - rect.x0, D = rect.z1 - rect.z0;
|
||||||
|
const xc = (rect.x0 + rect.x1) / 2, zc = (rect.z0 + rect.z1) / 2;
|
||||||
|
const a0 = alongX ? rect.x0 : rect.z0, a1 = alongX ? rect.x1 : rect.z1;
|
||||||
|
const kinks = shape.profile.slice(1, -1).map(q => q.p).filter(p => p > a0 + 1 && p < a1 - 1);
|
||||||
|
const cuts = [a0, ...kinks, a1];
|
||||||
|
const flat = FloorModel.computeFromState(roofState.deck, rims);
|
||||||
|
const H = (p) => profileHeight(shape.profile, p);
|
||||||
|
|
||||||
|
// Doorvoeren: positie in de plattegrond (zoals in de dak-editor) -> vlak.
|
||||||
|
const holes = flat.resolvedOpenings.filter(o => !o.removed).map(o => {
|
||||||
|
const px = xc + o.x, pz = rect.z0 + o.y;
|
||||||
|
return { o, px, pz, q: alongX ? px : pz };
|
||||||
|
});
|
||||||
|
|
||||||
|
const planes = cuts.slice(0, -1).map((p0, i) => {
|
||||||
|
const p1 = cuts[i + 1], last = i === cuts.length - 2;
|
||||||
|
const h0 = H(p0), h1 = H(p1), run = p1 - p0;
|
||||||
|
const slopeLen = Math.hypot(run, h1 - h0), cos = run / slopeLen;
|
||||||
|
const plane = { p0, p1, h0, h1, theta: Math.atan2(h1 - h0, run), cos, yaw: alongX ? Math.PI / 2 : 0 };
|
||||||
|
let data;
|
||||||
|
if (cuts.length === 2 && !alongX && Math.abs(h1 - h0) < 0.5) {
|
||||||
|
data = flat; // plat dak: het dek zoals het is
|
||||||
|
} else {
|
||||||
|
const deck = clone(roofState.deck);
|
||||||
|
deck.floor.width = Math.round(alongX ? D : W);
|
||||||
|
deck.floor.depth = Math.round(slopeLen);
|
||||||
|
deck.openings = holes.filter(hl => hl.q >= p0 && (hl.q < p1 || last)).map(hl => ({
|
||||||
|
id: hl.o.id, diameter: hl.o.requestedDiameter, anchorX: 'left',
|
||||||
|
// Bij helling langs X is het dek een kwartslag gedraaid: lokaal x -> -Z.
|
||||||
|
offsetX: Math.round(alongX ? rect.z1 - hl.pz : hl.px - rect.x0),
|
||||||
|
offsetY: Math.round((hl.q - p0) / cos)
|
||||||
|
}));
|
||||||
|
const pr = alongX ? {} : { front: !!rims.front && Math.abs(p0 - rect.z0) < 1, back: !!rims.back && Math.abs(p1 - rect.z1) < 1 };
|
||||||
|
data = FloorModel.computeFromState(deck, pr);
|
||||||
|
}
|
||||||
|
const mid = (p0 + p1) / 2;
|
||||||
|
return Object.assign(plane, {
|
||||||
|
data, specs: computeSpecs(data.calcData, data.resolvedOpenings),
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const planeAt = (q) => planes.find((pl, i) => q < pl.p1 || i === planes.length - 1);
|
||||||
|
const undersideAt = (x, z) => H(alongX ? x : z);
|
||||||
|
const topAt = (x, z) => {
|
||||||
|
const q = alongX ? x : z;
|
||||||
|
const onKink = planes.filter(pl => Math.abs(q - pl.p0) < 0.5 || Math.abs(q - pl.p1) < 0.5);
|
||||||
|
const t = onKink.length > 1 ? Math.max(...onKink.map(pl => pl.vThick)) : planeAt(q).vThick;
|
||||||
|
return H(q) + t;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
shape, 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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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');
|
||||||
|
group.add(g);
|
||||||
|
});
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Dakrand over de hele woning
|
// Dakrand over de hele woning
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
@@ -148,37 +295,89 @@ window.RoofModel = (() => {
|
|||||||
return Math.abs(pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + p.x * q.z - q.x * p.z; }, 0) / 2);
|
return Math.abs(pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + p.x * q.z - q.x * p.z; }, 0) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool }] (mm, woning-
|
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool, topAt(x, z),
|
||||||
// assen); topY = bovenkant dakleer (mm). Geeft per ribbe (segments) de
|
// ridges: [{ axis, value }] }] (mm, woning-assen); topAt = bovenkant
|
||||||
// geometrie terug; edge.spots wordt door LightingModel gevuld.
|
// dakleer (absoluut, loopt buiten het dak door met de helling). Geeft per
|
||||||
function computeEdge(settings, roofs, topY) {
|
// ribbe (segments) de geometrie terug; edge.spots wordt door LightingModel
|
||||||
|
// gevuld (met y = onderkant boeideel ter plaatse).
|
||||||
|
function computeEdge(settings, roofs) {
|
||||||
const s = normalizeSettings(settings);
|
const s = normalizeSettings(settings);
|
||||||
const rects = roofs.map(r => r.rect);
|
const rects = roofs.map(r => r.rect);
|
||||||
const loops = rectUnionLoops(rects);
|
if (!roofs.length) return null;
|
||||||
if (!loops.length) return null;
|
|
||||||
const base = splitAtCorners(loops[0].pts, rects);
|
|
||||||
const offBoei = offsetRectilinearLoop(base, s.overhang);
|
|
||||||
const offTrim = offsetRectilinearLoop(base, TRIM_THICK_MM);
|
|
||||||
const offSpot = offsetRectilinearLoop(base, s.overhang * SPOT_RATIO);
|
|
||||||
const n = base.length;
|
|
||||||
|
|
||||||
const segments = base.map((a, i) => {
|
// Hoogtegroepen: daken die tegen elkaar liggen horen bij één doorlopende
|
||||||
const j = (i + 1) % n, b = base[j];
|
// rand, tenzij het hoogteverschil langs de hele naad minstens de
|
||||||
const mid = { x: (a.x + b.x) / 2, z: (a.z + b.z) / 2 };
|
// boeideelhoogte is. Dan krijgt het hoge dak een eigen rand rondom (ook
|
||||||
const owner = roofs.find(r => onRectEdge(r.rect, mid)) || roofs[0];
|
// boven het lage dak), en loopt de rand van het lage dak tot de gevel
|
||||||
const fascia = owner.fascia !== false;
|
// van het hoge.
|
||||||
const off = fascia ? offBoei : offTrim;
|
const group = roofs.map((r, i) => i);
|
||||||
return {
|
const find = (i) => group[i] === i ? i : (group[i] = find(group[i]));
|
||||||
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
|
const sharedEdge = (A, B) => {
|
||||||
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j]
|
const e = 0.5;
|
||||||
};
|
for (const [ax, bx, lo0, lo1, hi0, hi1, axis] of [
|
||||||
|
[A.x1, B.x0, A.z0, A.z1, B.z0, B.z1, 'x'], [B.x1, A.x0, A.z0, A.z1, B.z0, B.z1, 'x'],
|
||||||
|
[A.z1, B.z0, A.x0, A.x1, B.x0, B.x1, 'z'], [B.z1, A.z0, A.x0, A.x1, B.x0, B.x1, 'z']]) {
|
||||||
|
const s0 = Math.max(lo0, hi0), s1 = Math.min(lo1, hi1);
|
||||||
|
if (Math.abs(ax - bx) < e && s1 - s0 > e) {
|
||||||
|
return [s0, (s0 + s1) / 2, s1].map(t => axis === 'x' ? { x: ax, z: t } : { x: t, z: ax });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
roofs.forEach((A, i) => roofs.forEach((B, j) => {
|
||||||
|
if (j <= i) return;
|
||||||
|
const pts = sharedEdge(A.rect, B.rect);
|
||||||
|
if (!pts) return;
|
||||||
|
const diff = Math.min(...pts.map(p => Math.abs(A.topAt(p.x, p.z) - B.topAt(p.x, p.z))));
|
||||||
|
if (diff < s.fasciaHeight) group[find(i)] = find(j);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
[...new Set(roofs.map((r, i) => find(i)))].forEach(g => {
|
||||||
|
const members = roofs.filter((r, i) => find(i) === g);
|
||||||
|
const others = roofs.filter((r, i) => find(i) !== g);
|
||||||
|
const loops = rectUnionLoops(members.map(r => r.rect));
|
||||||
|
if (!loops.length) return;
|
||||||
|
const base = splitAtCorners(loops[0].pts, rects);
|
||||||
|
const offBoei = offsetRectilinearLoop(base, s.overhang);
|
||||||
|
const offTrim = offsetRectilinearLoop(base, TRIM_THICK_MM);
|
||||||
|
const offSpot = offsetRectilinearLoop(base, s.overhang * SPOT_RATIO);
|
||||||
|
const n = base.length;
|
||||||
|
const segs = base.map((a, i) => {
|
||||||
|
const j = (i + 1) % n, b = base[j];
|
||||||
|
const mid = { x: (a.x + b.x) / 2, z: (a.z + b.z) / 2 };
|
||||||
|
const owner = members.find(r => onRectEdge(r.rect, mid)) || members[0];
|
||||||
|
const fascia = owner.fascia !== false;
|
||||||
|
const off = fascia ? offBoei : offTrim;
|
||||||
|
// Ribbe tegen een dak van een andere groep: tegen een hoger dak
|
||||||
|
// vervalt hij (daar staat de gevel van het hoge), boven een lager
|
||||||
|
// dak blijft hij, maar zonder spots.
|
||||||
|
const nb = others.find(r => onRectEdge(r.rect, mid));
|
||||||
|
const dropped = !!nb && nb.topAt(mid.x, mid.z) > owner.topAt(mid.x, mid.z);
|
||||||
|
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 || [],
|
||||||
|
overRoof: !!nb && !dropped, dropped
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Naast een vervallen ribbe loopt de rand recht door tot de gevel.
|
||||||
|
segs.forEach((sg, i) => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
segs.filter(sg => !sg.dropped).forEach(sg => segments.push(sg));
|
||||||
});
|
});
|
||||||
|
|
||||||
const segLen = (sg) => Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z);
|
const segLen = (sg) => Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z);
|
||||||
|
const top = Math.max(...segments.map(sg => Math.max(sg.topAt(sg.a), sg.topAt(sg.b))));
|
||||||
return {
|
return {
|
||||||
base, segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
||||||
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
|
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
|
||||||
top: topY, trimTop: topY + TRIM_ABOVE_MM,
|
top, trimAbove: TRIM_ABOVE_MM,
|
||||||
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
|
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),
|
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,
|
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
|
||||||
@@ -186,9 +385,40 @@ window.RoofModel = (() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3D van de dakrand: per ribbe een geëxtrudeerd vierhoek (dakrand ->
|
// Convexe veelhoek (plattegrond, mm) knippen met een halfvlak
|
||||||
// buitenrand, in verstek), naar beneden over de hoogte van boeideel resp.
|
// (Sutherland-Hodgman): houd de punten waar f(p) >= 0.
|
||||||
// daktrim. Plus de boeideel-spots (edge.spots) in de onderplaat.
|
function clipPoly(poly, f) {
|
||||||
|
const out = [];
|
||||||
|
poly.forEach((p, k) => {
|
||||||
|
const q = poly[(k + 1) % poly.length], fp = f(p), fq = f(q);
|
||||||
|
if (fp >= 0) out.push(p);
|
||||||
|
if ((fp >= 0) !== (fq >= 0)) {
|
||||||
|
const t = fp / (fp - fq);
|
||||||
|
out.push({ x: p.x + (q.x - p.x) * t, z: p.z + (q.z - p.z) * t });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prisma onder een convexe veelhoek: bovenkant per hoekpunt op topFn(p),
|
||||||
|
// onderkant h lager (mm -> meters).
|
||||||
|
function prismGeometry(poly, topFn, h) {
|
||||||
|
const toM = 0.001, n = poly.length, pos = [];
|
||||||
|
const T = poly.map(p => [p.x * toM, topFn(p) * toM, p.z * toM]);
|
||||||
|
const B = poly.map(p => [p.x * toM, (topFn(p) - h) * toM, p.z * toM]);
|
||||||
|
const tri = (a, b, c) => pos.push(...a, ...b, ...c);
|
||||||
|
for (let k = 1; k < n - 1; k++) { tri(T[0], T[k], T[k + 1]); tri(B[0], B[k + 1], B[k]); }
|
||||||
|
for (let k = 0; k < n; k++) { const j = (k + 1) % n; tri(T[k], B[k], B[j]); tri(T[k], B[j], T[j]); }
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||||
|
geo.computeVertexNormals();
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3D van de dakrand: per ribbe een vierhoek (dakrand -> buitenrand, in
|
||||||
|
// verstek), naar beneden over de hoogte van boeideel resp. daktrim. De
|
||||||
|
// bovenkant volgt het dak (sg.topAt); op een nok wordt de ribbe geknipt.
|
||||||
|
// Plus de boeideel-spots (edge.spots) in de onderplaat.
|
||||||
// opts: { edgeVisible (volgt "Beplating"), spotsVisible (volgt "Lampen") }
|
// opts: { edgeVisible (volgt "Beplating"), spotsVisible (volgt "Lampen") }
|
||||||
function buildEdge3D(edge, materials, opts) {
|
function buildEdge3D(edge, materials, opts) {
|
||||||
const o = opts || {};
|
const o = opts || {};
|
||||||
@@ -196,23 +426,24 @@ window.RoofModel = (() => {
|
|||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
if (!edge) return group;
|
if (!edge) return group;
|
||||||
edge.segments.forEach(sg => {
|
edge.segments.forEach(sg => {
|
||||||
const pts = [sg.a, sg.b, sg.outerB, sg.outerA];
|
let polys = [[sg.a, sg.b, sg.outerB, sg.outerA]];
|
||||||
const shape = new THREE.Shape(pts.map(p => new THREE.Vector2(p.x * toM, p.z * toM)));
|
sg.ridges.forEach(r => {
|
||||||
|
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 h = sg.fascia ? edge.fasciaHeight : edge.trimHeight;
|
||||||
const mesh = new THREE.Mesh(
|
const topFn = sg.fascia ? sg.topAt : (p) => sg.topAt(p) + edge.trimAbove;
|
||||||
new THREE.ExtrudeGeometry(shape, { steps: 1, depth: h * toM, bevelEnabled: false }),
|
polys.forEach(pl => {
|
||||||
sg.fascia ? materials.fasciaMaterial : materials.trimMaterial
|
const mesh = new THREE.Mesh(prismGeometry(pl, topFn, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
|
||||||
);
|
mesh.visible = o.edgeVisible !== false;
|
||||||
mesh.rotation.x = Math.PI / 2; // shape-y -> wereld-Z, extrusie naar beneden
|
group.add(mesh);
|
||||||
mesh.position.y = (sg.fascia ? edge.top : edge.trimTop) * toM;
|
});
|
||||||
mesh.visible = o.edgeVisible !== false;
|
|
||||||
group.add(mesh);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const spotR = 0.035, spotH = 0.012;
|
const spotR = 0.035, spotH = 0.012;
|
||||||
edge.spots.forEach(p => {
|
edge.spots.forEach(p => {
|
||||||
const spot = new THREE.Mesh(new THREE.CylinderGeometry(spotR, spotR, spotH, 16), materials.spotOnMaterial);
|
const spot = new THREE.Mesh(new THREE.CylinderGeometry(spotR, spotR, spotH, 16), materials.spotOnMaterial);
|
||||||
spot.position.set(p.x * toM, (edge.top - edge.fasciaHeight) * toM - spotH / 2 + 0.002, p.z * toM);
|
spot.position.set(p.x * toM, p.y * toM - spotH / 2 + 0.002, p.z * toM);
|
||||||
spot.visible = o.spotsVisible !== false;
|
spot.visible = o.spotsVisible !== false;
|
||||||
group.add(spot);
|
group.add(spot);
|
||||||
});
|
});
|
||||||
@@ -229,6 +460,7 @@ window.RoofModel = (() => {
|
|||||||
return {
|
return {
|
||||||
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
|
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
|
||||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||||
|
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D,
|
||||||
computeEdge, buildEdge3D, normalizeSettings
|
computeEdge, buildEdge3D, normalizeSettings
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+156
-138
@@ -95,6 +95,9 @@ window.WallModel = (() => {
|
|||||||
deur: { label: 'Deur', w: 900, h: 2150, y: 0 },
|
deur: { label: 'Deur', w: 900, h: 2150, y: 0 },
|
||||||
voordeur: { label: 'Voordeur', w: 1150, h: 2150, y: 0 }
|
voordeur: { label: 'Voordeur', w: 1150, h: 2150, y: 0 }
|
||||||
};
|
};
|
||||||
|
// Deuren krijgen aan weerszijden een tweede stijl direct tegen de
|
||||||
|
// koningsstijl (stevigheid, computeFrame).
|
||||||
|
const isDoor = (op) => op.type === 'deur' || op.type === 'voordeur';
|
||||||
|
|
||||||
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
||||||
// binnenwanden (aan beide zijden spaanplaat).
|
// binnenwanden (aan beide zijden spaanplaat).
|
||||||
@@ -220,34 +223,44 @@ window.WallModel = (() => {
|
|||||||
o.x = Math.max(edgeMargin, Math.min(o.x, wallWidth - o.w - edgeMargin));
|
o.x = Math.max(edgeMargin, Math.min(o.x, wallWidth - o.w - edgeMargin));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Links-naar-rechts: duw overlappende openingen uit elkaar. 2x stijldikte
|
// Openingen die elkaar in de hoogte raken (incl. boven-/onderdorpel en
|
||||||
// i.p.v. 1x: de OSB-uitsparing rond elke opening steekt aan weerszijden
|
// 2x stijldikte ertussen) moeten naast elkaar; staan ze boven elkaar
|
||||||
// al 1x stijldikte uit (om de koningsstijl zichtbaar te laten), dus bij
|
// (bijv. een raam boven een deur), dan mogen ze in x overlappen.
|
||||||
// minder afstand tussen twee openingen zouden hun uitsparingen elkaar
|
// Hoogte hier op basis van de invoer; na fase 2 wordt het nog gecontroleerd.
|
||||||
// kunnen overlappen (gecontroleerd, zie eerdere test).
|
const vBand = (o, h, y) => [y > plateThick ? y - studWidth : 0, y + h + studWidth];
|
||||||
const minGap = studWidth * 2;
|
const stacked = (a, b, useFinal) => {
|
||||||
|
const [a0, a1] = useFinal ? vBand(a, a.h, a.y) : vBand(a, Math.max(400, a.rawH), a.rawY);
|
||||||
|
const [b0, b1] = useFinal ? vBand(b, b.h, b.y) : vBand(b, Math.max(400, b.rawH), b.rawY);
|
||||||
|
return a1 <= b0 + 0.5 || b1 <= a0 + 0.5;
|
||||||
|
};
|
||||||
|
// Tussenruimte naast elkaar: 2x stijldikte i.p.v. 1x: de OSB-uitsparing
|
||||||
|
// rond elke opening steekt aan weerszijden al 1x stijldikte uit (om de
|
||||||
|
// koningsstijl zichtbaar te laten), dus bij minder afstand zouden hun
|
||||||
|
// uitsparingen elkaar kunnen overlappen. Naast een deur 1x extra voor
|
||||||
|
// de dubbele stijl.
|
||||||
|
const gapOf = (a, b) => studWidth * (2 + (isDoor(a) ? 1 : 0) + (isDoor(b) ? 1 : 0));
|
||||||
|
|
||||||
|
// Links-naar-rechts: duw botsende openingen uit elkaar.
|
||||||
for (let i = 1; i < resolved.length; i++) {
|
for (let i = 1; i < resolved.length; i++) {
|
||||||
const prev = resolved[i - 1];
|
|
||||||
const cur = resolved[i];
|
const cur = resolved[i];
|
||||||
const minX = prev.x + prev.w + minGap;
|
for (let j = 0; j < i; j++) {
|
||||||
if (cur.x < minX) cur.x = minX;
|
const prev = resolved[j];
|
||||||
|
if (stacked(prev, cur)) continue;
|
||||||
|
cur.x = Math.max(cur.x, prev.x + prev.w + gapOf(prev, cur));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rechts-naar-links correctie als de laatste opening nu over de rand steekt
|
// Rechts-naar-links: wat over de rechterrand steekt terugtrekken, en de
|
||||||
|
// botsende openingen links ervan mee.
|
||||||
const rightLimit = wallWidth - edgeMargin;
|
const rightLimit = wallWidth - edgeMargin;
|
||||||
let overflow = 0;
|
for (let i = resolved.length - 1; i >= 0; i--) {
|
||||||
if (resolved.length) {
|
const cur = resolved[i];
|
||||||
const last = resolved[resolved.length - 1];
|
let maxX = rightLimit - cur.w;
|
||||||
overflow = Math.max(0, (last.x + last.w) - rightLimit);
|
for (let j = i + 1; j < resolved.length; j++) {
|
||||||
}
|
const next = resolved[j];
|
||||||
if (overflow > 0) {
|
if (!stacked(cur, next)) maxX = Math.min(maxX, next.x - gapOf(cur, next) - cur.w);
|
||||||
for (let i = resolved.length - 1; i >= 0; i--) {
|
|
||||||
resolved[i].x -= overflow;
|
|
||||||
if (i > 0) {
|
|
||||||
const maxX = resolved[i].x - minGap - resolved[i - 1].w;
|
|
||||||
if (resolved[i - 1].x > maxX) resolved[i - 1].x = maxX;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (cur.x > maxX) cur.x = maxX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fase 2: hoogte (h) en verticale positie (y), per opening geklemd tegen
|
// Fase 2: hoogte (h) en verticale positie (y), per opening geklemd tegen
|
||||||
@@ -270,14 +283,16 @@ window.WallModel = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
||||||
// openingen voor deze wandbreedte) zodat de UI dit kan tonen.
|
// openingen voor deze wandbreedte, of boven elkaar zonder ruimte
|
||||||
|
// ertussen) zodat de UI dit kan tonen.
|
||||||
resolved.forEach(o => {
|
resolved.forEach(o => {
|
||||||
o.overflowing = (o.x < edgeMargin - 0.5) || (o.x + o.w > wallWidth - edgeMargin + 0.5);
|
o.overflowing = (o.x < edgeMargin - 0.5) || (o.x + o.w > wallWidth - edgeMargin + 0.5);
|
||||||
});
|
});
|
||||||
for (let i = 1; i < resolved.length; i++) {
|
for (let i = 0; i < resolved.length; i++) {
|
||||||
if (resolved[i].x < resolved[i - 1].x + resolved[i - 1].w + minGap - 0.5) {
|
for (let j = i + 1; j < resolved.length; j++) {
|
||||||
resolved[i].overflowing = true;
|
const a = resolved[i], b = resolved[j];
|
||||||
resolved[i - 1].overflowing = true;
|
const [l, r] = a.x <= b.x ? [a, b] : [b, a];
|
||||||
|
if (!stacked(a, b, true) && r.x < l.x + l.w + gapOf(l, r) - 0.5) { a.overflowing = true; b.overflowing = true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,52 +371,88 @@ window.WallModel = (() => {
|
|||||||
cornerStuds.push(outerLeftStudX + i * CONFIG.studWidth, outerRightStudX - i * CONFIG.studWidth);
|
cornerStuds.push(outerLeftStudX + i * CONFIG.studWidth, outerRightStudX - i * CONFIG.studWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullStuds = [];
|
// Stijlen als stukken { x, y0, y1, kind } (mm; y1 null = tot de
|
||||||
let jackStuds = [];
|
// daklijn). Eerst kandidaat-posities, daarna wordt elke kandidaat
|
||||||
|
// opgeknipt door de openingen (incl. boven-/onderdorpel) die hij kruist.
|
||||||
|
// Zo kunnen openingen ook boven elkaar staan (raam boven een deur): de
|
||||||
|
// stijlen ertussen lopen van dorpel tot dorpel.
|
||||||
|
// hoek dubbele hoekstijl
|
||||||
|
// koning koningsstijl direct naast een opening
|
||||||
|
// dubbel tweede stijl direct tegen de koningsstijl van een deur
|
||||||
|
// regel regelmatige verdeling in de vrije zones
|
||||||
|
// kreupel boven/onder een opening, binnen de openingbreedte
|
||||||
|
const sw = CONFIG.studWidth;
|
||||||
const ops = CONFIG.openings; // al gesorteerd op x door resolveOpenings
|
const ops = CONFIG.openings; // al gesorteerd op x door resolveOpenings
|
||||||
|
const bands = ops.map(op => ({
|
||||||
|
op, x0: startX + op.x, x1: startX + op.x + op.w,
|
||||||
|
y0: op.y > CONFIG.plateThick ? op.y - sw : 0, y1: op.y + op.h + sw
|
||||||
|
}));
|
||||||
|
const cands = [];
|
||||||
|
cornerStuds.forEach(x => cands.push({ x, kind: 'hoek' }));
|
||||||
|
ops.forEach(op => {
|
||||||
|
const x0 = startX + op.x, x1 = x0 + op.w;
|
||||||
|
cands.push({ x: x0 - sw / 2, kind: 'koning' }, { x: x1 + sw / 2, kind: 'koning' });
|
||||||
|
if (isDoor(op)) cands.push({ x: x0 - 1.5 * sw, kind: 'dubbel' }, { x: x1 + 1.5 * sw, kind: 'dubbel' });
|
||||||
|
});
|
||||||
|
|
||||||
if (ops.length === 0) {
|
// Kolommen: openingen (incl. hun stijlen) die elkaar in x overlappen.
|
||||||
fullStuds.push(...cornerStuds);
|
// Tussen de kolommen en de hoekstijlen: regelmatige verdeling.
|
||||||
const availWidth = wallRightStudX - wallLeftStudX;
|
const cols = [];
|
||||||
calculateSegmentPositions(availWidth, CONFIG.minSpacing, CONFIG.maxSpacing)
|
ops.map(op => {
|
||||||
.forEach(p => fullStuds.push(wallLeftStudX + p));
|
const extra = (isDoor(op) ? 2 : 1) * sw;
|
||||||
} else {
|
return [startX + op.x - extra + sw / 2, startX + op.x + op.w + extra - sw / 2]; // hartlijnen buitenste stijlen
|
||||||
// Hoekstijlen van de wand horen er, met of zonder opening, altijd bij.
|
}).sort((p, q) => p[0] - q[0]).forEach(r => {
|
||||||
fullStuds.push(...cornerStuds);
|
const last = cols[cols.length - 1];
|
||||||
|
if (last && r[0] < last[1] + sw) last[1] = Math.max(last[1], r[1]);
|
||||||
// Randen van de "vrije zones" tussen wand en openingen, per paar verwerkt
|
else cols.push(r.slice());
|
||||||
const edges = [wallLeftStudX];
|
});
|
||||||
|
const edges = [wallLeftStudX];
|
||||||
ops.forEach(op => {
|
cols.forEach(c => edges.push(c[0], c[1]));
|
||||||
const opStartX = startX + op.x;
|
edges.push(wallRightStudX);
|
||||||
const kingLeftX = opStartX - CONFIG.studWidth / 2;
|
for (let i = 0; i < edges.length; i += 2) {
|
||||||
const kingRightX = opStartX + op.w + CONFIG.studWidth / 2;
|
const a = edges[i], b = edges[i + 1];
|
||||||
|
if (b - a > sw) calculateSegmentPositions(b - a, CONFIG.minSpacing, CONFIG.maxSpacing).forEach(p => cands.push({ x: a + p, kind: 'regel' }));
|
||||||
fullStuds.push(kingLeftX, kingRightX);
|
|
||||||
edges.push(kingLeftX, kingRightX);
|
|
||||||
|
|
||||||
// Kreupele stijlen binnen de opening-breedte zelf
|
|
||||||
calculateSegmentPositions(op.w, CONFIG.minSpacing, CONFIG.maxSpacing)
|
|
||||||
.forEach(p => jackStuds.push({ x: opStartX + p, opening: op }));
|
|
||||||
});
|
|
||||||
|
|
||||||
edges.push(wallRightStudX);
|
|
||||||
|
|
||||||
for (let i = 0; i < edges.length; i += 2) {
|
|
||||||
const a = edges[i];
|
|
||||||
const b = edges[i + 1];
|
|
||||||
const span = (b - CONFIG.studWidth / 2) - (a + CONFIG.studWidth / 2);
|
|
||||||
if (span > 0) {
|
|
||||||
calculateSegmentPositions(span + CONFIG.studWidth, CONFIG.minSpacing, CONFIG.maxSpacing)
|
|
||||||
.forEach(p => fullStuds.push(a + p));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ops.forEach(op => calculateSegmentPositions(op.w, CONFIG.minSpacing, CONFIG.maxSpacing)
|
||||||
|
.forEach(p => cands.push({ x: startX + op.x + p, kind: 'kreupel', owner: op })));
|
||||||
|
|
||||||
fullStuds = [...new Set(fullStuds.map(x => Math.round(x)))].sort((a, b) => a - b);
|
// Knippen op de openingen.
|
||||||
jackStuds.sort((a, b) => a.x - b.x);
|
const PRIO = { hoek: 0, koning: 1, dubbel: 2, regel: 3, kreupel: 4 };
|
||||||
|
const roofMin = (x) => Math.min(...top.underPts(x - sw / 2, x + sw / 2).map(p => p.y));
|
||||||
|
const segs = [];
|
||||||
|
cands.forEach(c => {
|
||||||
|
const x = Math.round(c.x);
|
||||||
|
const hit = bands.filter(bd => Math.min(bd.x1, x + sw / 2) - Math.max(bd.x0, x - sw / 2) > 0.5).sort((p, q) => p.y0 - q.y0);
|
||||||
|
let cur = CONFIG.plateThick, below = null;
|
||||||
|
const push = (y0, y1, above) => {
|
||||||
|
if (c.kind === 'kreupel') {
|
||||||
|
// Alleen direct boven of onder de eigen opening; onder de opening
|
||||||
|
// niet als hij op een andere opening uitkomt (dat stuk vult de
|
||||||
|
// kreupele stijl van de onderste opening al).
|
||||||
|
const onTop = below && below.op === c.owner, underneath = above && above.op === c.owner;
|
||||||
|
if (!onTop && !(underneath && !below)) return;
|
||||||
|
}
|
||||||
|
segs.push({ x, y0, y1, kind: c.kind });
|
||||||
|
};
|
||||||
|
hit.forEach(bd => {
|
||||||
|
if (bd.y0 > cur + 1) push(cur, bd.y0, bd);
|
||||||
|
if (bd.y1 > cur) { cur = bd.y1; below = bd; }
|
||||||
|
});
|
||||||
|
if (roofMin(x) > cur + 1) push(cur, null, null);
|
||||||
|
});
|
||||||
|
// Dubbelingen (bijv. koningsstijl op de hoekstijl) weg: hoogste prioriteit wint.
|
||||||
|
const studs = [];
|
||||||
|
segs.sort((p, q) => PRIO[p.kind] - PRIO[q.kind]).forEach(sg => {
|
||||||
|
const top1 = sg.y1 === null ? Infinity : sg.y1;
|
||||||
|
const clash = studs.some(o => Math.abs(o.x - sg.x) < sw - 0.5 &&
|
||||||
|
Math.min(top1, o.y1 === null ? Infinity : o.y1) - Math.max(sg.y0, o.y0) > 1);
|
||||||
|
if (!clash) studs.push(sg);
|
||||||
|
});
|
||||||
|
studs.sort((p, q) => p.x - q.x || p.y0 - q.y0);
|
||||||
|
// Volle stijlen (onderregel tot daklijn), o.a. voor de maatvoering.
|
||||||
|
const fullStuds = [...new Set(studs.filter(sg => sg.y1 === null && sg.y0 <= CONFIG.plateThick).map(sg => sg.x))].sort((a, b) => a - b);
|
||||||
|
|
||||||
return { fullStuds, jackStuds, slope, angleDeg };
|
return { studs, fullStuds, slope, angleDeg };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uitsparing in de beplating/het vochtdoek per opening (mm; x t.o.v. het
|
// Uitsparing in de beplating/het vochtdoek per opening (mm; x t.o.v. het
|
||||||
@@ -457,8 +508,10 @@ window.WallModel = (() => {
|
|||||||
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
|
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
|
||||||
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
|
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
|
||||||
// alleen aan het einde van de gevel.
|
// alleen aan het einde van de gevel.
|
||||||
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade) {
|
// skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het
|
||||||
const y0 = skinStartY(trasraam); // boven het trasraam
|
// blootliggende deel van een naadwand boven het dak van de andere unit).
|
||||||
|
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) {
|
||||||
|
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 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;
|
const xL = -CONFIG.wallWidth / 2 - ext.left, xR = CONFIG.wallWidth / 2 + ext.right;
|
||||||
const gridX0 = facade ? -CONFIG.wallWidth / 2 - facade.u0 - (facade.extStart ? 2 * BATTEN.thick : 0) : xL;
|
const gridX0 = facade ? -CONFIG.wallWidth / 2 - facade.u0 - (facade.extStart ? 2 * BATTEN.thick : 0) : xL;
|
||||||
@@ -586,6 +639,10 @@ window.WallModel = (() => {
|
|||||||
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
|
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
|
||||||
const skinExt = opts.skinExt || null;
|
const skinExt = opts.skinExt || null;
|
||||||
const facade = opts.facade || null;
|
const facade = opts.facade || null;
|
||||||
|
const skinFrom = opts.skinFrom > 0 ? opts.skinFrom : 0; // buitenafwerking vanaf (mm), zie computeBattens
|
||||||
|
// 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';
|
||||||
// Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over
|
// Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over
|
||||||
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
|
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
|
||||||
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
|
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
|
||||||
@@ -697,46 +754,21 @@ window.WallModel = (() => {
|
|||||||
horizBeamCount++;
|
horizBeamCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
// C. VOLLEDIGE VERTICALE STIJLEN (incl. de "koningsstijlen" naast elke opening)
|
// C. VERTICALE STIJLEN (vol, koning, dubbel bij deuren, kreupel): elk
|
||||||
calcData.fullStuds.forEach((xPosMM) => {
|
// stuk van y0 tot y1, of tot onder de daklijn (y1 null), zie computeFrame.
|
||||||
const xM = xPosMM * toM;
|
calcData.studs.forEach(st => {
|
||||||
|
const xM = st.x * toM;
|
||||||
const xLeft = xM - (sWidthM / 2);
|
const xLeft = xM - (sWidthM / 2);
|
||||||
const xRight = xM + (sWidthM / 2);
|
const xRight = xM + (sWidthM / 2);
|
||||||
const pts = [{ x: xLeft, y: pThickM }, { x: xRight, y: pThickM }, ...underPts(xLeft, xRight).reverse()];
|
const y0 = st.y0 * toM;
|
||||||
|
const pts = st.y1 === null
|
||||||
|
? [{ x: xLeft, y: y0 }, { x: xRight, y: y0 }, ...underPts(xLeft, xRight).reverse()]
|
||||||
|
: [{ x: xLeft, y: y0 }, { x: xRight, y: y0 }, { x: xRight, y: st.y1 * toM }, { x: xLeft, y: st.y1 * toM }];
|
||||||
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
// D. KREUPELE STIJLEN & DORPELS, per opening
|
// D. DORPELS, per opening
|
||||||
opsM.forEach(op => {
|
opsM.forEach(op => {
|
||||||
calcData.jackStuds
|
|
||||||
.filter(j => j.opening.id === op.id)
|
|
||||||
.forEach(j => {
|
|
||||||
const xM = j.x * toM;
|
|
||||||
const xLeft = xM - (sWidthM / 2);
|
|
||||||
const xRight = xM + (sWidthM / 2);
|
|
||||||
const topPts = underPts(xLeft, xRight);
|
|
||||||
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
|
||||||
|
|
||||||
// Onderste kreupele stijl mag niet door de onderdorpel heen lopen
|
|
||||||
// (die neemt zelf de band net onder op.yMin in beslag).
|
|
||||||
const sillBottomM = op.yMin - sWidthM;
|
|
||||||
if (sillBottomM > pThickM) {
|
|
||||||
const studBotShape = new THREE.Shape();
|
|
||||||
studBotShape.moveTo(xLeft, pThickM);
|
|
||||||
studBotShape.lineTo(xRight, pThickM);
|
|
||||||
studBotShape.lineTo(xRight, sillBottomM);
|
|
||||||
studBotShape.lineTo(xLeft, sillBottomM);
|
|
||||||
studBotShape.closePath();
|
|
||||||
wallGroup3D.add(createExtrudedBeam(studBotShape, sDepthM, -sDepthM / 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualOpTop = op.yMax + sWidthM;
|
|
||||||
if (actualOpTop < yTopLeft || actualOpTop < yTopRight) {
|
|
||||||
const pts = [{ x: xLeft, y: actualOpTop }, { x: xRight, y: actualOpTop }, ...topPts.slice().reverse()];
|
|
||||||
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bovendorpel / linteel (horizontale balk)
|
// Bovendorpel / linteel (horizontale balk)
|
||||||
const opTopShape = new THREE.Shape();
|
const opTopShape = new THREE.Shape();
|
||||||
opTopShape.moveTo(op.xMin, op.yMax);
|
opTopShape.moveTo(op.xMin, op.yMax);
|
||||||
@@ -937,6 +969,11 @@ window.WallModel = (() => {
|
|||||||
|
|
||||||
// Buitenzijde (zuid): cement-gebonden platen (zelfde plaatlogica als
|
// Buitenzijde (zuid): cement-gebonden platen (zelfde plaatlogica als
|
||||||
// hierboven) OF vochtdoek (horizontale banen, zie onder) — nooit beide.
|
// hierboven) OF vochtdoek (horizontale banen, zie onder) — nooit beide.
|
||||||
|
if (spaanplaatBelow) {
|
||||||
|
const spaanOnder = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible, { y0: 0, y1: skinFrom * 0.001 });
|
||||||
|
spaanplaatBuitenAreaM2 = spaanOnder.areaM2;
|
||||||
|
spaanplaatBuitenVolumeM3 = spaanOnder.volumeM3;
|
||||||
|
}
|
||||||
if (buitenMateriaal === 'spaanplaat') {
|
if (buitenMateriaal === 'spaanplaat') {
|
||||||
// Binnenwand: aan beide zijden spaanplaat.
|
// Binnenwand: aan beide zijden spaanplaat.
|
||||||
const spaanBuiten = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible);
|
const spaanBuiten = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible);
|
||||||
@@ -967,7 +1004,7 @@ window.WallModel = (() => {
|
|||||||
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tl.pts.slice().reverse()];
|
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tl.pts.slice().reverse()];
|
||||||
const maxRoofY = tl.maxY;
|
const maxRoofY = tl.maxY;
|
||||||
|
|
||||||
let rowY0 = 0;
|
let rowY0 = skinFrom * toM;
|
||||||
let rowIndex = 0;
|
let rowIndex = 0;
|
||||||
while (rowY0 < maxRoofY - 1e-9) {
|
while (rowY0 < maxRoofY - 1e-9) {
|
||||||
const rowY1 = rowY0 + doekHeightM;
|
const rowY1 = rowY0 + doekHeightM;
|
||||||
@@ -1063,7 +1100,7 @@ window.WallModel = (() => {
|
|||||||
|
|
||||||
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
|
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
|
||||||
// het doek, bovenste laag (haaks op de gevelbekleding) daaroverheen.
|
// het doek, bovenste laag (haaks op de gevelbekleding) daaroverheen.
|
||||||
const bat = computeBattens(CONFIG, cladding, trasraam, skinExt, facade);
|
const bat = computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom);
|
||||||
const bT = BATTEN.thick * toM, bW = BATTEN.width * toM;
|
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;
|
||||||
const addLayer = (orientation, z) => {
|
const addLayer = (orientation, z) => {
|
||||||
@@ -1186,47 +1223,28 @@ window.WallModel = (() => {
|
|||||||
// de hoogste kant van de balk goed op de daklijn te staan, en blijft de
|
// de hoogste kant van de balk goed op de daklijn te staan, en blijft de
|
||||||
// laagste kant er (visueel) los van.
|
// laagste kant er (visueel) los van.
|
||||||
// (outline: ook de knik onder de nok, als de stijl daar staat.)
|
// (outline: ook de knik onder de nok, als de stijl daar staat.)
|
||||||
calcData.fullStuds.forEach(x => {
|
calcData.studs.forEach(st => {
|
||||||
const xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
const x = st.x, xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
||||||
|
const yBottom = st.y0;
|
||||||
|
const kind = st.kind === 'kreupel' ? 'kreupele stijl' : 'verticale balk';
|
||||||
|
if (st.y1 !== null) {
|
||||||
|
pieces.push({
|
||||||
|
length: st.y1 - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
||||||
|
y: (yBottom + st.y1) / 2,
|
||||||
|
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft: st.y1, yTopRight: st.y1
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const topPts = tl.underPts(xLeft, xRight);
|
const topPts = tl.underPts(xLeft, xRight);
|
||||||
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
||||||
const yBottom = CONFIG.plateThick;
|
|
||||||
const length = Math.max(...topPts.map(p => p.y)) - yBottom;
|
|
||||||
pieces.push({
|
pieces.push({
|
||||||
length, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'verticale balk', x,
|
length: Math.max(...topPts.map(p => p.y)) - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
||||||
y: (yBottom + yBottom + yTopLeft + yTopRight) / 4,
|
y: (yBottom + yBottom + yTopLeft + yTopRight) / 4,
|
||||||
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight,
|
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight,
|
||||||
outline: [{ x: xLeft, y: yBottom }, { x: xRight, y: yBottom }, ...topPts.slice().reverse()]
|
outline: [{ x: xLeft, y: yBottom }, { x: xRight, y: yBottom }, ...topPts.slice().reverse()]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
calcData.jackStuds.forEach(j => {
|
|
||||||
const op = j.opening;
|
|
||||||
const opYMin = op.y, opYMax = op.y + op.h;
|
|
||||||
const xLeft = j.x - CONFIG.studWidth / 2, xRight = j.x + CONFIG.studWidth / 2;
|
|
||||||
const topPts = tl.underPts(xLeft, xRight);
|
|
||||||
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
|
||||||
// De kreupele stijl mag niet door de onderdorpel heen lopen — die
|
|
||||||
// neemt zelf al de onderste stijldikte in vanaf opYMin in beslag.
|
|
||||||
const sillBottom = opYMin - CONFIG.studWidth;
|
|
||||||
if (sillBottom > CONFIG.plateThick) {
|
|
||||||
pieces.push({
|
|
||||||
length: sillBottom - CONFIG.plateThick, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'kreupele stijl', x: j.x,
|
|
||||||
y: (CONFIG.plateThick + sillBottom) / 2,
|
|
||||||
yBottomLeft: CONFIG.plateThick, yBottomRight: CONFIG.plateThick, yTopLeft: sillBottom, yTopRight: sillBottom
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const actualOpTop = opYMax + CONFIG.studWidth;
|
|
||||||
if (actualOpTop < yTopLeft || actualOpTop < yTopRight) {
|
|
||||||
pieces.push({
|
|
||||||
length: Math.max(...topPts.map(p => p.y)) - actualOpTop, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'kreupele stijl', x: j.x,
|
|
||||||
y: (actualOpTop + actualOpTop + yTopLeft + yTopRight) / 4,
|
|
||||||
yBottomLeft: actualOpTop, yBottomRight: actualOpTop, yTopLeft, yTopRight,
|
|
||||||
outline: [{ x: xLeft, y: actualOpTop }, { x: xRight, y: actualOpTop }, ...topPts.slice().reverse()]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
const opXWorld = startX + op.x;
|
const opXWorld = startX + op.x;
|
||||||
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + op.h + CONFIG.studWidth / 2 });
|
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + op.h + CONFIG.studWidth / 2 });
|
||||||
|
|||||||
+2
-2
@@ -80,7 +80,7 @@ window.WallPdf = (() => {
|
|||||||
doc.text('Isolatie: glaswol (spouwvullend)', tblX[0], my);
|
doc.text('Isolatie: glaswol (spouwvullend)', tblX[0], my);
|
||||||
if (buitenMateriaal === 'vochtdoek') {
|
if (buitenMateriaal === 'vochtdoek') {
|
||||||
// Kruislat op het vochtdoek (zelfde berekening als de 3D: WallModel.computeBattens).
|
// Kruislat op het vochtdoek (zelfde berekening als de 3D: WallModel.computeBattens).
|
||||||
const bat = WallModel.computeBattens(CONFIG, opts.cladding || 'horizontaal', opts.trasraam, opts.skinExt, opts.facade);
|
const bat = WallModel.computeBattens(CONFIG, opts.cladding || 'horizontaal', opts.trasraam, opts.skinExt, opts.facade, opts.skinFrom);
|
||||||
const B = WallModel.BATTEN;
|
const B = WallModel.BATTEN;
|
||||||
my += 4.5;
|
my += 4.5;
|
||||||
doc.text(`Latwerk ${B.thick}x${B.width}: ${bat.stock.count} st. \u00e0 ${B.length} mm, kruislat ${B.spacing} h.o.h.`, tblX[0], my);
|
doc.text(`Latwerk ${B.thick}x${B.width}: ${bat.stock.count} st. \u00e0 ${B.length} mm, kruislat ${B.spacing} h.o.h.`, tblX[0], my);
|
||||||
@@ -251,7 +251,7 @@ window.WallPdf = (() => {
|
|||||||
// Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis.
|
// Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis.
|
||||||
doc.setDrawColor(...RED);
|
doc.setDrawColor(...RED);
|
||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
const studXs = calcData.fullStuds.concat(calcData.jackStuds.map(j => j.x));
|
const studXs = [...new Set(calcData.studs.map(st => st.x))];
|
||||||
studXs.forEach(x => {
|
studXs.forEach(x => {
|
||||||
const x0 = px(x - CONFIG.studWidth / 2), x1 = px(x + CONFIG.studWidth / 2);
|
const x0 = px(x - CONFIG.studWidth / 2), x1 = px(x + CONFIG.studWidth / 2);
|
||||||
doc.rect(x0, tvFrame0, x1 - x0, tvFrame1 - tvFrame0, 'S');
|
doc.rect(x0, tvFrame0, x1 - x0, tvFrame1 - tvFrame0, 'S');
|
||||||
|
|||||||
+16
-14
@@ -274,7 +274,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
// Dichtheden/gewichten: zie WallModel (wall/wall-model.js).
|
// Dichtheden/gewichten: zie WallModel (wall/wall-model.js).
|
||||||
|
|
||||||
let CONFIG = {};
|
let CONFIG = {};
|
||||||
let calcData = { fullStuds: [], jackStuds: [] };
|
let calcData = { fullStuds: [], studs: [] };
|
||||||
let wallGroup3D = null;
|
let wallGroup3D = null;
|
||||||
let lastBaseHeight = 2400;
|
let lastBaseHeight = 2400;
|
||||||
|
|
||||||
@@ -759,7 +759,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
readInputs();
|
readInputs();
|
||||||
calcData = WallModel.computeFrame(CONFIG);
|
calcData = WallModel.computeFrame(CONFIG);
|
||||||
|
|
||||||
setSpecAll('vert-beams', (calcData.fullStuds.length + calcData.jackStuds.length) + ' stuks');
|
setSpecAll('vert-beams', calcData.studs.length + ' stuks');
|
||||||
setSpecAll('roof-angle', calcData.angleDeg + '°');
|
setSpecAll('roof-angle', calcData.angleDeg + '°');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,8 +811,10 @@ function mountWallEditor(container, callbacks) {
|
|||||||
ctx.strokeRect(screenX, screenY, studWidthPx, studDepthPx);
|
ctx.strokeRect(screenX, screenY, studWidthPx, studDepthPx);
|
||||||
};
|
};
|
||||||
|
|
||||||
calcData.fullStuds.forEach(x => drawStud2D(x, false));
|
// Bovenaanzicht: één doorsnede per stijlpositie (kreupel alleen als er
|
||||||
calcData.jackStuds.forEach(j => drawStud2D(j.x, true));
|
// op die plek geen volle stijl staat).
|
||||||
|
const fullXs = new Set(calcData.studs.filter(st => st.kind !== 'kreupel').map(st => st.x));
|
||||||
|
[...new Set(calcData.studs.map(st => st.x))].forEach(x => drawStud2D(x, !fullXs.has(x)));
|
||||||
|
|
||||||
const startX = -CONFIG.wallWidth / 2;
|
const startX = -CONFIG.wallWidth / 2;
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
@@ -949,22 +951,22 @@ function mountWallEditor(container, callbacks) {
|
|||||||
ctxFront.textAlign = 'start';
|
ctxFront.textAlign = 'start';
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawStudFront = (xMM, isJack) => {
|
// Stijlstuk van y0 tot y1 (of tot de daklijn), zie WallModel.computeFrame.
|
||||||
const xLeft = xMM - CONFIG.studWidth / 2;
|
const drawStudFront = (st) => {
|
||||||
const xRight = xMM + CONFIG.studWidth / 2;
|
const xLeft = st.x - CONFIG.studWidth / 2;
|
||||||
const yBot = CONFIG.plateThick;
|
const xRight = st.x + CONFIG.studWidth / 2;
|
||||||
ctxFront.fillStyle = isJack ? '#a37c45' : '#c29b62';
|
ctxFront.fillStyle = st.kind === 'kreupel' ? '#a37c45' : '#c29b62';
|
||||||
ctxFront.strokeStyle = '#8a6836';
|
ctxFront.strokeStyle = '#8a6836';
|
||||||
ctxFront.beginPath();
|
ctxFront.beginPath();
|
||||||
ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(yBot));
|
ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(st.y0));
|
||||||
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(yBot));
|
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(st.y0));
|
||||||
tl.underPts(xLeft, xRight).reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y)));
|
if (st.y1 === null) tl.underPts(xLeft, xRight).reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y)));
|
||||||
|
else { ctxFront.lineTo(toCanvasX(xRight), toCanvasY(st.y1)); ctxFront.lineTo(toCanvasX(xLeft), toCanvasY(st.y1)); }
|
||||||
ctxFront.closePath();
|
ctxFront.closePath();
|
||||||
ctxFront.fill();
|
ctxFront.fill();
|
||||||
ctxFront.stroke();
|
ctxFront.stroke();
|
||||||
};
|
};
|
||||||
calcData.fullStuds.forEach(x => drawStudFront(x, false));
|
calcData.studs.forEach(drawStudFront);
|
||||||
calcData.jackStuds.forEach(j => drawStudFront(j.x, true));
|
|
||||||
|
|
||||||
const startXMM = -halfWMM;
|
const startXMM = -halfWMM;
|
||||||
frontOpeningRects = [];
|
frontOpeningRects = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user