v0.6.0: ruimtes (verfemmer: functie, netto m², vloerafwerking, FIBO-wandpanelen, eigen verfkleur, vals plafond, ruimtestaat op de PDF), inrichting (sanitair, keuken, kookeiland, kasten, bed, zithoek, boiler; slepen met snappen op het wandvlak, draaien, kosten), kozijnen met deurrichting, glasdeur en schuifdeuren (inbouw/opbouw, deuren open in de loopmodus) plus dagkanten, gevel stucwerk en wandstukken splitsen met eigen gevel, dak over de hele woning met overkapping, geschilderd plafond, model Amstel als nieuw ontwerp, kosten en prijzen uit kosten/intern.csv en kosten/leveranciers.csv (CostCsv); easter egg verwijderd; overbodige comments opgeschoond (alleen copyright, uitzonderingen en valkuilen blijven)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b5510d56c7
commit
ceda0b554d
+115
-201
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user