v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
+343
-109
@@ -14,8 +14,9 @@
|
||||
// voor zowel de losse wand als de unit gelden.
|
||||
//
|
||||
// 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,hR?,y,anchor,offset}] }
|
||||
// { wall: {width, heightAll, heightLeft, heightRight, heightMid?, topPts?, studWidth, ...},
|
||||
// materials: {interior, exterior}, openings: [{id,w,h,hR?,y,anchor,offset,fill?}] }
|
||||
// fill = vulling van de opening (kozijn, zie wall/kozijn-model.js).
|
||||
//
|
||||
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
|
||||
// ============================================================
|
||||
@@ -51,7 +52,64 @@ window.WallModel = (() => {
|
||||
// vochtdoek; cement-gebonden platen zijn voor steenstrips.
|
||||
const BATTEN = { thick: 20, width: 30, length: 4000, spacing: 400 };
|
||||
const CLADDING_DIRECTIONS = { horizontaal: 'Horizontaal', verticaal: 'Verticaal' };
|
||||
const CLADDING_THICK_MM = 12; // gevelbekleding (WEO/Novalit), nog niet getekend
|
||||
const CLADDING_THICK_MM = 12; // gevelbekleding (WEO/Novalit)
|
||||
|
||||
// Rabat (gevelbekleding op de kruislat): planken van BOARD_MM werkende
|
||||
// breedte in de richting van materials.cladding, in een kleur uit
|
||||
// RABAT_COLORS (woning-instelling). In 3D één blok per wandstuk, van het
|
||||
// doek tot de voorkant van de planken (latwerk + plank): zo zijn hoeken,
|
||||
// dagkanten en onderrand dicht en vervallen doek en latten erachter. De
|
||||
// planken zijn een textuur in gevel-coördinaten (lopen door over naden).
|
||||
const RABAT = { BOARD_MM: 150, TILE_BOARDS: 8 };
|
||||
const RABAT_COLORS = {
|
||||
naturel: { label: 'Naturel', hex: '#a0805f' },
|
||||
antraciet: { label: 'Antraciet', hex: '#3c4045' },
|
||||
zwart: { label: 'Zwart', hex: '#1f2022' },
|
||||
grijs: { label: 'Grijs', hex: '#8b8f93' },
|
||||
wit: { label: 'Gebroken wit', hex: '#e6e3dc' },
|
||||
groen: { label: 'Mosgroen', hex: '#55604c' }
|
||||
};
|
||||
const rabatHex = (key) => (RABAT_COLORS[key] || RABAT_COLORS.naturel).hex;
|
||||
// Eén materiaal per kleur + richting (gedeeld). Textuur: TILE_BOARDS
|
||||
// planken, elk met een lichte bovenkant en een schaduwlijn onderaan
|
||||
// (overlap); kleine kleurverschillen per plank.
|
||||
const rabatMaterials = {};
|
||||
function rabatMaterial(colorKey, direction) {
|
||||
const key = (RABAT_COLORS[colorKey] ? colorKey : 'naturel') + '|' + (direction === 'verticaal' ? 'v' : 'h');
|
||||
if (rabatMaterials[key]) return rabatMaterials[key];
|
||||
const hex = rabatHex(colorKey);
|
||||
let mat;
|
||||
const canvas = typeof document !== 'undefined' && document.createElement('canvas');
|
||||
const ctx = canvas && canvas.getContext && canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
mat = new THREE.MeshStandardMaterial({ color: hex, roughness: 0.8 });
|
||||
} else {
|
||||
const B = 32, N = RABAT.TILE_BOARDS, vert = direction === 'verticaal';
|
||||
canvas.width = vert ? B * N : 16;
|
||||
canvas.height = vert ? 16 : B * N;
|
||||
const base = new THREE.Color(hex);
|
||||
const shade = (f) => '#' + base.clone().multiplyScalar(f).getHexString();
|
||||
// rect(a, b) = band over de lengte van de plank, van a tot b (px dwars).
|
||||
const rect = (a, b, color) => {
|
||||
ctx.fillStyle = color;
|
||||
if (vert) ctx.fillRect(a, 0, b - a, canvas.height); else ctx.fillRect(0, a, canvas.width, b - a);
|
||||
};
|
||||
for (let i = 0; i < N; i++) {
|
||||
const a = i * B, f = 0.96 + ((i * 37) % 9) / 100; // 0,96 .. 1,04
|
||||
rect(a, a + B, shade(f));
|
||||
rect(a, a + 2, shade(f * 1.12)); // bovenkant (licht)
|
||||
rect(a + B - 4, a + B, shade(f * 0.55)); // schaduw van de overlap
|
||||
}
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
const tileM = RABAT.BOARD_MM * N / 1000;
|
||||
if (vert) tex.repeat.set(1 / tileM, 1); else tex.repeat.set(1, 1 / tileM);
|
||||
tex.anisotropy = 8;
|
||||
mat = new THREE.MeshStandardMaterial({ color: 0xffffff, map: tex, roughness: 0.8 });
|
||||
}
|
||||
rabatMaterials[key] = mat;
|
||||
return mat;
|
||||
}
|
||||
|
||||
// Hoe ver de afgewerkte gevel buiten het wandvlak ligt (mm): bij vochtdoek
|
||||
// de kruislat (2 lagen) + de gevelbekleding. Gebruikt o.a. voor de
|
||||
@@ -198,9 +256,10 @@ window.WallModel = (() => {
|
||||
const EXTERIOR_MATERIALS = { vochtdoek: 'Vochtdoek', cementplaat: 'Cement-gebonden platen', spaanplaat: 'Spaanplaat', geen: 'Geen (naadzijde)' };
|
||||
|
||||
// Bovenrand van de wand (daklijn): één recht stuk van links naar rechts,
|
||||
// of met een middenpunt (nok, wallPeak) twee stukken die elkaar daar
|
||||
// raken. De nok ligt nooit lager dan de goten (buildConfig), dus de
|
||||
// bovenrand is overal het MINIMUM van de (doorgetrokken) stukken — dat
|
||||
// of via tussenpunten (CONFIG.wallTop: nok, of de knikken van een
|
||||
// mansarde-/wolfsdak) meerdere stukken. De daklijn is altijd bol
|
||||
// (wallTopPoints), dus de bovenrand is overal het MINIMUM van de
|
||||
// (doorgetrokken) stukken — dat
|
||||
// houdt at/under/rangeAbove simpel en maakt "laagste punt boven een
|
||||
// opening" gelijk aan het laagste van de twee uiteinden.
|
||||
// x t.o.v. het wandmidden, alles in mm x k (k = 0.001: meters).
|
||||
@@ -213,8 +272,8 @@ window.WallModel = (() => {
|
||||
k = k || 1;
|
||||
const halfW = CONFIG.wallWidth / 2 * k;
|
||||
const L = { x: -halfW, y: CONFIG.wallHeightLeft * k }, R = { x: halfW, y: CONFIG.wallHeightRight * k };
|
||||
const pk = CONFIG.wallPeak ? { x: (CONFIG.wallPeak.x - CONFIG.wallWidth / 2) * k, y: CONFIG.wallPeak.h * k } : null;
|
||||
const pts = pk ? [L, pk, R] : [L, R];
|
||||
const inner = (CONFIG.wallTop || []).map(q => ({ x: (q.x - CONFIG.wallWidth / 2) * k, y: q.h * k }));
|
||||
const pts = [L, ...inner, R];
|
||||
const segs = [];
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const a = pts[i], b = pts[i + 1], slope = (b.y - a.y) / (b.x - a.x);
|
||||
@@ -223,15 +282,15 @@ window.WallModel = (() => {
|
||||
const lineY = (sg, x) => sg.a.y + (x - sg.a.x) * sg.slope;
|
||||
const at = (x) => Math.min(...segs.map(sg => lineY(sg, x)));
|
||||
const under = (x) => Math.min(...segs.map(sg => lineY(sg, x) - sg.vo));
|
||||
let knee = null;
|
||||
if (segs.length === 2) {
|
||||
const [s0, s1] = segs, ds = s0.slope - s1.slope;
|
||||
// Knieën: snijpunt van de onderkanten van twee opeenvolgende bovenregels.
|
||||
const knees = segs.slice(0, -1).map((s0, i) => {
|
||||
const s1 = segs[i + 1], ds = s0.slope - s1.slope;
|
||||
// s0.a.y + (x - s0.a.x) s0.slope - vo0 = s1.a.y + (x - s1.a.x) s1.slope - vo1
|
||||
const x = Math.abs(ds) > 1e-12
|
||||
? ((s1.a.y - s1.a.x * s1.slope - s1.vo) - (s0.a.y - s0.a.x * s0.slope - s0.vo)) / ds
|
||||
: pk.x;
|
||||
knee = { x, y: under(x) };
|
||||
}
|
||||
: s0.b.x;
|
||||
return { x, y: under(x) };
|
||||
});
|
||||
const rangeAbove = (y) => {
|
||||
let x0 = -halfW, x1 = halfW;
|
||||
segs.forEach(sg => {
|
||||
@@ -246,30 +305,54 @@ window.WallModel = (() => {
|
||||
.filter(c => c.x > c.sg.a.x && c.x < c.sg.b.x).map(c => c.x);
|
||||
const underPts = (xa, xb) => {
|
||||
const out = [{ x: xa, y: under(xa) }];
|
||||
if (knee && knee.x > xa && knee.x < xb) out.push({ x: knee.x, y: knee.y });
|
||||
knees.filter(kn => kn.x > xa && kn.x < xb).forEach(kn => out.push({ x: kn.x, y: kn.y }));
|
||||
out.push({ x: xb, y: under(xb) });
|
||||
return out;
|
||||
};
|
||||
// peak: hoogste tussenpunt (nok), voor maatvoering.
|
||||
const peak = inner.length ? inner.reduce((m, q) => q.y > m.y ? q : m) : null;
|
||||
return {
|
||||
pts, segs, peak: pk, knee, at, under, rangeAbove, crossings, underPts,
|
||||
pts, segs, peak, knees, knee: knees[0] || null, at, under, rangeAbove, crossings, underPts,
|
||||
maxY: Math.max(...pts.map(p => p.y)), minY: Math.min(L.y, R.y),
|
||||
kinks: pk ? [pk.x] : []
|
||||
kinks: inner.map(q => q.x)
|
||||
};
|
||||
}
|
||||
|
||||
// Omtrek(ken) van de bovenregel(s) (zelfde eenheid als topLine): één
|
||||
// parallellogram, of met nok twee stukken in verstek (nok boven, knie onder).
|
||||
// parallellogram, of per stuk van de daklijn een bovenregel; die sluiten
|
||||
// in verstek op elkaar aan (knik boven, knie onder).
|
||||
function topPlateOutlines(tl) {
|
||||
const [L, R] = [tl.pts[0], tl.pts[tl.pts.length - 1]];
|
||||
if (!tl.peak) {
|
||||
const vo = tl.segs[0].vo;
|
||||
return [[L, R, { x: R.x, y: R.y - vo }, { x: L.x, y: L.y - vo }]];
|
||||
const n = tl.segs.length, L = tl.pts[0], R = tl.pts[n];
|
||||
return tl.segs.map((sg, i) => [
|
||||
sg.a, sg.b,
|
||||
i < n - 1 ? tl.knees[i] : { x: R.x, y: R.y - sg.vo },
|
||||
i > 0 ? tl.knees[i - 1] : { x: L.x, y: L.y - sg.vo }
|
||||
]);
|
||||
}
|
||||
|
||||
// Tussenpunten van de daklijn (mm vanaf links) uit de wand-parameters:
|
||||
// topPts [{ f (0..1 van de breedte), h }] (mansarde, wolfsdak), of het
|
||||
// middenpunt heightMid op peakX (standaard midden). Alleen punten die
|
||||
// de daklijn bol houden (boven de koorde tussen hun buren); de bovenrand
|
||||
// is dan overal het minimum van de doorgetrokken stukken (topLine).
|
||||
function wallTopPoints(params, wallWidth, hLeft, hRight) {
|
||||
let raw = [];
|
||||
if (Array.isArray(params.topPts) && params.topPts.length) {
|
||||
raw = params.topPts.map(q => ({ x: q.f * wallWidth, h: Math.round(q.h) }));
|
||||
} else if (params.heightMid > 0) {
|
||||
const px = params.peakX !== undefined ? params.peakX : wallWidth / 2;
|
||||
raw = [{ x: px, h: Math.round(Math.max(params.heightMid, hLeft, hRight)) }];
|
||||
}
|
||||
const P = tl.peak, K = tl.knee;
|
||||
return [
|
||||
[L, P, K, { x: L.x, y: L.y - tl.segs[0].vo }],
|
||||
[P, R, { x: R.x, y: R.y - tl.segs[1].vo }, K]
|
||||
];
|
||||
let pts = [{ x: 0, h: hLeft }, ...raw.filter(q => q.x > 1 && q.x < wallWidth - 1).sort((a, b) => a.x - b.x), { x: wallWidth, h: hRight }];
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (let i = 1; i < pts.length - 1; i++) {
|
||||
const a = pts[i - 1], b = pts[i + 1], q = pts[i];
|
||||
const chord = a.h + (q.x - a.x) / (b.x - a.x) * (b.h - a.h);
|
||||
if (q.x - a.x < 1 || b.x - q.x < 1 || q.h <= chord + 0.5) { pts.splice(i, 1); changed = true; break; }
|
||||
}
|
||||
}
|
||||
return pts.slice(1, -1);
|
||||
}
|
||||
|
||||
// Heuristiek (geen volwaardige constraint-solver): elke opening wordt eerst
|
||||
@@ -305,7 +388,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, rawHR: hRightOf(raw), rawY: raw.y };
|
||||
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 };
|
||||
});
|
||||
|
||||
resolved.sort((a, b) => a.x - b.x);
|
||||
@@ -400,24 +483,23 @@ window.WallModel = (() => {
|
||||
const hRight = Math.round(params.heightRight || 2900);
|
||||
const studWidth = Math.round(params.studWidth || STANDARD_BEAM.width);
|
||||
const plateThick = Math.round(params.plateThick || STANDARD_BEAM.width);
|
||||
// Middenpunt (nok): standaard midden op de wand, of op peakX (mm vanaf
|
||||
// links; een wandstuk uit een unit waar de nok niet in het midden
|
||||
// valt). Nooit lager dan de hoogste goot; gelijk = geen nok.
|
||||
let wallPeak = null;
|
||||
if (params.heightMid > 0) {
|
||||
const px = params.peakX !== undefined ? params.peakX : wallWidth / 2;
|
||||
const h = Math.round(Math.max(params.heightMid, hLeft, hRight));
|
||||
if (px > 1 && px < wallWidth - 1 && h > hLeft + (px / wallWidth) * (hRight - hLeft) + 0.5) wallPeak = { x: px, h };
|
||||
}
|
||||
const roofAt = (x) => wallPeak
|
||||
? (x <= wallPeak.x ? hLeft + x / wallPeak.x * (wallPeak.h - hLeft) : wallPeak.h + (x - wallPeak.x) / (wallWidth - wallPeak.x) * (hRight - wallPeak.h))
|
||||
: hLeft + (x / wallWidth) * (hRight - hLeft);
|
||||
// Tussenpunten van de daklijn (nok; knikken bij mansarde/wolfsdak),
|
||||
// zie wallTopPoints. Een wandstuk uit een unit krijgt ze op zijn eigen
|
||||
// positie (peakX / topPts).
|
||||
const wallTop = wallTopPoints(params, wallWidth, hLeft, hRight);
|
||||
const tp = [{ x: 0, h: hLeft }, ...wallTop, { x: wallWidth, h: hRight }];
|
||||
const roofAt = (x) => {
|
||||
let i = 0;
|
||||
while (i < tp.length - 2 && x > tp[i + 1].x) i++;
|
||||
const a = tp[i], b = tp[i + 1];
|
||||
return a.h + (x - a.x) / (b.x - a.x) * (b.h - a.h);
|
||||
};
|
||||
|
||||
return {
|
||||
wallWidth,
|
||||
wallHeightLeft: hLeft,
|
||||
wallHeightRight: hRight,
|
||||
wallPeak,
|
||||
wallTop,
|
||||
studWidth,
|
||||
studDepth: Math.round(params.studDepth || STANDARD_BEAM.depth),
|
||||
plateThick,
|
||||
@@ -447,7 +529,7 @@ window.WallModel = (() => {
|
||||
const top = topLine(CONFIG);
|
||||
const slope = top.segs[0].slope;
|
||||
// Met nok: de steilste helling (beide kanten lopen dan op naar de nok).
|
||||
const angleDeg = top.peak
|
||||
const angleDeg = top.segs.length > 1
|
||||
? Math.round(Math.max(...top.segs.map(sg => Math.atan(Math.abs(sg.slope)))) * (180 / Math.PI))
|
||||
: Math.round(Math.atan(slope) * (180 / Math.PI));
|
||||
|
||||
@@ -623,9 +705,12 @@ window.WallModel = (() => {
|
||||
// facade (optioneel, woning): { u0, extStart, isEnd } — plaats van dit
|
||||
// wandstuk in de doorlopende gevel (zie HouseModel.assignFacades): u0 =
|
||||
// afstand (mm, wandvlak) van het gevelbegin (incl. hoekdoorloop) tot de
|
||||
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de
|
||||
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
|
||||
// alleen aan het einde van de gevel.
|
||||
// linkerrand van dit stuk (gebruikt door de horizontale latten via gridY0).
|
||||
// Verticale latten: per wandstuk (de modules worden los vervoerd, de
|
||||
// bekleding eindigt op de rand van het stuk) een lat op beide randen en
|
||||
// langs beide zijden van elke opening (de bekleding moet daar op een lat
|
||||
// eindigen); daartussen gelijk verdeeld, h.o.h. max. BATTEN.spacing
|
||||
// (krimpend als het vak kleiner is, minimaal de twee randlatten).
|
||||
// facade.gridY0 (optioneel, mm, wand-coördinaten): hoogte waarop het
|
||||
// horizontale latten-raster van de hele gevel begint. Een wandstuk dat
|
||||
// hoger begint (opbouw, blootliggende naadwand) volgt zo hetzelfde raster.
|
||||
@@ -637,8 +722,6 @@ window.WallModel = (() => {
|
||||
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;
|
||||
const isEnd = facade ? facade.isEnd : true;
|
||||
const halfW = CONFIG.wallWidth / 2;
|
||||
const tl = topLine(CONFIG), topYAt = tl.at;
|
||||
const holes = sheathingHoles(CONFIG).concat(extraHoles || []);
|
||||
@@ -653,17 +736,35 @@ window.WallModel = (() => {
|
||||
yStart = g + Math.max(0, Math.ceil((yStart - g) / sp - 1e-9)) * sp;
|
||||
}
|
||||
for (let y = yStart; y < tl.maxY - w / 2; y += sp) {
|
||||
// Schuine bovenrand: afkappen op de bovenkant van de lat (y + w/2),
|
||||
// anders steekt het uiteinde tot w/2 boven de daklijn (door het dak).
|
||||
const cap = tl.rangeAbove(y + w / 2);
|
||||
if (!cap) continue;
|
||||
skinSegmentsAtY(CONFIG, y, extraHoles).forEach(([x0, x1]) => {
|
||||
x0 = Math.max(x0, cap[0]); x1 = Math.min(x1, cap[1]);
|
||||
if (x0 <= -halfW + 0.5) x0 = xL;
|
||||
if (x1 >= halfW - 0.5) x1 = xR;
|
||||
if (x1 - x0 > w) horizontaal.push({ x0, x1, y });
|
||||
});
|
||||
}
|
||||
// Verticale latten: vanaf de linkerrand elke 400 mm, plus een lat op de rechterrand.
|
||||
// Verticale latten: vaste posities (randen, zijden van openingen),
|
||||
// daartussen gelijk verdeeld met h.o.h. <= sp.
|
||||
const lo = xL + w / 2, hi = xR - w / 2;
|
||||
const fixed = [lo, hi];
|
||||
holes.forEach(h => {
|
||||
if (h.hy0 >= tl.maxY || holeTopAt(h, h.hx0) <= y0) return; // valt buiten de bekleding
|
||||
[h.hx0 - w / 2, h.hx1 + w / 2].forEach(x => { if (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); // niet dubbel op elkaar
|
||||
const xs = [];
|
||||
// Raster vanaf het gevelbegin; een lat hoort bij het stuk waar zijn hart in valt.
|
||||
for (let x = gridX0 + w / 2 + Math.max(0, Math.ceil((xL - gridX0 - w / 2) / sp - 1e-9)) * sp; x <= xR - (isEnd ? w / 2 - 0.5 : 0) && x < xR; x += sp) xs.push(x);
|
||||
if (isEnd && (!xs.length || xR - w / 2 - xs[xs.length - 1] > 50)) xs.push(xR - w / 2);
|
||||
anchors.forEach((x, i) => {
|
||||
xs.push(x);
|
||||
const next = anchors[i + 1];
|
||||
if (next === undefined) return;
|
||||
const n = Math.ceil((next - x) / sp - 1e-9);
|
||||
for (let k = 1; k < n; k++) xs.push(x + (next - x) * k / n);
|
||||
});
|
||||
const verticaal = [];
|
||||
xs.forEach(x => {
|
||||
const yTop = Math.min(topYAt(x - w / 2), topYAt(x + w / 2));
|
||||
@@ -757,14 +858,19 @@ window.WallModel = (() => {
|
||||
// Bouwt de volledige 3D-wand (in meters, lokaal assenstelsel: X = breedte
|
||||
// gecentreerd, Y = hoogte vanaf onderkant onderregel, +Z = buitenzijde/zuid)
|
||||
// en geeft daarnaast de volume-boekhouding terug voor de specificaties.
|
||||
// opts: { materials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible }
|
||||
// opts: { materials, buitenMateriaal, cladding, trasraam, spaanplaatVisible, buitenVisible, stage, rabatColor }
|
||||
// trasraam: { bottom, top } in mm (wand-coördinaten, zie trasraamFor) of null.
|
||||
// skinExt: { left, right } in mm (zie cornerExt) of null.
|
||||
// facade: { u0, extStart, isEnd } (zie computeBattens) of null.
|
||||
// stage: bouwfase (BuildStages, standaard afgewerkt). Lagen van een latere
|
||||
// fase en lagen die in deze fase volledig ingesloten zijn worden niet
|
||||
// gebouwd; de stats rekenen altijd met alle lagen.
|
||||
function build3D(CONFIG, calcData, opts) {
|
||||
const { woodMaterial, spaanplaatMaterial, doekMaterial, cementplaatMaterial, battenMaterial } = opts.materials;
|
||||
const buitenMateriaal = opts.buitenMateriaal || 'vochtdoek';
|
||||
const spaanplaatVisible = opts.spaanplaatVisible !== false;
|
||||
const stage = BuildStages.clamp(opts.stage);
|
||||
const show = (key) => BuildStages.shows(stage, key);
|
||||
const spaanplaatVisible = opts.spaanplaatVisible !== false && show('beplating');
|
||||
const buitenVisible = opts.buitenVisible !== false;
|
||||
const cladding = opts.cladding || 'horizontaal';
|
||||
// Trasraam: bij rabat (vochtdoek) en bij een steengevel (cementplaat + stone).
|
||||
@@ -778,6 +884,19 @@ window.WallModel = (() => {
|
||||
// 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';
|
||||
// Zichtbare buitenlagen in deze fase. Bij rabat (vochtdoek-gevel) dekt
|
||||
// het rabatblok doek en latten volledig af (zie RABAT).
|
||||
const rabatOn = buitenMateriaal === 'vochtdoek' && buitenVisible && show('gevel');
|
||||
const doekOn = buitenVisible && show('doek') && !rabatOn;
|
||||
const battensOn = buitenVisible && show('latwerk') && !rabatOn;
|
||||
// Regelwerk ingesloten: binnen spaanplaat, buiten een plaat of het doek
|
||||
// (naadzijde 'geen': het deel van de andere unit staat ertegen). Dan
|
||||
// alleen de stukken die in de dagkant van een opening of aan de rand
|
||||
// te zien zijn (hoek-, koningsstijlen, regels, dorpels).
|
||||
const outerCovered = buitenMateriaal === 'geen' ||
|
||||
(buitenVisible && show(buitenMateriaal === 'vochtdoek' ? 'doek' : 'beplating'));
|
||||
const frameEnclosed = opts.cull !== false && spaanplaatVisible && outerCovered;
|
||||
const HIDDEN_STUDS = { regel: true, kreupel: true, dubbel: true };
|
||||
// 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;
|
||||
@@ -805,7 +924,8 @@ window.WallModel = (() => {
|
||||
let doekAreaM2 = 0; // enkelzijdig, alleen als buitenMateriaal='vochtdoek'
|
||||
let spaanplaatBuitenAreaM2 = 0; // alleen als buitenMateriaal='spaanplaat' (binnenwanden)
|
||||
let spaanplaatBuitenVolumeM3 = 0;
|
||||
let battenLengthM = 0, battenVolumeM3 = 0, battenCount = 0; // kruislat (alleen vochtdoek)
|
||||
let battenLengthM = 0, battenVolumeM3 = 0, battenCount = 0, battenPieces = 0; // kruislat (alleen vochtdoek); count = inkoop, pieces = gezaagde stukken
|
||||
let rabatAreaM2 = 0; // gevelbekleding rabat (gevelvlak, alleen vochtdoek)
|
||||
let trasraamAreaM2 = 0, trasraamVolumeM3 = 0; // cement-gebonden plaat onderaan (alleen vochtdoek)
|
||||
let stoneAreaM2 = 0, stoneIsolatieM3 = 0; // steengevel (cementplaat + stone): strip + isolatieblok
|
||||
let trasraamStoneAreaM2 = 0, trasraamIsolatieM3 = 0; // stenen trasraam: steenstrip (gevelvlak) + isolatieblok
|
||||
@@ -845,9 +965,16 @@ window.WallModel = (() => {
|
||||
const skinAbove = opts.skinAbove > 0 ? opts.skinAbove : 0;
|
||||
const CONFIG_OUT = skinAbove ? Object.assign({}, CONFIG, {
|
||||
wallHeightLeft: CONFIG.wallHeightLeft + skinAbove, wallHeightRight: CONFIG.wallHeightRight + skinAbove,
|
||||
wallPeak: CONFIG.wallPeak ? { x: CONFIG.wallPeak.x, h: CONFIG.wallPeak.h + skinAbove } : null
|
||||
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + skinAbove }))
|
||||
}) : CONFIG;
|
||||
const tlOut = skinAbove ? topLine(CONFIG_OUT, toM) : tl;
|
||||
// Latwerk stopt onder het boeideel/de daktrim (opts.battenAbove, mm
|
||||
// t.o.v. de wandbovenkant, kan negatief zijn); zonder rand tot skinAbove.
|
||||
const battenAbove = typeof opts.battenAbove === 'number' ? opts.battenAbove : skinAbove;
|
||||
const CONFIG_BAT = battenAbove ? Object.assign({}, CONFIG, {
|
||||
wallHeightLeft: CONFIG.wallHeightLeft + battenAbove, wallHeightRight: CONFIG.wallHeightRight + battenAbove,
|
||||
wallTop: CONFIG.wallTop.map(q => ({ x: q.x, h: q.h + battenAbove }))
|
||||
}) : CONFIG;
|
||||
// Onderkant bovenregel van xLeft naar xRight (incl. verstek bij de nok).
|
||||
const underPts = tl.underPts;
|
||||
|
||||
@@ -904,9 +1031,32 @@ window.WallModel = (() => {
|
||||
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), pDepthM, -pDepthM / 2));
|
||||
horizBeamCount++;
|
||||
});
|
||||
// Schuine regel op de bovenregel (CONFIG.topWedge): vult de wig tot
|
||||
// de onderkant van het dak, over de diepte van het regelwerk.
|
||||
if (CONFIG.topWedge) {
|
||||
const wg = CONFIG.topWedge.pts, zO = sDepthM / 2, zI = -sDepthM / 2, pos = [], tlMM = topLine(CONFIG);
|
||||
const P = (q, z, h) => [q.x * toM, (tlMM.at(q.x) + h) * toM, z];
|
||||
const quad = (a, b, c, d) => pos.push(...a, ...b, ...c, ...a, ...c, ...d);
|
||||
for (let k = 0; k < wg.length - 1; k++) {
|
||||
const a = wg[k], b = wg[k + 1];
|
||||
quad(P(a, zO, a.o), P(b, zO, b.o), P(b, zI, b.i), P(a, zI, a.i)); // boven (dakkant)
|
||||
quad(P(a, zO, 0), P(b, zO, 0), P(b, zO, b.o), P(a, zO, a.o)); // buiten
|
||||
quad(P(a, zI, a.i), P(b, zI, b.i), P(b, zI, 0), P(a, zI, 0)); // binnen
|
||||
woodVolumeM3 += ((a.o + a.i) / 2 + (b.o + b.i) / 2) / 2 * (b.x - a.x) * CONFIG.studDepth * 1e-9;
|
||||
}
|
||||
[wg[0], wg[wg.length - 1]].forEach(q => quad(P(q, zO, 0), P(q, zO, q.o), P(q, zI, q.i), P(q, zI, 0))); // kopse kanten
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||||
geo.computeVertexNormals();
|
||||
const wedge = new THREE.Mesh(geo, woodMaterial.side === THREE.DoubleSide ? woodMaterial : wedgeMaterial(woodMaterial));
|
||||
wedge.castShadow = true;
|
||||
wallGroup3D.add(wedge);
|
||||
}
|
||||
|
||||
// C. VERTICALE STIJLEN (vol, koning, dubbel bij deuren, kreupel): elk
|
||||
// stuk van y0 tot y1, of tot onder de daklijn (y1 null), zie computeFrame.
|
||||
// Ingesloten stijlen (frameEnclosed) tellen mee in het volume, maar
|
||||
// worden niet gebouwd.
|
||||
calcData.studs.forEach(st => {
|
||||
const xM = st.x * toM;
|
||||
const xLeft = xM - (sWidthM / 2);
|
||||
@@ -916,6 +1066,7 @@ window.WallModel = (() => {
|
||||
const pts = st.y1 === null
|
||||
? [{ 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 }];
|
||||
if (frameEnclosed && HIDDEN_STUDS[st.kind]) { woodVolumeM3 += polygonArea(pts) * sDepthM; return; }
|
||||
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
||||
});
|
||||
|
||||
@@ -1063,7 +1214,7 @@ window.WallModel = (() => {
|
||||
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)) });
|
||||
capped ? { extraXs: tlOut.kinks.concat([-halfW, halfW]) } : { yCap, extraXs: tlOut.kinks.concat(tlOut.crossings(yCap), [-halfW, halfW]) });
|
||||
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)))));
|
||||
@@ -1071,10 +1222,9 @@ window.WallModel = (() => {
|
||||
});
|
||||
};
|
||||
const add = (shapes, depth, z, mat) => {
|
||||
if (!shapes.length) return;
|
||||
if (!shapes.length || !buitenVisible || !show('gevel')) return;
|
||||
const mesh = new THREE.Mesh(setUV(new THREE.ExtrudeGeometry(shapes, { steps: 1, depth, bevelEnabled: false })), mat);
|
||||
mesh.position.set(0, 0, z);
|
||||
mesh.visible = buitenVisible;
|
||||
mesh.castShadow = true;
|
||||
wallGroup3D.add(mesh);
|
||||
};
|
||||
@@ -1136,22 +1286,19 @@ window.WallModel = (() => {
|
||||
return shape;
|
||||
});
|
||||
if (!boardShapes.length) continue;
|
||||
|
||||
const boardExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
||||
const boardGeo = new THREE.ExtrudeGeometry(boardShapes, boardExtrudeSettings);
|
||||
const boardAreaM2 = boardShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
||||
|
||||
const board = new THREE.Mesh(boardGeo, material);
|
||||
board.position.set(0, 0, z);
|
||||
board.visible = visible;
|
||||
wallGroup3D.add(board);
|
||||
|
||||
if (!seamless) {
|
||||
const edgesGeo = new THREE.EdgesGeometry(boardGeo);
|
||||
const edgeLines = new THREE.LineSegments(edgesGeo, edgeMaterial);
|
||||
edgeLines.position.set(0, 0, z);
|
||||
edgeLines.visible = visible;
|
||||
wallGroup3D.add(edgeLines);
|
||||
// Niet zichtbaar (fase/weergave): alleen meten, niet bouwen.
|
||||
if (visible) {
|
||||
const boardGeo = new THREE.ExtrudeGeometry(boardShapes, { steps: 1, depth: sThickM, bevelEnabled: false });
|
||||
const board = new THREE.Mesh(boardGeo, material);
|
||||
board.position.set(0, 0, z);
|
||||
wallGroup3D.add(board);
|
||||
if (!seamless) {
|
||||
const edgeLines = new THREE.LineSegments(new THREE.EdgesGeometry(boardGeo), edgeMaterial);
|
||||
edgeLines.position.set(0, 0, z);
|
||||
wallGroup3D.add(edgeLines);
|
||||
}
|
||||
}
|
||||
|
||||
areaM2 += boardAreaM2;
|
||||
@@ -1163,15 +1310,15 @@ window.WallModel = (() => {
|
||||
const shape = new THREE.Shape([
|
||||
new THREE.Vector2(a, yB), new THREE.Vector2(b, yB), new THREE.Vector2(b, skinTopAt(b)), new THREE.Vector2(a, skinTopAt(a))
|
||||
]);
|
||||
const geo = new THREE.ExtrudeGeometry(shape, { steps: 1, depth: sThickM, bevelEnabled: false });
|
||||
const mesh = new THREE.Mesh(geo, material);
|
||||
mesh.position.set(0, 0, z + ext.zOut);
|
||||
mesh.visible = visible;
|
||||
wallGroup3D.add(mesh);
|
||||
const edgeLines = new THREE.LineSegments(new THREE.EdgesGeometry(geo), edgeMaterial);
|
||||
edgeLines.position.set(0, 0, z + ext.zOut);
|
||||
edgeLines.visible = visible;
|
||||
wallGroup3D.add(edgeLines);
|
||||
if (visible) {
|
||||
const geo = new THREE.ExtrudeGeometry(shape, { steps: 1, depth: sThickM, bevelEnabled: false });
|
||||
const mesh = new THREE.Mesh(geo, material);
|
||||
mesh.position.set(0, 0, z + ext.zOut);
|
||||
wallGroup3D.add(mesh);
|
||||
const edgeLines = new THREE.LineSegments(new THREE.EdgesGeometry(geo), edgeMaterial);
|
||||
edgeLines.position.set(0, 0, z + ext.zOut);
|
||||
wallGroup3D.add(edgeLines);
|
||||
}
|
||||
const a2 = shapeAreaWithHoles(shape);
|
||||
areaM2 += a2;
|
||||
volumeM3 += a2 * sThickM;
|
||||
@@ -1191,8 +1338,7 @@ window.WallModel = (() => {
|
||||
const cap = new THREE.Mesh(new THREE.PlaneGeometry(depth, h), spaanplaatMaterial);
|
||||
cap.rotation.y = dir * Math.PI / 2;
|
||||
cap.position.set(x + dir * 0.0003, h / 2, 0);
|
||||
cap.visible = spaanplaatVisible;
|
||||
wallGroup3D.add(cap);
|
||||
if (spaanplaatVisible) wallGroup3D.add(cap);
|
||||
});
|
||||
spaanplaatAreaM2 = spaanplaatResult.areaM2;
|
||||
spaanplaatVolumeM3 = spaanplaatResult.volumeM3;
|
||||
@@ -1200,19 +1346,19 @@ 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 });
|
||||
const spaanOnder = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible && show('beplating'), { 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);
|
||||
const spaanBuiten = buildVerticalBoardSkin(spaanplaatMaterial, 'front', buitenVisible && show('beplating'));
|
||||
spaanplaatBuitenAreaM2 = spaanBuiten.areaM2;
|
||||
spaanplaatBuitenVolumeM3 = spaanBuiten.volumeM3;
|
||||
} else if (buitenMateriaal === 'geen') {
|
||||
// Naadzijde: geen beplating (het deel van de andere unit staat ertegen).
|
||||
} else if (buitenMateriaal === 'cementplaat') {
|
||||
const cementResult = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, null,
|
||||
const cementResult = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible && show('beplating'), null,
|
||||
Object.assign(extM(0), { zOut: sThickM + 2 * skinGapM }));
|
||||
cementplaatAreaM2 = cementResult.areaM2;
|
||||
cementplaatVolumeM3 = cementResult.volumeM3;
|
||||
@@ -1227,7 +1373,7 @@ window.WallModel = (() => {
|
||||
trasraamIsolatieM3 = r.isoM3;
|
||||
y0 = trasraam.top * toM;
|
||||
} else if (trasraam) {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible && show('latwerk'), {
|
||||
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + skinGapM
|
||||
}, extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM));
|
||||
trasraamAreaM2 = tras.areaM2;
|
||||
@@ -1257,6 +1403,10 @@ 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
|
||||
// Doek tot de bovenrand van de gevel (achter de dakrand langs, ook
|
||||
// zichtbaar in fasen zonder dakrand); latwerk en rabat stoppen onder
|
||||
// het boeideel/de daktrim (tlBat).
|
||||
const tlBat = CONFIG_BAT === CONFIG_OUT ? tlOut : topLine(CONFIG_BAT, toM);
|
||||
const maxRoofY = tlOut.maxY;
|
||||
|
||||
let rowY0 = skinFrom * toM;
|
||||
@@ -1279,17 +1429,15 @@ window.WallModel = (() => {
|
||||
return shape;
|
||||
});
|
||||
if (rowShapes.length) {
|
||||
const rowExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
||||
const rowGeo = shiftUV(new THREE.ExtrudeGeometry(rowShapes, rowExtrudeSettings));
|
||||
const rowAreaM2 = rowShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
||||
|
||||
const doekRow = new THREE.Mesh(rowGeo, doekMaterial);
|
||||
// 1 mm naar buiten: niet in hetzelfde vlak als de dakrand (dak op
|
||||
// de wand), anders flikkert het dak door het doek heen waar de
|
||||
// gevel tot onder de dakrand doorloopt (skinAbove).
|
||||
doekRow.position.set(0, 0, (maxDepth / 2) + skinGapM + rowIndex * doekZStepM + DOEK_OUT_M);
|
||||
doekRow.visible = buitenVisible;
|
||||
wallGroup3D.add(doekRow);
|
||||
if (doekOn) {
|
||||
const doekRow = new THREE.Mesh(shiftUV(new THREE.ExtrudeGeometry(rowShapes, { steps: 1, depth: sThickM, bevelEnabled: false })), doekMaterial);
|
||||
// 1 mm naar buiten: niet in hetzelfde vlak als de dakrand (dak op
|
||||
// de wand), anders flikkert het dak door het doek heen waar de
|
||||
// gevel tot onder de dakrand doorloopt (skinAbove).
|
||||
doekRow.position.set(0, 0, (maxDepth / 2) + skinGapM + rowIndex * doekZStepM + DOEK_OUT_M);
|
||||
wallGroup3D.add(doekRow);
|
||||
}
|
||||
|
||||
// Elke baan telt met zijn volle oppervlak mee (incl. de overlapzone
|
||||
// met de vorige baan) — dat is fysiek correct voor een materiaal-
|
||||
@@ -1306,10 +1454,11 @@ window.WallModel = (() => {
|
||||
if (b - a <= edgeEps || Math.min(roofAt(a), roofAt(b)) <= rowY0 + edgeEps) return;
|
||||
const pts = [{ x: a, y: rowY0 }, { x: b, y: rowY0 }, { x: b, y: Math.min(rowY1, roofAt(b)) }, { x: a, y: Math.min(rowY1, roofAt(a)) }];
|
||||
const shape = new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y)));
|
||||
const mesh = new THREE.Mesh(shiftUV(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: stripM, bevelEnabled: false })), doekMaterial);
|
||||
mesh.position.set(0, 0, zOut);
|
||||
mesh.visible = buitenVisible;
|
||||
wallGroup3D.add(mesh);
|
||||
if (doekOn) {
|
||||
const mesh = new THREE.Mesh(shiftUV(new THREE.ExtrudeGeometry(shape, { steps: 1, depth: stripM, bevelEnabled: false })), doekMaterial);
|
||||
mesh.position.set(0, 0, zOut);
|
||||
wallGroup3D.add(mesh);
|
||||
}
|
||||
doekAreaM2 += shapeAreaWithHoles(shape);
|
||||
});
|
||||
rowY0 += doekStepM;
|
||||
@@ -1325,7 +1474,7 @@ window.WallModel = (() => {
|
||||
trasraamStoneAreaM2 = r.areaM2;
|
||||
trasraamIsolatieM3 = r.isoM3;
|
||||
} else if (trasraam) {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible, {
|
||||
const tras = buildVerticalBoardSkin(cementplaatMaterial, 'front', buitenVisible && show('latwerk'), {
|
||||
y0: trasraam.bottom * toM, y1: trasraam.top * toM, zExtra: sThickM + (rowIndex + 1) * doekZStepM + skinGapM
|
||||
}, extM(CONFIG.sheathingThick + 2 * SKIN_GAP_MM));
|
||||
trasraamAreaM2 = tras.areaM2;
|
||||
@@ -1334,7 +1483,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_OUT, cladding, trasraam, skinExt, facade, skinFrom, trimHolesMM);
|
||||
const bat = computeBattens(CONFIG_BAT, cladding, trasraam, skinExt, facade, skinFrom, trimHolesMM);
|
||||
const bT = BATTEN.thick * toM, bW = BATTEN.width * toM;
|
||||
const zDoek = (maxDepth / 2) + skinGapM + sThickM + rowIndex * doekZStepM + skinGapM + DOEK_OUT_M;
|
||||
const addLayer = (orientation, z) => {
|
||||
@@ -1343,15 +1492,69 @@ window.WallModel = (() => {
|
||||
const len = horiz ? (b.x1 - b.x0) * toM : (b.y1 - b.y0) * toM;
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(horiz ? len : bW, horiz ? bW : len, bT), battenMaterial);
|
||||
mesh.position.set(horiz ? (b.x0 + b.x1) / 2 * toM : b.x * toM, horiz ? b.y * toM : (b.y0 + b.y1) / 2 * toM, z + bT / 2);
|
||||
mesh.visible = buitenVisible;
|
||||
wallGroup3D.add(mesh);
|
||||
});
|
||||
};
|
||||
addLayer(bat.under, zDoek);
|
||||
addLayer(bat.top, zDoek + bT);
|
||||
if (battensOn) {
|
||||
addLayer(bat.under, zDoek);
|
||||
addLayer(bat.top, zDoek + bT);
|
||||
}
|
||||
battenLengthM = bat.totalLengthMM * toM;
|
||||
battenVolumeM3 = battenLengthM * bT * bW;
|
||||
battenCount = bat.stock.count;
|
||||
battenPieces = bat.lengths.length;
|
||||
|
||||
// I. RABAT op de kruislat: één blok van het doek tot de voorkant van
|
||||
// de planken (RABAT), boven het trasraam tot onder de dakrand (zoals
|
||||
// het latwerk), met dezelfde uitsparingen en hoekdoorloop als de
|
||||
// andere buitenlagen. Oppervlak (gevelvlak) altijd, het blok alleen
|
||||
// vanaf fase "gevel".
|
||||
const rabatDepthMM = 2 * BATTEN.thick + CLADDING_THICK_MM;
|
||||
const rExt = extM(rabatDepthMM);
|
||||
const rxL = -halfW - rExt.left, rxR = halfW + rExt.right;
|
||||
const rY0 = Math.max(skinStartY(trasraam), skinFrom) * toM;
|
||||
const rTop = (x) => tlBat.at(Math.max(-halfW, Math.min(halfW, x)));
|
||||
const rabatShapes = (rxR - rxL > edgeEps && tlBat.maxY > rY0 + edgeEps)
|
||||
? columnMinusRects(rxL, rxR, rY0, rTop, outerHoles.map(holeRect), { extraXs: tlBat.kinks.concat([-halfW, halfW]) }).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;
|
||||
}) : [];
|
||||
rabatAreaM2 = rabatShapes.reduce((sum, shape) => sum + shapeAreaWithHoles(shape), 0);
|
||||
if (rabatOn && rabatShapes.length) {
|
||||
const geo = new THREE.ExtrudeGeometry(rabatShapes, { steps: 1, depth: rabatDepthMM * toM, bevelEnabled: false });
|
||||
// UV's in gevel-coördinaten: u langs de gevel, v vanaf het latten-
|
||||
// raster (bovenkant trasraam), zodat de planknaden doorlopen.
|
||||
const gridM = (facade && facade.gridY0 !== undefined ? facade.gridY0 : skinStartY(trasraam)) * toM;
|
||||
const pos = geo.attributes.position, nrm = geo.attributes.normal, uv = geo.attributes.uv;
|
||||
for (let k = 0; k < pos.count; k++) {
|
||||
const x = pos.getX(k) + uvShiftM, y = pos.getY(k) - gridM, z = pos.getZ(k);
|
||||
if (Math.abs(nrm.getX(k)) > 0.5) uv.setXY(k, z, y); else if (Math.abs(nrm.getY(k)) > 0.5) uv.setXY(k, x, z); else uv.setXY(k, x, y);
|
||||
}
|
||||
uv.needsUpdate = true;
|
||||
const mesh = new THREE.Mesh(geo, rabatMaterial(opts.rabatColor, cladding));
|
||||
mesh.position.set(0, 0, zDoek);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
wallGroup3D.add(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
// Kozijnen (KozijnModel): vanaf de fase gevel. Buitenwand: buitenvlak
|
||||
// gelijk met de buitenbeplating, 120 mm diep, met dorpel; binnen- en
|
||||
// naadwand: over de volle wanddikte, zonder dorpel.
|
||||
if (show('gevel') && window.KozijnModel) {
|
||||
const sh = CONFIG.sheathingThick, half = CONFIG.studDepth / 2;
|
||||
const exterior = buitenMateriaal === 'vochtdoek' || buitenMateriaal === 'cementplaat';
|
||||
const place = exterior ? { zOut: half + sh, depth: KozijnModel.PROFILE.depth, exterior: true }
|
||||
: buitenMateriaal === 'geen' ? { zOut: half, depth: CONFIG.studDepth + sh, exterior: false }
|
||||
: { zOut: half + sh, depth: CONFIG.studDepth + 2 * sh, exterior: false };
|
||||
CONFIG.openings.forEach(op => {
|
||||
const k = op.fill ? KozijnModel.build3D(op, op.fill, place) : null;
|
||||
if (!k) return;
|
||||
k.position.set(startXM + op.x * toM, op.y * toM, 0);
|
||||
wallGroup3D.add(k);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1359,11 +1562,18 @@ window.WallModel = (() => {
|
||||
stats: {
|
||||
woodVolumeM3, spaanplaatVolumeM3, spaanplaatAreaM2, cementplaatVolumeM3, cementplaatAreaM2, doekAreaM2,
|
||||
spaanplaatBuitenAreaM2, spaanplaatBuitenVolumeM3, horizBeamCount,
|
||||
battenLengthM, battenVolumeM3, battenCount, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3
|
||||
battenLengthM, battenVolumeM3, battenCount, battenPieces, rabatAreaM2, trasraamAreaM2, trasraamVolumeM3, trasraamStoneAreaM2, trasraamIsolatieM3, stoneAreaM2, stoneIsolatieM3
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Hout, 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); }
|
||||
return wedgeMats.get(wood);
|
||||
}
|
||||
|
||||
// Specificaties (volumes -> gewichten), op basis van de stats uit build3D.
|
||||
function computeSpecs(CONFIG, stats, buitenMateriaal) {
|
||||
const toM = 0.001;
|
||||
@@ -1440,6 +1650,27 @@ window.WallModel = (() => {
|
||||
pieces.push({ length: halfW - cursor, width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'onderregel', x: (cursor + halfW) / 2, y: CONFIG.plateThick / 2, cutAngles: [0, 0] });
|
||||
}
|
||||
|
||||
// Schuine regel (CONFIG.topWedge): één stuk over de wand, uit een balk
|
||||
// van de grootste wighoogte gezaagd onder de dakhelling.
|
||||
// Per aaneengesloten stuk waar een wig zit (bijv. alleen onder een wolfseind).
|
||||
if (CONFIG.topWedge) {
|
||||
const wg = CONFIG.topWedge.pts, has = (q) => Math.max(q.o, q.i) >= 5, runs = [];
|
||||
for (let k = 0; k < wg.length - 1; k++) {
|
||||
if (!has(wg[k]) && !has(wg[k + 1])) continue;
|
||||
const last = runs[runs.length - 1];
|
||||
if (last && last[last.length - 1] === wg[k]) last.push(wg[k + 1]); else runs.push([wg[k], wg[k + 1]]);
|
||||
}
|
||||
runs.forEach(run => {
|
||||
const hMax = Math.max(...run.map(q => Math.max(q.o, q.i)));
|
||||
const bevel = Math.round(Math.max(...run.map(q => Math.atan(Math.abs(q.o - q.i) / CONFIG.studDepth))) * 1800 / Math.PI) / 10;
|
||||
let len = 0;
|
||||
for (let k = 0; k < run.length - 1; k++) len += Math.hypot(run[k + 1].x - run[k].x, tl.at(run[k + 1].x) - tl.at(run[k].x));
|
||||
const outline = run.map(q => ({ x: q.x, y: tl.at(q.x) })).concat(run.slice().reverse().map(q => ({ x: q.x, y: tl.at(q.x) + Math.max(q.o, q.i) })));
|
||||
const xm = (run[0].x + run[run.length - 1].x) / 2;
|
||||
pieces.push({ length: Math.round(len), width: Math.max(1, Math.round(hMax)), depth: CONFIG.studDepth, kind: 'schuine regel', x: xm, y: tl.at(xm) + hMax / 2, outline, cutAngles: [bevel, bevel] });
|
||||
});
|
||||
}
|
||||
|
||||
// Bovenregel(s): met nok twee stukken (verstek). outline = werkelijke
|
||||
// omtrek (PDF-tekening).
|
||||
topPlateOutlines(tl).forEach((outline, i) => {
|
||||
@@ -1580,6 +1811,9 @@ window.WallModel = (() => {
|
||||
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})`);
|
||||
}
|
||||
if (o.fill != null && (typeof o.fill !== 'object' || !KozijnModel.FILL_TYPES[o.fill.type])) {
|
||||
throw new Error(`opening #${i + 1}: fill.type moet ${Object.keys(KozijnModel.FILL_TYPES).join('/')} zijn`);
|
||||
}
|
||||
if (!['left', 'middle', 'right'].includes(o.anchor)) {
|
||||
throw new Error(`opening #${i + 1}: anchor moet 'left', 'middle' of 'right' zijn (was: ${o.anchor})`);
|
||||
}
|
||||
@@ -1587,8 +1821,8 @@ 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, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, RABAT, RABAT_COLORS, rabatHex, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||
sheathingHoles, skinSegmentsAtY, topLine, wallTopPoints, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||
createMaterials, build3D, computeSpecs
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user