v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
@@ -0,0 +1,716 @@
|
||||
/*!
|
||||
* Tectum
|
||||
* Copyright © 2026 Machiel Heinen.
|
||||
* 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).
|
||||
// - 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 %.
|
||||
//
|
||||
// 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
|
||||
apparaten: { koken: true, wasmachine: true, droger: true, warmtepomp: true, oven: false, vaatwasser: false },
|
||||
place: {} // handmatige plekken (zie boven)
|
||||
};
|
||||
// Apparaten met een eigen groep. code = label op de plattegrond (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' },
|
||||
vaatwasser: { label: 'Vaatwasser', code: 'VW', breaker: 'B16', cable: 'wcd', height: 600, zone: 'keuken', material: 'Wandcontactdoos enkel met randaarde' },
|
||||
wasmachine: { label: 'Wasmachine', code: 'WM', breaker: 'B16', cable: 'wcd', height: 1100, zone: 'techniek', material: 'Wandcontactdoos enkel met randaarde' },
|
||||
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
|
||||
|
||||
// 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 },
|
||||
wcd: { label: 'VD 3×2,5 mm² in buis Ø19', use: 'Wcd en apparaten', mm2: 2.5, cores: ['L', 'N', 'PE'], conduit: 19 },
|
||||
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);
|
||||
const n = m ? Number(m[1]) : 2;
|
||||
return { label: `VD ${n}×1,5 mm² in buis Ø16`, use: `Schakeldraden (${n - 1} schakelaar${n > 2 ? 's' : ''})`, mm2: 1.5, cores: ['L'].concat(Array(n - 1).fill('S')), conduit: 16 };
|
||||
}
|
||||
|
||||
function normalizeSettings(settings) {
|
||||
const i = settings || {};
|
||||
const s = Object.assign({}, DEFAULT_SETTINGS, i);
|
||||
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.enabled = s.enabled !== false;
|
||||
// v2: schakelaars 1050 -> 1500 mm, wcd 300 -> 400 mm (oude standaard).
|
||||
if (!i.v) {
|
||||
if (i.switchHeight === 1050) s.switchHeight = DEFAULT_SETTINGS.switchHeight;
|
||||
if (i.wcdHeight === 300) s.wcdHeight = DEFAULT_SETTINGS.wcdHeight;
|
||||
}
|
||||
s.v = 2;
|
||||
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.
|
||||
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);
|
||||
const axis = Math.abs(b.x - a.x) > Math.abs(b.z - a.z) ? 'x' : 'z';
|
||||
const uOf = (v) => (v - a[axis]) / (b[axis] - a[axis]) * W;
|
||||
const ua = uOf(axis === 'x' ? inner.x0 : inner.z0), ub = uOf(axis === 'x' ? inner.x1 : inner.z1);
|
||||
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
|
||||
side,
|
||||
sw: p.config.studWidth || 45,
|
||||
reserved: [] // [{ a, b, y0, y1 }] al bezet (meterkast, schakelaars, wcd)
|
||||
};
|
||||
}
|
||||
function facesOf(u, interior) {
|
||||
const inner = LightingModel.unitInnerRect(u, interior);
|
||||
const pieces = [];
|
||||
u.walls.forEach(w => w.pieces.forEach(p => { if (!p.baseY) pieces.push(p); }));
|
||||
interior.filter(p => p.seamUnit === u.id).forEach(p => pieces.push(p));
|
||||
const list = pieces.map(p => faceOf(p, inner, u, -1));
|
||||
const r = u.rect;
|
||||
interior.filter(p => !p.seam && p.place.cx >= r.x0 && p.place.cx <= r.x1 && p.place.cz >= r.z0 && p.place.cz <= r.z1)
|
||||
.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).
|
||||
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 => {
|
||||
if (op.y < y1 && WallModel.openingTopMax(op) > y0) blocked.push([op.x - f.sw - margin, op.x + op.w + f.sw + margin]);
|
||||
});
|
||||
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]) => {
|
||||
if (b - a < w) return;
|
||||
const u = Math.max(a + w / 2, Math.min(b - w / 2, uAt(f, target)));
|
||||
const q = facePoint(f, u, 0, 0);
|
||||
const d = Math.hypot(q.x - target.x, q.z - target.z);
|
||||
if (!best || d < best.d) best = { f, u, d };
|
||||
}));
|
||||
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.
|
||||
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);
|
||||
const fits = (lo, hi) => free.some(([a, b]) => lo >= a - 0.5 && hi <= b + 0.5);
|
||||
const r0 = op.x + op.w + f.sw + DOOR_GAP;
|
||||
if (fits(r0 - PLATE / 2, r0 - PLATE / 2 + width)) return Array.from({ length: n }, (_, i) => r0 + i * BOX_PITCH);
|
||||
const l0 = op.x - f.sw - DOOR_GAP;
|
||||
if (fits(l0 + PLATE / 2 - width, l0 + PLATE / 2)) return Array.from({ length: n }, (_, i) => l0 - i * BOX_PITCH);
|
||||
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) => {
|
||||
let u = unitAt(x, z);
|
||||
if (!u) u = units.reduce((best, c) => {
|
||||
const dx = Math.max(c.rect.x0 - x, 0, x - c.rect.x1), dz = Math.max(c.rect.z0 - z, 0, z - c.rect.z1);
|
||||
const d = Math.hypot(dx, dz);
|
||||
return !best || d < best.d ? { u: c, d } : best;
|
||||
}, null).u;
|
||||
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)));
|
||||
};
|
||||
// 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).
|
||||
function path(a, b) {
|
||||
const P = (p, wall, ceil) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil });
|
||||
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);
|
||||
const ya = top(a), yb = top(b), dropB = Math.abs(yb - b.y) > 1;
|
||||
const pts = [P(a, a.face, a.mount === 'plafond')];
|
||||
if (Math.abs(ya - a.y) > 1) pts.push(P({ x: a.x, y: ya, z: a.z }, a.face, true));
|
||||
pts.push(P({ x: b.x, y: ceilAt(b.x, a.z), z: a.z }, null, true));
|
||||
pts.push(P({ x: b.x, y: yb, z: b.z }, dropB ? b.face : null, true));
|
||||
if (dropB) pts.push(P(b, b.face, false));
|
||||
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) => {
|
||||
const p = pts[i], len = Math.hypot(q.x - p.x, q.z - p.z), steps = Math.max(1, Math.ceil(len / 100));
|
||||
for (let k = 0; k <= steps; k++) {
|
||||
const u = unitAt(p.x + (q.x - p.x) * k / steps, p.z + (q.z - p.z) * k / steps);
|
||||
if (!u) continue;
|
||||
if (last && u.id !== last) n++;
|
||||
last = u.id;
|
||||
}
|
||||
});
|
||||
return n;
|
||||
}
|
||||
return { unitAt, ceilAt, 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) {
|
||||
const s = normalizeSettings(settings);
|
||||
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: [],
|
||||
groups: [], runs: [], cables: [], wires: [], conduits: [], materials: [], warnings: [], items: [], faces: []
|
||||
};
|
||||
if (!s.enabled || !units.length) return out;
|
||||
const R = makeRouter(units);
|
||||
const faces = {};
|
||||
units.forEach(u => { faces[u.id] = facesOf(u, interior); });
|
||||
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;
|
||||
const op = p.config.openings.find(o => o.type === type);
|
||||
const f = op && faceOfPiece(u, p);
|
||||
if (f) door = { unit: u, face: f, op };
|
||||
return !!door;
|
||||
}))));
|
||||
if (!door) out.warnings.push('Geen voordeur of buitendeur gevonden: schakelaars en meterkast vrij geplaatst.');
|
||||
const mainUnit = door ? door.unit : units[0];
|
||||
const hasOutdoor = gevel.length + spots.length > 0;
|
||||
let letterCode = 65;
|
||||
const nextLetter = () => String.fromCharCode(letterCode++);
|
||||
const mainFuncs = hasOutdoor ? ['binnen', 'buiten'] : ['binnen'];
|
||||
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;
|
||||
|
||||
// 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;
|
||||
m = /^wcd:(.+):(\d+)$/.exec(key);
|
||||
if (m) return units.some(u => u.id === m[1]) && Number(m[2]) <= s.wcdPerUnit ? WCD_W : 0;
|
||||
m = /^app:(.+)$/.exec(key);
|
||||
return m && apps.includes(m[1]) ? PLATE : 0;
|
||||
};
|
||||
const fixed = {};
|
||||
Object.keys(s.place).forEach(key => {
|
||||
const o = s.place[key], w = widthOf(key);
|
||||
if (!o || !w) return;
|
||||
const f = allFaces.find(x => x.piece.element === o.element && x.side === (o.side || -1));
|
||||
if (!f) return;
|
||||
const u = Math.max(f.u0 + w / 2, Math.min(f.u1 - w / 2, num(o.u, (f.u0 + f.u1) / 2)));
|
||||
fixed[key] = { f, u, y: num(o.y, null) };
|
||||
f.reserved.push({ a: u - w / 2 - 50, b: u + w / 2 + 50, y0: 0, y1: 1e6 });
|
||||
});
|
||||
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 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: [] };
|
||||
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}` };
|
||||
sw.pos = facePoint(f, us[i], yy, 0);
|
||||
combo.switches.push(sw);
|
||||
out.switches.push(sw);
|
||||
});
|
||||
const lo = Math.min(...us) - PLATE / 2, hi = Math.max(...us) + PLATE / 2;
|
||||
if (!manual) f.reserved.push({ a: lo - 60, b: hi + 60, y0: 0, y1: 1e6 });
|
||||
combo.u = (lo + hi) / 2;
|
||||
combo.pos = facePoint(f, combo.u, yy, 0);
|
||||
combo.width = hi - lo;
|
||||
combo.center = facePoint(f, combo.u, yy, 5);
|
||||
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);
|
||||
else if (door) {
|
||||
const us = besideDoor(door.face, door.op, mainFuncs.length, s.switchHeight);
|
||||
if (us) addCombo(mainUnit, door.face, us, mainFuncs);
|
||||
else {
|
||||
const doorPt = facePoint(door.face, door.op.x + door.op.w / 2, 0, 0);
|
||||
const w = PLATE + (mainFuncs.length - 1) * BOX_PITCH;
|
||||
const nf = nearestFree(faces[mainUnit.id], doorPt, w, s.switchHeight - PLATE / 2, s.switchHeight + PLATE / 2, 0, 100);
|
||||
if (nf) addCombo(mainUnit, nf.f, mainFuncs.map((_, i) => nf.u - w / 2 + PLATE / 2 + i * BOX_PITCH), mainFuncs);
|
||||
}
|
||||
}
|
||||
if (!out.combos.length) {
|
||||
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;
|
||||
let spot = fxMk ? { f: fxMk.f, u: fxMk.u } : nearestFree(faces[mainUnit.id], target, mk.w, mkY, mkY + mk.h, 50, 50);
|
||||
if (!spot) units.some(u => u !== mainUnit && (spot = nearestFree(faces[u.id], target, mk.w, mkY, mkY + mk.h, 50, 50)));
|
||||
if (spot) {
|
||||
const f = spot.f;
|
||||
if (!fxMk) f.reserved.push({ a: spot.u - mk.w / 2 - 50, b: spot.u + mk.w / 2 + 50, y0: 0, y1: 1e6 });
|
||||
out.meterkast = {
|
||||
key: 'mk', manual: !!fxMk, unitId: f.unit.id, element: f.piece.element, face: f, u: spot.u, w: mk.w, h: mk.h, d: mk.d, y: mkY, rotY: f.piece.place.rotY,
|
||||
center: facePoint(f, spot.u, mkY + mk.h / 2, mk.d / 2),
|
||||
corners: [[-1, 0], [1, 0], [1, mk.d], [-1, mk.d]].map(([i, d]) => facePoint(f, spot.u + i * mk.w / 2, 0, d))
|
||||
};
|
||||
} 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 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.sort((a, b) => a.d - b.d);
|
||||
for (const c of cands) {
|
||||
const us = besideDoor(c.f, c.op, 1, s.switchHeight);
|
||||
if (us) { addCombo(u, c.f, us, ['binnen']); return; }
|
||||
}
|
||||
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 }); } });
|
||||
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 key = `wcd:${u.id}:${i}`, fx = fixed[key];
|
||||
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 });
|
||||
}
|
||||
while (freeKeys.length) {
|
||||
let best = null, bestScore = -1;
|
||||
cands.forEach(c => {
|
||||
if (chosen.some(x => x.f === c.f && Math.abs(x.u - c.u) < WCD_W + 150)) return;
|
||||
const score = chosen.length ? Math.min(...chosen.map(x => Math.hypot(x.p.x - c.p.x, x.p.z - c.p.z))) : c.len;
|
||||
if (score > bestScore) { bestScore = score; best = c; }
|
||||
});
|
||||
if (!best) break;
|
||||
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({
|
||||
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;
|
||||
const zones = {
|
||||
keuken: { target: { x: kitchen.rect.cx, z: kitchen.rect.cz }, order: [kitchen].concat(units.filter(u => u !== kitchen)) },
|
||||
techniek: { target: out.meterkast ? out.meterkast.center : entry, order: [mkUnit].concat(units.filter(u => u !== mkUnit)) }
|
||||
};
|
||||
apps.forEach(k => {
|
||||
const A = APPLIANCES[k], key = 'app:' + k, fx = fixed[key];
|
||||
let f, uu, y = A.height;
|
||||
if (fx) { f = fx.f; uu = fx.u; if (fx.y !== null) y = fx.y; }
|
||||
else {
|
||||
const z = zones[A.zone];
|
||||
let nf = null;
|
||||
z.order.some(u => (nf = nearestFree(faces[u.id], z.target, PLATE + 40, y - 60, y + 60, 100, 150)));
|
||||
if (!nf) { out.warnings.push(`${A.label}: geen vrije wand gevonden.`); return; }
|
||||
f = nf.f; uu = nf.u;
|
||||
f.reserved.push({ a: uu - PLATE / 2 - 80, b: uu + PLATE / 2 + 80, y0: 0, y1: 1e6 });
|
||||
}
|
||||
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) });
|
||||
});
|
||||
|
||||
// 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: [] });
|
||||
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) });
|
||||
});
|
||||
out.apps.forEach(a => {
|
||||
const A = APPLIANCES[a.app];
|
||||
out.groups.push({ kind: 'app', app: a.app, unitId: a.unitId, label: A.label, breaker: A.breaker, cable: A.cable, poles: A.poles || 1, switches: [] });
|
||||
});
|
||||
const nRcd = Math.max(2, Math.ceil(out.groups.length / 4));
|
||||
let ph = 0;
|
||||
out.groups.forEach((g, i) => {
|
||||
const n = g.poles || 1;
|
||||
g.nr = i + 1; g.rcd = i % nRcd + 1;
|
||||
g.phase = Array.from({ length: n }, (_, j) => 'L' + ((ph + j) % 3 + 1)).join('+');
|
||||
ph += n;
|
||||
});
|
||||
out.rcdCount = out.groups.length ? nRcd : 0;
|
||||
const groupOf = (kind, unitId) => out.groups.find(g => g.kind === kind && g.unitId === unitId);
|
||||
// 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);
|
||||
if (over < 0) break;
|
||||
const mv = out.groups.slice().reverse().find(g => g.rcd === over + 1 && g !== gOut && g !== gIn);
|
||||
if (!mv) break;
|
||||
mv.rcd = cnt.indexOf(Math.min(...cnt)) + 1;
|
||||
}
|
||||
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.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) => {
|
||||
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' });
|
||||
};
|
||||
// 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) {
|
||||
let best = null;
|
||||
rest.forEach((n, i) => done.forEach(t => { const c = R.cost(t, n); if (!best || c < best.c) best = { c, i, t }; }));
|
||||
const n = rest.splice(best.i, 1)[0];
|
||||
addRun(typeOf(best.t, n), best.t, n, group, letter);
|
||||
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 => {
|
||||
let e = byGroup.find(x => x.group === sw.group);
|
||||
if (!e) byGroup.push(e = { group: sw.group, switches: [] });
|
||||
e.switches.push(sw);
|
||||
});
|
||||
combo.vds = [];
|
||||
const along = { x: combo.face.b.x - combo.face.a.x, z: combo.face.b.z - combo.face.a.z }, al = Math.hypot(along.x, along.z) || 1;
|
||||
byGroup.forEach((e, i) => {
|
||||
const shift = (i - (byGroup.length - 1) / 2) * VD_OFFSET;
|
||||
const vd = {
|
||||
x: combo.pos.x + combo.face.n.x * VD_OFFSET + along.x / al * shift, z: combo.pos.z + combo.face.n.z * VD_OFFSET + along.z / al * shift,
|
||||
mount: 'plafond', name: `VD${combo.id}${byGroup.length > 1 ? String.fromCharCode(97 + i) : ''}`
|
||||
};
|
||||
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
|
||||
const swNode = Object.assign({}, combo.pos, { 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}` }));
|
||||
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));
|
||||
|
||||
// 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 }));
|
||||
const wires = {}, conduits = {};
|
||||
out.runs.forEach(r => {
|
||||
const c = cableInfo(r.type);
|
||||
if (!c.conduit) return;
|
||||
conduits[c.conduit] = (conduits[c.conduit] || 0) + r.lengthMM;
|
||||
c.cores.forEach(core => {
|
||||
const k = `${String(c.mm2).replace('.', ',')} mm² ${CORE_COLORS[core]}`;
|
||||
wires[k] = (wires[k] || 0) + r.lengthMM;
|
||||
});
|
||||
});
|
||||
out.wires = Object.keys(wires).sort().map(k => ({ label: `VD-draad ${k}`, meters: wires[k] / 1000 }));
|
||||
out.conduits = Object.keys(conduits).sort((a, b) => a - b).map(k => ({ label: `Flexibele installatiebuis Ø${k} mm`, meters: conduits[k] / 1000 }));
|
||||
out.groups.forEach(g => {
|
||||
const rs = out.runs.filter(r => r.group === g.nr);
|
||||
g.meters = rs.reduce((a, r) => a + r.lengthMM, 0) / 1000;
|
||||
g.points = g.kind === 'wcd' ? out.wcds.filter(w => w.group === g.nr).length
|
||||
: g.kind === 'app' ? 1
|
||||
: out.switches.filter(x => x.group === g.nr).reduce((a, x) => a + x.lamps.length, 0);
|
||||
});
|
||||
out.groups.forEach(g => {
|
||||
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);
|
||||
const nSpots = out.switches.reduce((a, x) => a + x.lamps.filter(l => l.kind === 'boeideelspot').length, 0);
|
||||
const nGevel = out.switches.reduce((a, x) => a + x.lamps.filter(l => l.kind === 'gevellamp').length, 0);
|
||||
const nLicht = out.groups.filter(g => g.kind === 'licht').length, nWcdG = out.groups.filter(g => g.kind === 'wcd').length;
|
||||
const frames = {};
|
||||
out.combos.forEach(c => { frames[c.switches.length] = (frames[c.switches.length] || 0) + 1; });
|
||||
const m = out.materials;
|
||||
const add = (naam, aantal, eenheid) => { if (aantal > 0) m.push({ naam, aantal, eenheid: eenheid || 'st.' }); };
|
||||
if (out.meterkast) {
|
||||
add('Groepenkast 3-fase (aansluiting 3×25 A)', 1);
|
||||
add('Hoofdschakelaar 4-polig 40 A', 1);
|
||||
}
|
||||
add('Aardlekschakelaar 2-polig 40 A / 30 mA', out.rcdCount);
|
||||
add('Installatieautomaat B10 1P+N (verlichting)', nLicht);
|
||||
add('Installatieautomaat B10 1P+N (buitenverlichting)', out.groups.filter(g => g.kind === 'buiten').length);
|
||||
add('Installatieautomaat B16 1P+N (wcd)', nWcdG);
|
||||
add('Installatieautomaat B16 1P+N (apparaten)', out.apps.filter(a => (APPLIANCES[a.app].poles || 1) === 1).length);
|
||||
add('Installatieautomaat 2× B16, 2-fase (kookgroep)', out.apps.filter(a => APPLIANCES[a.app].poles === 2).length);
|
||||
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);
|
||||
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]));
|
||||
add('Luchtdichte hollewanddoos (schakelaar/wcd/apparaat)', out.switches.length + 2 * out.wcds.length + out.apps.length);
|
||||
add('Luchtdichte verdeeldoos (plafond)', out.boxes.length);
|
||||
add('Lasklem 3-voudig (schatting)', out.boxes.length * 3 + out.switches.length + out.wcds.length);
|
||||
add('LED-inbouwspot binnen + luchtdichte spotdoos', nIndoor);
|
||||
add('Buitenspot boeideel (IP44)', nSpots);
|
||||
add('Gevellamp (IP44)', nGevel);
|
||||
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)
|
||||
});
|
||||
const k = out.meterkast;
|
||||
if (k) item('mk', 'mk', 'Meterkast', k.face, k.u, k.y, k.manual, k.d / 2);
|
||||
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));
|
||||
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
|
||||
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 CONDUIT_COLORS = { licht: 0xe0b020, wcd: 0xcc4a30, koken: 0x8a3a9a, schakel: 0x9a6a20 };
|
||||
function createMaterials() {
|
||||
const conduit = {};
|
||||
Object.keys(CONDUIT_COLORS).forEach(k => { conduit[k] = new THREE.MeshStandardMaterial({ color: CONDUIT_COLORS[k], roughness: 0.55 }); });
|
||||
return {
|
||||
kastMaterial: new THREE.MeshStandardMaterial({ color: 0xd8d8d2, roughness: 0.6 }),
|
||||
deurMaterial: new THREE.MeshStandardMaterial({ color: 0xc4c4bd, roughness: 0.5 }),
|
||||
plateMaterial: new THREE.MeshStandardMaterial({ color: 0xf6f6f2, roughness: 0.4 }),
|
||||
boxMaterial: new THREE.MeshStandardMaterial({ color: 0xe8742a, roughness: 0.6 }),
|
||||
conduit
|
||||
};
|
||||
}
|
||||
function build3D(el, materials, stage) {
|
||||
const toM = 0.001, group = new THREE.Group();
|
||||
if (!el || !el.enabled) return group;
|
||||
const mats = materials || createMaterials();
|
||||
const st = BuildStages.clamp(stage), show = (key) => BuildStages.shows(st, key);
|
||||
const rough = show('elektra') && !show('beplating');
|
||||
const box = (w, h, d, c, rotY, mat) => {
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w * toM, h * toM, d * toM), mat);
|
||||
mesh.position.set(c.x * toM, c.y * toM, c.z * toM);
|
||||
mesh.rotation.y = rotY;
|
||||
group.add(mesh);
|
||||
return mesh;
|
||||
};
|
||||
const k = el.meterkast;
|
||||
if (k && show('elektra')) {
|
||||
box(k.w, k.h, k.d, k.center, k.rotY, mats.kastMaterial);
|
||||
const front = facePoint(k.face, k.u, k.y + k.h / 2, k.d + 2);
|
||||
box(k.w - 30, k.h - 30, 4, front, k.rotY, mats.deurMaterial);
|
||||
}
|
||||
if (show('afwerking')) {
|
||||
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));
|
||||
}
|
||||
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)
|
||||
});
|
||||
el.runs.forEach(r => {
|
||||
const c = cableInfo(r.type);
|
||||
if (!c.conduit) return;
|
||||
const rad = c.conduit / 2 * toM;
|
||||
const mat = mats.conduit[r.type] || mats.conduit.schakel;
|
||||
const pts = r.path.map(shift);
|
||||
pts.slice(1).forEach((q, i) => {
|
||||
const p = pts[i], dx = q.x - p.x, dy = q.y - p.y, dz = q.z - p.z, len = Math.hypot(dx, dy, dz);
|
||||
if (len < 1) return;
|
||||
const m = new THREE.Mesh(new THREE.CylinderGeometry(rad, rad, len * toM, 6), mat);
|
||||
m.position.set((p.x + q.x) / 2 * toM, (p.y + q.y) / 2 * toM, (p.z + q.z) / 2 * toM);
|
||||
m.quaternion.setFromUnitVectors(up, new THREE.Vector3(dx / len, dy / len, dz / len));
|
||||
group.add(m);
|
||||
});
|
||||
pts.slice(1, -1).forEach(p => {
|
||||
const j = new THREE.Mesh(new THREE.IcosahedronGeometry(rad * 1.15, 0), mat);
|
||||
j.position.set(p.x * toM, p.y * toM, p.z * toM);
|
||||
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);
|
||||
m.position.set(c.x * toM, c.y * toM, c.z * toM);
|
||||
m.quaternion.setFromUnitVectors(up, new THREE.Vector3(f.n.x, 0, f.n.z));
|
||||
group.add(m);
|
||||
};
|
||||
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));
|
||||
// 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 => {
|
||||
const m = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.045, 0.07, 12), mats.boxMaterial);
|
||||
m.position.set(l.x * toM, (l.y + 18 + 35) * toM, l.z * toM);
|
||||
group.add(m);
|
||||
}));
|
||||
return group;
|
||||
}
|
||||
|
||||
return { DEFAULT_SETTINGS, CABLES, CORE_COLORS, APPLIANCES, cableInfo, normalizeSettings, compute, snapToWall, createMaterials, build3D };
|
||||
})();
|
||||
Reference in New Issue
Block a user