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