v0.7.0: exports voor CAD, BIM en renders (Bestand › Exporteren, los van Opslaan/Ctrl+S): CAD (DXF R12, plattegrond met wanden/sparingen/gevelbekleding/kozijnen met draaicirkel, ruimtes, inrichting, dakrand, elektra en echte maatvoering, plus vier gevels als lijntekening met verborgen lijnen weg; lagen PL-/AZ-, 1:1 mm, afgestemd op 1:50; in CAD op te slaan als DWG), BIM (IFC4 met alleen de bouwconstructie: stijlen, regels, vloer- en dakbalken, stalen nokbalk, palen, spaanplaat/OSB/cementplaat en elektra per leiding, gegroepeerd per unit en element met Pset_Tectum en kleur per soort) en Render (OBJ + MTL + texturen in een ZIP voor Blender, volledige afgewerkte woning, kleuren lineair, PBR); schoorsteen tegen een buitenwand (ChimneyModel: sparing in wand en dak met raveelwerk, steenstrip/stuc, verslepen langs de gevel); kozijnen koppelen met een eigen schakelaar; kozijnindeling (deurblad, zij-/bovenlicht, schuifbladen) gedeeld tussen 3D, plattegrond en aanzicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
86fcd57b96
commit
adb3c74f85
+100
-33
@@ -320,20 +320,26 @@ window.WallModel = (() => {
|
||||
const edgeMargin = studWidth * CORNER_STUDS;
|
||||
|
||||
let resolved = rawOpenings.map(raw => {
|
||||
const w = Math.min(Math.max(400, raw.w), maxOpW);
|
||||
// Schoorsteen: de sparing kan smaller zijn (half over een naad tussen units).
|
||||
const w = Math.min(Math.max(raw.type === 'schoorsteen' ? 100 : 400, raw.w), maxOpW);
|
||||
|
||||
let x;
|
||||
if (raw.anchor === 'right') x = wallWidth - raw.offset - w;
|
||||
else if (raw.anchor === 'middle') x = (wallWidth - w) / 2 + raw.offset;
|
||||
else x = raw.offset;
|
||||
|
||||
return { id: raw.id, type: raw.type || 'raam', fill: raw.fill || null, w, anchor: raw.anchor, offset: raw.offset, x, rawH: raw.h, rawHR: hRightOf(raw), rawY: raw.y };
|
||||
const o = { id: raw.id, type: raw.type || 'raam', fill: raw.fill || null, w, anchor: raw.anchor, offset: raw.offset, x, rawH: raw.h, rawHR: hRightOf(raw), rawY: raw.y };
|
||||
if (raw.chimney) o.chimney = raw.chimney;
|
||||
if (raw.koppel && o.type !== 'schoorsteen') o.koppel = true;
|
||||
return o;
|
||||
});
|
||||
|
||||
resolved.sort((a, b) => a.x - b.x);
|
||||
|
||||
// Een schoorsteen over een naad loopt door tot het wandeinde: daar geen hoekstijlen in de schoorsteen.
|
||||
const mL = (o) => o.chimney && o.chimney.atStart ? 0 : edgeMargin, mR = (o) => o.chimney && o.chimney.atEnd ? 0 : edgeMargin;
|
||||
resolved.forEach(o => {
|
||||
o.x = Math.max(edgeMargin, Math.min(o.x, wallWidth - o.w - edgeMargin));
|
||||
o.x = Math.max(mL(o), Math.min(o.x, wallWidth - o.w - mR(o)));
|
||||
});
|
||||
|
||||
// Boven elkaar (bijv. raam boven deur) mogen openingen in x overlappen. Schuin: de hoogste kant telt.
|
||||
@@ -347,22 +353,42 @@ window.WallModel = (() => {
|
||||
// uitsparingen. Naast een deur 1× extra voor de dubbele stijl.
|
||||
const gapOf = (a, b) => studWidth * (2 + (isDoor(a) ? 1 : 0) + (isDoor(b) ? 1 : 0));
|
||||
|
||||
// Koppelen: staat een gekoppelde opening tegen zijn buur aan (niet verder dan de normale tussenruimte),
|
||||
// met dezelfde onderkant en bovenkant op de naad, dan vormen ze één sparing zonder stijlen, dagkant of
|
||||
// latten ertussen; de rechter sluit direct aan. Samen niet breder dan de max. sparingbreedte.
|
||||
const rightOf = new Map(), leftOf = new Map(), koppelWhy = new Map();
|
||||
const groupW = (o) => { let w = 0; for (let p = o; p; p = leftOf.get(p)) w += p.w; return w; };
|
||||
const FAR = 'staat niet tegen de opening ernaast aan';
|
||||
const blame = (a, b, why) => [a, b].forEach(o => { if (o.koppel && (!koppelWhy.has(o) || koppelWhy.get(o) === FAR)) koppelWhy.set(o, why); });
|
||||
resolved.forEach((a, i) => {
|
||||
const b = resolved.slice(i + 1).find(o => !stacked(a, o));
|
||||
if (!b || !(a.koppel || b.koppel) || a.type === 'schoorsteen' || b.type === 'schoorsteen' || leftOf.has(b)) return;
|
||||
let why = null;
|
||||
if (b.x - a.x - a.w > gapOf(a, b) + 0.5) why = FAR;
|
||||
else if (Math.abs(a.rawY - b.rawY) > 0.5 || Math.abs(a.rawY + a.rawHR - b.rawY - b.rawH) > 0.5) why = 'onderkant en hoogte moeten gelijk zijn aan de opening ernaast';
|
||||
else if (groupW(a) + b.w > maxOpW + 0.5) why = `samen breder dan ${maxOpW} mm`;
|
||||
if (why) { blame(a, b, why); return; }
|
||||
rightOf.set(a, b);
|
||||
leftOf.set(b, a);
|
||||
});
|
||||
const gap = (a, b) => rightOf.get(a) === b ? 0 : gapOf(a, b);
|
||||
|
||||
for (let i = 1; i < resolved.length; i++) {
|
||||
const cur = resolved[i];
|
||||
const cur = resolved[i], partner = leftOf.get(cur);
|
||||
if (partner) cur.x = partner.x + partner.w;
|
||||
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));
|
||||
cur.x = Math.max(cur.x, prev.x + prev.w + gap(prev, cur));
|
||||
}
|
||||
}
|
||||
|
||||
const rightLimit = wallWidth - edgeMargin;
|
||||
for (let i = resolved.length - 1; i >= 0; i--) {
|
||||
const cur = resolved[i];
|
||||
let maxX = rightLimit - cur.w;
|
||||
let maxX = wallWidth - mR(cur) - 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 (!stacked(cur, next)) maxX = Math.min(maxX, next.x - gap(cur, next) - cur.w);
|
||||
}
|
||||
if (cur.x > maxX) cur.x = maxX;
|
||||
}
|
||||
@@ -370,22 +396,34 @@ window.WallModel = (() => {
|
||||
// De daklijn is bol, dus de rechte lijn tussen twee punten eronder blijft eronder.
|
||||
resolved.forEach(o => {
|
||||
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));
|
||||
// Schoorsteen: tot de bovenkant, zonder latei en bovenregel erin (geen hout in het rookkanaal).
|
||||
const keep = o.type === 'schoorsteen' ? 0 : plateThick * 2;
|
||||
const hL = Math.min(Math.max(400, o.rawH), Math.max(400, ceilL - keep));
|
||||
const hR = Math.min(Math.max(400, o.rawHR), Math.max(400, ceilR - keep));
|
||||
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;
|
||||
});
|
||||
|
||||
// Twee draaideuren: stolp (bladen tegen elkaar), anders één gedeelde tussenstijl.
|
||||
const swing = (o) => !!o.fill && o.fill.type === 'deur';
|
||||
rightOf.forEach((b, a) => {
|
||||
const fits = Math.abs(b.x - a.x - a.w) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.y + a.hR - b.y - b.h) < 0.5;
|
||||
if (!fits) { rightOf.delete(a); blame(a, b, 'past hier niet tegen de opening ernaast'); return; }
|
||||
a.joinR = b.joinL = swing(a) && swing(b) ? 'stolp' : 'stijl';
|
||||
});
|
||||
koppelWhy.forEach((why, o) => { if (!o.joinL && !o.joinR) o.koppelWarn = why; });
|
||||
resolved.forEach(o => { if (o.koppel && !o.joinL && !o.joinR && !o.koppelWarn) o.koppelWarn = 'geen opening direct ernaast'; });
|
||||
|
||||
resolved.forEach(o => {
|
||||
o.overflowing = (o.x < edgeMargin - 0.5) || (o.x + o.w > wallWidth - edgeMargin + 0.5);
|
||||
o.overflowing = (o.x < mL(o) - 0.5) || (o.x + o.w > wallWidth - mR(o) + 0.5);
|
||||
});
|
||||
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; }
|
||||
if (!stacked(a, b, true) && r.x < l.x + l.w + gap(l, r) - 0.5) { a.overflowing = true; b.overflowing = true; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,10 +502,14 @@ window.WallModel = (() => {
|
||||
});
|
||||
const cands = [];
|
||||
cornerStuds.forEach(x => cands.push({ x, kind: 'hoek' }));
|
||||
// Gekoppelde zijde: geen stijlen ernaast; boven (en onder) de naad één kreupele stijl.
|
||||
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 (!op.joinL) cands.push({ x: x0 - sw / 2, kind: 'koning' });
|
||||
if (!op.joinR) cands.push({ x: x1 + sw / 2, kind: 'koning' });
|
||||
else cands.push({ x: x1, kind: 'kreupel', seam: true });
|
||||
if (isDoor(op) && !op.joinL) cands.push({ x: x0 - 1.5 * sw, kind: 'dubbel' });
|
||||
if (isDoor(op) && !op.joinR) cands.push({ x: x1 + 1.5 * sw, kind: 'dubbel' });
|
||||
});
|
||||
|
||||
const cols = [];
|
||||
@@ -489,16 +531,17 @@ window.WallModel = (() => {
|
||||
ops.forEach(op => calculateSegmentPositions(op.w, CONFIG.minSpacing, CONFIG.maxSpacing)
|
||||
.forEach(p => cands.push({ x: startX + op.x + p, kind: 'kreupel', owner: op })));
|
||||
|
||||
const inWall = (c) => c.x >= startX + sw / 2 - 0.5 && c.x <= startX + CONFIG.wallWidth - sw / 2 + 0.5;
|
||||
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 => {
|
||||
cands.filter(inWall).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, CONFIG.plateThick], below = null;
|
||||
const curMax = () => Math.max(cur[0], cur[1]);
|
||||
const push = (y1, above) => {
|
||||
if (c.kind === 'kreupel') {
|
||||
if (c.kind === 'kreupel' && !c.seam) {
|
||||
// Onder de opening niet als hij op een andere opening uitkomt: dat stuk is al de kreupele stijl daarvan.
|
||||
const onTop = below && below.op === c.owner, underneath = above && above.op === c.owner;
|
||||
if (!onTop && !(underneath && !below)) return;
|
||||
@@ -535,16 +578,20 @@ window.WallModel = (() => {
|
||||
const roofSafetyMargin = 10;
|
||||
const sw = CONFIG.studWidth;
|
||||
return CONFIG.openings.map(op => {
|
||||
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);
|
||||
// Schoorsteen: geen kozijn of dagkant, de plaat loopt door tot op de stijl naast de sparing.
|
||||
const m = op.type === 'schoorsteen' ? 0 : sw;
|
||||
// Gekoppelde zijde: de uitsparing sluit precies aan op die van de buur.
|
||||
const mL = op.joinL ? 0 : m, mR = op.joinR ? 0 : m;
|
||||
const hx0 = -halfW + op.x - mL;
|
||||
const hx1 = -halfW + op.x + op.w + mR;
|
||||
const s = (op.hR - op.h) / op.w, vo = m * 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 hy1 = clampTop(op.y + op.h + vo - s * mL, hx0);
|
||||
const hy1r = clampTop(op.y + op.hR + vo + s * mR, hx1);
|
||||
return { hx0, hx1, hy0: op.y, hy1, hy1r, joinL: !!op.joinL, joinR: !!op.joinR };
|
||||
});
|
||||
}
|
||||
const holeTopR = (h) => h.hy1r !== undefined ? h.hy1r : h.hy1;
|
||||
@@ -604,7 +651,7 @@ window.WallModel = (() => {
|
||||
const fixed = [lo, hi];
|
||||
holes.forEach(h => {
|
||||
if (h.hy0 >= tl.maxY || holeTopAt(h, h.hx0) <= y0) return;
|
||||
[h.hx0 - w / 2, h.hx1 + w / 2].forEach(x => { if (x > lo && x < hi) fixed.push(x); });
|
||||
[!h.joinL && h.hx0 - w / 2, !h.joinR && h.hx1 + w / 2].forEach(x => { if (x !== false && x > lo && x < hi) fixed.push(x); });
|
||||
});
|
||||
fixed.sort((a, b) => a - b);
|
||||
const anchors = fixed.filter((x, i) => i === 0 || x - fixed[i - 1] > w);
|
||||
@@ -794,7 +841,8 @@ window.WallModel = (() => {
|
||||
yMin: op.y * toM,
|
||||
yMax: (op.y + op.h) * toM,
|
||||
yMaxR: (op.y + op.hR) * toM,
|
||||
wM: op.w * toM
|
||||
wM: op.w * toM,
|
||||
chimney: op.type === 'schoorsteen'
|
||||
}));
|
||||
|
||||
const floorCuts = opsM
|
||||
@@ -825,7 +873,21 @@ window.WallModel = (() => {
|
||||
horizBeamCount++;
|
||||
}
|
||||
|
||||
topPlateOutlines(tl).forEach(pts => {
|
||||
// Bovenregel onderbroken waar een schoorsteen doorloopt.
|
||||
const clipX = (pts, x, keepBelow) => {
|
||||
const out = [], f = (p) => keepBelow ? x - p.x : p.x - x;
|
||||
pts.forEach((p, k) => {
|
||||
const q = pts[(k + 1) % pts.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, y: p.y + (q.y - p.y) * t }); }
|
||||
});
|
||||
return out;
|
||||
};
|
||||
let plates = topPlateOutlines(tl);
|
||||
opsM.filter(op => op.chimney).forEach(op => {
|
||||
plates = plates.flatMap(pts => [clipX(pts, op.xMin, true), clipX(pts, op.xMax, false)]).filter(pts => pts.length >= 3 && polygonArea(pts) > 1e-6);
|
||||
});
|
||||
plates.forEach(pts => {
|
||||
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), pDepthM, -pDepthM / 2));
|
||||
horizBeamCount++;
|
||||
});
|
||||
@@ -863,6 +925,7 @@ window.WallModel = (() => {
|
||||
});
|
||||
|
||||
opsM.forEach(op => {
|
||||
if (op.chimney) return; // geen latei: de schoorsteen draagt de bovenkant
|
||||
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);
|
||||
@@ -1284,7 +1347,7 @@ window.WallModel = (() => {
|
||||
const holes = sheathingHoles(CONFIG);
|
||||
CONFIG.openings.forEach((op, i) => {
|
||||
const h = holes[i];
|
||||
if (!h) return;
|
||||
if (!h || op.type === 'schoorsteen') return; // de schoorsteen sluit de sparing zelf af
|
||||
const x0 = -CONFIG.wallWidth / 2 + op.x, x1 = x0 + op.w, y0 = op.y;
|
||||
const topAt = (x) => op.y + op.h + (x - x0) / op.w * (op.hR - op.h);
|
||||
const hTop = (x) => holeTopAt(h, x);
|
||||
@@ -1295,24 +1358,28 @@ window.WallModel = (() => {
|
||||
].filter(pts => area(pts) > 1);
|
||||
const fill = KozijnModel.normalize(op.fill);
|
||||
const isWindow = op.y >= CONFIG.plateThick;
|
||||
// Gekoppelde zijde: geen dagkant; vensterbank en waterslag lopen door tot de naad.
|
||||
const over = (d) => [op.joinL ? x0 : x0 - d, op.joinR ? x1 : x1 + d];
|
||||
if (finishIn) {
|
||||
ring.forEach(pts => prism(pts, zIn, zIn + sh, spaanplaatMaterial));
|
||||
if (buitenMateriaal === 'spaanplaat') ring.forEach(pts => prism(pts, zSheetOut - sh, zSheetOut, spaanplaatMaterial));
|
||||
const zB = fill ? (exterior ? kozBack : null) : (buitenMateriaal === 'geen' ? halfD : zSheetOut);
|
||||
if (zB !== null) {
|
||||
const boards = [
|
||||
[{ x: x0, y: y0 }, { x: x0 + T, y: y0 }, { x: x0 + T, y: topAt(x0 + T) }, { x: x0, y: topAt(x0) }],
|
||||
[{ x: x1 - T, y: y0 }, { x: x1, y: y0 }, { x: x1, y: topAt(x1) }, { x: x1 - T, y: topAt(x1 - T) }],
|
||||
!op.joinL && [{ x: x0, y: y0 }, { x: x0 + T, y: y0 }, { x: x0 + T, y: topAt(x0 + T) }, { x: x0, y: topAt(x0) }],
|
||||
!op.joinR && [{ x: x1 - T, y: y0 }, { x: x1, y: y0 }, { x: x1, y: topAt(x1) }, { x: x1 - T, y: topAt(x1 - T) }],
|
||||
[{ x: x0, y: topAt(x0) - T }, { x: x1, y: topAt(x1) - T }, { x: x1, y: topAt(x1) }, { x: x0, y: topAt(x0) }]
|
||||
];
|
||||
].filter(Boolean);
|
||||
boards.forEach(pts => prism(pts, zIn, zB, spaanplaatMaterial));
|
||||
if (isWindow) prism([{ x: x0 - 30, y: y0 - SILL + 5 }, { x: x1 + 30, y: y0 - SILL + 5 }, { x: x1 + 30, y: y0 + 5 }, { x: x0 - 30, y: y0 + 5 }], zIn - SILL_OUT, zB, spaanplaatMaterial);
|
||||
const [sa, sb] = over(30);
|
||||
if (isWindow) prism([{ x: sa, y: y0 - SILL + 5 }, { x: sb, y: y0 - SILL + 5 }, { x: sb, y: y0 + 5 }, { x: sa, y: y0 + 5 }], zIn - SILL_OUT, zB, spaanplaatMaterial);
|
||||
}
|
||||
}
|
||||
if (finishOut) {
|
||||
const outMat = KozijnModel.colorMaterial(fill ? fill.out : 'antraciet');
|
||||
ring.forEach(pts => prism(pts, halfD + G, zFacade, outMat));
|
||||
if (isWindow && fill) prism([{ x: x0 - 20, y: y0 - 25 }, { x: x1 + 20, y: y0 - 25 }, { x: x1 + 20, y: y0 + 5 }, { x: x0 - 20, y: y0 + 5 }],
|
||||
const [sa, sb] = over(20);
|
||||
if (isWindow && fill) prism([{ x: sa, y: y0 - 25 }, { x: sb, y: y0 - 25 }, { x: sb, y: y0 + 5 }, { x: sa, y: y0 + 5 }],
|
||||
CONFIG.studDepth / 2 + sh - 20, zFacade + SLAG_OUT, outMat);
|
||||
}
|
||||
});
|
||||
@@ -1331,7 +1398,7 @@ window.WallModel = (() => {
|
||||
// Dubbelzijdig: de schuine regel heeft geen vaste oriëntatie.
|
||||
const wedgeMats = new WeakMap();
|
||||
function wedgeMaterial(wood) {
|
||||
if (!wedgeMats.has(wood)) { const m = wood.clone(); m.side = THREE.DoubleSide; wedgeMats.set(wood, m); }
|
||||
if (!wedgeMats.has(wood)) { const m = wood.clone(); m.side = THREE.DoubleSide; m.userData.base = wood; wedgeMats.set(wood, m); }
|
||||
return wedgeMats.get(wood);
|
||||
}
|
||||
|
||||
@@ -1553,7 +1620,7 @@ window.WallModel = (() => {
|
||||
|
||||
return {
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, RABAT, RABAT_COLORS, rabatHex, STUC, STUC_COLORS, isStucColor, stucHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, skinConfig, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
stoneMaterial, stucMaterial, sheathingHoles, skinSegmentsAtY, topLine, skinConfig, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||
createMaterials, build3D, computeSpecs
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user