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:
Machiel
2026-10-11 00:34:27 +02:00
co-authored by Claude Opus 5.5
parent b5510d56c7
commit ceda0b554d
45 changed files with 4117 additions and 3700 deletions
+115 -201
View File
@@ -4,84 +4,26 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// elektra/elektra-model.js — ElektraModel: ENIGE bron van de elektra-
// installatie van een woning (eerste versie, automatisch). Geen DOM.
//
// Woning-breed (settings.elektra). Alles wordt afgeleid uit de woning:
// - Meterkast (3×25 A, 3-fase) als kast aan de binnenzijde van een wand
// in de unit met de voordeur, zo dicht mogelijk bij de voordeur, vrij van
// openingen (incl. raamwerk) en van de schakelaars.
// - Schakelaarcombinatie naast de voordeur (binnenzijde, 1500 mm), aan de
// slotzijde (deur draait links, KozijnModel; anders de scharnierzijde):
// A = binnenlicht van die unit, B = buitenlicht (boeideel-spots en
// gevellampen). Elke andere unit krijgt een schakelaar naast de deur
// waardoor je hem binnenkomt (naadwand, anders een buitendeur), C, D, ...
// Elke lamp/spot krijgt de letter van zijn schakelaar (p.letter).
// - Wandcontactdozen (dubbel, 400 mm): wcdPerUnit per unit, zo ver mogelijk
// uit elkaar langs de binnenzijde van de wanden.
// - Apparaten (settings.apparaten, APPLIANCES) met elk een eigen groep:
// kookgroep (perilex, 2-fase, 5×2,5), wasmachine, droger, warmtepomp,
// optioneel oven en vaatwasser. Keukenapparaten bij elkaar in de grootste
// andere unit, de rest naast de meterkast (nog geen keuken in het model).
// - Handmatig verplaatsen (settings.place[key] = { element, side, u, y }):
// key 'mk' (meterkast), 'sw:<unit>' (schakelaars), 'wcd:<unit>:<nr>',
// 'app:<apparaat>'. element = wandstuk, side = -1 (kamerzijde; bij een
// binnenwand ±1), u = hart langs de wand (wand-x), y = hoogte (optioneel).
// 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
// schakeldoos). Fasen L1/L2/L3 om en om; aardlekschakelaars (30 mA,
// max. 4 groepen elk, minstens 2) om en om, zodat licht en wcd van één
// unit niet op dezelfde aardlek zitten.
//
// Leidingen (HSB): binnen VD-draad in flexibele installatiebuis, via de
// stijlen (zelfde wand) of via het plafond (andere wand/plafondpunt): eerst
// verticaal in de wand naar het plafond, dan haaks (x, dan z) naar het
// volgende punt. Per schakeldoos en groep een luchtdichte verdeeldoos in het
// plafond erboven (groepen niet gemengd in één verdeeldoos); vandaar de schakeldraden omlaag en de lampen doorgelust volgens
// een minimale opspannende boom (kortste totale lengte). Buitenverlichting in
// 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.
// ============================================================
window.ElektraModel = (() => {
const DEFAULT_SETTINGS = {
enabled: true,
meterkast: { w: 600, h: 900, d: 250, y: 1000 }, // mm; y = onderkant boven vloer
switchHeight: 1500, // hart schakelaar boven vloer
wcdHeight: 400, // hart wcd boven vloer
wcdPerUnit: 2, // dubbele wcd per unit
slack: 10, // % extra op elke leiding
reserve: 300, // mm per leiding (lussen in de dozen)
meterkastReserve: 500, // mm extra per leiding vanuit de meterkast
meterkast: { w: 600, h: 900, d: 250, y: 1000 }, // y = onderkant boven vloer
switchHeight: 1500,
wcdHeight: 400,
wcdPerUnit: 2,
slack: 10, // %
reserve: 300,
meterkastReserve: 500,
apparaten: { koken: true, wasmachine: true, droger: true, warmtepomp: true, oven: false, vaatwasser: false },
place: {}, // handmatige plekken (zie boven)
vloerWcd: [] // vloer-wcd's (zie boven)
place: {},
vloerWcd: [],
wcdExtra: {},
removed: []
};
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 CLEAR = 50;
const FLOOR_DEPTH = 100;
// Codes van twee letters: geen verwarring met de schakelaarletters.
const APPLIANCES = {
koken: { label: 'Kookgroep (perilex)', code: 'KK', breaker: '2× B16', poles: 2, cable: 'koken', height: 600, zone: 'keuken', material: 'Perilex-wandcontactdoos (2-fase)' },
oven: { label: 'Oven', code: 'OV', breaker: 'B16', cable: 'wcd', height: 600, zone: 'keuken', material: 'Wandcontactdoos enkel met randaarde' },
@@ -90,14 +32,12 @@ window.ElektraModel = (() => {
droger: { label: 'Droger', code: 'DR', breaker: 'B16', cable: 'wcd', height: 1100, zone: 'techniek', material: 'Wandcontactdoos enkel met randaarde' },
warmtepomp: { label: 'Warmtepomp', code: 'WP', breaker: 'B16', cable: 'wcd', height: 1200, zone: 'techniek', material: 'Werkschakelaar 2-polig (warmtepomp)' }
};
const BOX_PITCH = 71; // h.o.h. inbouwdozen in een combinatie
const PLATE = 80; // afdekraam (vierkant)
const WCD_W = 150; // afdekraam dubbele wcd
const DOOR_GAP = 100; // kozijnstijl -> hart eerste doos (bovenop de stijl)
const VD_OFFSET = 150; // verdeeldoos: zoveel de kamer in vanaf het wandvlak
const BOX_PITCH = 71;
const PLATE = 80;
const WCD_W = 150;
const DOOR_GAP = 100;
const VD_OFFSET = 150;
// Leidingtypen. cores: aders (kleur via CORE_COLORS); conduit: buis-Ø (mm),
// 0 = kabel zonder buis.
const CORE_COLORS = { L: 'bruin', L2: 'zwart', L3: 'grijs', N: 'blauw', PE: 'geel/groen', S: 'zwart' };
const CABLES = {
licht: { label: 'VD 3×1,5 mm² in buis Ø16', use: 'Verlichting: voeding en doorlus', mm2: 1.5, cores: ['L', 'N', 'PE'], conduit: 16 },
@@ -105,7 +45,6 @@ window.ElektraModel = (() => {
koken: { label: 'VD 5×2,5 mm² in buis Ø25', use: 'Kookgroep (perilex, 2-fase)', mm2: 2.5, cores: ['L', 'L2', 'L3', 'N', 'PE'], conduit: 25 },
buiten: { label: 'XMvK 3×1,5 mm²', use: 'Buitenverlichting (boeideel/gevel)', mm2: 1.5, cores: ['L', 'N', 'PE'], conduit: 0 }
};
// Schakeldraden verdeeldoos -> schakeldoos: fase + één per schakelaar.
function cableInfo(key) {
if (CABLES[key]) return CABLES[key];
const m = /^schakel(\d)$/.exec(key);
@@ -119,6 +58,8 @@ 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.wcdExtra = Object.assign({}, i.wcdExtra || {});
s.removed = Array.isArray(i.removed) ? i.removed.slice() : [];
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).
@@ -130,12 +71,7 @@ window.ElektraModel = (() => {
return s;
}
// ------------------------------------------------------------
// Wandvlakken (binnenzijde) waarop iets gemonteerd kan worden.
// ------------------------------------------------------------
// Zijde van een wandstuk (side -1 = lokaal -Z: bij buitenwand en
// naadwanddeel de kamerkant; binnenwand: beide zijden), begrensd tot de
// binnenruimte van de unit.
// side -1 = lokaal -Z = kamerkant (buitenwand, naadwanddeel); binnenwand: beide zijden.
function faceOf(p, inner, u, side) {
const pl = p.place, W = pl.width, lz = side * pl.thickness / 2;
const a = UnitModel.localToWorld(pl, -W / 2, lz), b = UnitModel.localToWorld(pl, W / 2, lz);
@@ -145,10 +81,10 @@ window.ElektraModel = (() => {
return {
piece: p, unit: u, a, b, axis, W, lz,
u0: Math.max(0, Math.min(ua, ub)), u1: Math.min(W, Math.max(ua, ub)),
n: { x: side * Math.sin(pl.rotY), z: side * Math.cos(pl.rotY) }, // naar de kamer
n: { x: side * Math.sin(pl.rotY), z: side * Math.cos(pl.rotY) },
side,
sw: p.config.studWidth || 45,
reserved: [] // [{ a, b, y0, y1 }] al bezet (meterkast, schakelaars, wcd)
reserved: []
};
}
function facesOf(u, interior) {
@@ -162,15 +98,12 @@ window.ElektraModel = (() => {
.forEach(p => list.push(faceOf(p, inner, u, -1), faceOf(p, inner, u, 1)));
return list.filter(f => f.u1 - f.u0 > 200);
}
// Punt op een wandvlak: u langs de wand (0..W), y boven de vloer, off de
// kamer in (negatief = de wand in).
// off: de kamer in (negatief = de wand in).
function facePoint(f, u, y, off) {
const q = UnitModel.localToWorld(f.piece.place, u - f.W / 2, f.lz + f.side * (off || 0));
return { x: q.x, y: f.piece.place.floorTop + y, z: q.z };
}
const uAt = (f, q) => (q[f.axis] - f.a[f.axis]) / (f.b[f.axis] - f.a[f.axis]) * f.W;
// Vrije stukken van een vlak tussen hoogte y0..y1: openingen (incl.
// raamwerk + margin) en wat al bezet is eraf, edge vrij van de uiteinden.
function freeOn(f, y0, y1, margin, edge) {
const blocked = [];
f.piece.config.openings.forEach(op => {
@@ -179,8 +112,6 @@ window.ElektraModel = (() => {
f.reserved.forEach(r => { if (r.y0 < y1 && r.y1 > y0) blocked.push([r.a, r.b]); });
return LightingModel.freeIntervals(f.u0 + edge, f.u1 - edge, blocked);
}
// Dichtstbijzijnde vrije plek (breedte w) op een van de vlakken bij target
// (wereld {x, z}). Geeft { f, u, d } of null.
function nearestFree(faces, target, w, y0, y1, margin, edge) {
let best = null;
faces.forEach(f => freeOn(f, y0, y1, margin, edge).forEach(([a, b]) => {
@@ -193,9 +124,7 @@ window.ElektraModel = (() => {
return best;
}
// Schakelaarcombinatie van n dozen naast opening op op vlak f: eerst de
// slotzijde (rechts van de opening in wand-x), dan de scharnierzijde.
// Geeft de doos-centra (u, dichtst bij de deur eerst) of null.
// Eerst de slotzijde (rechts van de opening in wand-x), dan de scharnierzijde.
function besideDoor(f, op, n, height) {
const width = PLATE + (n - 1) * BOX_PITCH;
const free = freeOn(f, height - PLATE / 2, height + PLATE / 2, 0, f.sw);
@@ -207,9 +136,6 @@ window.ElektraModel = (() => {
return null;
}
// ------------------------------------------------------------
// Routes. Knooppunt: { x, y, z, mount: 'wand'|'plafond'|'buiten', face?, mk? }.
// ------------------------------------------------------------
function makeRouter(units) {
const unitAt = (x, z) => units.find(u => x >= u.rect.x0 - 1 && x <= u.rect.x1 + 1 && z >= u.rect.z0 - 1 && z <= u.rect.z1 + 1) || null;
const ceilAt = (x, z) => {
@@ -223,17 +149,14 @@ window.ElektraModel = (() => {
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, 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, 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
if (a.mount === 'vloer' || b.mount === '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.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)];
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);
const ya = top(a), yb = top(b), dropB = Math.abs(yb - b.y) > 1;
@@ -245,9 +168,7 @@ window.ElektraModel = (() => {
return pts;
}
const length = (pts) => pts.slice(1).reduce((s, q, i) => s + Math.hypot(q.x - pts[i].x, q.y - pts[i].y, q.z - pts[i].z), 0);
// Snelle schatting (voor de boom): haaks + hoogteverschil.
const cost = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.z - b.z) + Math.abs(a.y - b.y);
// Aantal keer dat het pad (in plattegrond) van de ene unit naar een andere gaat.
function crossings(pts) {
let last = null, n = 0;
pts.slice(1).forEach((q, i) => {
@@ -264,12 +185,7 @@ window.ElektraModel = (() => {
return { unitAt, ceilAt, floorAt, path, length, cost, crossings };
}
// ------------------------------------------------------------
// Alles voor de hele woning. units/interior = HouseModel-parts,
// lighting = parts.lighting (lamps, indoor), roofEdge = parts.roofEdge.
// Zet p.letter op elke lamp/spot. Geeft het elektra-resultaat (zie onder).
// ------------------------------------------------------------
function compute(settings, units, interior, lighting, roofEdge) {
function compute(settings, units, interior, lighting, roofEdge, rooms) {
const s = normalizeSettings(settings);
const indoor = lighting.indoor || [], gevel = lighting.lamps || [], spots = (roofEdge && roofEdge.spots) || [];
indoor.concat(gevel, spots).forEach(p => { delete p.letter; });
@@ -284,7 +200,6 @@ window.ElektraModel = (() => {
const faceOfPiece = (u, p) => faces[u.id].find(f => f.piece === p && f.side === -1);
const allFaces = out.faces = units.reduce((a, u) => a.concat(faces[u.id]), []);
// 1. Voordeur (anders de eerste buitendeur, anders de eerste unit).
let door = null;
['voordeur', 'deur'].some(type => units.some(u => u.walls.some(w => w.pieces.some(p => {
if (p.baseY) return false;
@@ -302,15 +217,20 @@ window.ElektraModel = (() => {
const mk = s.meterkast;
const apps = Object.keys(APPLIANCES).filter(k => s.apparaten[k]);
const num = (v, def) => typeof v === 'number' && isFinite(v) ? v : def;
const removed = (key) => s.removed.includes(key);
const wcdCount = (uid) => s.wcdPerUnit + Math.max(0, Math.round(num(s.wcdExtra[uid], 0)));
// Handmatige plekken eerst vastleggen (de automatische wijken uit).
const comboWidth = (n) => PLATE + (n - 1) * BOX_PITCH;
const widthOf = (key) => {
if (key === 'mk') return mk.w;
let m = /^sw:(.+)$/.exec(key);
if (m) return units.some(u => u.id === m[1]) ? comboWidth(m[1] === mainUnit.id ? mainFuncs.length : 1) : 0;
if (m) {
if (removed(key)) return 0;
if (m[1] === mainUnit.id) return comboWidth(mainFuncs.length);
return units.some(u => u.id === m[1]) ? (rooms ? 0 : comboWidth(1)) : comboWidth(1);
}
m = /^wcd:(.+):(\d+)$/.exec(key);
if (m) return units.some(u => u.id === m[1]) && Number(m[2]) <= s.wcdPerUnit ? WCD_W : 0;
if (m) return units.some(u => u.id === m[1]) && Number(m[2]) <= wcdCount(m[1]) && !removed(key) ? WCD_W : 0;
m = /^app:(.+)$/.exec(key);
return m && apps.includes(m[1]) ? PLATE : 0;
};
@@ -326,12 +246,11 @@ window.ElektraModel = (() => {
});
const centersAt = (u, n) => Array.from({ length: n }, (_, i) => u - comboWidth(n) / 2 + PLATE / 2 + i * BOX_PITCH);
// 2. Schakelaars bij de voordeur (A binnen, B buiten).
const addCombo = (unit, f, us, funcs, y, manual) => {
const addCombo = (unit, f, us, funcs, y, manual, key, name) => {
const yy = y === null || y === undefined ? s.switchHeight : y;
const combo = { id: out.combos.length + 1, key: 'sw:' + unit.id, manual: !!manual, unitId: unit.id, face: f, us, y: yy, rotY: f.piece.place.rotY, switches: [] };
const combo = { id: out.combos.length + 1, key: key || 'sw:' + unit.id, manual: !!manual, unitId: unit.id, face: f, us, y: yy, rotY: f.piece.place.rotY, switches: [] };
funcs.forEach((func, i) => {
const sw = { letter: nextLetter(), func, unitId: unit.id, combo, u: us[i], lamps: [], label: func === 'buiten' ? 'Buitenlicht' : `Binnenlicht unit ${unit.id}` };
const sw = { letter: nextLetter(), func, unitId: unit.id, combo, u: us[i], lamps: [], label: func === 'buiten' ? 'Buitenlicht' : `Binnenlicht ${name || 'unit ' + unit.id}` };
sw.pos = facePoint(f, us[i], yy, 0);
combo.switches.push(sw);
out.switches.push(sw);
@@ -345,8 +264,8 @@ window.ElektraModel = (() => {
out.combos.push(combo);
return combo;
};
const fxMain = fixed['sw:' + mainUnit.id];
if (fxMain) addCombo(mainUnit, fxMain.f, centersAt(fxMain.u, mainFuncs.length), mainFuncs, fxMain.y, true);
const fxMain = fixed['sw:' + mainUnit.id], mainOff = removed('sw:' + mainUnit.id);
if (mainOff) { } else if (fxMain) addCombo(mainUnit, fxMain.f, centersAt(fxMain.u, mainFuncs.length), mainFuncs, fxMain.y, true);
else if (door) {
const us = besideDoor(door.face, door.op, mainFuncs.length, s.switchHeight);
if (us) addCombo(mainUnit, door.face, us, mainFuncs);
@@ -357,13 +276,12 @@ window.ElektraModel = (() => {
if (nf) addCombo(mainUnit, nf.f, mainFuncs.map((_, i) => nf.u - w / 2 + PLATE / 2 + i * BOX_PITCH), mainFuncs);
}
}
if (!out.combos.length) {
if (!out.combos.length && !mainOff) {
const f = faces[mainUnit.id][0];
if (f) addCombo(mainUnit, f, mainFuncs.map((_, i) => (f.u0 + f.u1) / 2 + i * BOX_PITCH), mainFuncs);
}
const entry = out.combos[0] ? out.combos[0].pos : { x: mainUnit.rect.cx, z: mainUnit.rect.cz };
// 3. Meterkast: zo dicht mogelijk bij de voordeur, eerst in die unit.
const target = door ? facePoint(door.face, door.op.x + door.op.w / 2, 0, 0) : entry;
const fxMk = fixed.mk;
const mkY = fxMk && fxMk.y !== null ? fxMk.y : mk.y;
@@ -379,16 +297,59 @@ window.ElektraModel = (() => {
};
} else out.warnings.push('Geen vrije wand voor de meterkast gevonden.');
// 4. Schakelaar per andere unit, naast de deur waardoor je hem binnenkomt.
units.forEach(u => {
if (u === mainUnit) return;
const isDoorOp = (op) => op.type === 'deur' || op.type === 'voordeur' || (op.fill && (op.fill.type === 'deur' || op.fill.type === 'schuifdeur'));
const regionOf = (pt) => rooms ? rooms.regionAt(pt.x, pt.z) : null;
const roomSw = new Map();
if (rooms) {
const entryPt = door ? facePoint(door.face, door.op.x + door.op.w / 2, 0, 150) : entry;
const entryReg = regionOf(entryPt);
const mainSw = out.switches.find(x => x.func === 'binnen');
if (entryReg && mainSw) {
roomSw.set(entryReg.idx, mainSw);
const r0 = rooms.rooms.find(r => r.key === entryReg.idx);
if (r0) mainSw.label = `Binnenlicht ${r0.name.toLowerCase()}`;
}
const list = rooms.rooms.concat(rooms.free).filter(r => r.areaM2 >= 0.6 && !roomSw.has(r.key))
.sort((a, b) => Math.hypot(a.label.x - entryPt.x, a.label.z - entryPt.z) - Math.hypot(b.label.x - entryPt.x, b.label.z - entryPt.z));
list.forEach(r => {
const key = r.id !== undefined ? 'sw:r' + r.id : `sw:g${Math.round(r.label.x / 100)}_${Math.round(r.label.z / 100)}`;
const name = r.name ? r.name.toLowerCase() : 'ruimte';
if (removed(key)) return;
const inR = (f, uu) => { const g = regionOf(facePoint(f, uu, 0, 150)); return g && g.idx === r.key; };
const fx = fixed[key];
let combo = null;
if (fx) combo = addCombo(fx.f.unit, fx.f, centersAt(fx.u, 1), ['binnen'], fx.y, true, key, name);
else {
const cands = [];
allFaces.forEach(f => f.piece.config.openings.forEach(op => {
if (!isDoorOp(op) || !inR(f, op.x + op.w / 2)) return;
const q = facePoint(f, op.x + op.w / 2, 0, 0);
cands.push({ f, op, d: Math.hypot(q.x - entryPt.x, q.z - entryPt.z) });
}));
cands.sort((a, b) => a.d - b.d);
for (const c of cands) {
const us = besideDoor(c.f, c.op, 1, s.switchHeight);
if (us) { combo = addCombo(c.f.unit, c.f, us, ['binnen'], null, false, key, name); break; }
}
if (!combo) {
const own = allFaces.filter(f => inR(f, (f.u0 + f.u1) / 2));
const nf = nearestFree(own, r.label, PLATE, s.switchHeight - PLATE / 2, s.switchHeight + PLATE / 2, 0, 100);
if (nf && inR(nf.f, nf.u)) combo = addCombo(nf.f.unit, nf.f, [nf.u], ['binnen'], null, false, key, name);
}
}
if (combo) roomSw.set(r.key, combo.switches[0]);
else out.warnings.push(`${r.name || 'Ruimte'}: geen plek voor een schakelaar; licht op een andere schakelaar.`);
});
}
if (!rooms) units.forEach(u => {
if (u === mainUnit || removed('sw:' + u.id)) return;
const fx = fixed['sw:' + u.id];
if (fx) { addCombo(u, fx.f, centersAt(fx.u, 1), ['binnen'], fx.y, true); return; }
const cands = [];
faces[u.id].forEach(f => f.piece.config.openings.forEach(op => {
if (op.type !== 'deur' && op.type !== 'voordeur') return;
const q = facePoint(f, op.x + op.w / 2, 0, 0);
cands.push({ f, op, d: Math.hypot(q.x - entry.x, q.z - entry.z) + (f.piece.seamUnit ? 0 : 5000) }); // naadwand-deur eerst
cands.push({ f, op, d: Math.hypot(q.x - entry.x, q.z - entry.z) + (f.piece.seamUnit ? 0 : 5000) });
}));
cands.sort((a, b) => a.d - b.d);
for (const c of cands) {
@@ -398,22 +359,24 @@ window.ElektraModel = (() => {
out.warnings.push(`Unit ${u.id}: geen deur met ruimte voor een schakelaar; binnenlicht op schakelaar ${out.switches[0] ? out.switches[0].letter : '-'}.`);
});
// 5. Lampen aan schakelaars koppelen (letter).
const swOf = (unitId, func) => out.switches.find(x => x.unitId === unitId && x.func === func) || out.switches.find(x => x.func === func) || out.switches[0];
indoor.forEach(p => { const sw = swOf(p.unitId, 'binnen'); if (sw) { p.letter = sw.letter; sw.lamps.push({ x: p.x, y: p.y, z: p.z, mount: 'plafond', kind: 'binnenspot', ref: p }); } });
const roomSwOf = (p) => { const g = regionOf(p); return g ? roomSw.get(g.idx) : null; };
indoor.forEach(p => { const sw = roomSwOf(p) || swOf(p.unitId, 'binnen'); if (sw) { p.letter = sw.letter; sw.lamps.push({ x: p.x, y: p.y, z: p.z, mount: 'plafond', kind: 'binnenspot', ref: p }); } });
gevel.forEach(p => { const sw = swOf(mainUnit.id, 'buiten'); if (sw) { p.letter = sw.letter; sw.lamps.push({ x: p.x + p.normal.x * 60, y: p.y, z: p.z + p.normal.z * 60, mount: 'buiten', kind: 'gevellamp', ref: p }); } });
spots.forEach(p => { const sw = swOf(mainUnit.id, 'buiten'); if (sw) { p.letter = sw.letter; sw.lamps.push({ x: p.x, y: p.y, z: p.z, mount: 'buiten', kind: 'boeideelspot', ref: p }); } });
// 6. Wandcontactdozen per unit (zo ver mogelijk uit elkaar).
units.forEach(u => {
const y0 = s.wcdHeight - 60, y1 = s.wcdHeight + 60, cands = [];
faces[u.id].forEach(f => freeOn(f, y0, y1, 100, 150).forEach(([a, b]) => {
if (b - a < WCD_W + 20) return;
(b - a > 1200 ? [a + 300, (a + b) / 2, b - 300] : [(a + b) / 2]).forEach(uu => cands.push({ f, u: uu, len: b - a, p: facePoint(f, uu, s.wcdHeight, 0) }));
}));
const chosen = [], freeKeys = [];
for (let i = 1; i <= s.wcdPerUnit; i++) {
const chosen = [], freeKeys = [], nWcd = wcdCount(u.id);
let want = 0;
for (let i = 1; i <= nWcd; i++) {
const key = `wcd:${u.id}:${i}`, fx = fixed[key];
if (removed(key)) continue;
want++;
if (!fx) { freeKeys.push(key); continue; }
const y = fx.y !== null ? fx.y : s.wcdHeight;
chosen.push({ f: fx.f, u: fx.u, y, p: facePoint(fx.f, fx.u, y, 0), key, manual: true });
@@ -429,14 +392,12 @@ window.ElektraModel = (() => {
chosen.push(Object.assign({}, best, { key: freeKeys.shift(), y: s.wcdHeight }));
best.f.reserved.push({ a: best.u - WCD_W / 2 - 75, b: best.u + WCD_W / 2 + 75, y0: 0, y1: s.wcdHeight + 200 });
}
if (chosen.length < s.wcdPerUnit) out.warnings.push(`Unit ${u.id}: ruimte voor ${chosen.length} van ${s.wcdPerUnit} wcd's.`);
chosen.sort((a, b) => a.key < b.key ? -1 : 1).forEach(c => out.wcds.push({
if (chosen.length < want) out.warnings.push(`Unit ${u.id}: ruimte voor ${chosen.length} van ${want} wcd's.`);
chosen.sort((a, b) => Number(a.key.split(':')[2]) - Number(b.key.split(':')[2])).forEach(c => out.wcds.push({
key: c.key, manual: !!c.manual, unitId: u.id, face: c.f, u: c.u, y: c.y, rotY: c.f.piece.place.rotY, pos: c.p, center: facePoint(c.f, c.u, c.y, 6)
}));
});
// 6b. Apparaten: keuken bij elkaar in de grootste andere unit, de rest
// naast de meterkast.
const others = units.filter(u => u !== mainUnit);
const kitchen = (others.length ? others : units).reduce((a, b) => a.rect.W * a.rect.D >= b.rect.W * b.rect.D ? a : b);
const mkUnit = out.meterkast ? units.find(u => u.id === out.meterkast.unitId) : mainUnit;
@@ -459,7 +420,6 @@ 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;
@@ -468,7 +428,6 @@ window.ElektraModel = (() => {
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) });
@@ -493,8 +452,6 @@ window.ElektraModel = (() => {
// Buitenlicht zit in de schakeldoos van het binnenlicht: zelfde aardlek.
const gOut = out.groups.find(g => g.kind === 'buiten'), gIn = gOut && groupOf('licht', gOut.unitId);
if (gOut && gIn) gOut.rcd = gIn.rcd;
// Max. 4 groepen per aardlek: anders een andere groep (niet die twee)
// naar de aardlek met de minste groepen.
for (let guard = 0; guard < 20; guard++) {
const cnt = Array.from({ length: nRcd }, (_, i) => out.groups.filter(g => g.rcd === i + 1).length);
const over = cnt.findIndex(c => c > 4);
@@ -508,7 +465,6 @@ window.ElektraModel = (() => {
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.
const mkNode = out.meterkast ? Object.assign(facePoint(out.meterkast.face, out.meterkast.u, mkY + mk.h, 0), { mount: 'wand', face: out.meterkast.face, mk: true, name: 'MK' }) : null;
const mkLow = out.meterkast ? Object.assign(facePoint(out.meterkast.face, out.meterkast.u, mkY, 0), { mount: 'wand', face: out.meterkast.face, mk: true, name: 'MK' }) : null;
const addRun = (type, a, b, group, letter) => {
@@ -516,7 +472,6 @@ window.ElektraModel = (() => {
const raw = R.length(pts);
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) => {
const done = [root], rest = nodes.slice();
while (rest.length) {
@@ -527,7 +482,6 @@ window.ElektraModel = (() => {
done.push(n);
}
};
// Per schakeldoos één verdeeldoos per groep (naast elkaar langs de wand).
for (const combo of out.combos) {
const byGroup = [];
combo.switches.forEach(sw => {
@@ -546,8 +500,7 @@ window.ElektraModel = (() => {
vd.y = R.ceilAt(vd.x, vd.z);
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
// Naar de eigen inbouwdoos (of het midden ervan) van deze groep.
if (mkNode) addRun('licht', mkNode, vd, e.group);
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);
@@ -560,10 +513,8 @@ window.ElektraModel = (() => {
.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 => {
@@ -574,7 +525,6 @@ window.ElektraModel = (() => {
});
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; });
out.cables = Object.keys(byType).sort().map(k => ({ key: k, label: cableInfo(k).label, use: cableInfo(k).use, meters: byType[k] / 1000 }));
@@ -601,7 +551,6 @@ window.ElektraModel = (() => {
if ((g.kind === 'licht' || g.kind === 'buiten') && g.points > 50) out.warnings.push(`Groep ${g.nr} (${g.label}): ${g.points} lichtpunten; controleer het vermogen (LED).`);
});
// 10. Materiaal (aantallen).
const crossBy = {};
out.runs.forEach(r => { if (r.crossings) { const n = cableInfo(r.type).cores.length; crossBy[n] = (crossBy[n] || 0) + r.crossings; } });
const nIndoor = out.switches.reduce((a, x) => a + x.lamps.filter(l => l.kind === 'binnenspot').length, 0);
@@ -638,8 +587,6 @@ window.ElektraModel = (() => {
add('Luchtdichte kabeldoorvoer naar buiten', out.runs.filter(r => r.type === 'buiten' && r.from.startsWith('VD') && r.to !== '' && !r.to.startsWith('VD')).length);
Object.keys(crossBy).forEach(n => add(`Steekverbinder module-naad ${n}-polig (set m/v)`, crossBy[n]));
// 11. Verplaatsbare onderdelen (plattegrond: slepen, selecteren).
// marker = plek van het symbool in de plattegrond (iets de kamer in).
const item = (key, kind, label, f, u, y, manual, off, unitId) => out.items.push({
key, kind, label, unitId: unitId || f.unit.id, element: f.piece.element, side: f.side, u, y, manual: !!manual, marker: facePoint(f, u, 0, off)
});
@@ -655,16 +602,7 @@ window.ElektraModel = (() => {
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.
// ------------------------------------------------------------
// Uitzondering: het laatste stuk naar de meterkast; daar komen de buizen 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) => {
@@ -690,8 +628,6 @@ window.ElektraModel = (() => {
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);
@@ -711,17 +647,16 @@ window.ElektraModel = (() => {
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
if (len >= 1 && axisOf(orig[k], orig[k + 1]) === dAx) break;
if (k + 1 === orig.length - 1) { out.splice(k + 1, 0, add(orig[k + 1], d, o)); break; }
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
placed.push(seg(r, out[0], out[1], radius(r)));
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;
@@ -731,8 +666,7 @@ window.ElektraModel = (() => {
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.
// Knikken tellen mee, behalve direct aan een doos/lamp 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++) {
@@ -742,7 +676,7 @@ window.ElektraModel = (() => {
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
i += 2;
done = true;
break;
}
@@ -753,7 +687,6 @@ window.ElektraModel = (() => {
});
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]));
@@ -767,31 +700,21 @@ window.ElektraModel = (() => {
return out;
}
// Dichtstbijzijnde wandvlak (kamerzijde) bij plattegrondpunt (x, z), voor
// het slepen: { element, side, u, x, z, n } of null.
function snapToWall(el, x, z) {
let best = null;
((el && el.faces) || []).forEach(f => {
const u = Math.max(f.u0, Math.min(f.u1, uAt(f, { x, z })));
const q = facePoint(f, u, 0, 0);
if ((x - q.x) * f.n.x + (z - q.z) * f.n.z < -(f.piece.place.thickness + 50)) return; // aan de andere kant van de wand
if ((x - q.x) * f.n.x + (z - q.z) * f.n.z < -(f.piece.place.thickness + 50)) return;
const d = Math.hypot(q.x - x, q.z - z);
if (!best || d < best.d) best = { d, element: f.piece.element, side: f.side, u: Math.round(u / 10) * 10, x: q.x, z: q.z, n: f.n };
});
return best;
}
// ------------------------------------------------------------
// 3D per bouwfase (BuildStages):
// elektra meterkast (blijft), installatiebuizen (zeskant, kleur per
// leidingtype) in de wanden en boven het plafond, luchtdichte
// inbouwdozen, verdeeldozen en spotdozen. Vanaf "beplating"
// zitten buizen en dozen achter de platen: niet meer gebouwd.
// afwerking afdekramen van schakelaars, wcd's en apparaten.
// Kabel zonder buis (XMvK buiten) en losse draad worden niet getekend.
// ------------------------------------------------------------
const WALL_IN = 35; // buis: zoveel achter het wandvlak (in de stijlruimte)
const CEIL_UP = 35; // buis: zoveel boven de onderkant plafond (op de plafondplaat)
const WALL_IN = 35;
const CEIL_UP = 35;
const CONDUIT_COLORS = { licht: 0xe0b020, wcd: 0xcc4a30, koken: 0x8a3a9a, schakel: 0x9a6a20 };
function createMaterials() {
const conduit = {};
@@ -827,11 +750,10 @@ 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
el.floorWcds.forEach(w => box(130, 8, 130, { x: w.x, y: w.y + 4, z: w.z }, 0, mats.plateMaterial));
}
if (!rough) return group;
// Installatiebuizen: zeskantige cilinders per padstuk + bolletje op elke knik.
const up = new THREE.Vector3(0, 1, 0);
const shift = (p) => ({
x: p.x - (p.wall ? p.wall.n.x * WALL_IN : 0), y: p.y + (p.ceil ? CEIL_UP : 0), z: p.z - (p.wall ? p.wall.n.z * WALL_IN : 0)
@@ -856,7 +778,6 @@ window.ElektraModel = (() => {
group.add(j);
});
});
// Luchtdichte inbouwdozen (schakelaars, wcd, apparaten): in de wand.
const wallBox = (f, u, y) => {
const c = facePoint(f, u, y, -22);
const m = new THREE.Mesh(new THREE.CylinderGeometry(0.034, 0.034, 0.045, 12), mats.boxMaterial);
@@ -867,8 +788,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.floorWcds.forEach(w => box(100, 80, 100, { x: w.x, y: w.y - 40, z: w.z }, 0, mats.boxMaterial));
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 => {
const m = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.045, 0.07, 12), mats.boxMaterial);
@@ -878,10 +798,8 @@ window.ElektraModel = (() => {
return group;
}
// 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]]) {
@@ -895,10 +813,7 @@ window.ElektraModel = (() => {
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.
// Zelfde omrekening als de doorvoeren in RoofModel.computeUnitRoof: gelijk houden.
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;
@@ -909,7 +824,6 @@ window.ElektraModel = (() => {
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();
@@ -920,7 +834,7 @@ window.ElektraModel = (() => {
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
if (!cableInfo(run.type).conduit) return run.type === 'buiten' && run.from.startsWith('VD') ? 1 : 0;
let n = 0;
run.path.slice(1).forEach((q, i) => {
const p = run.path[i];
@@ -930,7 +844,7 @@ window.ElektraModel = (() => {
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
if (p.floor !== q.floor) n += 1;
} 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;
}
@@ -944,7 +858,7 @@ window.ElektraModel = (() => {
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
} else if (vertical && (p.floor || q.floor) && !(p.wall || q.wall)) n += 1;
});
return n;
};
-31
View File
@@ -4,27 +4,15 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// elektra/elektra-pdf.js — ElektraPdf: elektra in de woning-PDF.
// drawPlan : meterkast, schakelaars (letter), wcd's (groep), lamp-
// letters en leidingen op de plattegrond (blad Plattegrond)
// drawSheet : eigen blad met groepenverklaring, schakelaars, strekkende
// meters per leidingtype, draad, buis en materiaal.
// Alle gegevens komen uit ElektraModel.compute (parts.elektra).
// Plain script, geladen na pdf-sheet.js en elektra-model.js.
// ============================================================
window.ElektraPdf = (() => {
const fmt = (n, d = 1) => Number(n).toFixed(d).replace('.', ',');
// Kleur per leidingtype (ook de legenda).
const RUN_COLORS = { licht: [215, 160, 0], wcd: [205, 55, 35], buiten: [40, 140, 60], schakel: [150, 95, 20] };
const runColor = (type) => RUN_COLORS[type] || RUN_COLORS.schakel;
// px/py: woning-mm -> blad-mm.
function drawPlan(doc, el, px, py) {
if (!el || !el.enabled) return;
const BLACK = PdfSheet.COLORS.BLACK;
// Leidingen (gestippeld, plattegrond-projectie van het pad).
doc.setLineWidth(0.15);
doc.setLineDashPattern([0.8, 0.6], 0);
el.runs.forEach(r => {
@@ -36,7 +24,6 @@ window.ElektraPdf = (() => {
});
doc.setLineDashPattern([], 0);
// Meterkast
const k = el.meterkast;
if (k) {
doc.setDrawColor(...BLACK);
@@ -50,12 +37,10 @@ window.ElektraPdf = (() => {
doc.text('MK', px(m.x), py(m.z) + 0.8, { align: 'center' });
}
// Verdeeldozen (plafond): klein vierkantje.
doc.setDrawColor(...runColor('schakel'));
doc.setLineWidth(0.15);
el.boxes.forEach(b => doc.rect(px(b.x) - 0.6, py(b.z) - 0.6, 1.2, 1.2, 'S'));
// Schakelaars: kadertje met de letters, iets de kamer in.
doc.setFontSize(5);
el.combos.forEach(c => {
const q = { x: c.pos.x + c.face.n.x * 260, z: c.pos.z + c.face.n.z * 260 };
@@ -69,7 +54,6 @@ window.ElektraPdf = (() => {
doc.text(text, px(q.x), py(q.z) + 0.7, { align: 'center' });
});
// Wcd: halve cirkel tegen de wand + groepnummer.
el.wcds.forEach(w => {
const q = { x: w.pos.x + w.face.n.x * 120, z: w.pos.z + w.face.n.z * 120 };
doc.setDrawColor(...RUN_COLORS.wcd);
@@ -81,7 +65,6 @@ window.ElektraPdf = (() => {
doc.text(String(w.group), px(q.x + w.face.n.x * 250), py(q.z + w.face.n.z * 250) + 0.6, { align: 'center' });
});
// Apparaten: kadertje met de code (KK, WM, ...) + groepnummer.
el.apps.forEach(a => {
const it = el.items.find(x => x.key === a.key), q = it.marker;
doc.setDrawColor(...RUN_COLORS.wcd);
@@ -94,7 +77,6 @@ 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);
@@ -105,12 +87,10 @@ window.ElektraPdf = (() => {
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);
el.switches.forEach(s => s.lamps.forEach(l => doc.text(s.letter, px(l.x) + 0.9, py(l.z) - 0.7)));
}
// 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, V = vloer-wcd; gestippeld = leidingen (geel licht, bruin schakeldraad, rood wcd/apparaat, groen buiten).'
@@ -161,13 +141,8 @@ window.ElektraPdf = (() => {
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
// ------------------------------------------------------------
// 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 => {
@@ -187,8 +162,6 @@ window.ElektraPdf = (() => {
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;
@@ -219,7 +192,6 @@ window.ElektraPdf = (() => {
};
}
// 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 };
@@ -238,9 +210,6 @@ window.ElektraPdf = (() => {
};
}
// 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;