v0.5.3: elektra-buizen min. 50 mm uit elkaar (gebundeld alleen naar de meterkast), vloer-wcd met leiding door de vloer, elektra op de PDF-bladen per wand/vloer/dakvlak, kosten elektra (buis + draad per meter, boorgaten 90 s geteld op stijlen/regels/balken, dozen, groepenkast, keuring), dakbeschot OSB 18 mm bovenop de sporen, felsplaten als dakbedekking

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-06 20:54:49 +02:00
co-authored by Claude Opus 5.5
parent e1a688728c
commit b5510d56c7
13 changed files with 668 additions and 64 deletions
+249 -11
View File
@@ -31,6 +31,9 @@
// Handmatige plekken gaan voor: de automatische plaatsing wijkt uit.
// Wandvlakken: binnenzijde van buitenwanden en naadwanddelen, beide
// zijden van binnenwanden (in de unit waar hun midden ligt).
// - Vloer-wcd's (settings.vloerWcd = [{ id, unitId, x, z }], x/z vanaf de
// noordwesthoek van de unit): vloerdoos met dubbele wcd (bijv. bij de
// eettafel), op de wcd-groep van die unit, leiding door de vloer.
// - Groepen: per unit een lichtgroep (B10, 1,5 mm²) en een wcd-groep
// (B16, 2,5 mm²); het buitenlicht op een eigen groep (B10, XMvK 3×1,5),
// op dezelfde aardlek als het binnenlicht van de voordeur-unit (zelfde
@@ -47,6 +50,16 @@
// XMvK. Een leiding over een naad tussen units krijgt een steekverbinder
// (modules worden los gebouwd en op de bouwplaats gekoppeld).
// Elke leiding: + reserve (lus in de doos, meterkast extra) en + slack %.
// Boorgaten per leiding (run.holes, voor de kosten): in een wand de stijlen
// (horizontaal stuk) of de regels/dorpels (verticaal stuk) die de buis
// kruist (WallModel.computeBOM), in plafond en vloer de balken dwars op de
// buis (balkposities van het dek), een gat in de vloerplaat waar een buis
// de vloer in gaat, en een doorvoer naar buiten voor de buitenverlichting.
// Buizen houden onderling minstens CLEAR (50 mm) vrij: na het routeren
// schuift elk te dicht (evenwijdig, overlappend) padstuk naar een eigen baan
// met een korte knik ervoor en erna (separateRuns). Alleen het laatste stuk
// naar de meterkast is gebundeld: de buizen komen tegen elkaar aan de kast
// binnen. Leidingen naar een vloer-wcd lopen in de vloer (onder de OSB).
//
// Plain script, geladen na lighting-model.js en unit-model.js.
// ============================================================
@@ -62,8 +75,11 @@ window.ElektraModel = (() => {
reserve: 300, // mm per leiding (lussen in de dozen)
meterkastReserve: 500, // mm extra per leiding vanuit de meterkast
apparaten: { koken: true, wasmachine: true, droger: true, warmtepomp: true, oven: false, vaatwasser: false },
place: {} // handmatige plekken (zie boven)
place: {}, // handmatige plekken (zie boven)
vloerWcd: [] // vloer-wcd's (zie boven)
};
const CLEAR = 50; // min. vrije ruimte tussen buizen
const FLOOR_DEPTH = 100; // buis in de vloer: zoveel onder de bovenkant vloer
// Apparaten met een eigen groep. code = label op de plattegrond (twee
// letters: geen verwarring met de schakelaarletters).
const APPLIANCES = {
@@ -103,6 +119,7 @@ window.ElektraModel = (() => {
s.meterkast = Object.assign({}, DEFAULT_SETTINGS.meterkast, i.meterkast || {});
s.apparaten = Object.assign({}, DEFAULT_SETTINGS.apparaten, i.apparaten || {});
s.place = Object.assign({}, i.place || {});
s.vloerWcd = Array.isArray(i.vloerWcd) ? i.vloerWcd.filter(o => o && o.unitId).map(o => Object.assign({}, o)) : [];
s.enabled = s.enabled !== false;
// v2: schakelaars 1050 -> 1500 mm, wcd 300 -> 400 mm (oude standaard).
if (!i.v) {
@@ -205,10 +222,17 @@ window.ElektraModel = (() => {
const r = u.rect;
return u.roof.ceilingAt(Math.max(r.x0 + 1, Math.min(r.x1 - 1, x)), Math.max(r.z0 + 1, Math.min(r.z1 - 1, z)));
};
const floorAt = (x, z) => { const u = unitAt(x, z) || units[0]; return u.layout.floorTop; };
// Pad als 3D-punten (mm). Per punt: wall = wandvlak waar het in de wand
// ligt, ceil = boven het plafond (voor de buizen in de 3D, build3D).
// ligt, ceil = boven het plafond, floor = in de vloer (voor de buizen in
// de 3D en de bladen per wand/vloer/plafond).
function path(a, b) {
const P = (p, wall, ceil) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil });
const P = (p, wall, ceil, floor) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil, floor: !!floor });
if (a.mount === 'vloer' || b.mount === 'vloer') { // door de vloer
const fa = floorAt(a.x, a.z) - FLOOR_DEPTH, fb = floorAt(b.x, b.z) - FLOOR_DEPTH;
return [P(a, a.face, false, a.mount === 'vloer'), P({ x: a.x, y: fa, z: a.z }, a.face, false, true), P({ x: b.x, y: fa, z: a.z }, null, false, true),
P({ x: b.x, y: fb, z: b.z }, b.face, false, true), P(b, b.face, false, b.mount === 'vloer')];
}
if (a.face && a.face === b.face) return [P(a, a.face), P({ x: a.x, y: b.y, z: a.z }, a.face), P(b, b.face)]; // door de stijlen
if (a.mount === 'buiten' && b.mount === 'buiten') return [P(a), P({ x: b.x, y: a.y, z: a.z }), P(b)];
const top = (p) => p.mount === 'plafond' ? p.y : ceilAt(p.x, p.z);
@@ -237,7 +261,7 @@ window.ElektraModel = (() => {
});
return n;
}
return { unitAt, ceilAt, path, length, cost, crossings };
return { unitAt, ceilAt, floorAt, path, length, cost, crossings };
}
// ------------------------------------------------------------
@@ -250,7 +274,7 @@ window.ElektraModel = (() => {
const indoor = lighting.indoor || [], gevel = lighting.lamps || [], spots = (roofEdge && roofEdge.spots) || [];
indoor.concat(gevel, spots).forEach(p => { delete p.letter; });
const out = {
enabled: s.enabled, settings: s, meterkast: null, combos: [], switches: [], wcds: [], apps: [], boxes: [],
enabled: s.enabled, settings: s, meterkast: null, combos: [], switches: [], wcds: [], apps: [], floorWcds: [], boxes: [],
groups: [], runs: [], cables: [], wires: [], conduits: [], materials: [], warnings: [], items: [], faces: []
};
if (!s.enabled || !units.length) return out;
@@ -435,11 +459,20 @@ window.ElektraModel = (() => {
out.apps.push({ key, manual: !!fx, app: k, label: A.label, code: A.code, unitId: f.unit.id, face: f, u: uu, y, rotY: f.piece.place.rotY, pos: facePoint(f, uu, y, 0), center: facePoint(f, uu, y, 6) });
});
// 6c. Vloer-wcd's (handmatig toegevoegd, x/z vanaf de noordwesthoek van de unit).
s.vloerWcd.forEach(o => {
const u = units.find(x => x.id === o.unitId);
if (!u) return;
const x = u.rect.x0 + Math.max(100, Math.min(u.rect.W - 100, num(o.x, u.rect.W / 2)));
const z = u.rect.z0 + Math.max(100, Math.min(u.rect.D - 100, num(o.z, u.rect.D / 2)));
out.floorWcds.push({ key: 'vwcd:' + o.id, id: o.id, unitId: u.id, x, z, y: u.layout.floorTop });
});
// 7. Groepen: per unit licht (B10) + wcd (B16); fasen en aardlek om en om.
units.forEach(u => {
const sws = out.switches.filter(x => x.unitId === u.id && x.func === 'binnen');
if (sws.length) out.groups.push({ kind: 'licht', unitId: u.id, label: `Verlichting unit ${u.id}`, breaker: 'B10', cable: 'licht', switches: sws.map(x => x.letter) });
if (out.wcds.some(w => w.unitId === u.id)) out.groups.push({ kind: 'wcd', unitId: u.id, label: `Wcd unit ${u.id}`, breaker: 'B16', cable: 'wcd', switches: [] });
if (out.wcds.some(w => w.unitId === u.id) || out.floorWcds.some(w => w.unitId === u.id)) out.groups.push({ kind: 'wcd', unitId: u.id, label: `Wcd unit ${u.id}`, breaker: 'B16', cable: 'wcd', switches: [] });
const outSw = out.switches.filter(x => x.unitId === u.id && x.func === 'buiten');
if (outSw.length) out.groups.push({ kind: 'buiten', unitId: u.id, label: 'Buitenverlichting', breaker: 'B10', cable: 'buiten', switches: outSw.map(x => x.letter) });
});
@@ -472,6 +505,7 @@ window.ElektraModel = (() => {
}
out.switches.forEach(sw => { const g = groupOf(sw.func === 'buiten' ? 'buiten' : 'licht', sw.unitId); sw.group = g ? g.nr : null; });
out.wcds.forEach(w => { const g = groupOf('wcd', w.unitId); w.group = g ? g.nr : null; });
out.floorWcds.forEach(w => { const g = groupOf('wcd', w.unitId); w.group = g ? g.nr : null; });
out.apps.forEach(a => { const g = out.groups.find(x => x.kind === 'app' && x.app === a.app); a.group = g ? g.nr : null; });
// 8. Leidingen.
@@ -480,8 +514,7 @@ window.ElektraModel = (() => {
const addRun = (type, a, b, group, letter) => {
const pts = R.path(a, b);
const raw = R.length(pts);
const lengthMM = (raw + s.reserve + (a.mk || b.mk ? s.meterkastReserve : 0)) * (1 + s.slack / 100);
out.runs.push({ type, group, letter: letter || null, from: a.name || '', to: b.name || '', path: pts, rawMM: raw, lengthMM, crossings: R.crossings(pts), outdoor: a.mount === 'buiten' || b.mount === 'buiten' });
out.runs.push({ type, group, letter: letter || null, from: a.name || '', to: b.name || '', path: pts, mk: !!(a.mk || b.mk), outdoor: a.mount === 'buiten' || b.mount === 'buiten' });
};
// Minimale opspannende boom vanaf root (Prim, op de snelle schatting).
const tree = (root, nodes, typeOf, group, letter) => {
@@ -514,19 +547,33 @@ window.ElektraModel = (() => {
combo.vds.push(vd);
out.boxes.push({ kind: 'verdeeldoos', combo: combo.id, group: e.group, x: vd.x, y: vd.y, z: vd.z, unitId: combo.unitId });
if (mkNode) addRun('licht', mkNode, vd, e.group); // voeding binnen: VD in buis
const swNode = Object.assign({}, combo.pos, { mount: 'wand', face: combo.face, name: e.switches.map(x => x.letter).join('') });
// Naar de eigen inbouwdoos (of het midden ervan) van deze groep.
const uSw = e.switches.reduce((t, x) => t + x.u, 0) / e.switches.length;
const swNode = Object.assign(facePoint(combo.face, uSw, combo.y, 0), { mount: 'wand', face: combo.face, name: e.switches.map(x => x.letter).join('') });
addRun('schakel' + (e.switches.length + 1), vd, swNode, e.group);
e.switches.forEach(sw => tree(vd, sw.lamps.map((l, k) => Object.assign(l, { name: `${sw.letter}${k + 1}` })),
(a, b) => a.mount === 'buiten' || b.mount === 'buiten' ? 'buiten' : 'licht', e.group, sw.letter));
});
}
out.groups.filter(g => g.kind === 'wcd').forEach(g => {
const nodes = out.wcds.filter(w => w.group === g.nr).map((w, i) => Object.assign({}, w.pos, { mount: 'wand', face: w.face, name: `WCD${g.nr}.${i + 1}` }));
const nodes = out.wcds.filter(w => w.group === g.nr).map((w, i) => Object.assign({}, w.pos, { mount: 'wand', face: w.face, name: `WCD${g.nr}.${i + 1}` }))
.concat(out.floorWcds.filter(w => w.group === g.nr).map((w, i) => ({ x: w.x, y: w.y, z: w.z, mount: 'vloer', name: `VWCD${g.nr}.${i + 1}` })));
if (mkLow && nodes.length) tree(mkLow, nodes, () => 'wcd', g.nr);
});
// Apparaten: elk een eigen leiding vanuit de meterkast.
if (mkLow) out.apps.forEach(a => addRun(APPLIANCES[a.app].cable, mkLow, Object.assign({}, a.pos, { mount: 'wand', face: a.face, name: a.code }), a.group));
// 8b. Buizen uit elkaar (min. CLEAR), daarna lengtes, naad-overgangen en boorgaten.
separateRuns(out.runs);
const holesOf = makeHoleCounter(units, R);
out.runs.forEach(r => {
r.rawMM = R.length(r.path);
r.lengthMM = (r.rawMM + s.reserve + (r.mk ? s.meterkastReserve : 0)) * (1 + s.slack / 100);
r.crossings = R.crossings(r.path);
r.holes = holesOf(r);
});
out.holes = out.runs.reduce((a, r) => a + r.holes, 0);
// 9. Totalen: per leidingtype, draad per doorsnede/kleur, buis, groep.
const byType = {};
out.runs.forEach(r => { byType[r.type] = (byType[r.type] || 0) + r.lengthMM; });
@@ -578,6 +625,7 @@ window.ElektraModel = (() => {
add('Schakelaar enkelpolig (inbouw)', out.switches.length);
Object.keys(frames).forEach(n => add(`Afdekraam ${n}-voudig`, frames[n]));
add('Wandcontactdoos dubbel met randaarde', out.wcds.length);
add('Vloerdoos met dubbele wcd (randaarde)', out.floorWcds.length);
const appMat = {};
out.apps.forEach(a => { const n = APPLIANCES[a.app].material; appMat[n] = (appMat[n] || 0) + 1; });
Object.keys(appMat).forEach(n => add(n, appMat[n]));
@@ -600,6 +648,122 @@ window.ElektraModel = (() => {
out.combos.forEach(c => item(c.key, 'sw', 'Schakelaar ' + c.switches.map(x => x.letter).join(' '), c.face, c.u, c.y, c.manual, 260, c.unitId));
out.wcds.forEach(w => item(w.key, 'wcd', `Wcd ${w.key.split(':')[2]} unit ${w.unitId} (groep ${w.group})`, w.face, w.u, w.y, w.manual, 120, w.unitId));
out.apps.forEach(a => item(a.key, 'app', `${a.label} (groep ${a.group})`, a.face, a.u, a.y, a.manual, 150));
out.floorWcds.forEach(w => {
const u = units.find(x => x.id === w.unitId);
out.items.push({ key: w.key, kind: 'vwcd', label: `Vloer-wcd unit ${w.unitId} (groep ${w.group})`, unitId: w.unitId, x: w.x - u.rect.x0, z: w.z - u.rect.z0, manual: true, marker: { x: w.x, z: w.z } });
});
return out;
}
// ------------------------------------------------------------
// Buizen uit elkaar (muteert run.path). Padstukken zijn haaks (x, y of z).
// Te dicht = evenwijdig, overlappend langs de as en minder dan CLEAR vrij
// (buitenkant tot buitenkant). Zo'n stuk schuift naar de eerste vrije baan
// (±1, ±2, ... × CLEAR + buis-Ø): in een wand langs de wand (verticaal
// stuk) of omhoog/omlaag (horizontaal stuk), in plafond/vloer horizontaal
// dwars. Een knik ervoor en erna verbindt het weer met de rest van het pad.
// Uitzondering: het laatste stuk naar de meterkast. Daar komen de buizen
// naast elkaar (tegen elkaar aan) de kast binnen.
// ------------------------------------------------------------
function separateRuns(runs) {
const add = (p, d, k) => Object.assign({}, p, { x: p.x + d.x * k, y: p.y + d.y * k, z: p.z + d.z * k });
const axisOf = (p, q) => {
const dx = Math.abs(q.x - p.x), dy = Math.abs(q.y - p.y), dz = Math.abs(q.z - p.z);
return dx >= dy && dx >= dz ? 'x' : dz >= dy ? 'z' : 'y';
};
const lenOf = (p, q) => Math.hypot(q.x - p.x, q.y - p.y, q.z - p.z);
const seg = (run, p, q, r) => {
const axis = axisOf(p, q), perp = {};
['x', 'y', 'z'].filter(k => k !== axis).forEach(k => { perp[k] = (p[k] + q[k]) / 2; });
return { run, axis, lo: Math.min(p[axis], q[axis]), hi: Math.max(p[axis], q[axis]), perp, r };
};
const placed = [];
const conflict = (sg) => placed.some(t => t.run !== sg.run && t.axis === sg.axis && Math.min(t.hi, sg.hi) - Math.max(t.lo, sg.lo) > 1 &&
Math.hypot(...Object.keys(sg.perp).map(k => sg.perp[k] - t.perp[k])) < CLEAR + sg.r + t.r - 0.5);
const faceDir = (f) => { const L = Math.hypot(f.b.x - f.a.x, f.b.z - f.a.z) || 1; return { x: (f.b.x - f.a.x) / L, y: 0, z: (f.b.z - f.a.z) / L }; };
const shiftDir = (p, q, axis) => {
const plane = (p.ceil || p.floor) && (q.ceil || q.floor);
if (axis === 'y') { const f = p.wall || q.wall; return f ? faceDir(f) : { x: 1, y: 0, z: 0 }; }
if (!plane && (p.wall || q.wall)) return { x: 0, y: 1, z: 0 };
return axis === 'x' ? { x: 0, y: 0, z: 1 } : { x: 1, y: 0, z: 0 };
};
const conduit = runs.filter(r => cableInfo(r.type).conduit > 0);
const radius = (r) => cableInfo(r.type).conduit / 2;
// 1. Meterkast: per aansluitpunt (boven/onder) de buizen naast elkaar,
// tegen elkaar aan, gesorteerd naar de kant waar ze heen gaan.
const bundles = {};
conduit.filter(r => r.mk && r.path[0].wall).forEach(r => {
const k = Math.round(r.path[0].y);
(bundles[k] = bundles[k] || []).push(r);
});
Object.values(bundles).forEach(list => {
const d = faceDir(list[0].path[0].wall);
const along = (r) => { const q = r.path[Math.min(2, r.path.length - 1)]; return (q.x - r.path[0].x) * d.x + (q.z - r.path[0].z) * d.z; };
list.sort((a, b) => along(a) - along(b));
const total = list.reduce((a, r) => a + 2 * radius(r), 0);
let off = -total / 2;
list.forEach(r => {
const o = off + radius(r);
off += 2 * radius(r);
const orig = r.path, dAx = Math.abs(d.x) > Math.abs(d.z) ? 'x' : 'z';
const out = orig.map(p => p);
out[0] = add(orig[0], d, o);
for (let k = 0; k < orig.length - 1; k++) {
const len = lenOf(orig[k], orig[k + 1]);
if (len >= 1 && axisOf(orig[k], orig[k + 1]) === dAx) break; // loopt langs de kast: alleen langer/korter
if (k + 1 === orig.length - 1) { out.splice(k + 1, 0, add(orig[k + 1], d, o)); break; } // eindpunt blijft: knik ervoor
out[k + 1] = add(orig[k + 1], d, o);
}
r.path = out;
placed.push(seg(r, out[0], out[1], radius(r))); // gebundeld stuk: vast
r.fixed = 1;
});
});
// 2. De rest: per padstuk een vrije baan zoeken.
conduit.forEach(r => {
const rad = radius(r), step = CLEAR + 2 * rad;
let pts = r.path;
for (let i = r.fixed || 0; i < pts.length - 1; i++) {
const p = pts[i], q = pts[i + 1];
if (lenOf(p, q) < 1) continue;
const sg = seg(r, p, q, rad);
if (!conflict(sg)) { placed.push(sg); continue; }
const dir = shiftDir(p, q, sg.axis);
// Knikken tellen ook mee, behalve direct aan een doos/lamp (daar
// komen buizen samen) en aan het eind van de meterkastbundel.
const freeStart = i === 0 || (r.fixed && i === r.fixed), freeEnd = i + 1 === pts.length - 1;
let done = false;
for (let k = 1; k <= 12 && !done; k++) {
for (const sign of [1, -1]) {
const kk = sign * k * step, p2 = add(p, dir, kk), q2 = add(q, dir, kk), s2 = seg(r, p2, q2, rad);
const j1 = seg(r, p, p2, rad), j2 = seg(r, q2, q, rad);
if (conflict(s2) || (!freeStart && conflict(j1)) || (!freeEnd && conflict(j2))) continue;
pts = pts.slice(0, i + 1).concat([p2, q2], pts.slice(i + 1));
placed.push(s2, j1, j2);
i += 2; // verder vanaf de knik terug naar q
done = true;
break;
}
}
if (!done) placed.push(sg);
}
r.path = simplifyPath(pts);
});
runs.forEach(r => { delete r.fixed; });
}
// Dubbele punten en heen-en-weer-knikken eruit.
function simplifyPath(pts) {
const same = (a, b) => Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.z - b.z) < 0.5;
let out = pts.filter((p, i) => i === 0 || !same(p, pts[i - 1]));
let changed = true;
while (changed) {
changed = false;
for (let i = 1; i < out.length - 1; i++) {
if (i + 1 < out.length && same(out[i - 1], out[i + 1])) { out.splice(i, 2); changed = true; break; }
}
}
return out;
}
@@ -663,6 +827,7 @@ window.ElektraModel = (() => {
el.combos.forEach(c => box(c.width, PLATE, 10, c.center, c.rotY, mats.plateMaterial));
el.wcds.forEach(w => box(WCD_W, PLATE, 12, w.center, w.rotY, mats.plateMaterial));
el.apps.forEach(a => box(PLATE, PLATE, 12, a.center, a.rotY, mats.plateMaterial));
el.floorWcds.forEach(w => box(130, 8, 130, { x: w.x, y: w.y + 4, z: w.z }, 0, mats.plateMaterial)); // deksel vloerdoos
}
if (!rough) return group;
@@ -702,6 +867,7 @@ window.ElektraModel = (() => {
el.switches.forEach(sw => wallBox(sw.combo.face, sw.u, sw.combo.y));
el.wcds.forEach(w => { wallBox(w.face, w.u - 36, w.y); wallBox(w.face, w.u + 36, w.y); });
el.apps.forEach(a => wallBox(a.face, a.u, a.y));
el.floorWcds.forEach(w => box(100, 80, 100, { x: w.x, y: w.y - 40, z: w.z }, 0, mats.boxMaterial)); // vloerdoos
// Verdeeldozen en spotdozen boven het plafond.
el.boxes.forEach(b => box(90, 50, 90, { x: b.x, y: b.y + CEIL_UP + 10, z: b.z }, 0, mats.boxMaterial));
el.switches.forEach(sw => sw.lamps.filter(l => l.kind === 'binnenspot').forEach(l => {
@@ -712,5 +878,77 @@ window.ElektraModel = (() => {
return group;
}
return { DEFAULT_SETTINGS, CABLES, CORE_COLORS, APPLIANCES, cableInfo, normalizeSettings, compute, snapToWall, createMaterials, build3D };
// Plek van een punt op een wandvlak: { u, y } (wand-x en hoogte boven de vloer).
const wallCoords = (f, p) => ({ u: uAt(f, p), y: p.y - f.piece.place.floorTop });
// Lijnstuk p-q (wereld-mm) binnen grenzen b = { x0, x1, z0, z1 } (plattegrond); null = erbuiten.
function clipPlan(p, q, b) {
let t0 = 0, t1 = 1;
for (const [k, lo, hi] of [['x', b.x0, b.x1], ['z', b.z0, b.z1]]) {
const d = q[k] - p[k];
if (Math.abs(d) < 1e-9) { if (p[k] < lo - 0.5 || p[k] > hi + 0.5) return null; continue; }
let a = (lo - p[k]) / d, c = (hi - p[k]) / d;
if (a > c) [a, c] = [c, a];
t0 = Math.max(t0, a); t1 = Math.min(t1, c);
if (t0 > t1) return null;
}
const at = (t) => Object.assign({}, p, { x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t, z: p.z + (q.z - p.z) * t });
return [at(t0), at(t1)];
}
// Dakvlak pl van unit: deel van de plattegrond (bounds) en omrekening naar
// het dek zoals de dakbladen (map: { sx, sy }, mm vanaf linksvoor), gelijk
// aan de doorvoeren in RoofModel.computeUnitRoof. De balken van het dek
// staan op sx = joist + breedte/2 en lopen langs sy.
function roofDeckMap(unit, pl) {
const alongX = Math.abs(Math.sin(pl.yaw)) > 0.5, rr = unit.roof.rect, r = unit.rect;
const qOf = (p) => alongX ? p.x - r.cx : p.z - r.cz;
return {
bounds: alongX
? { x0: Math.max(r.x0, r.cx + pl.p0), x1: Math.min(r.x1, r.cx + pl.p1), z0: r.z0, z1: r.z1 }
: { x0: r.x0, x1: r.x1, z0: Math.max(r.z0, r.cz + pl.p0), z1: Math.min(r.z1, r.cz + pl.p1) },
map: (p) => ({ sx: alongX ? rr.z1 - p.z : p.x - rr.x0, sy: (qOf(p) - pl.p0) / pl.cos })
};
}
// Boorgaten van een leiding (zie kop). Telt per padstuk.
const VERTICAL_KINDS = ['verticale balk', 'kreupele stijl'];
function makeHoleCounter(units, R) {
const boms = new Map();
const bomOf = (piece) => {
if (!boms.has(piece)) boms.set(piece, piece.frame ? WallModel.computeBOM(piece.config, piece.frame) : []);
return boms.get(piece);
};
const between = (v, a, b) => v > Math.min(a, b) + 1 && v < Math.max(a, b) - 1;
const joistsBetween = (cd, a, b) => cd.joists.filter(j => between(j + cd.width / 2, a, b)).length;
return (run) => {
if (!cableInfo(run.type).conduit) return run.type === 'buiten' && run.from.startsWith('VD') ? 1 : 0; // doorvoer naar buiten
let n = 0;
run.path.slice(1).forEach((q, i) => {
const p = run.path[i];
const vertical = Math.abs(q.y - p.y) > Math.hypot(q.x - p.x, q.z - p.z);
if (p.wall && q.wall && p.wall.piece === q.wall.piece && !(p.ceil && q.ceil)) {
const f = p.wall, a = wallCoords(f, p), b = wallCoords(f, q), bom = bomOf(f.piece);
if (vertical) {
const x = a.u - f.W / 2;
n += bom.filter(bp => !VERTICAL_KINDS.includes(bp.kind) && Math.abs(x - bp.x) <= bp.length / 2 && between(bp.y, a.y, b.y)).length;
if (p.floor !== q.floor) n += 1; // door de vloerplaat/randbalk de vloer in
} else {
n += bom.filter(bp => VERTICAL_KINDS.includes(bp.kind) && between(bp.x + f.W / 2, a.u, b.u) && Math.abs(a.y - bp.y) <= bp.length / 2).length;
}
} else if (!vertical && p.ceil && q.ceil) {
const mid = { x: (p.x + q.x) / 2, z: (p.z + q.z) / 2 }, u = R.unitAt(mid.x, mid.z);
if (u) u.roof.planes.forEach(pl => {
const dm = roofDeckMap(u, pl), c = clipPlan(p, q, dm.bounds);
if (c) n += joistsBetween(pl.data.calcData, dm.map(c[0]).sx, dm.map(c[1]).sx);
});
} else if (!vertical && p.floor && q.floor) {
const u = R.unitAt((p.x + q.x) / 2, (p.z + q.z) / 2);
const c = u && clipPlan(p, q, u.rect);
if (c) n += joistsBetween(u.floorData.calcData, c[0].x - u.rect.x0, c[1].x - u.rect.x0);
} else if (vertical && (p.floor || q.floor) && !(p.wall || q.wall)) n += 1; // vloerdoos: gat in de vloerplaat
});
return n;
};
}
return { DEFAULT_SETTINGS, CABLES, CORE_COLORS, APPLIANCES, PLATE, WCD_W, cableInfo, normalizeSettings, compute, snapToWall, wallCoords, clipPlan, roofDeckMap, createMaterials, build3D };
})();
+125 -2
View File
@@ -94,6 +94,17 @@ window.ElektraPdf = (() => {
doc.text(String(a.group), px(q.x + a.face.n.x * 330), py(q.z + a.face.n.z * 330) + 0.5, { align: 'center' });
});
// Vloer-wcd: vierkantje met V + groepnummer.
el.floorWcds.forEach(w => {
doc.setDrawColor(...RUN_COLORS.wcd);
doc.setFillColor(255, 255, 255);
doc.setLineWidth(0.2);
doc.rect(px(w.x) - 1.1, py(w.z) - 1.1, 2.2, 2.2, 'FD');
doc.setTextColor(...RUN_COLORS.wcd);
doc.setFontSize(3.6);
doc.text('V' + w.group, px(w.x), py(w.z) + 0.5, { align: 'center' });
});
// Lampletters (zelfde letter als de schakelaar).
doc.setFontSize(3.6);
doc.setTextColor(...BLACK);
@@ -102,7 +113,7 @@ window.ElektraPdf = (() => {
// Twee regels (doc.text met een array).
const PLAN_LEGEND = [
'Elektra: MK = meterkast, kadertje = schakelaar(s) met letter (dezelfde letter staat bij de lampen erop), rood rondje = dubbele wcd (groepnummer), vierkantje = verdeeldoos in plafond;',
'rood kadertje = apparaat (KK kookgroep, WM wasmachine, DR droger, WP warmtepomp, OV oven, VW vaatwasser) met groepnummer; gestippeld = leidingen (geel licht, bruin schakeldraad, rood wcd/apparaat, groen buiten).'
'rood kadertje = apparaat (KK kookgroep, WM wasmachine, DR droger, WP warmtepomp, OV oven, VW vaatwasser) met groepnummer, V = vloer-wcd; gestippeld = leidingen (geel licht, bruin schakeldraad, rood wcd/apparaat, groen buiten).'
];
function drawSheet(doc, el, sheetOpts) {
@@ -150,5 +161,117 @@ window.ElektraPdf = (() => {
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
return { RUN_COLORS, drawPlan, drawSheet, PLAN_LEGEND };
// ------------------------------------------------------------
// Elektra op de bladen per onderdeel. Elk geeft een overlay-functie voor
// WallPdf/FloorPdf (opts.overlay) of null als er niets te tekenen is.
// ------------------------------------------------------------
const lineCol = (doc, type) => doc.setDrawColor(...runColor(type));
const clip = (p, q, b) => ElektraModel.clipPlan(p, q, b);
// Leidingstukken van alle buizen waarvoor test(p, q) geldt.
const conduitSegs = (el, test) => {
const out = [];
el.runs.forEach(r => {
if (!ElektraModel.cableInfo(r.type).conduit) return;
r.path.slice(1).forEach((q, i) => { const p = r.path[i]; if (test(p, q)) out.push({ type: r.type, p, q }); });
});
return out;
};
const labelBox = (doc, x, y, text, color) => {
const w = 1.6 + text.length * 1.05;
doc.setDrawColor(...color);
doc.setFillColor(255, 255, 255);
doc.setLineWidth(0.2);
doc.rect(x - w / 2, y - 1.2, w, 2.4, 'FD');
doc.setTextColor(...color);
doc.setFontSize(5);
doc.text(text, x, y + 0.7, { align: 'center' });
};
// Wand (vooraanzicht, wand-x zoals de openingen): buizen in de wand,
// schakelaars, wcd's, apparaten en de meterkast op dit wandstuk.
function wallOverlay(el, piece) {
if (!el || !el.enabled) return null;
const on = (p) => p.wall && p.wall.piece === piece;
const segs = conduitSegs(el, (p, q) => on(p) && on(q));
const combos = el.combos.filter(c => c.face.piece === piece), wcds = el.wcds.filter(w => w.face.piece === piece);
const apps = el.apps.filter(a => a.face.piece === piece), mk = el.meterkast && el.meterkast.face.piece === piece ? el.meterkast : null;
if (!segs.length && !combos.length && !wcds.length && !apps.length && !mk) return null;
const BLACK = PdfSheet.COLORS.BLACK;
return (doc, pu, py) => {
doc.setLineWidth(0.35);
segs.forEach(sg => {
const a = ElektraModel.wallCoords(sg.p.wall, sg.p), b = ElektraModel.wallCoords(sg.q.wall, sg.q);
lineCol(doc, sg.type);
doc.line(pu(a.u), py(a.y), pu(b.u), py(b.y));
});
if (mk) {
doc.setDrawColor(...BLACK);
doc.setFillColor(110, 110, 110);
doc.setLineWidth(0.25);
doc.rect(pu(mk.u - mk.w / 2), py(mk.y + mk.h), (pu(mk.u + mk.w / 2) - pu(mk.u - mk.w / 2)), py(mk.y) - py(mk.y + mk.h), 'FD');
doc.setTextColor(255, 255, 255);
doc.setFontSize(6);
doc.text('MK', pu(mk.u), py(mk.y + mk.h / 2) + 1, { align: 'center' });
}
combos.forEach(c => labelBox(doc, pu(c.u), py(c.y), c.switches.map(x => x.letter).join(' '), BLACK));
wcds.forEach(w => labelBox(doc, pu(w.u), py(w.y), `wcd ${w.group}`, RUN_COLORS.wcd));
apps.forEach(a => labelBox(doc, pu(a.u), py(a.y), `${a.code} ${a.group}`, RUN_COLORS.wcd));
};
}
// Vloer van een unit (bovenaanzicht vanaf linksvoor): buizen in de vloer + vloer-wcd's.
function floorOverlay(el, unit) {
if (!el || !el.enabled) return null;
const r = unit.rect, b = { x0: r.x0, x1: r.x1, z0: r.z0, z1: r.z1 };
const segs = conduitSegs(el, (p, q) => p.floor && q.floor).map(sg => Object.assign(sg, { c: clip(sg.p, sg.q, b) })).filter(sg => sg.c);
const vw = el.floorWcds.filter(w => w.unitId === unit.id);
if (!segs.length && !vw.length) return null;
return (doc, px, py) => {
doc.setLineWidth(0.35);
segs.forEach(sg => {
const [a, c] = sg.c;
if (Math.hypot(c.x - a.x, c.z - a.z) < 1) return;
lineCol(doc, sg.type);
doc.line(px(a.x - r.x0), py(a.z - r.z0), px(c.x - r.x0), py(c.z - r.z0));
});
vw.forEach(w => labelBox(doc, px(w.x - r.x0), py(w.z - r.z0), `vloer-wcd ${w.group}`, RUN_COLORS.wcd));
};
}
// Dakvlak (dek zoals de dakbladen, zelfde omrekening als de doorvoeren in
// RoofModel.computeUnitRoof): buizen boven het plafond, verdeeldozen en
// binnenspots (met letter) in dit vlak.
function roofOverlay(el, unit, pl) {
if (!el || !el.enabled) return null;
const dm = ElektraModel.roofDeckMap(unit, pl), map = dm.map, b = dm.bounds;
const inside = (p) => p.x >= b.x0 - 0.5 && p.x <= b.x1 + 0.5 && p.z >= b.z0 - 0.5 && p.z <= b.z1 + 0.5;
const segs = conduitSegs(el, (p, q) => p.ceil && q.ceil).map(sg => Object.assign(sg, { c: clip(sg.p, sg.q, b) })).filter(sg => sg.c);
const boxes = el.boxes.filter(bx => inside(bx));
const lamps = [];
el.switches.forEach(sw => sw.lamps.forEach(l => { if (l.kind === 'binnenspot' && inside(l)) lamps.push({ l, letter: sw.letter }); }));
if (!segs.length && !boxes.length && !lamps.length) return null;
const BLACK = PdfSheet.COLORS.BLACK;
return (doc, px, py) => {
doc.setLineWidth(0.35);
segs.forEach(sg => {
const a = map(sg.c[0]), c = map(sg.c[1]);
if (Math.hypot(c.sx - a.sx, c.sy - a.sy) < 1) return;
lineCol(doc, sg.type);
doc.line(px(a.sx), py(a.sy), px(c.sx), py(c.sy));
});
doc.setLineWidth(0.2);
boxes.forEach(bx => { const m = map(bx); lineCol(doc, 'schakel'); doc.rect(px(m.sx) - 0.9, py(m.sy) - 0.9, 1.8, 1.8, 'S'); });
doc.setFontSize(4);
lamps.forEach(({ l, letter }) => {
const m = map(l);
doc.setDrawColor(...BLACK);
doc.setFillColor(255, 233, 168);
doc.circle(px(m.sx), py(m.sy), 0.8, 'FD');
doc.setTextColor(...BLACK);
doc.text(letter, px(m.sx) + 1.1, py(m.sy) - 0.8);
});
};
}
return { RUN_COLORS, drawPlan, drawSheet, PLAN_LEGEND, wallOverlay, floorOverlay, roofOverlay };
})();