Files
Tectum/js/elektra/elektra-pdf.js
T

278 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// ============================================================
// elektra/elektra-pdf.js — ElektraPdf: elektra in de woning-PDF.
// drawPlan : meterkast, schakelaars (letter), wcd's (groep), lamp-
// letters en leidingen op de plattegrond (blad Plattegrond)
// drawSheet : eigen blad met groepenverklaring, schakelaars, strekkende
// meters per leidingtype, draad, buis en materiaal.
// Alle gegevens komen uit ElektraModel.compute (parts.elektra).
// Plain script, geladen na pdf-sheet.js en elektra-model.js.
// ============================================================
window.ElektraPdf = (() => {
const fmt = (n, d = 1) => Number(n).toFixed(d).replace('.', ',');
// Kleur per leidingtype (ook de legenda).
const RUN_COLORS = { licht: [215, 160, 0], wcd: [205, 55, 35], buiten: [40, 140, 60], schakel: [150, 95, 20] };
const runColor = (type) => RUN_COLORS[type] || RUN_COLORS.schakel;
// px/py: woning-mm -> blad-mm.
function drawPlan(doc, el, px, py) {
if (!el || !el.enabled) return;
const BLACK = PdfSheet.COLORS.BLACK;
// Leidingen (gestippeld, plattegrond-projectie van het pad).
doc.setLineWidth(0.15);
doc.setLineDashPattern([0.8, 0.6], 0);
el.runs.forEach(r => {
doc.setDrawColor(...runColor(r.type));
r.path.slice(1).forEach((b, i) => {
const a = r.path[i];
if (Math.hypot(px(b.x) - px(a.x), py(b.z) - py(a.z)) > 0.05) doc.line(px(a.x), py(a.z), px(b.x), py(b.z));
});
});
doc.setLineDashPattern([], 0);
// Meterkast
const k = el.meterkast;
if (k) {
doc.setDrawColor(...BLACK);
doc.setFillColor(90, 90, 90);
doc.setLineWidth(0.2);
const c = k.corners;
doc.lines(c.slice(1).map((p, i) => [px(p.x) - px(c[i].x), py(p.z) - py(c[i].z)]), px(c[0].x), py(c[0].z), [1, 1], 'FD', true);
doc.setFontSize(5);
doc.setTextColor(...BLACK);
const m = { x: (c[0].x + c[2].x) / 2 + k.face.n.x * 350, z: (c[0].z + c[2].z) / 2 + k.face.n.z * 350 };
doc.text('MK', px(m.x), py(m.z) + 0.8, { align: 'center' });
}
// Verdeeldozen (plafond): klein vierkantje.
doc.setDrawColor(...runColor('schakel'));
doc.setLineWidth(0.15);
el.boxes.forEach(b => doc.rect(px(b.x) - 0.6, py(b.z) - 0.6, 1.2, 1.2, 'S'));
// Schakelaars: kadertje met de letters, iets de kamer in.
doc.setFontSize(5);
el.combos.forEach(c => {
const q = { x: c.pos.x + c.face.n.x * 260, z: c.pos.z + c.face.n.z * 260 };
const text = c.switches.map(s => s.letter).join(' ');
const w = 1.6 + text.length * 1.0;
doc.setDrawColor(...BLACK);
doc.setFillColor(255, 255, 255);
doc.setLineWidth(0.2);
doc.rect(px(q.x) - w / 2, py(q.z) - 1.2, w, 2.4, 'FD');
doc.setTextColor(...BLACK);
doc.text(text, px(q.x), py(q.z) + 0.7, { align: 'center' });
});
// Wcd: halve cirkel tegen de wand + groepnummer.
el.wcds.forEach(w => {
const q = { x: w.pos.x + w.face.n.x * 120, z: w.pos.z + w.face.n.z * 120 };
doc.setDrawColor(...RUN_COLORS.wcd);
doc.setFillColor(255, 255, 255);
doc.setLineWidth(0.2);
doc.circle(px(q.x), py(q.z), 0.9, 'FD');
doc.setTextColor(...RUN_COLORS.wcd);
doc.setFontSize(4);
doc.text(String(w.group), px(q.x + w.face.n.x * 250), py(q.z + w.face.n.z * 250) + 0.6, { align: 'center' });
});
// Apparaten: kadertje met de code (KK, WM, ...) + groepnummer.
el.apps.forEach(a => {
const it = el.items.find(x => x.key === a.key), q = it.marker;
doc.setDrawColor(...RUN_COLORS.wcd);
doc.setFillColor(255, 255, 255);
doc.setLineWidth(0.2);
doc.rect(px(q.x) - 1.6, py(q.z) - 1.1, 3.2, 2.2, 'FD');
doc.setTextColor(...RUN_COLORS.wcd);
doc.setFontSize(3.6);
doc.text(a.code, px(q.x), py(q.z) + 0.5, { align: 'center' });
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);
el.switches.forEach(s => s.lamps.forEach(l => doc.text(s.letter, px(l.x) + 0.9, py(l.z) - 0.7)));
}
// Twee regels (doc.text met een array).
const PLAN_LEGEND = [
'Elektra: MK = meterkast, kadertje = schakelaar(s) met letter (dezelfde letter staat bij de lampen erop), rood rondje = dubbele wcd (groepnummer), vierkantje = verdeeldoos in plafond;',
'rood kadertje = apparaat (KK kookgroep, WM wasmachine, DR droger, WP warmtepomp, OV oven, VW vaatwasser) met groepnummer, V = vloer-wcd; gestippeld = leidingen (geel licht, bruin schakeldraad, rood wcd/apparaat, groen buiten).'
];
function drawSheet(doc, el, sheetOpts) {
const BLACK = PdfSheet.COLORS.BLACK;
doc.setFont('helvetica', 'normal');
doc.setTextColor(...BLACK);
doc.setFontSize(14);
doc.text('Elektra', 15, 18);
const t = (title) => ({ title, fontSize: 7, rowH: 4.4, lastColW: 16 });
let y = 30;
y = PdfSheet.table(doc, 15, y, [0, 12, 62, 74, 88, 104, 140, 156], ['Groep', 'Omschrijving', 'Fase', 'Aardlek', 'Automaat', 'Leiding', 'Punten', 'Lengte'],
el.groups.map(g => [String(g.nr), g.label, g.phase, `AL${g.rcd}`, g.breaker, ElektraModel.cableInfo(g.cable).label.replace(/ in buis.*/, ''),
g.kind === 'app' ? 'apparaat' : `${g.points} ${g.kind === 'wcd' ? 'wcd' : 'lamp'}`, fmt(g.meters) + ' m']),
t('Groepenverklaring (aansluiting 3×25 A)'));
y += 4;
y = PdfSheet.table(doc, 15, y, [0, 14, 60, 76, 92], ['Letter', 'Functie', 'Unit', 'Groep', 'Lampen'],
el.switches.map(s => [s.letter, s.label, s.unitId, String(s.group || '-'),
`${s.lamps.length} (${[['binnenspot', 'binnenspot'], ['boeideelspot', 'boeideel-spot'], ['gevellamp', 'gevellamp']]
.map(([k, l]) => [s.lamps.filter(x => x.kind === k).length, l]).filter(([n]) => n).map(([n, l]) => `${n} ${l}`).join(', ') || '-'})`]),
t('Schakelaars (letter staat ook bij de lampen op de plattegrond)'));
y += 4;
const k = el.meterkast;
const notes = [
k ? `Meterkast ${k.w}×${k.h}×${k.d} mm (b×h×d) in unit ${k.unitId}, wand ${k.element}, onderkant ${k.y} mm boven vloer.` : 'Geen meterkast geplaatst.',
`Schakelaars op ${el.settings.switchHeight} mm, wcd op ${el.settings.wcdHeight} mm (hart, boven bovenkant vloer).`,
`Leidinglengte = route via stijlen/plafond + ${el.settings.reserve} mm reserve (meterkast +${el.settings.meterkastReserve} mm), + ${el.settings.slack}% marge.`,
'Groepen per unit: licht B10 (1,5 mm²), wcd B16 (2,5 mm²); buitenlicht eigen groep B10 (XMvK), zelfde aardlek als het binnenlicht in dezelfde schakeldoos.',
'Per schakeldoos één verdeeldoos per groep; fasen en aardlekschakelaars om en om.',
'Apparaten (kookgroep, wasmachine, ...) automatisch geplaatst zolang er geen keuken/bijkeuken is; verplaatsbaar in de plattegrond.',
'Indicatief eerste ontwerp; uitvoering en controle volgens NEN 1010 door de installateur.'
].concat(el.warnings.map(w => 'Let op: ' + w));
doc.setFontSize(7);
notes.forEach(n => { doc.text(n, 15, y); y += 4.2; });
let y2 = 30;
y2 = PdfSheet.table(doc, 205, y2, [0, 46, 102], ['Leidingtype', 'Gebruik', 'Lengte'],
el.cables.map(c => [c.label, c.use, fmt(c.meters) + ' m']), t('Leidingen (strekkende meter)'));
y2 += 4;
y2 = PdfSheet.table(doc, 205, y2, [0, 102], ['Draad', 'Lengte'], el.wires.map(w => [w.label, fmt(w.meters) + ' m']), t('Draad (per ader)'));
y2 += 4;
PdfSheet.table(doc, 205, y2, [0, 102], ['Buis', 'Lengte'], el.conduits.map(c => [c.label, fmt(c.meters) + ' m']), t('Installatiebuis'));
PdfSheet.table(doc, 330, 30, [0, 66], ['Onderdeel', 'Aantal'], el.materials.map(m => [m.naam, `${m.aantal} ${m.eenheid}`]), Object.assign(t('Materiaal'), { maxY: 240 }));
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
// ------------------------------------------------------------
// Elektra op de bladen per onderdeel. Elk geeft een overlay-functie voor
// WallPdf/FloorPdf (opts.overlay) of null als er niets te tekenen is.
// ------------------------------------------------------------
const lineCol = (doc, type) => doc.setDrawColor(...runColor(type));
const clip = (p, q, b) => ElektraModel.clipPlan(p, q, b);
// Leidingstukken van alle buizen waarvoor test(p, q) geldt.
const conduitSegs = (el, test) => {
const out = [];
el.runs.forEach(r => {
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 };
})();