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
|
||||
js/wall/ WallModel (logica) + wand-PDF + wand-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/sandwich/ SandwichModel: sandwichpanelen rondom
|
||||
js/unit/ UnitModel: 1 vloer + 4 wanden, naden, wandstukken
|
||||
@@ -47,19 +47,30 @@ assets/ logo, texturen, skybox
|
||||
|
||||
## Functionaliteit
|
||||
|
||||
- **Wand:** openingen (raam/deur/voordeur) met ankers, regelwerk 38x235 met
|
||||
dubbele hoekstijlen, zaagplan op handelslengtes, spaanplaat binnen,
|
||||
vochtdoek (met kruislat en trasraam) of cementplaat buiten.
|
||||
- **Wand:** openingen (raam/deur/voordeur) met ankers, ook boven elkaar
|
||||
(raam boven een deur; stijlen ertussen van dorpel tot dorpel), regelwerk
|
||||
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
|
||||
een unit bepaalt de laatst gewijzigde wand de dakvorm: de tegenoverliggende
|
||||
wand spiegelt mee, de haakse wanden worden vlak op de hoekhoogtes.
|
||||
- **Vloer:** balken, cirkelvormige doorvoeren, OSB, optioneel chassis.
|
||||
- **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op
|
||||
naden vervallen de buitenwanden en komt een naadwand in twee delen (één per
|
||||
unit, met deur). Binnenwanden teken je in de plattegrond.
|
||||
- **Dak:** plat dak per unit (vloer gespiegeld + dakleer), dakrand als één
|
||||
doorlopende rand met boeidelen of daktrim. Daklogica voor schuine
|
||||
wanden/nok volgt nog.
|
||||
naden vervallen de buitenwanden en komt een naadwand met deur: midden op
|
||||
de naad in twee delen (één per unit), of als één binnenwand geheel in unit
|
||||
A of B. De hoogte volgt per deel de daklijn van zijn unit; steekt een deel
|
||||
boven het dak van de andere unit uit, dan krijgt dat stuk buitenafwerking
|
||||
(vochtdoek + latwerk) die met de gevel meeloopt.
|
||||
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;
|
||||
sandwichpanelen rondom, 600 mm de grond in.
|
||||
- **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld,
|
||||
@@ -72,7 +83,8 @@ assets/ logo, texturen, skybox
|
||||
## Bekende beperkingen
|
||||
|
||||
- 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).
|
||||
- Eigen OrbitControls: roteren/zoomen/pannen, geen damping.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* 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.
|
||||
|
||||
let tabs = [];
|
||||
|
||||
+126
-51
@@ -303,15 +303,16 @@ window.HouseModel = (() => {
|
||||
// { u0, extStart, isEnd } (zie WallModel.computeBattens): u0 = afstand
|
||||
// langs het wandvlak van het gevelbegin (links, van buitenaf gezien, incl.
|
||||
// hoekdoorloop van het eerste stuk) tot de linkerrand van het stuk.
|
||||
function assignFacades(units) {
|
||||
// pieces: buitenwandstukken + naadwanddelen met een blootliggend buitendeel.
|
||||
function assignFacades(pieces) {
|
||||
const segs = [];
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
||||
pieces.forEach(p => {
|
||||
const W = p.place.width, T = p.place.thickness;
|
||||
const normal = { x: p.place.normal.x, z: p.place.normal.y };
|
||||
const axis = Math.abs(normal.z) > 0.5 ? 'x' : 'z', perp = axis === 'x' ? 'z' : 'x';
|
||||
const q0 = UnitModel.localToWorld(p.place, -W / 2, T / 2), q1 = UnitModel.localToWorld(p.place, W / 2, T / 2);
|
||||
segs.push({ piece: p, axis, normal, line: q0[perp], sAt0: q0[axis], sAtW: q1[axis], s0: Math.min(q0[axis], q1[axis]), s1: Math.max(q0[axis], q1[axis]) });
|
||||
})));
|
||||
});
|
||||
groupFacadeRuns(segs).forEach(run => {
|
||||
const dir = Math.sign(run.segs[0].sAtW - run.segs[0].sAt0) || 1;
|
||||
const sLeft = dir > 0 ? run.s0 : run.s1, sRight = dir > 0 ? run.s1 : run.s0;
|
||||
@@ -333,10 +334,10 @@ window.HouseModel = (() => {
|
||||
const wallThickness = (id, side) => WallModel.thicknessOf(WallModel.configFromState(state.units.find(u => u.id === id).unit.walls[side]));
|
||||
const { cuts, seams } = computeSeams(rects, wallThickness);
|
||||
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade) {
|
||||
function buildWall(config, frame, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow) {
|
||||
if (!build) return { group: null, stats: null, specs: null };
|
||||
const built = WallModel.build3D(config, frame, {
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade,
|
||||
materials: o.wallMaterials, buitenMateriaal, cladding, trasraam, skinExt, facade, skinFrom, spaanplaatBelow,
|
||||
spaanplaatVisible: o.beplatingVisible !== false, buitenVisible: o.beplatingVisible !== false
|
||||
});
|
||||
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
|
||||
@@ -372,21 +373,33 @@ window.HouseModel = (() => {
|
||||
return Object.assign({}, wl, { pieces, dropped: wp.dropped, removed: pieces.length === 0 });
|
||||
});
|
||||
|
||||
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), op de hoogste
|
||||
// wandbovenkant van deze unit.
|
||||
const roofData = FloorModel.computeFromState(u.roof.deck, rims);
|
||||
const roofSpecs = RoofModel.computeSpecs(roofData.calcData, roofData.resolvedOpenings);
|
||||
const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => UnitModel.wallTopOf(ws)));
|
||||
const roofW = roofData.calcData.width, roofD = roofData.calcData.depth;
|
||||
const roofCx = rect.cx + (u.roof.offsetX || 0), roofCz = rect.cz + (u.roof.offsetZ || 0);
|
||||
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
|
||||
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape).
|
||||
const shape = RoofModel.computeShape(u.unit, layout);
|
||||
const deckFrame = FloorModel.computeFrame(u.roof.deck.floor);
|
||||
const roofW = deckFrame.width, roofD = deckFrame.depth;
|
||||
const ox = u.roof.offsetX || 0, oz = u.roof.offsetZ || 0;
|
||||
const ur = RoofModel.computeUnitRoof(u.roof, shape, { x0: ox - roofW / 2, x1: ox + roofW / 2, z0: oz - roofD / 2, z1: oz + roofD / 2 }, rims);
|
||||
const roofCx = rect.cx + ox, roofCz = rect.cz + oz;
|
||||
const shapeLabel = { plat: 'plat', lessenaar: 'lessenaar', zadel: 'zadel' }[shape.type];
|
||||
const multi = ur.planes.length > 1;
|
||||
ur.planes.forEach((pl, i) => {
|
||||
pl.element = `${u.id}-D${i + 1}`;
|
||||
const deg = Math.round(Math.abs(pl.theta) * 180 / Math.PI * 10) / 10;
|
||||
pl.label = `Unit ${u.id} dak${multi ? ' deel ' + (i + 1) : ''} (${shapeLabel}${deg ? ', ' + String(deg).replace('.', ',') + '°' : ''})`;
|
||||
});
|
||||
const roof = {
|
||||
element: `${u.id}-D1`, data: roofData, specs: roofSpecs,
|
||||
element: `${u.id}-D1`, shape, planes: ur.planes, specs: ur.specs,
|
||||
rect: { x0: roofCx - roofW / 2, x1: roofCx + roofW / 2, z0: roofCz - roofD / 2, z1: roofCz + roofD / 2, cx: roofCx, cz: roofCz, W: roofW, D: roofD },
|
||||
base: layout.floorTop + wallTop,
|
||||
top: layout.floorTop + wallTop + RoofModel.thickness(roofData.calcData),
|
||||
base: layout.floorTop, // 3D: group-Y = 0 op het loopvlak
|
||||
// Onderkant plafond resp. bovenkant dakleer op (x, z) (woning-mm, absoluut).
|
||||
ceilingAt: (x, z) => layout.floorTop + ur.undersideAt(x - rect.cx, z - rect.cz),
|
||||
topAt: (x, z) => layout.floorTop + ur.topAt(x - rect.cx, z - rect.cz),
|
||||
ridges: ur.ridges.map(r => ({ axis: r.axis, value: r.value + (r.axis === 'x' ? rect.cx : rect.cz) })),
|
||||
top: layout.floorTop + ur.maxTop,
|
||||
// Zichtbaarheid zoals wanden/vloeren: plafondplaat + dakleer (EPDM)
|
||||
// horen bij "Beplating".
|
||||
group: build ? RoofModel.build3D(roofData.calcData, roofData.resolvedOpenings, {
|
||||
group: build ? RoofModel.buildUnit3D(ur, {
|
||||
materials: o.roofMaterials,
|
||||
osbVisible: o.beplatingVisible !== false,
|
||||
dakleerVisible: o.beplatingVisible !== false
|
||||
@@ -399,12 +412,6 @@ window.HouseModel = (() => {
|
||||
};
|
||||
});
|
||||
|
||||
// Doek en latwerk volgen de hele gevel (niet per unit/wandstuk).
|
||||
assignFacades(units);
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
|
||||
Object.assign(p, buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade));
|
||||
})));
|
||||
|
||||
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
|
||||
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
|
||||
const interior = [];
|
||||
@@ -418,23 +425,36 @@ window.HouseModel = (() => {
|
||||
const config = WallModel.configFromState(iw.wall);
|
||||
const frame = WallModel.computeFrame(config);
|
||||
const buitenMateriaal = (iw.wall.materials || {}).exterior || 'spaanplaat';
|
||||
interior.push(Object.assign({
|
||||
interior.push({
|
||||
id: iw.id, element: `BW-${bwNr}`, label: `Binnenwand ${bwNr}`, config, frame, buitenMateriaal,
|
||||
place: { cx: lay.cx, cz: lay.cz, rotY: lay.rotY, width: lay.width, thickness: WallModel.thicknessOf(config), floorTop }
|
||||
}, buildWall(config, frame, buitenMateriaal)));
|
||||
});
|
||||
});
|
||||
|
||||
// Twee delen van een naadwand. Elk deel staat met zijn regelwerk op de
|
||||
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit, en loopt van
|
||||
// wand tot wand: waar een buitenwandstuk de hartlijn van het deel kruist,
|
||||
// stopt het (voetafdrukken, zoals bij de gevellampen).
|
||||
// naad (buitenzijde 'geen' = naadzijde) in zijn eigen unit. Een deel loopt van wand tot wand: waar een buitenwandstuk de hartlijn
|
||||
// van het deel kruist, stopt het (voetafdrukken, zoals bij de
|
||||
// gevellampen). De hoogte volgt de daklijn van de buitenwand van de unit
|
||||
// aan de naadzijde (RoofModel.wallProfile), zodat het deel onder het dak
|
||||
// van zijn eigen unit past. Steekt een deel boven het dak van de andere
|
||||
// unit uit (bijv. hoge unit tegen een lage), dan krijgt dat stuk van de
|
||||
// naadzijde buitenafwerking (vochtdoek + latwerk, skinFrom) die met de
|
||||
// gevel van de woning meeloopt; het regelwerk blijft dat van de naadwand.
|
||||
function seamLeaves(iw, nr) {
|
||||
const sm = seams.find(x => sameSeam(x, iw.seam.a, iw.seam.b));
|
||||
if (!sm) return []; // units niet (meer) tegen elkaar: naadwand vervalt
|
||||
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
|
||||
// Buitenwand van de unit aan de naadzijde (side +1: unit a, lage kant).
|
||||
const seamSide = (side) => horiz ? (side > 0 ? 'achter' : 'voor') : (side > 0 ? 'rechts' : 'links');
|
||||
const unitState = (uid) => state.units.find(u => u.id === uid).unit;
|
||||
const baseConfig = WallModel.configFromState(iw.wall);
|
||||
const maxD = Math.max(baseConfig.studDepth, baseConfig.plateDepth);
|
||||
const T1 = WallModel.thicknessOf(baseConfig, 'geen');
|
||||
// Plaats (iw.seam.in): leeg = midden op de naad (twee delen), 'a'/'b' =
|
||||
// geheel in die unit: één binnenwand (beplating aan beide zijden) tegen
|
||||
// de naad aan, met de hoogte van de daklijn van die unit.
|
||||
const inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
|
||||
const T2 = WallModel.thicknessOf(baseConfig, 'spaanplaat');
|
||||
const extPieces = [];
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
|
||||
const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
|
||||
@@ -444,7 +464,9 @@ window.HouseModel = (() => {
|
||||
const leaves = [];
|
||||
[[sm.a, 1], [sm.b, -1]].forEach(([uid, side]) => {
|
||||
// side +1: unit a ligt aan de lage kant, de naad aan zijn +kant.
|
||||
const line = seamPerp - side * T1 / 2;
|
||||
if (inUnit && uid !== inUnit) return;
|
||||
const T = inUnit ? T2 : T1;
|
||||
const line = seamPerp - side * T / 2;
|
||||
const blocked = [];
|
||||
extPieces.forEach(p => {
|
||||
const cs = [[-1, -1], [1, -1], [-1, 1], [1, 1]].map(([i, j]) => UnitModel.localToWorld(p.place, i * p.place.width / 2, j * p.place.thickness / 2));
|
||||
@@ -461,18 +483,49 @@ window.HouseModel = (() => {
|
||||
const width = Math.round(best[1] - best[0]), mid = (best[0] + best[1]) / 2;
|
||||
const rotY = horiz ? (side > 0 ? 0 : Math.PI) : (side > 0 ? Math.PI / 2 : -Math.PI / 2);
|
||||
const place = {
|
||||
cx: horiz ? mid : line, cz: horiz ? line : mid, rotY, width, thickness: T1,
|
||||
cx: horiz ? mid : line, cz: horiz ? line : mid, rotY, width, thickness: T,
|
||||
normal: { x: Math.round(Math.sin(rotY)), y: Math.round(Math.cos(rotY)) }, // naadzijde
|
||||
floorTop: units.find(u => u.id === uid).layout.floorTop
|
||||
};
|
||||
leaves.push({ uid, side, place });
|
||||
});
|
||||
if (!leaves.length) return [];
|
||||
|
||||
// Hoogtes van een deel: daklijn van de buitenwand aan de naadzijde, op
|
||||
// de uiteinden van het deel (incl. nok als die erin valt).
|
||||
const heightsOf = (lf) => {
|
||||
const u = units.find(x => x.id === lf.uid);
|
||||
const prof = RoofModel.wallProfile(unitState(lf.uid), u.layout, seamSide(lf.side));
|
||||
const axisOf = (q) => horiz ? q.x - u.rect.cx : q.z - u.rect.cz;
|
||||
const W = lf.place.width;
|
||||
const pL = axisOf(UnitModel.localToWorld(lf.place, -W / 2, 0)), pR = axisOf(UnitModel.localToWorld(lf.place, W / 2, 0));
|
||||
const peak = prof.length > 2 ? prof[1] : null;
|
||||
const hasPeak = !!peak && peak.p > Math.min(pL, pR) + 1 && peak.p < Math.max(pL, pR) - 1;
|
||||
return {
|
||||
heightLeft: Math.round(RoofModel.profileHeight(prof, pL)), heightRight: Math.round(RoofModel.profileHeight(prof, pR)),
|
||||
heightMid: hasPeak ? Math.round(peak.h) : null, peakX: hasPeak ? Math.abs(peak.p - pL) : undefined
|
||||
};
|
||||
};
|
||||
const paramsOf = (lf) => Object.assign({}, iw.wall.wall, { width: lf.place.width }, heightsOf(lf));
|
||||
// Laagste bovenkant dak van de andere unit langs dit deel (mm vanaf het
|
||||
// loopvlak van het deel), of null als het deel er niet bovenuit steekt.
|
||||
const exposedFrom = (lf, config) => {
|
||||
const other = units.find(u => u.id === (lf.uid === sm.a ? sm.b : sm.a));
|
||||
const W = lf.place.width;
|
||||
const ends = [-W / 2, W / 2].map(lx => along(UnitModel.localToWorld(lf.place, lx, 0)));
|
||||
const lo = Math.min(...ends), hi = Math.max(...ends);
|
||||
const ts = ends.concat(other.roof.ridges.filter(r => r.axis === (horiz ? 'x' : 'z') && r.value > lo && r.value < hi).map(r => r.value));
|
||||
const from = Math.min(...ts.map(t => horiz ? other.roof.topAt(t, seamPerp) : other.roof.topAt(seamPerp, t))) - lf.place.floorTop;
|
||||
return from < WallModel.topLine(config).maxY - 50 ? Math.max(0, Math.round(from)) : null;
|
||||
};
|
||||
|
||||
// Openingen: opgeslagen voor het eerste deel; het andere deel krijgt
|
||||
// dezelfde openingen op dezelfde plek in de wereld (gespiegeld).
|
||||
const first = leaves[0];
|
||||
iw.wall.wall.width = first.place.width; // voor de wand-editor
|
||||
const firstConfig = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: first.place.width }), iw.wall.openings || []);
|
||||
const firstParams = paramsOf(first);
|
||||
// Voor de wand-editor: breedte en daklijn van het eerste deel.
|
||||
Object.assign(iw.wall.wall, { width: first.place.width, heightLeft: firstParams.heightLeft, heightRight: firstParams.heightRight, heightMid: firstParams.heightMid });
|
||||
const firstConfig = WallModel.buildConfig(firstParams, iw.wall.openings || []);
|
||||
const toLocalU = (pl, q) => {
|
||||
const c = Math.cos(pl.rotY), sn = Math.sin(pl.rotY);
|
||||
return (q.x - pl.cx) * c - (q.z - pl.cz) * sn + pl.width / 2;
|
||||
@@ -486,32 +539,52 @@ window.HouseModel = (() => {
|
||||
const u = Math.min(toLocalU(lf.place, wa), toLocalU(lf.place, wb));
|
||||
return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: Math.round(u) };
|
||||
});
|
||||
config = WallModel.buildConfig(Object.assign({}, iw.wall.wall, { width: lf.place.width }), ops);
|
||||
config = WallModel.buildConfig(paramsOf(lf), ops);
|
||||
}
|
||||
const frame = WallModel.computeFrame(config);
|
||||
const built = buildWall(config, frame, 'geen');
|
||||
if (built.group) {
|
||||
// Het regelwerk staat gecentreerd op lokaal z = 0, de beplating
|
||||
// alleen aan de achterzijde: schuif zodat het midden van de totale
|
||||
// dikte op place.cx/cz ligt (regelwerk op de naad).
|
||||
const outer = new THREE.Group();
|
||||
built.group.position.z = (T1 - maxD) / 2 * 0.001;
|
||||
outer.add(built.group);
|
||||
built.group = outer;
|
||||
}
|
||||
return Object.assign({
|
||||
const skinFrom = exposedFrom(lf, config);
|
||||
return {
|
||||
id: iw.id, seam: iw.seam, seamUnit: lf.uid,
|
||||
element: `NW-${nr}${lf.uid}`, label: `Naadwand ${nr} (unit ${lf.uid})`,
|
||||
config, frame, buitenMateriaal: 'geen', place: lf.place
|
||||
}, built);
|
||||
config, frame, place: lf.place,
|
||||
// Het regelwerk staat gecentreerd op lokaal z = 0, de beplating
|
||||
// alleen aan de achterzijde: bij het bouwen schuiven zodat het
|
||||
// midden van de totale dikte op place.cx/cz ligt (regelwerk op de naad).
|
||||
// Geheel in één unit: beplating aan beide zijden, dus symmetrisch.
|
||||
shiftZ: inUnit ? 0 : (T1 - maxD) / 2,
|
||||
...(skinFrom === null
|
||||
? { buitenMateriaal: inUnit ? 'spaanplaat' : 'geen' }
|
||||
: { buitenMateriaal: 'vochtdoek', skinFrom, spaanplaatBelow: !!inUnit, cladding: state.settings.cladding || 'horizontaal', trasraam: null })
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Doek en latwerk volgen de hele gevel (niet per unit/wandstuk), ook
|
||||
// over het blootliggende deel van een naadwand. Daarna pas bouwen.
|
||||
const extPieces = [];
|
||||
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
|
||||
assignFacades(extPieces.concat(interior.filter(p => p.skinFrom !== undefined)));
|
||||
extPieces.concat(interior).forEach(p => {
|
||||
const built = buildWall(p.config, p.frame, p.buitenMateriaal, p.cladding, p.trasraam, p.skinExt, p.facade, p.skinFrom, p.spaanplaatBelow);
|
||||
if (built.group && p.shiftZ) {
|
||||
const outer = new THREE.Group();
|
||||
built.group.position.z = p.shiftZ * 0.001;
|
||||
outer.add(built.group);
|
||||
built.group = outer;
|
||||
}
|
||||
Object.assign(p, built);
|
||||
});
|
||||
|
||||
// Dakrand (boeidelen/daktrim) als één doorlopende rand om alle daken;
|
||||
// boeidelen aan/uit per unit.
|
||||
// boeidelen aan/uit per unit. De rand volgt de dakvorm van de unit waar
|
||||
// de ribbe bij hoort; platte daken delen één randhoogte (de hoogste).
|
||||
const flatTops = units.filter(u => u.roof.shape.type === 'plat').map(u => u.roof.top);
|
||||
const flatTop = flatTops.length ? Math.max(...flatTops) : 0;
|
||||
const roofEdge = units.length ? RoofModel.computeEdge(state.settings.roof,
|
||||
units.map(u => ({ unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false })),
|
||||
Math.max(...units.map(u => u.roof.top))) : null;
|
||||
units.map(u => ({
|
||||
unitId: u.id, rect: u.roof.rect, fascia: state.units.find(su => su.id === u.id).roof.fascia !== false,
|
||||
topAt: u.roof.shape.type === 'plat' ? () => flatTop : u.roof.topAt, ridges: u.roof.ridges
|
||||
}))) : null;
|
||||
// Buitenverlichting: gevellampen per wandstuk + boeideel-spots (vult roofEdge.spots).
|
||||
const lighting = LightingModel.compute(state.settings.lighting, state.settings.roof, units, state.units, roofEdge);
|
||||
// Zichtbaarheid: boeideel/daktrim volgt "Beplating", spots en lampen volgen "Lampen".
|
||||
@@ -561,9 +634,11 @@ window.HouseModel = (() => {
|
||||
const fc = u.floorData.calcData;
|
||||
fc.joists.forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.joistLen));
|
||||
['front', 'back'].filter(k => fc.rims[k]).forEach(() => addPiece('vloerbalk', fc.joistW, fc.joistH, fc.width));
|
||||
const rc = u.roof.data.calcData;
|
||||
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen));
|
||||
['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width));
|
||||
u.roof.planes.forEach(pl => {
|
||||
const rc = pl.data.calcData;
|
||||
rc.joists.forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.joistLen));
|
||||
['front', 'back'].filter(k => rc.rims[k]).forEach(() => addPiece('dakbalk', rc.joistW, rc.joistH, rc.width));
|
||||
});
|
||||
});
|
||||
const wallBom = [];
|
||||
pieces.forEach(p => WallModel.computeBOM(p.config, p.frame).forEach(b => { wallBom.push(b); addPiece(b.kind, b.width, b.depth, b.length); }));
|
||||
|
||||
@@ -39,7 +39,7 @@ window.HousePdf = (() => {
|
||||
parts.units.forEach(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 })));
|
||||
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 }));
|
||||
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}` };
|
||||
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 === 'roof') FloorPdf.drawSheet(doc, sheet.unit.roof.data.calcData, sheet.unit.roof.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 === '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, skinFrom: sheet.piece.skinFrom, lamps: sheet.piece.lamps || [] }, opts));
|
||||
else drawTotalsSheet(doc, totals, opts);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const leaves = parts.interior.filter(x => x.id === iw.id);
|
||||
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.`)) +
|
||||
cols(
|
||||
section('Opbouw', 'layers',
|
||||
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)),
|
||||
section('Opbouw', 'layers', sel('hi-seam-in', 'Plaats', plaatsen, iw.seam.in || 'midden') + dikte),
|
||||
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(
|
||||
button('Openingen bewerken', 'pencil-square', `data-go="iw:${iw.id}"`),
|
||||
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.onNum('hi-height', v => { if (v >= 500) iw.wall.wall.heightAll = iw.wall.wall.heightLeft = iw.wall.wall.heightRight = v; }, after);
|
||||
UI.onSel('hi-seam-in', v => { if (v === 'midden') delete iw.seam.in; else iw.seam.in = v; }, rerender);
|
||||
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
|
||||
|
||||
@@ -212,10 +212,13 @@ window.LightingModel = (() => {
|
||||
const spots = [];
|
||||
if (s.mode === 'spots') {
|
||||
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;
|
||||
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
|
||||
@@ -223,7 +226,7 @@ window.LightingModel = (() => {
|
||||
edge.spots = [];
|
||||
spots.forEach(p => {
|
||||
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 };
|
||||
});
|
||||
}
|
||||
@@ -245,7 +248,7 @@ window.LightingModel = (() => {
|
||||
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
|
||||
// [{ unitId, x, z, y, floorY }] (mm, y = onderkant plafond, floorY = loopvlak).
|
||||
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 };
|
||||
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;
|
||||
@@ -330,7 +333,7 @@ window.LightingModel = (() => {
|
||||
// gekanteld: de binnenrand van de bundel loopt verticaal langs de gevel;
|
||||
// - binnenspots hebben een harde reikwijdte net voorbij de vloer
|
||||
// (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-
|
||||
// lampen (overdag lager: dan vallen ze nauwelijks op).
|
||||
const OUTDOOR = { lamp: 2.0, spot: 1.6, angle: 0.5 };
|
||||
@@ -359,7 +362,7 @@ window.LightingModel = (() => {
|
||||
// hoek diagonaal).
|
||||
(spots || []).forEach(p => {
|
||||
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 => {
|
||||
const drop = (p.y - p.floorY) * toM; // plafond -> loopvlak
|
||||
|
||||
+272
-40
@@ -5,14 +5,20 @@
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
// ============================================================
|
||||
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica (voorlopig
|
||||
// alleen plat dak). Geen DOM.
|
||||
// roof/roof-model.js — RoofModel: ENIGE bron van de dak-logica. 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
|
||||
// onderzijde (plafond), de balken erboven, met dakleer als buitenlaag
|
||||
// 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
|
||||
// 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
|
||||
@@ -114,6 +120,147 @@ window.RoofModel = (() => {
|
||||
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
|
||||
// ------------------------------------------------------------
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool }] (mm, woning-
|
||||
// assen); topY = bovenkant dakleer (mm). Geeft per ribbe (segments) de
|
||||
// geometrie terug; edge.spots wordt door LightingModel gevuld.
|
||||
function computeEdge(settings, roofs, topY) {
|
||||
// roofs: [{ unitId, rect: { x0, x1, z0, z1 }, fascia: bool, topAt(x, z),
|
||||
// ridges: [{ axis, value }] }] (mm, woning-assen); topAt = bovenkant
|
||||
// dakleer (absoluut, loopt buiten het dak door met de helling). Geeft per
|
||||
// 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 rects = roofs.map(r => r.rect);
|
||||
const loops = rectUnionLoops(rects);
|
||||
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;
|
||||
if (!roofs.length) return null;
|
||||
|
||||
const segments = 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 = roofs.find(r => onRectEdge(r.rect, mid)) || roofs[0];
|
||||
const fascia = owner.fascia !== false;
|
||||
const off = fascia ? offBoei : offTrim;
|
||||
return {
|
||||
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
|
||||
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j]
|
||||
};
|
||||
// Hoogtegroepen: daken die tegen elkaar liggen horen bij één doorlopende
|
||||
// rand, tenzij het hoogteverschil langs de hele naad minstens de
|
||||
// boeideelhoogte is. Dan krijgt het hoge dak een eigen rand rondom (ook
|
||||
// boven het lage dak), en loopt de rand van het lage dak tot de gevel
|
||||
// van het hoge.
|
||||
const group = roofs.map((r, i) => i);
|
||||
const find = (i) => group[i] === i ? i : (group[i] = find(group[i]));
|
||||
const sharedEdge = (A, B) => {
|
||||
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 top = Math.max(...segments.map(sg => Math.max(sg.topAt(sg.a), sg.topAt(sg.b))));
|
||||
return {
|
||||
base, segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
||||
segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
|
||||
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),
|
||||
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,
|
||||
@@ -186,9 +385,40 @@ window.RoofModel = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
// 3D van de dakrand: per ribbe een geëxtrudeerd vierhoek (dakrand ->
|
||||
// buitenrand, in verstek), naar beneden over de hoogte van boeideel resp.
|
||||
// daktrim. Plus de boeideel-spots (edge.spots) in de onderplaat.
|
||||
// Convexe veelhoek (plattegrond, mm) knippen met een halfvlak
|
||||
// (Sutherland-Hodgman): houd de punten waar f(p) >= 0.
|
||||
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") }
|
||||
function buildEdge3D(edge, materials, opts) {
|
||||
const o = opts || {};
|
||||
@@ -196,23 +426,24 @@ window.RoofModel = (() => {
|
||||
const group = new THREE.Group();
|
||||
if (!edge) return group;
|
||||
edge.segments.forEach(sg => {
|
||||
const pts = [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)));
|
||||
let polys = [[sg.a, sg.b, sg.outerB, sg.outerA]];
|
||||
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 mesh = new THREE.Mesh(
|
||||
new THREE.ExtrudeGeometry(shape, { steps: 1, depth: h * toM, bevelEnabled: false }),
|
||||
sg.fascia ? materials.fasciaMaterial : materials.trimMaterial
|
||||
);
|
||||
mesh.rotation.x = Math.PI / 2; // shape-y -> wereld-Z, extrusie naar beneden
|
||||
mesh.position.y = (sg.fascia ? edge.top : edge.trimTop) * toM;
|
||||
mesh.visible = o.edgeVisible !== false;
|
||||
group.add(mesh);
|
||||
const topFn = sg.fascia ? sg.topAt : (p) => sg.topAt(p) + edge.trimAbove;
|
||||
polys.forEach(pl => {
|
||||
const mesh = new THREE.Mesh(prismGeometry(pl, topFn, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
|
||||
mesh.visible = o.edgeVisible !== false;
|
||||
group.add(mesh);
|
||||
});
|
||||
});
|
||||
|
||||
const spotR = 0.035, spotH = 0.012;
|
||||
edge.spots.forEach(p => {
|
||||
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;
|
||||
group.add(spot);
|
||||
});
|
||||
@@ -229,6 +460,7 @@ window.RoofModel = (() => {
|
||||
return {
|
||||
DAKLEER_MM, OVERHANG_MIN, OVERHANG_MAX, DEFAULT_SETTINGS, SPOT_RATIO,
|
||||
createDefaultRoof, thickness, createMaterials, build3D, computeSpecs,
|
||||
computeShape, wallProfile, profileHeight, computeUnitRoof, buildUnit3D,
|
||||
computeEdge, buildEdge3D, normalizeSettings
|
||||
};
|
||||
})();
|
||||
|
||||
+156
-138
@@ -95,6 +95,9 @@ window.WallModel = (() => {
|
||||
deur: { label: 'Deur', w: 900, 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
|
||||
// binnenwanden (aan beide zijden spaanplaat).
|
||||
@@ -220,34 +223,44 @@ window.WallModel = (() => {
|
||||
o.x = Math.max(edgeMargin, Math.min(o.x, wallWidth - o.w - edgeMargin));
|
||||
});
|
||||
|
||||
// Links-naar-rechts: duw overlappende openingen uit 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 tussen twee openingen zouden hun uitsparingen elkaar
|
||||
// kunnen overlappen (gecontroleerd, zie eerdere test).
|
||||
const minGap = studWidth * 2;
|
||||
// Openingen die elkaar in de hoogte raken (incl. boven-/onderdorpel en
|
||||
// 2x stijldikte ertussen) moeten naast elkaar; staan ze boven elkaar
|
||||
// (bijv. een raam boven een deur), dan mogen ze in x overlappen.
|
||||
// Hoogte hier op basis van de invoer; na fase 2 wordt het nog gecontroleerd.
|
||||
const vBand = (o, h, y) => [y > plateThick ? y - studWidth : 0, y + h + studWidth];
|
||||
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++) {
|
||||
const prev = resolved[i - 1];
|
||||
const cur = resolved[i];
|
||||
const minX = prev.x + prev.w + minGap;
|
||||
if (cur.x < minX) cur.x = minX;
|
||||
for (let j = 0; j < i; j++) {
|
||||
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;
|
||||
let overflow = 0;
|
||||
if (resolved.length) {
|
||||
const last = resolved[resolved.length - 1];
|
||||
overflow = Math.max(0, (last.x + last.w) - rightLimit);
|
||||
}
|
||||
if (overflow > 0) {
|
||||
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;
|
||||
}
|
||||
for (let i = resolved.length - 1; i >= 0; i--) {
|
||||
const cur = resolved[i];
|
||||
let maxX = rightLimit - cur.w;
|
||||
for (let j = i + 1; j < resolved.length; j++) {
|
||||
const next = resolved[j];
|
||||
if (!stacked(cur, next)) maxX = Math.min(maxX, next.x - gapOf(cur, next) - cur.w);
|
||||
}
|
||||
if (cur.x > maxX) cur.x = maxX;
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 => {
|
||||
o.overflowing = (o.x < edgeMargin - 0.5) || (o.x + o.w > wallWidth - edgeMargin + 0.5);
|
||||
});
|
||||
for (let i = 1; i < resolved.length; i++) {
|
||||
if (resolved[i].x < resolved[i - 1].x + resolved[i - 1].w + minGap - 0.5) {
|
||||
resolved[i].overflowing = true;
|
||||
resolved[i - 1].overflowing = true;
|
||||
for (let i = 0; i < resolved.length; i++) {
|
||||
for (let j = i + 1; j < resolved.length; j++) {
|
||||
const a = resolved[i], b = resolved[j];
|
||||
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);
|
||||
}
|
||||
|
||||
let fullStuds = [];
|
||||
let jackStuds = [];
|
||||
// Stijlen als stukken { x, y0, y1, kind } (mm; y1 null = tot de
|
||||
// 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 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) {
|
||||
fullStuds.push(...cornerStuds);
|
||||
const availWidth = wallRightStudX - wallLeftStudX;
|
||||
calculateSegmentPositions(availWidth, CONFIG.minSpacing, CONFIG.maxSpacing)
|
||||
.forEach(p => fullStuds.push(wallLeftStudX + p));
|
||||
} else {
|
||||
// Hoekstijlen van de wand horen er, met of zonder opening, altijd bij.
|
||||
fullStuds.push(...cornerStuds);
|
||||
|
||||
// Randen van de "vrije zones" tussen wand en openingen, per paar verwerkt
|
||||
const edges = [wallLeftStudX];
|
||||
|
||||
ops.forEach(op => {
|
||||
const opStartX = startX + op.x;
|
||||
const kingLeftX = opStartX - CONFIG.studWidth / 2;
|
||||
const kingRightX = opStartX + op.w + CONFIG.studWidth / 2;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
// Kolommen: openingen (incl. hun stijlen) die elkaar in x overlappen.
|
||||
// Tussen de kolommen en de hoekstijlen: regelmatige verdeling.
|
||||
const cols = [];
|
||||
ops.map(op => {
|
||||
const extra = (isDoor(op) ? 2 : 1) * sw;
|
||||
return [startX + op.x - extra + sw / 2, startX + op.x + op.w + extra - sw / 2]; // hartlijnen buitenste stijlen
|
||||
}).sort((p, q) => p[0] - q[0]).forEach(r => {
|
||||
const last = cols[cols.length - 1];
|
||||
if (last && r[0] < last[1] + sw) last[1] = Math.max(last[1], r[1]);
|
||||
else cols.push(r.slice());
|
||||
});
|
||||
const edges = [wallLeftStudX];
|
||||
cols.forEach(c => edges.push(c[0], c[1]));
|
||||
edges.push(wallRightStudX);
|
||||
for (let i = 0; i < edges.length; i += 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' }));
|
||||
}
|
||||
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);
|
||||
jackStuds.sort((a, b) => a.x - b.x);
|
||||
// Knippen op de openingen.
|
||||
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
|
||||
@@ -457,8 +508,10 @@ window.WallModel = (() => {
|
||||
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
|
||||
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
|
||||
// alleen aan het einde van de gevel.
|
||||
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade) {
|
||||
const y0 = skinStartY(trasraam); // boven het trasraam
|
||||
// skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het
|
||||
// blootliggende deel van een naadwand boven het dak van de andere unit).
|
||||
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) {
|
||||
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 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;
|
||||
@@ -586,6 +639,10 @@ window.WallModel = (() => {
|
||||
const trasraam = buitenMateriaal === 'vochtdoek' ? (opts.trasraam || null) : null;
|
||||
const skinExt = opts.skinExt || 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
|
||||
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
|
||||
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
|
||||
@@ -697,46 +754,21 @@ window.WallModel = (() => {
|
||||
horizBeamCount++;
|
||||
});
|
||||
|
||||
// C. VOLLEDIGE VERTICALE STIJLEN (incl. de "koningsstijlen" naast elke opening)
|
||||
calcData.fullStuds.forEach((xPosMM) => {
|
||||
const xM = xPosMM * toM;
|
||||
// C. VERTICALE STIJLEN (vol, koning, dubbel bij deuren, kreupel): elk
|
||||
// stuk van y0 tot y1, of tot onder de daklijn (y1 null), zie computeFrame.
|
||||
calcData.studs.forEach(st => {
|
||||
const xM = st.x * toM;
|
||||
const xLeft = 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));
|
||||
});
|
||||
|
||||
// D. KREUPELE STIJLEN & DORPELS, per opening
|
||||
// D. DORPELS, per opening
|
||||
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)
|
||||
const opTopShape = new THREE.Shape();
|
||||
opTopShape.moveTo(op.xMin, op.yMax);
|
||||
@@ -937,6 +969,11 @@ window.WallModel = (() => {
|
||||
|
||||
// Buitenzijde (zuid): cement-gebonden platen (zelfde plaatlogica als
|
||||
// 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') {
|
||||
// Binnenwand: aan beide zijden spaanplaat.
|
||||
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 maxRoofY = tl.maxY;
|
||||
|
||||
let rowY0 = 0;
|
||||
let rowY0 = skinFrom * toM;
|
||||
let rowIndex = 0;
|
||||
while (rowY0 < maxRoofY - 1e-9) {
|
||||
const rowY1 = rowY0 + doekHeightM;
|
||||
@@ -1063,7 +1100,7 @@ window.WallModel = (() => {
|
||||
|
||||
// G. KRUISLAT op het vochtdoek (computeBattens): onderste laag direct op
|
||||
// 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 zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM;
|
||||
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
|
||||
// laagste kant er (visueel) los van.
|
||||
// (outline: ook de knik onder de nok, als de stijl daar staat.)
|
||||
calcData.fullStuds.forEach(x => {
|
||||
const xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
||||
calcData.studs.forEach(st => {
|
||||
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 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({
|
||||
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,
|
||||
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight,
|
||||
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 => {
|
||||
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 });
|
||||
|
||||
+2
-2
@@ -80,7 +80,7 @@ window.WallPdf = (() => {
|
||||
doc.text('Isolatie: glaswol (spouwvullend)', tblX[0], my);
|
||||
if (buitenMateriaal === 'vochtdoek') {
|
||||
// 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;
|
||||
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);
|
||||
@@ -251,7 +251,7 @@ window.WallPdf = (() => {
|
||||
// Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis.
|
||||
doc.setDrawColor(...RED);
|
||||
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 => {
|
||||
const x0 = px(x - CONFIG.studWidth / 2), x1 = px(x + CONFIG.studWidth / 2);
|
||||
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).
|
||||
|
||||
let CONFIG = {};
|
||||
let calcData = { fullStuds: [], jackStuds: [] };
|
||||
let calcData = { fullStuds: [], studs: [] };
|
||||
let wallGroup3D = null;
|
||||
let lastBaseHeight = 2400;
|
||||
|
||||
@@ -759,7 +759,7 @@ function mountWallEditor(container, callbacks) {
|
||||
readInputs();
|
||||
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 + '°');
|
||||
}
|
||||
|
||||
@@ -811,8 +811,10 @@ function mountWallEditor(container, callbacks) {
|
||||
ctx.strokeRect(screenX, screenY, studWidthPx, studDepthPx);
|
||||
};
|
||||
|
||||
calcData.fullStuds.forEach(x => drawStud2D(x, false));
|
||||
calcData.jackStuds.forEach(j => drawStud2D(j.x, true));
|
||||
// Bovenaanzicht: één doorsnede per stijlpositie (kreupel alleen als er
|
||||
// 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;
|
||||
CONFIG.openings.forEach(op => {
|
||||
@@ -949,22 +951,22 @@ function mountWallEditor(container, callbacks) {
|
||||
ctxFront.textAlign = 'start';
|
||||
}
|
||||
|
||||
const drawStudFront = (xMM, isJack) => {
|
||||
const xLeft = xMM - CONFIG.studWidth / 2;
|
||||
const xRight = xMM + CONFIG.studWidth / 2;
|
||||
const yBot = CONFIG.plateThick;
|
||||
ctxFront.fillStyle = isJack ? '#a37c45' : '#c29b62';
|
||||
// Stijlstuk van y0 tot y1 (of tot de daklijn), zie WallModel.computeFrame.
|
||||
const drawStudFront = (st) => {
|
||||
const xLeft = st.x - CONFIG.studWidth / 2;
|
||||
const xRight = st.x + CONFIG.studWidth / 2;
|
||||
ctxFront.fillStyle = st.kind === 'kreupel' ? '#a37c45' : '#c29b62';
|
||||
ctxFront.strokeStyle = '#8a6836';
|
||||
ctxFront.beginPath();
|
||||
ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(yBot));
|
||||
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(yBot));
|
||||
tl.underPts(xLeft, xRight).reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y)));
|
||||
ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(st.y0));
|
||||
ctxFront.lineTo(toCanvasX(xRight), toCanvasY(st.y0));
|
||||
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.fill();
|
||||
ctxFront.stroke();
|
||||
};
|
||||
calcData.fullStuds.forEach(x => drawStudFront(x, false));
|
||||
calcData.jackStuds.forEach(j => drawStudFront(j.x, true));
|
||||
calcData.studs.forEach(drawStudFront);
|
||||
|
||||
const startXMM = -halfWMM;
|
||||
frontOpeningRects = [];
|
||||
|
||||
Reference in New Issue
Block a user