/*! * 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 }; })();