v0.4.17: openingen met schuine bovenzijde (hoogte links/rechts, volg dakhelling), 9 sleeppunten in het vooraanzicht; regelwerk, beplating, doek, steen en latwerk volgen de schuinte (columnMinusRects met schuine uitsparingen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23bfe153a2
commit
8cdfa21f46
+137
-133
@@ -15,7 +15,7 @@
|
||||
//
|
||||
// Werkt met een "wand-state" (zelfde formaat als getStateJson/JSON-export):
|
||||
// { wall: {width, heightAll, heightLeft, heightRight, heightMid?, studWidth, ...},
|
||||
// materials: {interior, exterior}, openings: [{id,w,h,y,anchor,offset}] }
|
||||
// materials: {interior, exterior}, openings: [{id,w,h,hR?,y,anchor,offset}] }
|
||||
//
|
||||
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
|
||||
// ============================================================
|
||||
@@ -173,6 +173,24 @@ window.WallModel = (() => {
|
||||
// koningsstijl (stevigheid, computeFrame).
|
||||
const isDoor = (op) => op.type === 'deur' || op.type === 'voordeur';
|
||||
|
||||
// Schuine bovenzijde (zoals de wand zelf: schuin bovenin mag, onderin
|
||||
// niet): h = hoogte links, hR = hoogte rechts. Ruwe openingen hebben hR
|
||||
// alleen als hij (bewust) schuin is; opgeloste openingen altijd.
|
||||
// x is lokaal (0..wandbreedte), zoals op.x.
|
||||
const hRightOf = (op) => op.hR !== undefined ? op.hR : op.h;
|
||||
const openingTopAt = (op, x) => op.y + op.h + (x - op.x) / op.w * (hRightOf(op) - op.h);
|
||||
const openingTopMax = (op) => op.y + Math.max(op.h, hRightOf(op));
|
||||
const openingArea = (op) => op.w * (op.h + hRightOf(op)) / 2;
|
||||
// Stuk [x0, x1] van een vlak met waterpas onderkant y0 en schuine
|
||||
// bovenkant (yl links, yr rechts) op hoogte y, of null.
|
||||
function slopedSpanAtY(x0, x1, y0, yl, yr, y) {
|
||||
if (y <= y0 || y >= Math.max(yl, yr)) return null;
|
||||
if (y <= Math.min(yl, yr)) return [x0, x1];
|
||||
const xc = x0 + (y - yl) / (yr - yl) * (x1 - x0);
|
||||
return yr > yl ? [xc, x1] : [x0, xc];
|
||||
}
|
||||
const openingSpanAtY = (op, y) => slopedSpanAtY(op.x, op.x + op.w, op.y, op.y + op.h, op.y + hRightOf(op), y);
|
||||
|
||||
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
||||
// binnenwanden (aan beide zijden spaanplaat).
|
||||
// 'geen': geen beplating aan de buitenzijde — naadwand-deel dat tegen het
|
||||
@@ -287,7 +305,7 @@ window.WallModel = (() => {
|
||||
else if (raw.anchor === 'middle') x = (wallWidth - w) / 2 + raw.offset;
|
||||
else x = raw.offset;
|
||||
|
||||
return { id: raw.id, type: raw.type || 'raam', w, anchor: raw.anchor, offset: raw.offset, x, rawH: raw.h, rawY: raw.y };
|
||||
return { id: raw.id, type: raw.type || 'raam', w, anchor: raw.anchor, offset: raw.offset, x, rawH: raw.h, rawHR: hRightOf(raw), rawY: raw.y };
|
||||
});
|
||||
|
||||
resolved.sort((a, b) => a.x - b.x);
|
||||
@@ -301,10 +319,11 @@ window.WallModel = (() => {
|
||||
// 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.
|
||||
// (Schuine bovenzijde: de hoogste kant telt.)
|
||||
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);
|
||||
const [a0, a1] = useFinal ? vBand(a, Math.max(a.h, a.hR), a.y) : vBand(a, Math.max(400, a.rawH, a.rawHR), a.rawY);
|
||||
const [b0, b1] = useFinal ? vBand(b, Math.max(b.h, b.hR), b.y) : vBand(b, Math.max(400, b.rawH, b.rawHR), b.rawY);
|
||||
return a1 <= b0 + 0.5 || b1 <= a0 + 0.5;
|
||||
};
|
||||
// Tussenruimte naast elkaar: 2x stijldikte i.p.v. 1x: de OSB-uitsparing
|
||||
@@ -337,23 +356,22 @@ window.WallModel = (() => {
|
||||
if (cur.x > maxX) cur.x = maxX;
|
||||
}
|
||||
|
||||
// Fase 2: hoogte (h) en verticale positie (y), per opening geklemd tegen
|
||||
// de daklijn zoals die loopt op de EIGEN (nu definitieve) x-positie van
|
||||
// die opening — het punt waar een verticale lijn op die plek de
|
||||
// (eventueel hellende) horizontale topbalk zou snijden. Bij een
|
||||
// opening die zelf een breedte heeft, wordt het laagste van de twee
|
||||
// uiteinden aangehouden (de daklijn is lineair of heeft een nok als
|
||||
// hoogste punt, dus dat is voldoende).
|
||||
const localRoofHeightAt = roofAt;
|
||||
|
||||
// Fase 2: hoogte (h links, hR rechts) en verticale positie (y), per
|
||||
// opening geklemd tegen de daklijn zoals die loopt op de EIGEN (nu
|
||||
// definitieve) x-positie van die opening — per kant: de linkerhoogte
|
||||
// tegen de daklijn boven de linkerrand, de rechterhoogte tegen die
|
||||
// boven de rechterrand. Zo kan een schuine bovenzijde het dak volgen.
|
||||
// Daartussen blijft de bovenkant onder het dak: de daklijn is lineair
|
||||
// of heeft een nok als hoogste punt (bol), dus de rechte lijn tussen
|
||||
// twee punten eronder blijft eronder.
|
||||
resolved.forEach(o => {
|
||||
const localCeiling = Math.min(localRoofHeightAt(o.x), localRoofHeightAt(o.x + o.w));
|
||||
const maxAllowedOpH = Math.max(400, localCeiling - (plateThick * 2));
|
||||
const h = Math.min(Math.max(400, o.rawH), maxAllowedOpH);
|
||||
const maxOpY = Math.max(0, localCeiling - h - plateThick);
|
||||
const y = Math.max(0, Math.min(o.rawY, maxOpY));
|
||||
o.h = h;
|
||||
o.y = y;
|
||||
const ceilL = roofAt(o.x), ceilR = roofAt(o.x + o.w);
|
||||
const hL = Math.min(Math.max(400, o.rawH), Math.max(400, ceilL - plateThick * 2));
|
||||
const hR = Math.min(Math.max(400, o.rawHR), Math.max(400, ceilR - plateThick * 2));
|
||||
const maxOpY = Math.max(0, Math.min(ceilL - hL, ceilR - hR) - plateThick);
|
||||
o.y = Math.max(0, Math.min(o.rawY, maxOpY));
|
||||
o.h = hL;
|
||||
o.hR = hR;
|
||||
});
|
||||
|
||||
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
||||
@@ -457,10 +475,16 @@ window.WallModel = (() => {
|
||||
// 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
|
||||
}));
|
||||
// Bovendorpel volgt een schuine bovenzijde (loodrechte dikte sw):
|
||||
// topAt(x) = bovenkant dorpel, y1 = het hoogste punt daarvan.
|
||||
const bands = ops.map(op => {
|
||||
const x0 = startX + op.x, s = (op.hR - op.h) / op.w, vo = sw * Math.sqrt(1 + s * s);
|
||||
return {
|
||||
op, x0, x1: x0 + op.w,
|
||||
y0: op.y > CONFIG.plateThick ? op.y - sw : 0, y1: op.y + Math.max(op.h, op.hR) + vo,
|
||||
topAt: (x) => op.y + op.h + (x - x0) * s + vo
|
||||
};
|
||||
});
|
||||
const cands = [];
|
||||
cornerStuds.forEach(x => cands.push({ x, kind: 'hoek' }));
|
||||
ops.forEach(op => {
|
||||
@@ -497,8 +521,11 @@ window.WallModel = (() => {
|
||||
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) => {
|
||||
// cur = onderkant van het volgende stuk [links, rechts]: schuin als
|
||||
// het op een schuine bovendorpel staat (dan ook y0l/y0r; y0 = laagste).
|
||||
let cur = [CONFIG.plateThick, CONFIG.plateThick], below = null;
|
||||
const curMax = () => Math.max(cur[0], cur[1]);
|
||||
const push = (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
|
||||
@@ -506,13 +533,16 @@ window.WallModel = (() => {
|
||||
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 });
|
||||
const sg = { x, y0: Math.min(cur[0], cur[1]), y1, kind: c.kind };
|
||||
if (Math.abs(cur[0] - cur[1]) > 0.05) { sg.y0l = cur[0]; sg.y0r = cur[1]; }
|
||||
segs.push(sg);
|
||||
};
|
||||
hit.forEach(bd => {
|
||||
if (bd.y0 > cur + 1) push(cur, bd.y0, bd);
|
||||
if (bd.y1 > cur) { cur = bd.y1; below = bd; }
|
||||
if (bd.y0 > curMax() + 1) push(bd.y0, bd);
|
||||
const t = [bd.topAt(x - sw / 2), bd.topAt(x + sw / 2)];
|
||||
if (Math.max(t[0], t[1]) > curMax()) { cur = t; below = bd; }
|
||||
});
|
||||
if (roofMin(x) > cur + 1) push(cur, null, null);
|
||||
if (roofMin(x) > curMax() + 1) push(null, null);
|
||||
});
|
||||
// Dubbelingen (bijv. koningsstijl op de hoekstijl) weg: hoogste prioriteit wint.
|
||||
const studs = [];
|
||||
@@ -536,19 +566,30 @@ window.WallModel = (() => {
|
||||
// bij een sterk hellende wand met een opening dicht bij de lage kant kan
|
||||
// de marge anders lokaal boven het dak uitkomen (kapotte uitsparing).
|
||||
// Gebruikt door build3D (fill) én de 2D-tekening (naad-/baanlijnen).
|
||||
// Schuine bovenzijde: hy1 = bovenkant links (bij hx0), hy1r = rechts (bij
|
||||
// hx1), langs de bovenkant van de schuine bovendorpel; per kant geklemd
|
||||
// (de daklijn is bol, dus de lijn ertussen blijft eronder).
|
||||
function sheathingHoles(CONFIG) {
|
||||
const halfW = CONFIG.wallWidth / 2;
|
||||
const topYAt = topLine(CONFIG).at;
|
||||
const roofSafetyMargin = 10;
|
||||
const sw = CONFIG.studWidth;
|
||||
return CONFIG.openings.map(op => {
|
||||
const hx0 = -halfW + op.x - CONFIG.studWidth;
|
||||
const hx1 = -halfW + op.x + op.w + CONFIG.studWidth;
|
||||
let hy1 = op.y + op.h + CONFIG.studWidth;
|
||||
const localRoofCeiling = Math.min(topYAt(hx0), topYAt(hx1)) - roofSafetyMargin;
|
||||
if (hy1 > localRoofCeiling) hy1 = Math.max(op.y + roofSafetyMargin, localRoofCeiling);
|
||||
return { hx0, hx1, hy0: op.y, hy1 };
|
||||
const hx0 = -halfW + op.x - sw;
|
||||
const hx1 = -halfW + op.x + op.w + sw;
|
||||
const s = (op.hR - op.h) / op.w, vo = sw * Math.sqrt(1 + s * s);
|
||||
const clampTop = (y, x) => {
|
||||
const ceil = topYAt(x) - roofSafetyMargin;
|
||||
return y > ceil ? Math.max(op.y + roofSafetyMargin, ceil) : y;
|
||||
};
|
||||
const hy1 = clampTop(op.y + op.h + vo - s * sw, hx0);
|
||||
const hy1r = clampTop(op.y + op.hR + vo + s * sw, hx1);
|
||||
return { hx0, hx1, hy0: op.y, hy1, hy1r };
|
||||
});
|
||||
}
|
||||
const holeTopR = (h) => h.hy1r !== undefined ? h.hy1r : h.hy1;
|
||||
const holeTopAt = (h, x) => h.hy1 + (x - h.hx0) / (h.hx1 - h.hx0) * (holeTopR(h) - h.hy1);
|
||||
const holeRect = (h) => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1, y1r: holeTopR(h) });
|
||||
|
||||
// Stukken van een horizontale lijn op hoogte y (mm) die OP de beplating
|
||||
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
|
||||
@@ -559,12 +600,15 @@ window.WallModel = (() => {
|
||||
const range = topLine(CONFIG).rangeAbove(y);
|
||||
if (!range) return [];
|
||||
let segments = [range];
|
||||
sheathingHoles(CONFIG).concat(extraHoles || []).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
|
||||
sheathingHoles(CONFIG).concat(extraHoles || []).forEach(h => {
|
||||
const span = slopedSpanAtY(h.hx0, h.hx1, h.hy0, h.hy1, holeTopR(h), y);
|
||||
if (!span) return;
|
||||
const [hx0, hx1] = span;
|
||||
segments = segments.flatMap(([a, b]) => {
|
||||
if (h.hx1 <= a || h.hx0 >= b) return [[a, b]];
|
||||
if (hx1 <= a || hx0 >= b) return [[a, b]];
|
||||
const out = [];
|
||||
if (h.hx0 > a) out.push([a, h.hx0]);
|
||||
if (h.hx1 < b) out.push([h.hx1, b]);
|
||||
if (hx0 > a) out.push([a, hx0]);
|
||||
if (hx1 < b) out.push([hx1, b]);
|
||||
return out;
|
||||
});
|
||||
});
|
||||
@@ -625,11 +669,13 @@ window.WallModel = (() => {
|
||||
const yTop = Math.min(topYAt(x - w / 2), topYAt(x + w / 2));
|
||||
let segs = [[y0, yTop]];
|
||||
holes.filter(h => x + w / 2 > h.hx0 && x - w / 2 < h.hx1).forEach(h => {
|
||||
// Schuine bovenkant: de hoogste waarde over de latbreedte.
|
||||
const hy1 = Math.max(holeTopAt(h, Math.max(h.hx0, x - w / 2)), holeTopAt(h, Math.min(h.hx1, x + w / 2)));
|
||||
segs = segs.flatMap(([a, b]) => {
|
||||
if (h.hy1 <= a || h.hy0 >= b) return [[a, b]];
|
||||
if (hy1 <= a || h.hy0 >= b) return [[a, b]];
|
||||
const out = [];
|
||||
if (h.hy0 > a) out.push([a, h.hy0]);
|
||||
if (h.hy1 < b) out.push([h.hy1, b]);
|
||||
if (hy1 < b) out.push([hy1, b]);
|
||||
return out;
|
||||
});
|
||||
});
|
||||
@@ -815,7 +861,8 @@ window.WallModel = (() => {
|
||||
xMin: startXM + op.x * toM,
|
||||
xMax: startXM + (op.x + op.w) * toM,
|
||||
yMin: op.y * toM,
|
||||
yMax: (op.y + op.h) * toM,
|
||||
yMax: (op.y + op.h) * toM, // bovenkant links
|
||||
yMaxR: (op.y + op.hR) * toM, // bovenkant rechts (schuine bovenzijde)
|
||||
wM: op.w * toM
|
||||
}));
|
||||
|
||||
@@ -864,21 +911,24 @@ window.WallModel = (() => {
|
||||
const xM = st.x * toM;
|
||||
const xLeft = xM - (sWidthM / 2);
|
||||
const xRight = xM + (sWidthM / 2);
|
||||
const y0 = st.y0 * toM;
|
||||
// Onderkant schuin als het stuk op een schuine bovendorpel staat.
|
||||
const y0l = (st.y0l !== undefined ? st.y0l : st.y0) * toM, y0r = (st.y0r !== undefined ? st.y0r : 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 }];
|
||||
? [{ x: xLeft, y: y0l }, { x: xRight, y: y0r }, ...underPts(xLeft, xRight).reverse()]
|
||||
: [{ x: xLeft, y: y0l }, { x: xRight, y: y0r }, { 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. DORPELS, per opening
|
||||
opsM.forEach(op => {
|
||||
// Bovendorpel / linteel (horizontale balk)
|
||||
// Bovendorpel / linteel (liggende balk, volgt een schuine bovenzijde;
|
||||
// loodrechte dikte = stijldikte)
|
||||
const s = (op.yMaxR - op.yMax) / op.wM, vo = sWidthM * Math.sqrt(1 + s * s);
|
||||
const opTopShape = new THREE.Shape();
|
||||
opTopShape.moveTo(op.xMin, op.yMax);
|
||||
opTopShape.lineTo(op.xMax, op.yMax);
|
||||
opTopShape.lineTo(op.xMax, op.yMax + sWidthM);
|
||||
opTopShape.lineTo(op.xMin, op.yMax + sWidthM);
|
||||
opTopShape.lineTo(op.xMax, op.yMaxR);
|
||||
opTopShape.lineTo(op.xMax, op.yMaxR + vo);
|
||||
opTopShape.lineTo(op.xMin, op.yMax + vo);
|
||||
opTopShape.closePath();
|
||||
wallGroup3D.add(createExtrudedBeam(opTopShape, sDepthM, -sDepthM / 2));
|
||||
horizBeamCount++;
|
||||
@@ -949,7 +999,7 @@ window.WallModel = (() => {
|
||||
// Hoekdoorloop per buitenlaag, in m (zie cornerExt).
|
||||
const extM = (depthMM) => { const e = cornerExt(skinExt, depthMM); return { left: e.left * toM, right: e.right * toM }; };
|
||||
const holeExtents = sheathingHoles(CONFIG).map(hole => ({
|
||||
hx0: hole.hx0 * toM, hx1: hole.hx1 * toM, hy0: hole.hy0 * toM, hy1: hole.hy1 * toM
|
||||
hx0: hole.hx0 * toM, hx1: hole.hx1 * toM, hy0: hole.hy0 * toM, hy1: hole.hy1 * toM, hy1r: hole.hy1r * toM
|
||||
}));
|
||||
// Binnenhoek-doorloop (negatieve skinExt): dat stuk zit binnen de wand
|
||||
// en het dak van de buur-unit; daar geen buitenlagen, tot opts.trimTop
|
||||
@@ -1008,13 +1058,13 @@ window.WallModel = (() => {
|
||||
const xL = -halfW - ext.left, xR = halfW + ext.right;
|
||||
const capL = ext.left > 0 ? xL + tAll : xL, capR = ext.right > 0 ? xR - tAll : xR;
|
||||
const topClamped = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x)));
|
||||
const rects = outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 }));
|
||||
const rects = outerHoles.map(holeRect);
|
||||
const shapesIn = (a, b) => {
|
||||
if (b - a <= edgeEps) return [];
|
||||
const capped = yCap === Infinity;
|
||||
const pieces = columnMinusRects(a, b, y0, capped ? topClamped : (x) => Math.min(yCap, topClamped(x)), rects,
|
||||
capped ? { extraXs: tlOut.kinks } : { yCap, extraXs: tlOut.kinks.concat(tlOut.crossings(yCap)) });
|
||||
return (pieces || []).map(piece => {
|
||||
return pieces.map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
|
||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(q => new THREE.Vector2(q.x, q.y)))));
|
||||
return shape;
|
||||
@@ -1078,42 +1128,13 @@ window.WallModel = (() => {
|
||||
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
|
||||
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
|
||||
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
|
||||
HX.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
|
||||
HX.map(holeRect),
|
||||
{ yCap: range ? yCap : undefined, extraXs: range ? TL.kinks.concat(TL.crossings(yCap)) : TL.kinks });
|
||||
let boardShapes;
|
||||
if (pieces) {
|
||||
boardShapes = pieces.map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
||||
return shape;
|
||||
});
|
||||
} else {
|
||||
// Terugval (uitsparing tot in de schuine bovenrand): oude
|
||||
// gat-methode, net binnen de plaatrand afgekapt.
|
||||
const boardShape = new THREE.Shape();
|
||||
boardShape.moveTo(ex0, yB);
|
||||
boardShape.lineTo(ex1, yB);
|
||||
boardShape.lineTo(ex1, skinTopAt(ex1));
|
||||
TL.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
|
||||
boardShape.lineTo(ex0, skinTopAt(ex0));
|
||||
boardShape.closePath();
|
||||
holeExtents.forEach(h => {
|
||||
const chx0 = Math.max(ex0 + edgeEps, h.hx0);
|
||||
const chx1 = Math.min(ex1 - edgeEps, h.hx1);
|
||||
if (chx1 > chx0) {
|
||||
const holePath = new THREE.Path();
|
||||
const chy0 = Math.max(yB + edgeEps, h.hy0), chy1 = Math.min(yCap - edgeEps, h.hy1);
|
||||
if (chy1 <= chy0) return;
|
||||
holePath.moveTo(chx0, chy0);
|
||||
holePath.lineTo(chx0, chy1);
|
||||
holePath.lineTo(chx1, chy1);
|
||||
holePath.lineTo(chx1, chy0);
|
||||
holePath.closePath();
|
||||
boardShape.holes.push(holePath);
|
||||
}
|
||||
});
|
||||
boardShapes = [boardShape];
|
||||
}
|
||||
const boardShapes = pieces.map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
||||
return shape;
|
||||
});
|
||||
if (!boardShapes.length) continue;
|
||||
|
||||
const boardExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
||||
@@ -1236,7 +1257,6 @@ window.WallModel = (() => {
|
||||
// transparant zijn).
|
||||
const doekZStepM = 0.0002;
|
||||
const DOEK_OUT_M = 0.001; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
||||
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tlOut.pts.slice().reverse()];
|
||||
const maxRoofY = tlOut.maxY;
|
||||
|
||||
let rowY0 = skinFrom * toM;
|
||||
@@ -1251,41 +1271,13 @@ window.WallModel = (() => {
|
||||
const [rx0, rx1] = tlOut.rangeAbove(rowY0) || [0, 0];
|
||||
const extraXs = tlOut.crossings(rowY1).concat(tlOut.kinks);
|
||||
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
|
||||
outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
|
||||
outerHoles.map(holeRect), { yCap: rowY1, extraXs }) : [];
|
||||
|
||||
let rowShapes;
|
||||
if (pieces) {
|
||||
rowShapes = pieces.map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
||||
return shape;
|
||||
});
|
||||
} else {
|
||||
// Terugval (uitsparing tot in de schuine bovenrand): oude methode.
|
||||
let rowPts = clipPolygonYLE(wallOutlinePts, rowY1);
|
||||
rowPts = clipPolygonYGE(rowPts, rowY0);
|
||||
rowShapes = [];
|
||||
if (rowPts.length >= 3) {
|
||||
const rowShape = new THREE.Shape();
|
||||
rowShape.moveTo(rowPts[0].x, rowPts[0].y);
|
||||
for (let k = 1; k < rowPts.length; k++) rowShape.lineTo(rowPts[k].x, rowPts[k].y);
|
||||
rowShape.closePath();
|
||||
holeExtents.forEach(h => {
|
||||
const chy0 = Math.max(rowY0 + edgeEps, h.hy0);
|
||||
const chy1 = Math.min(rowY1 - edgeEps, h.hy1);
|
||||
if (chy1 > chy0) {
|
||||
const holePath = new THREE.Path();
|
||||
holePath.moveTo(h.hx0, chy0);
|
||||
holePath.lineTo(h.hx0, chy1);
|
||||
holePath.lineTo(h.hx1, chy1);
|
||||
holePath.lineTo(h.hx1, chy0);
|
||||
holePath.closePath();
|
||||
rowShape.holes.push(holePath);
|
||||
}
|
||||
});
|
||||
rowShapes.push(rowShape);
|
||||
}
|
||||
}
|
||||
const rowShapes = pieces.map(piece => {
|
||||
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
||||
return shape;
|
||||
});
|
||||
if (rowShapes.length) {
|
||||
const rowExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
||||
const rowGeo = shiftUV(new THREE.ExtrudeGeometry(rowShapes, rowExtrudeSettings));
|
||||
@@ -1388,9 +1380,7 @@ window.WallModel = (() => {
|
||||
const envelopeAreaM2 = polygonArea(envelopePoints);
|
||||
const envelopeVolumeM3 = envelopeAreaM2 * sDepthM;
|
||||
|
||||
const openingsVolumeM3 = CONFIG.openings.reduce((sum, op) => {
|
||||
return sum + (op.w * toM) * (op.h * toM) * sDepthM;
|
||||
}, 0);
|
||||
const openingsVolumeM3 = CONFIG.openings.reduce((sum, op) => sum + openingArea(op) * toM * toM * sDepthM, 0);
|
||||
|
||||
let voidVolumeM3 = envelopeVolumeM3 - stats.woodVolumeM3 - openingsVolumeM3;
|
||||
if (voidVolumeM3 < 0) voidVolumeM3 = 0;
|
||||
@@ -1474,11 +1464,14 @@ window.WallModel = (() => {
|
||||
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';
|
||||
// Schuine onderkant: stuk op een schuine bovendorpel.
|
||||
const yBottomLeft = st.y0l !== undefined ? st.y0l : yBottom, yBottomRight = st.y0r !== undefined ? st.y0r : yBottom;
|
||||
const botCut = cutDeg(yBottomRight - yBottomLeft, xRight - xLeft);
|
||||
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, cutAngles: [0, 0]
|
||||
yBottomLeft, yBottomRight, yTopLeft: st.y1, yTopRight: st.y1, cutAngles: [botCut, 0]
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1486,16 +1479,24 @@ window.WallModel = (() => {
|
||||
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
||||
pieces.push({
|
||||
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()],
|
||||
cutAngles: [0].concat(topPts.slice(1).map((q, k) => cutDeg(q.y - topPts[k].y, q.x - topPts[k].x)))
|
||||
y: (yBottomLeft + yBottomRight + yTopLeft + yTopRight) / 4,
|
||||
yBottomLeft, yBottomRight, yTopLeft, yTopRight,
|
||||
outline: [{ x: xLeft, y: yBottomLeft }, { x: xRight, y: yBottomRight }, ...topPts.slice().reverse()],
|
||||
cutAngles: [botCut].concat(topPts.slice(1).map((q, k) => cutDeg(q.y - topPts[k].y, q.x - topPts[k].x)))
|
||||
});
|
||||
});
|
||||
|
||||
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, cutAngles: [0, 0] });
|
||||
// Schuine bovenzijde: dorpel langs de helling (loodrechte sneden,
|
||||
// zoals de bovenregel), met de werkelijke omtrek voor de tekening.
|
||||
const dh = op.hR - op.h;
|
||||
const header = { length: Math.hypot(op.w, dh), width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + (op.h + op.hR) / 2 + CONFIG.studWidth / 2, cutAngles: [cutDeg(dh, op.w), cutDeg(dh, op.w)] };
|
||||
if (Math.abs(dh) > 0.5) {
|
||||
const vo = CONFIG.studWidth * Math.hypot(op.w, dh) / op.w;
|
||||
header.outline = [{ x: opXWorld, y: op.y + op.h }, { x: opXWorld + op.w, y: op.y + op.hR }, { x: opXWorld + op.w, y: op.y + op.hR + vo }, { x: opXWorld, y: op.y + op.h + vo }];
|
||||
}
|
||||
pieces.push(header);
|
||||
if (op.y > CONFIG.plateThick) {
|
||||
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'onderdorpel', x: opXWorld + op.w / 2, y: op.y - CONFIG.studWidth / 2, cutAngles: [0, 0] });
|
||||
}
|
||||
@@ -1573,6 +1574,9 @@ window.WallModel = (() => {
|
||||
if (typeof o.w !== 'number' || typeof o.h !== 'number' || typeof o.y !== 'number' || typeof o.offset !== 'number') {
|
||||
throw new Error(`opening #${i + 1}: w/h/y/offset moeten getallen zijn`);
|
||||
}
|
||||
if (o.hR !== undefined && (typeof o.hR !== 'number' || !isFinite(o.hR))) {
|
||||
throw new Error(`opening #${i + 1}: hR (hoogte rechts) moet een getal zijn`);
|
||||
}
|
||||
if (o.type !== undefined && !OPENING_TYPES[o.type]) {
|
||||
throw new Error(`opening #${i + 1}: type moet ${Object.keys(OPENING_TYPES).join('/')} zijn (was: ${o.type})`);
|
||||
}
|
||||
@@ -1584,7 +1588,7 @@ window.WallModel = (() => {
|
||||
|
||||
return {
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||
createMaterials, build3D, computeSpecs
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user