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:
Machiel
2026-09-30 14:43:42 +02:00
co-authored by Claude Opus 5.5
parent 001206a108
commit 373c2c2fb7
10 changed files with 627 additions and 274 deletions
+156 -138
View File
@@ -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
View File
@@ -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
View File
@@ -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 = [];