v0.6.0: ruimtes (verfemmer: functie, netto m², vloerafwerking, FIBO-wandpanelen, eigen verfkleur, vals plafond, ruimtestaat op de PDF), inrichting (sanitair, keuken, kookeiland, kasten, bed, zithoek, boiler; slepen met snappen op het wandvlak, draaien, kosten), kozijnen met deurrichting, glasdeur en schuifdeuren (inbouw/opbouw, deuren open in de loopmodus) plus dagkanten, gevel stucwerk en wandstukken splitsen met eigen gevel, dak over de hele woning met overkapping, geschilderd plafond, model Amstel als nieuw ontwerp, kosten en prijzen uit kosten/intern.csv en kosten/leveranciers.csv (CostCsv); easter egg verwijderd; overbodige comments opgeschoond (alleen copyright, uitzonderingen en valkuilen blijven)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b5510d56c7
commit
ceda0b554d
+155
-78
@@ -4,24 +4,9 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
// ============================================================
|
||||
// wall/kozijn-model.js — Vulling van een opening (window.KozijnModel):
|
||||
// kunststof kozijnen naar het voorbeeld van de Aluplast Ideal 7000
|
||||
// (examples/Buiten kozijnen.pdf): kader 70 mm in het aanzicht, 120 mm
|
||||
// diep, buitenzijde in een andere kleur dan de binnenzijde (folie).
|
||||
//
|
||||
// Een opening krijgt een (optionele) vulling, opgeslagen bij de opening:
|
||||
// op.fill = { type: 'raam'|'deur', rooster, glas, out, in }
|
||||
// raam vast glas in het kader; rooster = ventilatierooster bovenin
|
||||
// deur deurblad (draait links) met dorpel; is de opening breder of
|
||||
// hoger dan één deur, dan vult vast glas de rest (zijlicht,
|
||||
// bovenlicht). glas = glasstrook in het deurblad
|
||||
// out/in kleur buiten/binnen (COLORS)
|
||||
// Zonder fill blijft de opening open (zoals voorheen). Het formaat volgt
|
||||
// altijd de opening (ook een schuine bovenzijde).
|
||||
//
|
||||
// Plain script (geen ES-module), geen DOM. Gebruikt door WallModel.build3D.
|
||||
// ============================================================
|
||||
// Kunststof kozijnen naar de Aluplast Ideal 7000 (examples/Buiten kozijnen.pdf).
|
||||
// links/rechts (leaf, hinge, dir) zijn van buitenaf gezien = vanaf +z.
|
||||
// userData.door: open/target in radialen (draaien) of meters (schuiven); de loopmodus (house.js) zet target.
|
||||
|
||||
window.KozijnModel = (() => {
|
||||
const COLORS = {
|
||||
@@ -30,47 +15,54 @@ window.KozijnModel = (() => {
|
||||
antraciet: { label: 'Antraciet', ral: 'RAL 7016', hex: 0x383e42 },
|
||||
zwart: { label: 'Zwart', ral: 'RAL 9005', hex: 0x151515 }
|
||||
};
|
||||
const FILL_TYPES = { geen: 'Geen (open)', raam: 'Raam (vast glas)', deur: 'Deur' };
|
||||
const FILL_TYPES = { geen: 'Geen (open)', raam: 'Raam (vast glas)', deur: 'Deur', schuifdeur: 'Schuifdeur' };
|
||||
const HINGES = { links: 'Links', rechts: 'Rechts' };
|
||||
const SWINGS = { binnen: 'Naar binnen', buiten: 'Naar buiten' };
|
||||
const SLIDE_MOUNTS = { inbouw: 'In de wand', opbouw: 'Op de wand (rail)' };
|
||||
const SLIDE_SIDES = { binnen: 'Binnenzijde', buiten: 'Buitenzijde' };
|
||||
const SLIDE_DIRS = { links: 'Naar links', rechts: 'Naar rechts' };
|
||||
const DEFAULT_OUT = 'antraciet', DEFAULT_IN = 'wit';
|
||||
|
||||
// Maten (mm).
|
||||
const P = {
|
||||
gap: 5, // stelruimte rondom
|
||||
kader: 70, // kader in het aanzicht
|
||||
depth: 120, // kaderdiepte
|
||||
dorpel: 30, // deurdorpel (hoogte)
|
||||
gap: 5,
|
||||
kader: 70,
|
||||
depth: 120,
|
||||
dorpel: 30,
|
||||
tussen: 70, // tussenstijl/-regel (deur naast zijlicht, onder bovenlicht)
|
||||
rooster: 80, // ventilatierooster (hoogte)
|
||||
glass: 24, // glasdikte
|
||||
rooster: 80,
|
||||
glass: 24,
|
||||
leafW: 930, // deurblad (binnenmaat kader) als er een zijlicht naast past
|
||||
leafH: 2200, // deurblad als er een bovenlicht boven past
|
||||
leafT: 68, // deurbladdikte
|
||||
minLight: 250 // kleinste zijlicht/bovenlicht
|
||||
leafT: 68,
|
||||
minLight: 200
|
||||
};
|
||||
|
||||
const colorKey = (k, def) => COLORS[k] ? k : def;
|
||||
// Volledige vulling (defaults ingevuld) of null (geen vulling).
|
||||
const pick = (v, opts, def) => opts[v] ? v : def;
|
||||
function normalize(fill) {
|
||||
if (!fill || !FILL_TYPES[fill.type] || fill.type === 'geen') return null;
|
||||
return {
|
||||
const f = {
|
||||
type: fill.type, rooster: !!fill.rooster, glas: fill.glas !== false,
|
||||
out: colorKey(fill.out, DEFAULT_OUT), in: colorKey(fill.in, DEFAULT_IN)
|
||||
};
|
||||
if (f.type === 'deur') {
|
||||
f.hinge = pick(fill.hinge, HINGES, 'links');
|
||||
f.leaf = pick(fill.leaf, HINGES, f.hinge);
|
||||
f.swing = pick(fill.swing, SWINGS, 'binnen');
|
||||
f.volglas = !!fill.volglas;
|
||||
}
|
||||
if (f.type === 'schuifdeur') Object.assign(f, {
|
||||
leaves: Number(fill.leaves) === 2 ? 2 : 1, mount: pick(fill.mount, SLIDE_MOUNTS, 'inbouw'),
|
||||
side: pick(fill.side, SLIDE_SIDES, 'binnen'), dir: pick(fill.dir, SLIDE_DIRS, 'links')
|
||||
});
|
||||
return f;
|
||||
}
|
||||
// Standaardvulling bij een openingtype (WallModel.OPENING_TYPES); kleuren
|
||||
// van `like` (bijv. de vorige opening) overnemen.
|
||||
function defaultFill(openingType, like) {
|
||||
const l = normalize(like) || {};
|
||||
const type = openingType === 'deur' || openingType === 'voordeur' ? 'deur' : 'raam';
|
||||
return { type, rooster: false, glas: openingType === 'voordeur', out: l.out || DEFAULT_OUT, in: l.in || DEFAULT_IN };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 2D-veelhoeken (mm, tegen de klok in, x vanaf de linkerkant van de
|
||||
// opening, y vanaf de onderkant).
|
||||
// ------------------------------------------------------------
|
||||
// Convexe veelhoek naar binnen verschoven, per zijde (zijde i = punt i ->
|
||||
// punt i+1) een eigen afstand d[i].
|
||||
function insetPoly(pts, d) {
|
||||
const n = pts.length;
|
||||
const lines = pts.map((p, i) => {
|
||||
@@ -87,8 +79,6 @@ window.KozijnModel = (() => {
|
||||
return { x: a.x + a.dx * t, y: a.y + a.dy * t };
|
||||
});
|
||||
}
|
||||
// Veelhoek afgekapt op een halfvlak (Sutherland-Hodgman): f(p) >= 0
|
||||
// blijft, f is lineair.
|
||||
function clipPoly(pts, f) {
|
||||
const out = [];
|
||||
pts.forEach((p, i) => {
|
||||
@@ -105,9 +95,6 @@ window.KozijnModel = (() => {
|
||||
const rect = (x0, x1, y0, y1) => [{ x: x0, y: y0 }, { x: x1, y: y0 }, { x: x1, y: y1 }, { x: x0, y: y1 }];
|
||||
const areaOf = (pts) => Math.abs(pts.reduce((a, p, i) => { const q = pts[(i + 1) % pts.length]; return a + p.x * q.y - q.x * p.y; }, 0)) / 2;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 3D
|
||||
// ------------------------------------------------------------
|
||||
const mats = {};
|
||||
function colorMat(key) {
|
||||
const k = 'c:' + colorKey(key, DEFAULT_OUT);
|
||||
@@ -117,7 +104,7 @@ window.KozijnModel = (() => {
|
||||
if (mats[key]) return mats[key];
|
||||
if (key === 'glass') mats[key] = new THREE.MeshStandardMaterial({ color: 0x9fbfcc, roughness: 0.05, metalness: 0.2, transparent: true, opacity: 0.28, depthWrite: false, side: THREE.DoubleSide });
|
||||
else if (key === 'rubber') mats[key] = new THREE.MeshStandardMaterial({ color: 0x1b1b1b, roughness: 0.8 });
|
||||
else mats[key] = new THREE.MeshStandardMaterial({ color: 0x9a9da0, roughness: 0.3, metalness: 0.7 }); // beslag
|
||||
else mats[key] = new THREE.MeshStandardMaterial({ color: 0x9a9da0, roughness: 0.3, metalness: 0.7 });
|
||||
return mats[key];
|
||||
}
|
||||
|
||||
@@ -127,8 +114,6 @@ window.KozijnModel = (() => {
|
||||
(holes || []).forEach(h => s.holes.push(new THREE.Path(h.map(p => new THREE.Vector2(p.x * toM, p.y * toM)))));
|
||||
return s;
|
||||
};
|
||||
// Plaat van z0 tot z1 (mm); buitenvlak (z1) in matOut als dat afwijkt
|
||||
// (folie aan de buitenzijde, de rest in de binnenkleur).
|
||||
function slab(g, pts, holes, z0, z1, matIn, matOut, shadow) {
|
||||
if (pts.length < 3 || areaOf(pts) < 1) return;
|
||||
const FOIL = matOut && matOut !== matIn ? 1 : 0;
|
||||
@@ -149,10 +134,7 @@ window.KozijnModel = (() => {
|
||||
g.add(mesh);
|
||||
};
|
||||
|
||||
// Kozijn in een opening (opgelost: w, h links, hR rechts). Geeft een
|
||||
// groep in mm-lokale coördinaten van de opening (x vanaf links, y vanaf
|
||||
// de onderkant, +z = buiten) in meters, of null.
|
||||
// place: { zOut (mm, buitenvlak kozijn), depth (mm), exterior (dorpel) }
|
||||
// Lokaal: x vanaf links, y vanaf de onderkant, +z = buiten (mm in, meters uit).
|
||||
function build3D(op, fill, place) {
|
||||
const f = normalize(fill);
|
||||
if (!f) return null;
|
||||
@@ -162,8 +144,6 @@ window.KozijnModel = (() => {
|
||||
const z1 = place.zOut, z0 = z1 - depth;
|
||||
const hR = op.hR !== undefined ? op.hR : op.h;
|
||||
const outer = insetPoly([{ x: 0, y: 0 }, { x: op.w, y: 0 }, { x: op.w, y: hR }, { x: 0, y: op.h }], [P.gap, P.gap, P.gap, P.gap]);
|
||||
// Bovenkant (schuin) van een veelhoek binnen het kader: >= 0 onder de
|
||||
// lijn door TL-TR van `q` (lineair, voor clipPoly).
|
||||
const below = (q) => (p) => {
|
||||
const a = q[3], b = q[2];
|
||||
return (b.y - a.y) * (p.x - a.x) - (b.x - a.x) * (p.y - a.y);
|
||||
@@ -178,7 +158,6 @@ window.KozijnModel = (() => {
|
||||
pane = insetPoly(inner, [0, 0, P.rooster, 0]);
|
||||
const band = [pane[3], pane[2], inner[2], inner[3]];
|
||||
slab(g, [band[0], band[1], band[2], band[3]], null, z1 - 105, z1 - 8, mIn, mOut);
|
||||
// Lamellen: donkere sleuven op beide vlakken.
|
||||
[0.3, 0.55, 0.8].forEach(t => {
|
||||
const a = insetPoly(inner, [0, 20, P.rooster * t - 4, 20]), b = insetPoly(inner, [0, 20, P.rooster * t + 4, 20]);
|
||||
const slot = [b[3], b[2], a[2], a[3]];
|
||||
@@ -190,8 +169,6 @@ window.KozijnModel = (() => {
|
||||
return g;
|
||||
}
|
||||
|
||||
// Deur: kader als U (zonder onderregel) op een dorpel (buitenwand) of
|
||||
// tot de vloer (binnenwand).
|
||||
const sill = place.exterior !== false ? P.dorpel : 0;
|
||||
const frameOuter = insetPoly(outer, [sill, 0, 0, 0]);
|
||||
const inner = insetPoly(frameOuter, [0, P.kader, P.kader, P.kader]);
|
||||
@@ -202,20 +179,25 @@ window.KozijnModel = (() => {
|
||||
const ix0 = inner[0].x, ix1 = inner[1].x, iy0 = inner[0].y;
|
||||
const top = below(inner);
|
||||
const topAt = (x) => inner[3].y + (x - inner[3].x) / (inner[2].x - inner[3].x) * (inner[2].y - inner[3].y);
|
||||
// Zijlicht rechts als er naast een deurblad nog genoeg ruimte is.
|
||||
const region = (x0, x1, y0, y1) => clipPoly(rect(x0, x1, y0, Math.min(y1, 1e6)), top);
|
||||
const bar = (pts) => slab(g, pts, null, z0 + 10, z1 - 10, mIn, mOut);
|
||||
const link = `${Math.round(op.x || 0)}|${Math.round(op.y || 0)}|${op.w}`;
|
||||
if (f.type === 'schuifdeur') {
|
||||
buildSlideLeaves(g, f, { ix0, ix1, iy0, topAt, z0, z1, mIn, mOut, sill, place, link, op });
|
||||
return g;
|
||||
}
|
||||
|
||||
const hingeR = f.hinge === 'rechts', leafR = f.leaf === 'rechts';
|
||||
const side = ix1 - ix0 - P.leafW - P.tussen >= P.minLight;
|
||||
const lx1 = side ? ix0 + P.leafW : ix1;
|
||||
// Bovenlicht als er boven een deurblad nog genoeg ruimte is (overal).
|
||||
const la = side && leafR ? ix1 - P.leafW : ix0, lb = side && !leafR ? ix0 + P.leafW : ix1;
|
||||
const lowTop = Math.min(topAt(ix0), topAt(ix1));
|
||||
const transom = lowTop - iy0 - P.leafH - P.tussen >= P.minLight;
|
||||
const ly1 = transom ? iy0 + P.leafH : Infinity;
|
||||
const region = (x0, x1, y0, y1) => clipPoly(rect(x0, x1, y0, Math.min(y1, 1e6)), top);
|
||||
const bar = (pts) => slab(g, pts, null, z0 + 10, z1 - 10, mIn, mOut);
|
||||
if (side) {
|
||||
bar(region(lx1, lx1 + P.tussen, iy0, ly1));
|
||||
// Zijlicht: onderregel + glas.
|
||||
bar(region(lx1 + P.tussen, ix1, iy0, iy0 + P.kader));
|
||||
glass(region(lx1 + P.tussen, ix1, iy0 + P.kader, ly1));
|
||||
const [t0, g0, g1] = leafR ? [la - P.tussen, ix0, la - P.tussen] : [lb, lb + P.tussen, ix1];
|
||||
bar(region(t0, t0 + P.tussen, iy0, ly1));
|
||||
bar(region(g0, g1, iy0, iy0 + P.kader));
|
||||
glass(region(g0, g1, iy0 + P.kader, ly1));
|
||||
}
|
||||
if (transom) {
|
||||
bar(region(ix0, ix1, ly1, ly1 + P.tussen));
|
||||
@@ -223,31 +205,126 @@ window.KozijnModel = (() => {
|
||||
}
|
||||
|
||||
// Deurblad (3 mm speling, onderaan 10 mm boven dorpel/vloer).
|
||||
const leaf = clipPoly(rect(ix0 + 3, lx1 - 3, iy0 + (sill ? 3 : 10), Math.min(ly1 - 3, 1e6)), (p) => top({ x: p.x, y: p.y + 3 }));
|
||||
const leaf = clipPoly(rect(la + 3, lb - 3, iy0 + (sill ? 3 : 10), Math.min(ly1 - 3, 1e6)), (p) => top({ x: p.x, y: p.y + 3 }));
|
||||
if (leaf.length < 3) return g;
|
||||
const xs = leaf.map(p => p.x), ys = leaf.map(p => p.y);
|
||||
const lx0 = Math.min(...xs), lxR = Math.max(...xs), lb = Math.min(...ys), lt = Math.min(topAt(lx0), topAt(lxR), ly1) - 3;
|
||||
const lx0 = Math.min(...xs), lxR = Math.max(...xs), lb0 = Math.min(...ys), lt = Math.min(topAt(lx0), topAt(lxR), ly1) - 3;
|
||||
const lzOut = z1 - 15, lzIn = lzOut - P.leafT;
|
||||
let holes = null;
|
||||
const lw = lxR - lx0;
|
||||
if (f.glas && lw >= 500 && lt - lb >= 1400) {
|
||||
const gx = lx0 + lw * 0.3, gw = Math.min(160, lw * 0.18);
|
||||
const strip = rect(gx - gw / 2, gx + gw / 2, lb + 350, lt - 300);
|
||||
holes = [strip];
|
||||
slab(g, strip, null, (lzIn + lzOut) / 2 - P.glass / 2, (lzIn + lzOut) / 2 + P.glass / 2, mat('glass'), null, false);
|
||||
if (f.volglas && lw >= 400 && lt - lb0 >= 800) {
|
||||
holes = [rect(lx0 + 110, lxR - 110, lb0 + 140, lt - 110)]; // glasdeur: smal kader rondom
|
||||
} else if (f.glas && lw >= 500 && lt - lb0 >= 1400) {
|
||||
const gx = hingeR ? lxR - lw * 0.3 : lx0 + lw * 0.3, gw = Math.min(160, lw * 0.18);
|
||||
holes = [rect(gx - gw / 2, gx + gw / 2, lb0 + 350, lt - 300)]; // glas: in het deurblad (hieronder)
|
||||
}
|
||||
slab(g, leaf, holes, lzIn, lzOut, mIn, mOut);
|
||||
// Kruk aan beide zijden (scharnieren links, slot rechts).
|
||||
const outward = f.swing === 'buiten';
|
||||
const hx = hingeR ? lxR : lx0, hz = outward ? lzOut : lzIn;
|
||||
const pivot = new THREE.Group(), lg = new THREE.Group();
|
||||
pivot.position.set(hx * toM, 0, hz * toM);
|
||||
lg.position.set(-hx * toM, 0, -hz * toM);
|
||||
pivot.add(lg);
|
||||
g.add(pivot);
|
||||
if (holes) slab(lg, holes[0], null, (lzIn + lzOut) / 2 - P.glass / 2, (lzIn + lzOut) / 2 + P.glass / 2, mat('glass'), null, false);
|
||||
slab(lg, leaf, holes, lzIn, lzOut, mIn, mOut);
|
||||
const hy = Math.min(lb0 + 1050, (lb0 + lt) / 2);
|
||||
if (lw >= 400) {
|
||||
const hx = lxR - 75, hy = Math.min(lb + 1050, (lb + lt) / 2);
|
||||
const kx = hingeR ? lx0 + 75 : lxR - 75, gd = hingeR ? 60 : -60;
|
||||
[[lzOut, 1], [lzIn, -1]].forEach(([z, s]) => {
|
||||
box(g, mat('beslag'), hx, hy - 30, z + s * 5, 34, 210, 10); // schild
|
||||
box(g, mat('beslag'), hx, hy, z + s * 30, 20, 20, 50); // nek
|
||||
box(g, mat('beslag'), hx - 60, hy, z + s * 55, 140, 20, 20); // greep
|
||||
box(lg, mat('beslag'), kx, hy - 30, z + s * 5, 34, 210, 10); // schild
|
||||
box(lg, mat('beslag'), kx, hy, z + s * 30, 20, 20, 50); // nek
|
||||
box(lg, mat('beslag'), kx + gd, hy, z + s * 55, 140, 20, 20); // greep
|
||||
});
|
||||
}
|
||||
const dir = (hingeR ? -1 : 1) * (outward ? -1 : 1);
|
||||
pivot.userData.door = {
|
||||
kind: 'swing', dir, max: Math.PI / 2, open: 0, target: 0, link,
|
||||
base: pivot.position.x, center: new THREE.Vector3((lx0 + lxR) / 2 * toM, hy * toM, (lzIn + lzOut) / 2 * toM)
|
||||
};
|
||||
return g;
|
||||
}
|
||||
|
||||
return { COLORS, FILL_TYPES, PROFILE: P, normalize, defaultFill, build3D };
|
||||
const SLIDE = { overlap: 50, gap: 12, railH: 40, railD: 45, leafT: 40, frame: 70 };
|
||||
function buildSlideLeaves(g, f, o) {
|
||||
const n = f.leaves, opbouw = f.mount === 'opbouw';
|
||||
const out = f.side === 'buiten';
|
||||
const faceIn = o.place.faceIn !== undefined ? o.place.faceIn : o.z0;
|
||||
const faceOut = o.place.faceOut !== undefined ? o.place.faceOut : o.z1;
|
||||
const ov = opbouw ? SLIDE.overlap : -3;
|
||||
const xa = o.ix0 - ov, xb = o.ix1 + ov;
|
||||
const yb = o.iy0 + (o.sill ? 3 : 10);
|
||||
const yt = Math.min(o.topAt(o.ix0), o.topAt(o.ix1)) + (opbouw ? SLIDE.overlap : -3);
|
||||
const lt = opbouw ? SLIDE.leafT : P.leafT;
|
||||
const [lz0, lz1] = opbouw
|
||||
? (out ? [faceOut + SLIDE.gap, faceOut + SLIDE.gap + lt] : [faceIn - SLIDE.gap - lt, faceIn - SLIDE.gap])
|
||||
: [(faceIn + faceOut) / 2 - lt / 2, (faceIn + faceOut) / 2 + lt / 2];
|
||||
const lw = (xb - xa) / n;
|
||||
const dirs = n === 2 ? [-1, 1] : [f.dir === 'rechts' ? 1 : -1];
|
||||
dirs.forEach((dir, i) => {
|
||||
const x0 = xa + i * lw + (n === 2 ? (i ? 1 : 0) : 0), x1 = xa + (i + 1) * lw - (n === 2 && !i ? 1 : 0);
|
||||
const lg = new THREE.Group();
|
||||
g.add(lg);
|
||||
const pts = rect(x0, x1, yb, yt);
|
||||
if (f.glas && x1 - x0 > 2 * SLIDE.frame + 100) {
|
||||
const pane = rect(x0 + SLIDE.frame, x1 - SLIDE.frame, yb + SLIDE.frame + 30, yt - SLIDE.frame);
|
||||
slab(lg, pts, [pane], lz0, lz1, colorMat(f.in), opbouw ? null : colorMat(f.out));
|
||||
slab(lg, pane, null, (lz0 + lz1) / 2 - 4, (lz0 + lz1) / 2 + 4, mat('glass'), null, false);
|
||||
} else {
|
||||
slab(lg, pts, null, lz0, lz1, colorMat(f.in), opbouw ? null : colorMat(f.out));
|
||||
}
|
||||
const hx = dir < 0 ? x1 - 60 : x0 + 60, hy = Math.min(yb + 1050, (yb + yt) / 2);
|
||||
if (opbouw) [lz0 - 18, lz1 + 18].forEach(z => box(lg, mat('beslag'), hx, hy, z, 20, 400, 20));
|
||||
else [lz0 - 1, lz1 + 1].forEach(z => box(lg, mat('beslag'), hx, hy, z, 24, 160, 2));
|
||||
lg.userData.door = {
|
||||
kind: 'slide', dir, max: (x1 - x0 - (opbouw ? 2 * SLIDE.overlap : 60)) * toM, open: 0, target: 0, link: o.link,
|
||||
base: 0, center: new THREE.Vector3((x0 + x1) / 2 * toM, hy * toM, (lz0 + lz1) / 2 * toM)
|
||||
};
|
||||
});
|
||||
if (opbouw) {
|
||||
const ra = n === 2 ? xa - lw : (dirs[0] < 0 ? xa - lw : xa), rb = n === 2 ? xb + lw : (dirs[0] < 0 ? xb : xb + lw);
|
||||
const rz = out ? faceOut + SLIDE.railD / 2 : faceIn - SLIDE.railD / 2;
|
||||
box(g, mat('beslag'), (ra + rb) / 2, yt + SLIDE.railH / 2 + 5, rz, rb - ra, SLIDE.railH, SLIDE.railD);
|
||||
}
|
||||
}
|
||||
|
||||
function planSymbol(op, fill, faces) {
|
||||
const f = normalize(fill);
|
||||
if (!f || f.type === 'raam') return [];
|
||||
const zIn = faces.zIn, zOut = faces.zOut;
|
||||
const ix0 = P.gap + P.kader, ix1 = op.w - P.gap - P.kader;
|
||||
const out = [];
|
||||
if (f.type === 'deur') {
|
||||
const hingeR = f.hinge === 'rechts', leafR = f.leaf === 'rechts';
|
||||
const side = ix1 - ix0 - P.leafW - P.tussen >= P.minLight;
|
||||
const la = side && leafR ? ix1 - P.leafW : ix0, lb = side && !leafR ? ix0 + P.leafW : ix1;
|
||||
const L = lb - la, hx = hingeR ? lb : la, s = hingeR ? -1 : 1;
|
||||
const outward = f.swing === 'buiten', hz = outward ? zOut : zIn, dz = outward ? 1 : -1;
|
||||
out.push({ kind: 'leaf', pts: [{ x: hx, z: hz }, { x: hx, z: hz + dz * L }] });
|
||||
const arc = [];
|
||||
for (let k = 0; k <= 16; k++) { const t = k / 16 * Math.PI / 2; arc.push({ x: hx + s * L * Math.cos(t), z: hz + dz * L * Math.sin(t) }); }
|
||||
out.push({ kind: 'arc', pts: arc });
|
||||
return out;
|
||||
}
|
||||
const n = f.leaves, opbouw = f.mount === 'opbouw', sideOut = f.side === 'buiten';
|
||||
const ov = opbouw ? SLIDE.overlap : -3;
|
||||
const xa = ix0 - ov, xb = ix1 + ov, lw = (xb - xa) / n;
|
||||
const lt = opbouw ? SLIDE.leafT : P.leafT;
|
||||
const [z0, z1] = opbouw
|
||||
? (sideOut ? [zOut + SLIDE.gap, zOut + SLIDE.gap + lt] : [zIn - SLIDE.gap - lt, zIn - SLIDE.gap])
|
||||
: [(zIn + zOut) / 2 - lt / 2, (zIn + zOut) / 2 + lt / 2];
|
||||
const dirs = n === 2 ? [-1, 1] : [f.dir === 'rechts' ? 1 : -1];
|
||||
const za = sideOut ? Math.max(zOut, z1) + 140 : Math.min(zIn, z0) - 140;
|
||||
dirs.forEach((dir, i) => {
|
||||
const x0 = xa + i * lw, x1 = x0 + lw;
|
||||
out.push({ kind: 'leaf', closed: true, pts: [{ x: x0, z: z0 }, { x: x1, z: z0 }, { x: x1, z: z1 }, { x: x0, z: z1 }] });
|
||||
const c = (x0 + x1) / 2, len = Math.min(500, lw * 0.6);
|
||||
const a = { x: c - dir * len / 2, z: za }, b = { x: c + dir * len / 2, z: za };
|
||||
out.push({ kind: 'arrow', pts: [a, b] });
|
||||
out.push({ kind: 'arrow', pts: [{ x: b.x - dir * 90, z: za - 45 }, b, { x: b.x - dir * 90, z: za + 45 }] });
|
||||
out.push({ kind: 'rail', closed: true, pts: [{ x: x0 + dir * lw, z: z0 }, { x: x1 + dir * lw, z: z0 }, { x: x1 + dir * lw, z: z1 }, { x: x0 + dir * lw, z: z1 }] });
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
return { COLORS, FILL_TYPES, HINGES, SWINGS, SLIDE_MOUNTS, SLIDE_SIDES, SLIDE_DIRS, PROFILE: P, normalize, defaultFill, build3D, planSymbol, colorMaterial: colorMat };
|
||||
})();
|
||||
|
||||
+235
-511
File diff suppressed because it is too large
Load Diff
+3
-40
@@ -4,18 +4,8 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
// ============================================================
|
||||
// wall/wall-pdf.js — ENIGE bron van het wand-tekenblad (A3 liggend):
|
||||
// frame-materiaallijst, plaatmateriaal, vooraanzicht met posnums en
|
||||
// maatketens, bovenaanzicht (doorsnede regelwerk + beplating), zijaanzicht
|
||||
// (laagopbouw) en titelblok. Gebruikt door de losse wand-editor (1 blad) en
|
||||
// door de unit (1 blad per wand in de gezamenlijke PDF).
|
||||
// Plain script, geladen na wall-model.js en core/pdf-sheet.js.
|
||||
// ============================================================
|
||||
|
||||
window.WallPdf = (() => {
|
||||
// Tekent één volledig wandblad op de HUIDIGE pagina van `doc`.
|
||||
// opts: { buitenMateriaal, cladding, trasraam, skinExt, project, titel, element, blad, lamps: [{ u, y }] }
|
||||
function drawSheet(doc, CONFIG, calcData, opts) {
|
||||
const buitenMateriaal = opts.buitenMateriaal || 'vochtdoek';
|
||||
const pieces = WallModel.computeBOM(CONFIG, calcData);
|
||||
@@ -26,7 +16,6 @@ window.WallPdf = (() => {
|
||||
const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS;
|
||||
doc.setFont('helvetica', 'normal');
|
||||
|
||||
// --- Frame materiaal-tabel (linksboven) ---
|
||||
const tblX = [8, 26, 42, 58, 76, 92];
|
||||
let ty = 14;
|
||||
doc.setTextColor(...BLACK);
|
||||
@@ -41,13 +30,12 @@ window.WallPdf = (() => {
|
||||
doc.line(tblX[0], ty, tblX[4] + 12, ty);
|
||||
ty += 4;
|
||||
groups.forEach(g => {
|
||||
if (ty > 232) return; // laat ruimte vrij voor materiaaltabellen eronder
|
||||
if (ty > 232) return;
|
||||
[String(g.posnum), String(g.width), String(g.depth), String(g.length), String(g.count)]
|
||||
.forEach((v, i) => doc.text(v, tblX[i], ty));
|
||||
ty += 4.3;
|
||||
});
|
||||
|
||||
// --- Inkoop balken (handelslengtes, zaagplan uit WallModel) ---
|
||||
const purchase = WallModel.planPurchase(pieces);
|
||||
ty += 3;
|
||||
doc.setFontSize(9);
|
||||
@@ -65,7 +53,6 @@ window.WallPdf = (() => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Materiaal binnen/buiten + isolatie ---
|
||||
let my = 240;
|
||||
const midH = Math.round(WallModel.topLine(CONFIG).maxY);
|
||||
doc.setFontSize(8.5);
|
||||
@@ -79,7 +66,6 @@ window.WallPdf = (() => {
|
||||
doc.text(`${CONFIG.wallWidth} x ${CONFIG.sheathingThick} x ${midH} ${buitenNaam} 1`, tblX[0], my); my += 6;
|
||||
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, opts.skinFrom);
|
||||
const B = WallModel.BATTEN;
|
||||
my += 4.5;
|
||||
@@ -95,12 +81,8 @@ window.WallPdf = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Titelblok (rechtsonder) ---
|
||||
PdfSheet.titleBlock(doc, opts.project, { titel: opts.titel, element: opts.element, blad: opts.blad });
|
||||
|
||||
// --- Hoofdaanzicht ---
|
||||
// drawX1/marginBottom laten ruimte vrij voor het zijaanzicht (rechts)
|
||||
// en het bovenaanzicht (onder de maatketens).
|
||||
const drawX0 = 95, drawX1 = 380, drawY0 = 15, drawY1 = 225;
|
||||
const marginSides = 25, marginTop = 15, marginBottom = 68;
|
||||
const availW = (drawX1 - drawX0) - marginSides * 2;
|
||||
@@ -126,14 +108,9 @@ window.WallPdf = (() => {
|
||||
doc.setLineWidth(0.3);
|
||||
pieces.forEach(p => {
|
||||
if (p.outline) {
|
||||
// Werkelijke omtrek: schuine kop (volgt de daklijn, evt. met knik
|
||||
// onder de nok) resp. bovenregel in verstek.
|
||||
const c = p.outline.map(q => [px(q.x), py(q.y)]);
|
||||
c.forEach((q, i) => { const n = c[(i + 1) % c.length]; doc.line(q[0], q[1], n[0], n[1]); });
|
||||
} else if (p.kind === 'verticale balk' || p.kind === 'kreupele stijl') {
|
||||
// Viervlak i.p.v. rechthoek: de bovenkant loopt schuin mee met de
|
||||
// daklijn (yTopLeft/yTopRight kunnen verschillen), zodat zowel de
|
||||
// hoogste als de laagste kant van de balk op de juiste plek komt.
|
||||
const wx0 = p.x - p.width / 2, wx1 = p.x + p.width / 2;
|
||||
const corners = [
|
||||
[px(wx0), py(p.yBottomLeft)], [px(wx1), py(p.yBottomRight)],
|
||||
@@ -154,16 +131,13 @@ window.WallPdf = (() => {
|
||||
doc.setLineWidth(0.2);
|
||||
CONFIG.openings.forEach(op => {
|
||||
const opXWorld = -wallWidth / 2 + op.x;
|
||||
// Schuine bovenzijde: y0 = bovenkant links, y0r = rechts.
|
||||
const x0 = px(opXWorld), y0 = py(op.y + op.h), y0r = py(op.y + op.hR), x1 = px(opXWorld + op.w), y1 = py(op.y);
|
||||
[[x0, y1, x1, y1], [x1, y1, x1, y0r], [x1, y0r, x0, y0], [x0, y0, x0, y1]].forEach(l => doc.line(...l));
|
||||
doc.line(x0, y0, x1, y1);
|
||||
doc.line(x0, y1, x1, y0r);
|
||||
doc.setTextColor(...BLACK);
|
||||
doc.setFontSize(6);
|
||||
// Dagmaat-label BINNEN de opening zetten (net onder de bovenkant),
|
||||
// i.p.v. erboven — daar overlapte het met de maatketen/omtrek
|
||||
// erboven.
|
||||
// Label binnen de opening: erboven overlapte het met de maatketen.
|
||||
doc.text(op.hR !== op.h ? `${op.w}x${op.h}/${op.hR}` : `${op.w}x${op.h}`, (x0 + x1) / 2, Math.max(y0, y0r) + 5, { align: 'center' });
|
||||
});
|
||||
|
||||
@@ -180,8 +154,6 @@ window.WallPdf = (() => {
|
||||
doc.text(String(Math.round(hLeft)), px(-wallWidth / 2) - 9, (py(0) + py(hLeft)) / 2, { align: 'center', angle: 90 });
|
||||
drawDimLine(px(wallWidth / 2) + 7, py(0), px(wallWidth / 2) + 7, py(hRight));
|
||||
doc.text(String(Math.round(hRight)), px(wallWidth / 2) + 11, (py(0) + py(hRight)) / 2, { align: 'center', angle: 90 });
|
||||
// Tussenpunten van de daklijn: stippellijn van vloer tot nok/knik
|
||||
// (nok = het enige hoogste punt; mansarde-/wolfsgevel: knikken).
|
||||
const inner = tl.pts.slice(1, -1);
|
||||
inner.forEach(q => {
|
||||
const isNok = inner.filter(o => o.y >= q.y - 0.5).length === 1;
|
||||
@@ -219,7 +191,6 @@ window.WallPdf = (() => {
|
||||
doc.text(String(Math.round(openingPoints[i + 1] - openingPoints[i])), px(mid - wallWidth / 2), openY, { align: 'center' });
|
||||
}
|
||||
|
||||
// --- Gevellampen (woning): rechthoekje met "L" op lamphoogte ---
|
||||
(opts.lamps || []).forEach(l => {
|
||||
const lx = px(-wallWidth / 2 + l.u), ly = py(l.y);
|
||||
doc.setDrawColor(...BLACK);
|
||||
@@ -231,13 +202,9 @@ window.WallPdf = (() => {
|
||||
doc.text('L', lx + 2, ly + 1);
|
||||
});
|
||||
|
||||
// --- Extra laag van de aanroeper (bijv. elektra, ElektraPdf):
|
||||
// overlay(doc, pu, py), pu = wand-x (0..breedte) -> blad-mm.
|
||||
if (opts.overlay) opts.overlay(doc, (u) => px(-wallWidth / 2 + u), py);
|
||||
|
||||
// --- Bovenaanzicht (doorsnede op stijlhoogte, zoals de voorbeeld-
|
||||
// tekeningen): binnenzijde boven, buitenzijde onder (= de kant van
|
||||
// waaruit het vooraanzicht bekeken wordt). Zelfde horizontale schaal.
|
||||
// Binnenzijde boven, buitenzijde onder (de kant van het vooraanzicht).
|
||||
const sT = CONFIG.sheathingThick;
|
||||
const frameD = Math.max(CONFIG.studDepth, CONFIG.plateDepth);
|
||||
const tvY0 = floorY + 34;
|
||||
@@ -246,7 +213,6 @@ window.WallPdf = (() => {
|
||||
const tvY1 = tvFrame1 + sT * scale;
|
||||
const halfW = wallWidth / 2;
|
||||
|
||||
// Beplating, onderbroken ter plaatse van de openingen.
|
||||
const skinCuts = CONFIG.openings.map(op => [op.x, op.x + op.w]).sort((a, b) => a[0] - b[0]);
|
||||
const skinSegments = [];
|
||||
let skinCursor = 0;
|
||||
@@ -259,7 +225,6 @@ window.WallPdf = (() => {
|
||||
doc.rect(px(a - halfW), tvFrame1, (b - a) * scale, sT * scale, 'S');
|
||||
});
|
||||
|
||||
// Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis.
|
||||
doc.setDrawColor(...RED);
|
||||
doc.setLineWidth(0.2);
|
||||
const studXs = [...new Set(calcData.studs.map(st => st.x))];
|
||||
@@ -288,8 +253,6 @@ window.WallPdf = (() => {
|
||||
doc.setFontSize(7);
|
||||
doc.text(String(Math.round(wallWidth)), px(0), tvDimY + 3.5, { align: 'center' });
|
||||
|
||||
// --- Zijaanzicht (rechterkopse kant): laagopbouw binnen | regelwerk |
|
||||
// buiten, met onder- en bovenregel, op dezelfde schaal.
|
||||
const svX0 = 392;
|
||||
const svFrame0 = svX0 + sT * scale;
|
||||
const svFrame1 = svFrame0 + frameD * scale;
|
||||
|
||||
+46
-156
@@ -4,21 +4,6 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
// ============================================================
|
||||
// wall/wall.js — Wand-editor (ruwbouw compleet: regelwerk, openingen, OSB
|
||||
// achterzijde, vochtdoek voorzijde, isolatie/specificaties). Ongewijzigde
|
||||
// logica t.o.v. de vorige single-file versie, alleen verpakt als los te
|
||||
// laden/lossen module via mount()/unmount(), en met de gedeelde geometrie-
|
||||
// functies (calculateSegmentPositions, polygonArea, clip*, shapeAreaWithHoles)
|
||||
// verplaatst naar core/geometry.js (moet VOOR dit bestand geladen worden).
|
||||
//
|
||||
// De rekenlogica (ankerresolutie, regelwerk, 3D-opbouw, specificaties) staat
|
||||
// in wall/wall-model.js (WallModel) — gedeeld met de unit-editor. Dit bestand
|
||||
// bevat alleen de UI: invoer, 2D-tekeningen, interactie, JSON/PDF-export.
|
||||
//
|
||||
// Plain script (geen ES-module) — werkt zo ook bij rechtstreeks openen van
|
||||
// index.html via file://.
|
||||
// ============================================================
|
||||
|
||||
const WALL_TEMPLATE = `
|
||||
<div id="app-container">
|
||||
@@ -244,24 +229,18 @@ function mountWallEditor(container, callbacks) {
|
||||
const onSaved = callbacks && callbacks.onSaved;
|
||||
container.innerHTML = WALL_TEMPLATE;
|
||||
|
||||
// Binnen een unit volgt de wandbreedte uit de vloer (zie unit/unit.js);
|
||||
// callbacks.lockWidth is dan de uitleg-tekst voor de tooltip.
|
||||
if (callbacks && callbacks.lockWidth) {
|
||||
const widthInput = document.getElementById('inp-wall-w');
|
||||
widthInput.disabled = true;
|
||||
widthInput.title = callbacks.lockWidth;
|
||||
}
|
||||
// Binnen een woning is de richting van de gevelbekleding (voorlopig) globaal.
|
||||
const lockCladding = callbacks && callbacks.lockCladding;
|
||||
// ... en de buitenzijde (gevelbekleding per woning, override per wand).
|
||||
const lockExterior = callbacks && callbacks.lockExterior;
|
||||
// Alles op het tabblad Materiaal ligt dan vast: tabblad weg, uitleg op Opbouw.
|
||||
if (lockExterior && lockCladding) {
|
||||
const tabBtn = container.querySelector('.tab-btn[data-tab="tab-materials"]');
|
||||
if (tabBtn) tabBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Binnen een woning is de h.o.h. globaal (woning-instellingen), niet per wand.
|
||||
if (callbacks && callbacks.lockSpacing) {
|
||||
['inp-min-sp', 'inp-max-sp'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
@@ -269,75 +248,44 @@ function mountWallEditor(container, callbacks) {
|
||||
el.title = callbacks.lockSpacing;
|
||||
});
|
||||
}
|
||||
// Binnen een woning: delen van de wand die op een naad met een andere unit
|
||||
// vallen (en dus vervallen), als [vanaf-links, tot] in mm.
|
||||
const seamCuts = (callbacks && callbacks.seamCuts) || [];
|
||||
// Binnen een woning: gevellampen op deze wand ({ u vanaf links, y }), zie LightingModel.
|
||||
const lampMarks = (callbacks && callbacks.lamps) || [];
|
||||
|
||||
// ============================================================
|
||||
// STATE
|
||||
// ============================================================
|
||||
// Dichtheden/gewichten: zie WallModel (wall/wall-model.js).
|
||||
|
||||
let CONFIG = {};
|
||||
let calcData = { fullStuds: [], studs: [] };
|
||||
let wallGroup3D = null;
|
||||
let lastBaseHeight = 2400;
|
||||
// Daklijn met knikken (mansarde/wolfsdak, uit de unit-dakvorm): blijft
|
||||
// staan tot een hoogte hier met de hand wordt gewijzigd.
|
||||
let wallTopPts = null;
|
||||
|
||||
// Elke opening: { id, w, h, hR?, y, anchor: 'left'|'middle'|'right', offset, fill? }
|
||||
// fill = vulling (kozijn, KozijnModel); zonder fill blijft hij open.
|
||||
// hR = hoogte rechts (schuine bovenzijde); zonder hR is de bovenkant
|
||||
// recht en is h de hoogte. De onderkant is altijd waterpas.
|
||||
// 'offset' betekent afhankelijk van anchor:
|
||||
// left -> afstand vanaf linker wandrand tot linkerkant opening
|
||||
// right -> afstand vanaf rechter wandrand tot rechterkant opening
|
||||
// middle -> verschuiving t.o.v. het midden (0 = gecentreerd, + = naar rechts)
|
||||
// offset: left = vanaf de linker wandrand, right = vanaf de rechter, middle = t.o.v. het midden (+ = rechts).
|
||||
// hR = hoogte rechts bij een schuine bovenzijde; de onderkant is altijd waterpas.
|
||||
let openings = [
|
||||
{ id: 1, type: 'raam', w: 1200, h: 1200, y: 600, anchor: 'middle', offset: 0, fill: KozijnModel.defaultFill('raam') }
|
||||
];
|
||||
let openingIdCounter = 2;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Interactieve selectie/verslepen van openingen in het vooraanzicht
|
||||
// (nu het primaire aanzicht). Snap op 50mm, net als de stapgrootte van
|
||||
// de invoervelden. 9 sleeppunten: 4 hoeken, 4 zijden en het midden
|
||||
// (verplaatsen). N/Z wijzigen de hoogte, O/W de breedte. De bovenste
|
||||
// hoeken (NW/NO) verslepen ook alleen hun eigen kant in de hoogte: zo
|
||||
// ontstaat een schuine bovenzijde. Onderin blijft het altijd waterpas
|
||||
// (ZW/ZO: breedte + onderkant).
|
||||
// ------------------------------------------------------------
|
||||
let selectedOpeningId = null;
|
||||
let dragInfo = null; // { type:'move'|'resize', handle, openingId, startMouseX, startMouseY, startLocalX, startY, startW, startH, startHR }
|
||||
let frontView = { scale: 1, centerX: 0, floorY: 0 }; // bijgewerkt door draw2DFront()
|
||||
let frontOpeningRects = []; // hit-test data, bijgewerkt door draw2DFront()
|
||||
let frontHandleRects = []; // idem, alleen voor de geselecteerde opening
|
||||
let dragInfo = null;
|
||||
let frontView = { scale: 1, centerX: 0, floorY: 0 };
|
||||
let frontOpeningRects = [];
|
||||
let frontHandleRects = [];
|
||||
|
||||
function snap50(mm) { return Math.round(mm / 50) * 50; }
|
||||
|
||||
// Zet offset (en evt. breedte) zodat de opening's linkerrand op
|
||||
// `leftLocalX` komt te liggen met breedte `w`, ongeacht welk ankertype
|
||||
// actief is — dit is de kern die verslepen/resizen mogelijk maakt zonder
|
||||
// dat het ankertype zelf hoeft te veranderen.
|
||||
function applyOpeningPosition(op, leftLocalX, w, wallWidth) {
|
||||
if (op.anchor === 'right') op.offset = wallWidth - leftLocalX - w;
|
||||
else if (op.anchor === 'middle') op.offset = leftLocalX - (wallWidth - w) / 2;
|
||||
else op.offset = leftLocalX; // 'left'
|
||||
else op.offset = leftLocalX;
|
||||
op.w = w;
|
||||
}
|
||||
|
||||
const isSloped = (op) => op.hR !== undefined;
|
||||
// Ruwe opening zoals opgeslagen (hR alleen bij een schuine bovenzijde).
|
||||
const rawOpening = (o) => Object.assign({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset },
|
||||
o.hR !== undefined ? { hR: o.hR } : {}, o.fill ? { fill: Object.assign({}, o.fill) } : {});
|
||||
|
||||
function syncOpeningCardInputs(op) {
|
||||
const card = document.querySelector(`.opening-card[data-opening-id="${op.id}"]`);
|
||||
if (!card) return;
|
||||
// Schuin aan/uit veranderd (hoek slepen): kaart opnieuw opbouwen.
|
||||
if (!!card.querySelector('input[data-field="hR"]') !== isSloped(op)) { renderOpeningsList(); return; }
|
||||
['w', 'h', 'hR', 'y', 'offset'].forEach(field => {
|
||||
const input = card.querySelector(`input[data-field="${field}"]`);
|
||||
@@ -362,9 +310,6 @@ function mountWallEditor(container, callbacks) {
|
||||
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// OPENINGEN: UI (lijst met kaarten, ankerkeuze, toevoegen/verwijderen)
|
||||
// ============================================================
|
||||
function anchorOffsetLabel(anchor) {
|
||||
if (anchor === 'right') return 'Afstand tot rechts';
|
||||
if (anchor === 'middle') return 'Verschuiving t.o.v. midden';
|
||||
@@ -389,7 +334,7 @@ function mountWallEditor(container, callbacks) {
|
||||
}
|
||||
|
||||
function toggleFullscreen3D() {
|
||||
UI.cycle3D(document.getElementById('app-container')); // T: standaard -> 3D groot -> alleen render
|
||||
UI.cycle3D(document.getElementById('app-container'));
|
||||
requestAnimationFrame(onWindowResize);
|
||||
}
|
||||
|
||||
@@ -433,16 +378,7 @@ function mountWallEditor(container, callbacks) {
|
||||
updateAll();
|
||||
}
|
||||
|
||||
// Horizontaal spiegelen (Bewerken-menu): wisselt links/rechts om. De
|
||||
// hoogte-links/-rechts worden verwisseld (de schuine kant "klapt om"),
|
||||
// en elke opening wordt zo herankerd dat 'ie fysiek op dezelfde plek
|
||||
// blijft t.o.v. zijn (nu omgewisselde) referentierand:
|
||||
// - anchor 'left' <-> 'right', offset ONGEWIJZIGD (het is en blijft
|
||||
// een afstand-tot-een-rand, alleen de rand zelf wisselt)
|
||||
// - anchor 'middle': offset wordt genegeerd (spiegelbeeld t.o.v. midden)
|
||||
// Verticaal spiegelen is bewust niet geïmplementeerd: een wand mag aan de
|
||||
// onderzijde niet scheef worden, dus dat menu-item blijft permanent
|
||||
// disabled (zie index.html/app.js).
|
||||
// Verticaal spiegelen bewust niet: een wand mag onderin niet scheef worden (menu-item blijft uit).
|
||||
function mirrorHorizontal() {
|
||||
const hLeftInput = document.getElementById('inp-wall-h-left');
|
||||
const hRightInput = document.getElementById('inp-wall-h-right');
|
||||
@@ -452,7 +388,6 @@ function mountWallEditor(container, callbacks) {
|
||||
if (wallTopPts) wallTopPts = wallTopPts.map(q => ({ f: 1 - q.f, h: q.h })).reverse();
|
||||
|
||||
openings = openings.map(o => {
|
||||
// Schuine bovenzijde klapt mee om.
|
||||
if (isSloped(o)) o = { ...o, h: o.hR, hR: o.h };
|
||||
if (o.anchor === 'left') return { ...o, anchor: 'right' };
|
||||
if (o.anchor === 'right') return { ...o, anchor: 'left' };
|
||||
@@ -465,7 +400,6 @@ function mountWallEditor(container, callbacks) {
|
||||
setIoStatus('Horizontaal gespiegeld.', 'ok');
|
||||
}
|
||||
|
||||
// Vulling (kozijn, KozijnModel): type, rooster/glasstrook en kleuren.
|
||||
function fillControls(op) {
|
||||
const f = KozijnModel.normalize(op.fill);
|
||||
const type = f ? f.type : 'geen';
|
||||
@@ -475,6 +409,10 @@ function mountWallEditor(container, callbacks) {
|
||||
<label>${label}</label>
|
||||
<div><label class="ui-switch"><input type="checkbox" data-field="${field}" data-id="${op.id}" ${on ? 'checked' : ''}><span></span></label></div>
|
||||
</div>`;
|
||||
const pick = (field, label, entries, cur, title) => `<div class="control-group"${title ? ` title="${title}"` : ''}>
|
||||
<label>${label}</label>
|
||||
<div><select data-field="${field}" data-id="${op.id}">${opts(Object.entries(entries), cur)}</select></div>
|
||||
</div>`;
|
||||
return `<div class="controls-grid opening-fill">
|
||||
<div>
|
||||
<div class="control-group">
|
||||
@@ -483,6 +421,15 @@ function mountWallEditor(container, callbacks) {
|
||||
</div>
|
||||
${type === 'raam' ? sw('fillRooster', 'Rooster', f.rooster, 'Ventilatierooster bovenin het kozijn') : ''}
|
||||
${type === 'deur' ? sw('fillGlas', 'Glasstrook', f.glas, 'Glasstrook in het deurblad') : ''}
|
||||
${type === 'schuifdeur' ? sw('fillGlas', 'Glazen blad', f.glas, 'Glas in een smal kader; uit = dicht blad') : ''}
|
||||
${type === 'deur' ? sw('fillVolglas', 'Glasdeur', f.volglas, 'Glas in het hele blad (smal kader)') +
|
||||
pick('fillLeaf', 'Blad', KozijnModel.HINGES, f.leaf, 'Kant van het deurblad (van buitenaf gezien) als er een zijlicht naast past') +
|
||||
pick('fillHinge', 'Scharnier', KozijnModel.HINGES, f.hinge, 'Van buitenaf gezien. Openslaande deuren: twee deuren naast elkaar, bladen naar het midden, scharnieren buiten') +
|
||||
pick('fillSwing', 'Draait', KozijnModel.SWINGS, f.swing) : ''}
|
||||
${type === 'schuifdeur' ? pick('fillLeaves', 'Bladen', { 1: '1 blad', 2: '2 bladen (uit elkaar)' }, String(f.leaves)) +
|
||||
pick('fillMount', 'Montage', KozijnModel.SLIDE_MOUNTS, f.mount) +
|
||||
(f.mount === 'opbouw' ? pick('fillSide', 'Rail aan', KozijnModel.SLIDE_SIDES, f.side, 'Buitenzijde = +z (bij een binnenwand de "buiten"-kant)') : '') +
|
||||
(f.leaves === 1 ? pick('fillDir', 'Schuift', KozijnModel.SLIDE_DIRS, f.dir, 'Van buitenaf gezien') : '') : ''}
|
||||
</div>
|
||||
<div>
|
||||
${f ? `<div class="control-group">
|
||||
@@ -496,7 +443,6 @@ function mountWallEditor(container, callbacks) {
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
// Kleuren van de laatste opening met een vulling (voor nieuwe openingen).
|
||||
const lastFill = () => { const o = openings.slice().reverse().find(x => x.fill); return o ? o.fill : null; };
|
||||
|
||||
function renderOpeningsList() {
|
||||
@@ -563,8 +509,6 @@ function mountWallEditor(container, callbacks) {
|
||||
highlightSelectedCard();
|
||||
}
|
||||
|
||||
// Klik op de titelbalk van een kaart selecteert 'm ook (symmetrisch met
|
||||
// klikken op de opening in het vooraanzicht).
|
||||
document.getElementById('openings-list').addEventListener('click', (e) => {
|
||||
const header = e.target.closest('.opening-card-header');
|
||||
if (header && !e.target.closest('.opening-remove-btn')) {
|
||||
@@ -575,8 +519,6 @@ function mountWallEditor(container, callbacks) {
|
||||
}
|
||||
});
|
||||
|
||||
// Event delegation: 1 listener voor alle inputs/knoppen in de openingen-lijst,
|
||||
// in plaats van opnieuw listeners koppelen na elke re-render.
|
||||
document.getElementById('openings-list').addEventListener('input', (e) => {
|
||||
const id = parseInt(e.target.dataset.id, 10);
|
||||
const field = e.target.dataset.field;
|
||||
@@ -584,7 +526,6 @@ function mountWallEditor(container, callbacks) {
|
||||
const op = openings.find(o => o.id === id);
|
||||
if (!op) return;
|
||||
if (field === 'type') {
|
||||
// Type kiezen = standaardmaat en -vulling van dat type overnemen (daarna vrij aan te passen).
|
||||
const preset = WallModel.OPENING_TYPES[e.target.value];
|
||||
op.type = e.target.value;
|
||||
op.w = preset.w; op.h = preset.h; op.y = preset.y;
|
||||
@@ -594,18 +535,20 @@ function mountWallEditor(container, callbacks) {
|
||||
} else if (field === 'fill') {
|
||||
const v = e.target.value;
|
||||
if (v === 'geen') delete op.fill;
|
||||
else op.fill = Object.assign(KozijnModel.defaultFill(v === 'deur' ? (op.type === 'raam' ? 'deur' : op.type) : 'raam', op.fill || lastFill()), { type: v });
|
||||
else op.fill = Object.assign(KozijnModel.defaultFill(v === 'deur' || v === 'schuifdeur' ? (op.type === 'raam' ? 'deur' : op.type) : 'raam', op.fill || lastFill()), { type: v });
|
||||
renderOpeningsList();
|
||||
} else if (/^fill(Rooster|Glas|Out|In)$/.test(field)) {
|
||||
} else if (/^fill(Rooster|Glas|Volglas|Out|In|Hinge|Leaf|Swing|Leaves|Mount|Side|Dir)$/.test(field)) {
|
||||
if (!op.fill) return;
|
||||
const key = { fillRooster: 'rooster', fillGlas: 'glas', fillOut: 'out', fillIn: 'in' }[field];
|
||||
op.fill[key] = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
||||
const key = { fillRooster: 'rooster', fillGlas: 'glas', fillVolglas: 'volglas', fillOut: 'out', fillIn: 'in', fillHinge: 'hinge', fillLeaf: 'leaf', fillSwing: 'swing',
|
||||
fillLeaves: 'leaves', fillMount: 'mount', fillSide: 'side', fillDir: 'dir' }[field];
|
||||
op.fill[key] = e.target.type === 'checkbox' ? e.target.checked : key === 'leaves' ? Number(e.target.value) : e.target.value;
|
||||
if (key === 'leaves' || key === 'mount') renderOpeningsList();
|
||||
} else if (field === 'sloped') {
|
||||
if (e.target.checked) op.hR = op.h; else delete op.hR;
|
||||
renderOpeningsList();
|
||||
} else if (field === 'anchor') {
|
||||
op.anchor = e.target.value;
|
||||
renderOpeningsList(); // label van 'offset' hangt af van anchor
|
||||
renderOpeningsList();
|
||||
} else {
|
||||
op[field] = parseFloat(e.target.value) || 0;
|
||||
}
|
||||
@@ -620,9 +563,6 @@ function mountWallEditor(container, callbacks) {
|
||||
});
|
||||
document.getElementById('btn-add-opening').addEventListener('click', addOpening);
|
||||
|
||||
// Bovenkant evenwijdig aan de daklijn: hoogte rechts = hoogte links +
|
||||
// het verschil in daklijn tussen de rechter- en linkerrand (op de
|
||||
// opgeloste positie; met een nok ertussen het rechte stuk ertussen).
|
||||
function followRoof(id) {
|
||||
const op = openings.find(o => o.id === id);
|
||||
const res = (CONFIG.openings || []).find(o => o.id === id);
|
||||
@@ -633,10 +573,6 @@ function mountWallEditor(container, callbacks) {
|
||||
updateAll();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// JSON EXPORT / IMPORT — sla het volledige wand-ontwerp (parameters +
|
||||
// ruwe, niet-opgeloste openingen) op als JSON, en laad het weer terug.
|
||||
// ============================================================
|
||||
function setIoStatus(msg, cls) {
|
||||
const el = document.getElementById('io-status');
|
||||
el.textContent = msg;
|
||||
@@ -651,7 +587,6 @@ function mountWallEditor(container, callbacks) {
|
||||
heightAll: parseFloat(document.getElementById('inp-wall-h-all').value),
|
||||
heightLeft: parseFloat(document.getElementById('inp-wall-h-left').value),
|
||||
heightRight: parseFloat(document.getElementById('inp-wall-h-right').value),
|
||||
// Middenpunt (nok); null = rechte bovenrand.
|
||||
heightMid: document.getElementById('inp-wall-peak').checked ? parseFloat(document.getElementById('inp-wall-h-mid').value) : null,
|
||||
topPts: wallTopPts ? wallTopPts.map(q => Object.assign({}, q)) : undefined,
|
||||
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
|
||||
@@ -667,7 +602,7 @@ function mountWallEditor(container, callbacks) {
|
||||
function getPureState() {
|
||||
return {
|
||||
wall: collectWallParams(),
|
||||
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone, rabatColor },
|
||||
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone, stuc, rabatColor },
|
||||
openings: openings.map(rawOpening)
|
||||
};
|
||||
}
|
||||
@@ -692,9 +627,10 @@ function mountWallEditor(container, callbacks) {
|
||||
|
||||
buitenMateriaal = (state.materials && state.materials.exterior) || 'vochtdoek';
|
||||
cladding = (state.materials && state.materials.cladding) || 'horizontaal';
|
||||
trasraam = (state.materials && state.materials.trasraam) || null; // woning-instelling
|
||||
stone = (state.materials && state.materials.stone) || null; // woning-instelling (steen)
|
||||
rabatColor = (state.materials && state.materials.rabatColor) || null; // woning-instelling (rabat)
|
||||
trasraam = (state.materials && state.materials.trasraam) || null;
|
||||
stone = (state.materials && state.materials.stone) || null;
|
||||
stuc = (state.materials && state.materials.stuc) || null;
|
||||
rabatColor = (state.materials && state.materials.rabatColor) || null;
|
||||
syncMaterialButtons();
|
||||
|
||||
openings = state.openings.map(rawOpening);
|
||||
@@ -705,10 +641,6 @@ function mountWallEditor(container, callbacks) {
|
||||
updateAll();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// PDF-export: het tekenblad zelf komt uit WallPdf (wall/wall-pdf.js),
|
||||
// gedeeld met de unit-PDF.
|
||||
// ------------------------------------------------------------
|
||||
function generateWallPdf() {
|
||||
recalculate();
|
||||
const doc = PdfSheet.createDoc();
|
||||
@@ -784,11 +716,6 @@ function mountWallEditor(container, callbacks) {
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// OPENINGEN: ankerresolutie + overlap-handling — zie
|
||||
// WallModel.resolveOpenings (wall/wall-model.js).
|
||||
// ============================================================
|
||||
|
||||
function updateOpeningWarnings(resolvedOpenings) {
|
||||
const anyOverflow = resolvedOpenings.some(o => o.overflowing);
|
||||
const globalWarn = document.getElementById('openings-global-warning');
|
||||
@@ -842,7 +769,6 @@ function mountWallEditor(container, callbacks) {
|
||||
document.getElementById('lbl-wall-h-left').textContent = peakOn ? 'Goothoogte links' : 'Hoogte links';
|
||||
document.getElementById('lbl-wall-h-right').textContent = peakOn ? 'Goothoogte rechts' : 'Hoogte rechts';
|
||||
|
||||
// Afronding/fallbacks + ankerresolutie van de openingen: WallModel.
|
||||
CONFIG = WallModel.buildConfig({
|
||||
width: wallWidth,
|
||||
heightLeft: hLeft,
|
||||
@@ -868,7 +794,6 @@ function mountWallEditor(container, callbacks) {
|
||||
setSpecAll('roof-angle', calcData.angleDeg + '°');
|
||||
}
|
||||
|
||||
// --- 2D CANVAS RENDERER: BOVENAANZICHT ---
|
||||
const canvas2DTop = document.getElementById('canvas-2d-top');
|
||||
const ctx = canvas2DTop.getContext('2d');
|
||||
|
||||
@@ -916,8 +841,6 @@ function mountWallEditor(container, callbacks) {
|
||||
ctx.strokeRect(screenX, screenY, studWidthPx, studDepthPx);
|
||||
};
|
||||
|
||||
// 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)));
|
||||
|
||||
@@ -941,7 +864,6 @@ function mountWallEditor(container, callbacks) {
|
||||
});
|
||||
}
|
||||
|
||||
// --- 2D CANVAS RENDERER: VOORAANZICHT (ZUID) ---
|
||||
const canvas2DFront = document.getElementById('canvas-2d-front');
|
||||
const ctxFront = canvas2DFront.getContext('2d');
|
||||
|
||||
@@ -975,9 +897,6 @@ function mountWallEditor(container, callbacks) {
|
||||
for (let x = 0; x < w; x += gridSize) { ctxFront.beginPath(); ctxFront.moveTo(x, 0); ctxFront.lineTo(x, h); ctxFront.stroke(); }
|
||||
for (let y = 0; y < h; y += gridSize) { ctxFront.beginPath(); ctxFront.moveTo(0, y); ctxFront.lineTo(w, y); ctxFront.stroke(); }
|
||||
|
||||
// Referentielijnen vochtdoek-banen (1500mm h.o.h. vanaf de vloer).
|
||||
// Zelfde uitsparingen als de fill (WallModel.skinSegmentsAtY): de
|
||||
// lijnen stoppen bij openingen en bij de (schuine) bovenrand.
|
||||
ctxFront.strokeStyle = '#555';
|
||||
ctxFront.setLineDash([4, 4]);
|
||||
for (let bandY = 1500; buitenMateriaal === 'vochtdoek' && bandY < maxRoofMM; bandY += 1500) {
|
||||
@@ -990,7 +909,6 @@ function mountWallEditor(container, callbacks) {
|
||||
}
|
||||
ctxFront.setLineDash([]);
|
||||
|
||||
// Bovenkant trasraam (cement-gebonden plaat, woning-instelling).
|
||||
if (buitenMateriaal === 'vochtdoek' && trasraam && trasraam.top > 0) {
|
||||
ctxFront.strokeStyle = '#a0a3a8';
|
||||
ctxFront.lineWidth = 1.5;
|
||||
@@ -1005,7 +923,6 @@ function mountWallEditor(container, callbacks) {
|
||||
ctxFront.fillText('trasraam', toCanvasX(-halfWMM) + 4, toCanvasY(trasraam.top) - 3);
|
||||
}
|
||||
|
||||
// Wand-omtrek (incl. schuine bovenrand)
|
||||
ctxFront.strokeStyle = '#666';
|
||||
ctxFront.lineWidth = 2;
|
||||
ctxFront.beginPath();
|
||||
@@ -1015,9 +932,6 @@ function mountWallEditor(container, callbacks) {
|
||||
ctxFront.closePath();
|
||||
ctxFront.stroke();
|
||||
|
||||
// Onder- en bovenregel als volwaardig gevuld vlak (was alleen de
|
||||
// omtreklijn hierboven — dat liet lege ruimtes zien i.p.v. de
|
||||
// daadwerkelijke leggers).
|
||||
ctxFront.fillStyle = '#c29b62';
|
||||
ctxFront.strokeStyle = '#8a6836';
|
||||
ctxFront.lineWidth = 1;
|
||||
@@ -1034,8 +948,6 @@ function mountWallEditor(container, callbacks) {
|
||||
ctxFront.stroke();
|
||||
});
|
||||
|
||||
// X/Y-assen (lijn + label), altijd binnen het canvas gehouden zodat er
|
||||
// niks buiten het zichtbare venster valt. Stijl: core/canvas-labels.js.
|
||||
const xAxisY = Math.min(h - 6, toCanvasY(0) + 16);
|
||||
drawCanvasDim(ctxFront, toCanvasX(-halfWMM), xAxisY, toCanvasX(halfWMM), xAxisY,
|
||||
'X: ' + Math.round(wallWidthMM) + 'mm', (toCanvasX(-halfWMM) + toCanvasX(halfWMM)) / 2, Math.min(h - 2, xAxisY + 11));
|
||||
@@ -1047,7 +959,6 @@ function mountWallEditor(container, callbacks) {
|
||||
const yAxisRightX = Math.min(w - 10, toCanvasX(halfWMM) + 14);
|
||||
drawCanvasDim(ctxFront, yAxisRightX, toCanvasY(0), yAxisRightX, toCanvasY(hRightMM),
|
||||
'Y: ' + Math.round(hRightMM) + 'mm', Math.min(w - 4, yAxisRightX + 12), (toCanvasY(0) + toCanvasY(hRightMM)) / 2);
|
||||
// Tussenpunten van de daklijn (nok = het enige hoogste punt, anders knik).
|
||||
const inner = tl.pts.slice(1, -1);
|
||||
inner.forEach(q => {
|
||||
const isNok = inner.filter(o => o.y >= q.y - 0.5).length === 1;
|
||||
@@ -1058,7 +969,6 @@ function mountWallEditor(container, callbacks) {
|
||||
ctxFront.textAlign = 'start';
|
||||
});
|
||||
|
||||
// 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;
|
||||
@@ -1084,8 +994,8 @@ function mountWallEditor(container, callbacks) {
|
||||
const x0 = toCanvasX(opLeftMM);
|
||||
const x1 = toCanvasX(opLeftMM + op.w);
|
||||
const y0 = toCanvasY(op.y);
|
||||
const y1 = toCanvasY(op.y + op.h); // bovenkant links
|
||||
const y1r = toCanvasY(op.y + op.hR); // bovenkant rechts (schuine bovenzijde)
|
||||
const y1 = toCanvasY(op.y + op.h);
|
||||
const y1r = toCanvasY(op.y + op.hR);
|
||||
const isSelected = op.id === selectedOpeningId;
|
||||
|
||||
frontOpeningRects.push({ id: op.id, x0, y0, x1, y1, y1r, localX: op.x, y: op.y, w: op.w, h: op.h, hR: op.hR });
|
||||
@@ -1128,7 +1038,6 @@ function mountWallEditor(container, callbacks) {
|
||||
ctxFront.fillStyle = hd.handle === 'c' ? '#ffffff' : '#4da6ff';
|
||||
ctxFront.strokeStyle = '#0a1a2a';
|
||||
ctxFront.beginPath();
|
||||
// Midden (verplaatsen) rond, de rest vierkant.
|
||||
if (hd.handle === 'c') ctxFront.arc(hd.cx, hd.cy, 5, 0, Math.PI * 2);
|
||||
else ctxFront.rect(hd.cx - 5, hd.cy - 5, 10, 10);
|
||||
ctxFront.fill();
|
||||
@@ -1139,7 +1048,6 @@ function mountWallEditor(container, callbacks) {
|
||||
|
||||
drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM);
|
||||
|
||||
// Gevellampen (buitenverlichting woning)
|
||||
lampMarks.forEach(l => {
|
||||
const x = toCanvasX(-halfWMM + l.u), y = toCanvasY(l.y);
|
||||
ctxFront.fillStyle = '#ffd36b'; ctxFront.strokeStyle = '#1a1a1a'; ctxFront.lineWidth = 1;
|
||||
@@ -1148,8 +1056,6 @@ function mountWallEditor(container, callbacks) {
|
||||
});
|
||||
}
|
||||
|
||||
// Naad-zones (wand vervalt daar, zie woning) als gearceerde overlay.
|
||||
// Met upTo (mm): opbouw boven het lagere dak, alleen gearceerd tot daar.
|
||||
function drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM) {
|
||||
seamCuts.forEach(([u0, u1, upTo]) => {
|
||||
const x0 = toCanvasX(-halfWMM + u0), x1 = toCanvasX(-halfWMM + u1);
|
||||
@@ -1180,7 +1086,6 @@ function mountWallEditor(container, callbacks) {
|
||||
return frontHandleRects.find(hd => Math.abs(hd.cx - mx) <= 8 && Math.abs(hd.cy - my) <= 8);
|
||||
}
|
||||
function frontOpeningAt(mx, my) {
|
||||
// Binnen de (evt. schuine) omtrek: canvas-y loopt omlaag.
|
||||
return frontOpeningRects.find(r => {
|
||||
if (mx < r.x0 || mx > r.x1) return false;
|
||||
const yTop = r.y1 + (mx - r.x0) / ((r.x1 - r.x0) || 1) * (r.y1r - r.y1);
|
||||
@@ -1242,22 +1147,18 @@ function mountWallEditor(container, callbacks) {
|
||||
applyOpeningPosition(op, newLocalX, dragInfo.startW, wallWidth);
|
||||
op.y = Math.max(0, snap50(dragInfo.startY + dyMM));
|
||||
} else if (dragInfo.type === 'resize') {
|
||||
// Samengestelde handles: 'nw' = bovenkant links + linkerrand, enz.
|
||||
const hd = dragInfo.handle;
|
||||
const sloped = dragInfo.startHR !== dragInfo.startH || isSloped(op);
|
||||
if (hd === 'n') {
|
||||
// Hele bovenkant omhoog/omlaag (helling blijft).
|
||||
const dh = snap50(dyMM);
|
||||
op.h = Math.max(400, dragInfo.startH + dh);
|
||||
if (sloped) op.hR = Math.max(400, dragInfo.startHR + dh);
|
||||
} else if (hd === 'nw' || hd === 'ne') {
|
||||
// Eén bovenhoek: alleen die kant in de hoogte -> schuine bovenzijde.
|
||||
op.h = hd === 'nw' ? Math.max(400, snap50(dragInfo.startH + dyMM)) : dragInfo.startH;
|
||||
op.hR = hd === 'ne' ? Math.max(400, snap50(dragInfo.startHR + dyMM)) : dragInfo.startHR;
|
||||
if (op.h === op.hR) delete op.hR; // weer recht
|
||||
if (op.h === op.hR) delete op.hR;
|
||||
}
|
||||
if (hd === 's' || hd === 'sw' || hd === 'se') {
|
||||
// Onderkant; de bovenkant blijft staan (onderin altijd waterpas).
|
||||
const topL = dragInfo.startY + dragInfo.startH, topR = dragInfo.startY + dragInfo.startHR;
|
||||
const newY = Math.min(snap50(dragInfo.startY + dyMM), Math.min(topL, topR) - 400);
|
||||
op.y = Math.max(0, newY);
|
||||
@@ -1343,7 +1244,6 @@ function mountWallEditor(container, callbacks) {
|
||||
draw2DSide();
|
||||
}
|
||||
|
||||
// --- THREE.JS 3D RENDERER ---
|
||||
const pane3D = document.getElementById('pane-3d');
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x1a1a1a);
|
||||
@@ -1376,16 +1276,14 @@ function mountWallEditor(container, callbacks) {
|
||||
|
||||
const wallMaterials = WallModel.createMaterials();
|
||||
|
||||
// Buitenzijde-materiaalkeuze (tab 3): 'vochtdoek' of 'cementplaat',
|
||||
// wederzijds exclusief. Binnenzijde is altijd spaanplaat (geen alternatief).
|
||||
// (Alleen knoppen met data-material/-cladding: de "Opslaan"-knop van
|
||||
// Project eigenschappen heeft ook de klasse material-toggle-btn.)
|
||||
// Alleen knoppen met data-material/-cladding: de "Opslaan"-knop van Project eigenschappen heeft ook de klasse material-toggle-btn.
|
||||
let buitenMateriaal = 'vochtdoek';
|
||||
let cladding = 'horizontaal'; // richting gevelbekleding (alleen bij vochtdoek)
|
||||
let trasraam = null; // { bottom, top } mm, komt uit de woning-instellingen (WallModel.trasraamFor)
|
||||
let stone = null; // steencode bij cementplaat, komt uit de woning-instellingen
|
||||
let rabatColor = null; // kleur rabat (WallModel.RABAT_COLORS), komt uit de woning-instellingen
|
||||
let stage = BuildStages.FINAL; // bouwfase van de 3D (schuif linksonder)
|
||||
let cladding = 'horizontaal';
|
||||
let trasraam = null;
|
||||
let stone = null;
|
||||
let stuc = null;
|
||||
let rabatColor = null;
|
||||
let stage = BuildStages.FINAL;
|
||||
function syncMaterialButtons() {
|
||||
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => b.classList.toggle('active', b.dataset.material === buitenMateriaal));
|
||||
document.querySelectorAll('.material-toggle-btn[data-cladding]').forEach(b => b.classList.toggle('active', b.dataset.cladding === cladding));
|
||||
@@ -1402,7 +1300,6 @@ function mountWallEditor(container, callbacks) {
|
||||
|
||||
let spaanplaatVisible = true;
|
||||
let buitenVisible = true;
|
||||
// Weergave-filter (core/view-filter.js): beplating binnen/buiten + isolatie.
|
||||
createViewFilter(document.getElementById('wall-toolbar-3d'), [
|
||||
{ key: 'binnen', label: 'Beplating binnenzijde' },
|
||||
{ key: 'buiten', label: 'Beplating buitenzijde' }
|
||||
@@ -1411,7 +1308,6 @@ function mountWallEditor(container, callbacks) {
|
||||
buitenVisible = v.buiten;
|
||||
update3DScene();
|
||||
});
|
||||
// Omgeving (core/skybox.js: geen/dag/nacht); de wand staat op de tegelvloer.
|
||||
let skyMode = Skybox.getMode();
|
||||
const skyEnv = Skybox.createEnvironment(scene);
|
||||
const applySky = () => {
|
||||
@@ -1421,26 +1317,21 @@ function mountWallEditor(container, callbacks) {
|
||||
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (m) => { skyMode = m; applySky(); });
|
||||
BuildStages.createBar(pane3D, stage, (st) => { stage = st; update3DScene(); });
|
||||
|
||||
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
|
||||
// met de unit-editor).
|
||||
let lastWallStats = null;
|
||||
|
||||
function update3DScene() {
|
||||
if (wallGroup3D) scene.remove(wallGroup3D);
|
||||
const result = WallModel.build3D(CONFIG, calcData, {
|
||||
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, rabatColor, spaanplaatVisible, buitenVisible, stage
|
||||
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, stuc, rabatColor, spaanplaatVisible, buitenVisible, stage
|
||||
});
|
||||
wallGroup3D = result.group;
|
||||
lastWallStats = result.stats;
|
||||
scene.add(wallGroup3D);
|
||||
applySky(); // schaduw van de nieuwe wand
|
||||
applySky();
|
||||
|
||||
updateSpecificaties();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SPECIFICATIES (tab 3)
|
||||
// ============================================================
|
||||
function fmt(n, decimals = 1) {
|
||||
return n.toLocaleString('nl-NL', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
||||
}
|
||||
@@ -1474,7 +1365,6 @@ function mountWallEditor(container, callbacks) {
|
||||
setSpecAll('void-m3', fmt(specs.voidVolumeM3, 3) + ' m3');
|
||||
setSpecAll('horiz-beams', lastWallStats.horizBeamCount + ' stuks');
|
||||
|
||||
// Inkoop in handelslengtes (zaagplan uit WallModel).
|
||||
const purchase = WallModel.planPurchase(WallModel.computeBOM(CONFIG, calcData));
|
||||
const wasteMM = purchase.reduce((a, sec) => a + sec.wasteMM, 0);
|
||||
const stockMM = purchase.reduce((a, sec) => a + sec.stockMM, 0);
|
||||
|
||||
Reference in New Issue
Block a user