278 lines
14 KiB
JavaScript
278 lines
14 KiB
JavaScript
/*!
|
||
* 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 };
|
||
})();
|