/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // ============================================================ // wall/wall-pdf.js — ENIGE bron van het wand-tekenblad (A3 liggend): // frame-materiaallijst, plaatmateriaal, vooraanzicht met posnums en // maatketens, bovenaanzicht (doorsnede regelwerk + beplating), zijaanzicht // (laagopbouw) en titelblok. Gebruikt door de losse wand-editor (1 blad) en // door de unit (1 blad per wand in de gezamenlijke PDF). // Plain script, geladen na wall-model.js en core/pdf-sheet.js. // ============================================================ window.WallPdf = (() => { // Tekent één volledig wandblad op de HUIDIGE pagina van `doc`. // opts: { buitenMateriaal, cladding, trasraam, skinExt, project, titel, element, blad, lamps: [{ u, y }] } function drawSheet(doc, CONFIG, calcData, opts) { const buitenMateriaal = opts.buitenMateriaal || 'vochtdoek'; const pieces = WallModel.computeBOM(CONFIG, calcData); const groups = WallModel.groupBOM(pieces); const PAGE_W = doc.internal.pageSize.getWidth(), PAGE_H = doc.internal.pageSize.getHeight(); const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS; doc.setFont('helvetica', 'normal'); // --- Frame materiaal-tabel (linksboven) --- const tblX = [8, 26, 42, 58, 76, 92]; let ty = 14; doc.setTextColor(...BLACK); doc.setFontSize(9); doc.text('Frame materiaal', tblX[0], ty); ty += 6; doc.setFontSize(6.5); ['Posnum', 'Dikte', 'Hoogte', 'Lengte', 'Aantal'].forEach((h, i) => doc.text(h, tblX[i], ty)); ty += 1.5; doc.setDrawColor(...GREY); doc.setLineWidth(0.15); doc.line(tblX[0], ty, tblX[4] + 12, ty); ty += 4; groups.forEach(g => { if (ty > 232) return; // laat ruimte vrij voor materiaaltabellen eronder [String(g.posnum), String(g.width), String(g.depth), String(g.length), String(g.count)] .forEach((v, i) => doc.text(v, tblX[i], ty)); ty += 4.3; }); // --- Inkoop balken (handelslengtes, zaagplan uit WallModel) --- const purchase = WallModel.planPurchase(pieces); ty += 3; doc.setFontSize(9); doc.text('Inkoop balken', tblX[0], ty); ty += 5; doc.setFontSize(6.5); ['Doorsnede', 'Lengte', 'Aantal', 'Verlies'].forEach((h, i) => doc.text(h, tblX[i], ty)); ty += 4; purchase.forEach(sec => { sec.perStock.forEach(st => { if (ty > 232) return; const waste = sec.bars.filter(b => b.stock === st.stock).reduce((a, b) => a + b.waste, 0); [`${sec.width}x${sec.depth}`, String(st.stock), String(st.count), String(waste)].forEach((v, i) => doc.text(v, tblX[i], ty)); ty += 4.3; }); }); // --- Materiaal binnen/buiten + isolatie --- let my = 240; const midH = Math.round(WallModel.topLine(CONFIG).maxY); doc.setFontSize(8.5); doc.text('Materiaal binnen', tblX[0], my); my += 4.5; doc.setFontSize(6.5); doc.text(`${CONFIG.wallWidth} x ${CONFIG.sheathingThick} x ${midH} Spaanplaat 1`, tblX[0], my); my += 6; doc.setFontSize(8.5); doc.text('Materiaal buiten', tblX[0], my); my += 4.5; doc.setFontSize(6.5); const buitenNaam = WallModel.EXTERIOR_MATERIALS[buitenMateriaal] || 'Vochtdoek'; doc.text(`${CONFIG.wallWidth} x ${CONFIG.sheathingThick} x ${midH} ${buitenNaam} 1`, tblX[0], my); my += 6; doc.text('Isolatie: glaswol (spouwvullend)', tblX[0], my); if (buitenMateriaal === 'vochtdoek') { // Kruislat op het vochtdoek (zelfde berekening als de 3D: WallModel.computeBattens). const bat = WallModel.computeBattens(CONFIG, opts.cladding || 'horizontaal', opts.trasraam, opts.skinExt, opts.facade, opts.skinFrom); const B = WallModel.BATTEN; my += 4.5; doc.text(`Latwerk ${B.thick}x${B.width}: ${bat.stock.count} st. \u00e0 ${B.length} mm, kruislat ${B.spacing} h.o.h.`, tblX[0], my); my += 4; doc.text(`Bovenste lat ${bat.top}, gevelbekleding ${opts.cladding || 'horizontaal'} (WEO/Novalit)`, tblX[0], my); if (opts.trasraam) { my += 4; const finish = WallModel.isStoneFinish(opts.trasraam.finish) ? `steenstrip ${opts.trasraam.finish} op isolatieblok (${WallModel.STONE.THICK_MM} mm)` : 'cement-gebonden plaat'; doc.text(`Trasraam: ${finish} tot ${opts.trasraam.top} mm boven bovenkant vloer`, tblX[0], my); } } // --- Titelblok (rechtsonder) --- PdfSheet.titleBlock(doc, opts.project, { titel: opts.titel, element: opts.element, blad: opts.blad }); // --- Hoofdaanzicht --- // drawX1/marginBottom laten ruimte vrij voor het zijaanzicht (rechts) // en het bovenaanzicht (onder de maatketens). const drawX0 = 95, drawX1 = 380, drawY0 = 15, drawY1 = 225; const marginSides = 25, marginTop = 15, marginBottom = 68; const availW = (drawX1 - drawX0) - marginSides * 2; const availH = (drawY1 - drawY0) - marginTop - marginBottom; const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight; const tl = WallModel.topLine(CONFIG); const maxH = tl.maxY; const scale = Math.min(availW / wallWidth, availH / maxH); const originX = drawX0 + marginSides; const floorY = drawY1 - marginBottom; const px = (xmm) => originX + (xmm + wallWidth / 2) * scale; const py = (ymm) => floorY - ymm * scale; doc.setDrawColor(...GREY); doc.setLineWidth(0.25); doc.line(px(-wallWidth / 2), py(0), px(wallWidth / 2), py(0)); doc.line(px(wallWidth / 2), py(0), px(wallWidth / 2), py(hRight)); for (let i = 0; i < tl.pts.length - 1; i++) doc.line(px(tl.pts[i].x), py(tl.pts[i].y), px(tl.pts[i + 1].x), py(tl.pts[i + 1].y)); doc.line(px(-wallWidth / 2), py(hLeft), px(-wallWidth / 2), py(0)); doc.setDrawColor(...RED); doc.setLineWidth(0.3); pieces.forEach(p => { if (p.outline) { // Werkelijke omtrek: schuine kop (volgt de daklijn, evt. met knik // onder de nok) resp. bovenregel in verstek. const c = p.outline.map(q => [px(q.x), py(q.y)]); c.forEach((q, i) => { const n = c[(i + 1) % c.length]; doc.line(q[0], q[1], n[0], n[1]); }); } else if (p.kind === 'verticale balk' || p.kind === 'kreupele stijl') { // Viervlak i.p.v. rechthoek: de bovenkant loopt schuin mee met de // daklijn (yTopLeft/yTopRight kunnen verschillen), zodat zowel de // hoogste als de laagste kant van de balk op de juiste plek komt. const wx0 = p.x - p.width / 2, wx1 = p.x + p.width / 2; const corners = [ [px(wx0), py(p.yBottomLeft)], [px(wx1), py(p.yBottomRight)], [px(wx1), py(p.yTopRight)], [px(wx0), py(p.yTopLeft)] ]; for (let i = 0; i < 4; i++) doc.line(corners[i][0], corners[i][1], corners[(i + 1) % 4][0], corners[(i + 1) % 4][1]); } else { const wx0 = p.x - p.length / 2, wx1 = p.x + p.length / 2; const wy0 = p.y - p.width / 2, wy1 = p.y + p.width / 2; doc.rect(px(wx0), py(wy1), px(wx1) - px(wx0), py(wy0) - py(wy1), 'S'); } doc.setTextColor(...BLACK); doc.setFontSize(5); doc.text(String(p.posnum), px(p.x), py(p.y), { align: 'center' }); }); doc.setDrawColor(...BLACK); doc.setLineWidth(0.2); CONFIG.openings.forEach(op => { const opXWorld = -wallWidth / 2 + op.x; // Schuine bovenzijde: y0 = bovenkant links, y0r = rechts. const x0 = px(opXWorld), y0 = py(op.y + op.h), y0r = py(op.y + op.hR), x1 = px(opXWorld + op.w), y1 = py(op.y); [[x0, y1, x1, y1], [x1, y1, x1, y0r], [x1, y0r, x0, y0], [x0, y0, x0, y1]].forEach(l => doc.line(...l)); doc.line(x0, y0, x1, y1); doc.line(x0, y1, x1, y0r); doc.setTextColor(...BLACK); doc.setFontSize(6); // Dagmaat-label BINNEN de opening zetten (net onder de bovenkant), // i.p.v. erboven — daar overlapte het met de maatketen/omtrek // erboven. doc.text(op.hR !== op.h ? `${op.w}x${op.h}/${op.hR}` : `${op.w}x${op.h}`, (x0 + x1) / 2, Math.max(y0, y0r) + 5, { align: 'center' }); }); const drawDimLine = (x1, y1, x2, y2) => PdfSheet.dimLine(doc, x1, y1, x2, y2); doc.setDrawColor(...BLUE); doc.setTextColor(...BLUE); doc.setLineWidth(0.15); doc.setFontSize(7); const dimTopY = py(maxH) - 7; drawDimLine(px(-wallWidth / 2), dimTopY, px(wallWidth / 2), dimTopY); doc.text(String(Math.round(wallWidth)), (px(-wallWidth / 2) + px(wallWidth / 2)) / 2, dimTopY - 1.5, { align: 'center' }); drawDimLine(px(-wallWidth / 2) - 7, py(0), px(-wallWidth / 2) - 7, py(hLeft)); doc.text(String(Math.round(hLeft)), px(-wallWidth / 2) - 9, (py(0) + py(hLeft)) / 2, { align: 'center', angle: 90 }); drawDimLine(px(wallWidth / 2) + 7, py(0), px(wallWidth / 2) + 7, py(hRight)); doc.text(String(Math.round(hRight)), px(wallWidth / 2) + 11, (py(0) + py(hRight)) / 2, { align: 'center', angle: 90 }); // Tussenpunten van de daklijn: stippellijn van vloer tot nok/knik // (nok = het enige hoogste punt; mansarde-/wolfsgevel: knikken). const inner = tl.pts.slice(1, -1); inner.forEach(q => { const isNok = inner.filter(o => o.y >= q.y - 0.5).length === 1; doc.setLineDashPattern([1, 1], 0); doc.line(px(q.x), py(0), px(q.x), py(q.y)); doc.setLineDashPattern([], 0); doc.text((isNok ? 'nok ' : 'knik ') + Math.round(q.y), px(q.x) + 1.5, py(q.y) + 4); }); const studEdges = new Set([0, wallWidth]); calcData.fullStuds.forEach(x => studEdges.add(Math.round(x - CONFIG.studWidth / 2 + wallWidth / 2))); const cumPoints = [...studEdges].filter(v => v >= 0 && v <= wallWidth).sort((a, b) => a - b); doc.setDrawColor(...BLACK); doc.setTextColor(...BLACK); doc.setLineWidth(0.1); doc.setFontSize(5.5); const segY = floorY + 9, cumY = floorY + 17; for (let i = 0; i < cumPoints.length - 1; i++) { const mid = (cumPoints[i] + cumPoints[i + 1]) / 2; doc.text(String(cumPoints[i + 1] - cumPoints[i]), px(mid - wallWidth / 2), segY, { align: 'center' }); } cumPoints.forEach(v => { doc.text(String(v), px(v - wallWidth / 2), cumY, { align: 'center' }); doc.line(px(v - wallWidth / 2), cumY - 2.5, px(v - wallWidth / 2), floorY + 2); }); const openingPoints = [0]; CONFIG.openings.slice().sort((a, b) => a.x - b.x).forEach(op => { openingPoints.push(op.x, op.x + op.w); }); openingPoints.push(wallWidth); const openY = floorY + 25; doc.text('Opening', drawX0, openY); for (let i = 0; i < openingPoints.length - 1; i++) { const mid = (openingPoints[i] + openingPoints[i + 1]) / 2; doc.text(String(Math.round(openingPoints[i + 1] - openingPoints[i])), px(mid - wallWidth / 2), openY, { align: 'center' }); } // --- Gevellampen (woning): rechthoekje met "L" op lamphoogte --- (opts.lamps || []).forEach(l => { const lx = px(-wallWidth / 2 + l.u), ly = py(l.y); doc.setDrawColor(...BLACK); doc.setFillColor(255, 211, 107); doc.setLineWidth(0.2); doc.rect(lx - 1.2, ly - 2, 2.4, 4, 'FD'); doc.setTextColor(...BLACK); doc.setFontSize(5); doc.text('L', lx + 2, ly + 1); }); // --- Bovenaanzicht (doorsnede op stijlhoogte, zoals de voorbeeld- // tekeningen): binnenzijde boven, buitenzijde onder (= de kant van // waaruit het vooraanzicht bekeken wordt). Zelfde horizontale schaal. const sT = CONFIG.sheathingThick; const frameD = Math.max(CONFIG.studDepth, CONFIG.plateDepth); const tvY0 = floorY + 34; const tvFrame0 = tvY0 + sT * scale; const tvFrame1 = tvFrame0 + frameD * scale; const tvY1 = tvFrame1 + sT * scale; const halfW = wallWidth / 2; // Beplating, onderbroken ter plaatse van de openingen. const skinCuts = CONFIG.openings.map(op => [op.x, op.x + op.w]).sort((a, b) => a[0] - b[0]); const skinSegments = []; let skinCursor = 0; skinCuts.forEach(([a, b]) => { if (a > skinCursor) skinSegments.push([skinCursor, a]); skinCursor = Math.max(skinCursor, b); }); if (skinCursor < wallWidth) skinSegments.push([skinCursor, wallWidth]); doc.setDrawColor(...GREY); doc.setLineWidth(0.15); skinSegments.forEach(([a, b]) => { doc.rect(px(a - halfW), tvY0, (b - a) * scale, sT * scale, 'S'); doc.rect(px(a - halfW), tvFrame1, (b - a) * scale, sT * scale, 'S'); }); // Stijlen (vol + kreupel) als doorsneden: rechthoek met kruis. doc.setDrawColor(...RED); doc.setLineWidth(0.2); const studXs = [...new Set(calcData.studs.map(st => st.x))]; studXs.forEach(x => { const x0 = px(x - CONFIG.studWidth / 2), x1 = px(x + CONFIG.studWidth / 2); doc.rect(x0, tvFrame0, x1 - x0, tvFrame1 - tvFrame0, 'S'); doc.line(x0, tvFrame0, x1, tvFrame1); doc.line(x0, tvFrame1, x1, tvFrame0); }); doc.setTextColor(...GREY); doc.setFontSize(6); doc.text('Bovenaanzicht', drawX0, (tvY0 + tvY1) / 2 + 1); doc.text('binnen', px(-halfW) - 2, tvY0 + 1, { align: 'right' }); doc.text('buiten', px(-halfW) - 2, tvY1 + 1, { align: 'right' }); doc.setDrawColor(...BLUE); doc.setTextColor(...BLUE); doc.setLineWidth(0.15); doc.setFontSize(6); const tvDimX = px(halfW) + 5; drawDimLine(tvDimX, tvY0, tvDimX, tvY1); doc.text(`${sT} / ${frameD} / ${sT}`, tvDimX + 2, (tvY0 + tvY1) / 2 + 1); const tvDimY = tvY1 + 6; drawDimLine(px(-halfW), tvDimY, px(halfW), tvDimY); doc.setFontSize(7); doc.text(String(Math.round(wallWidth)), px(0), tvDimY + 3.5, { align: 'center' }); // --- Zijaanzicht (rechterkopse kant): laagopbouw binnen | regelwerk | // buiten, met onder- en bovenregel, op dezelfde schaal. const svX0 = 392; const svFrame0 = svX0 + sT * scale; const svFrame1 = svFrame0 + frameD * scale; const svX1 = svFrame1 + sT * scale; const svSlope = (hRight - hLeft) / wallWidth; const svVOff = CONFIG.plateThick * Math.sqrt(1 + svSlope * svSlope); doc.setDrawColor(...GREY); doc.setLineWidth(0.15); doc.rect(svX0, py(hRight), sT * scale, hRight * scale, 'S'); doc.rect(svFrame1, py(hRight), sT * scale, hRight * scale, 'S'); doc.setDrawColor(...RED); doc.setLineWidth(0.2); doc.rect(svFrame0, py(hRight), svFrame1 - svFrame0, hRight * scale, 'S'); doc.rect(svFrame0, py(CONFIG.plateThick), svFrame1 - svFrame0, CONFIG.plateThick * scale, 'S'); doc.rect(svFrame0, py(hRight), svFrame1 - svFrame0, svVOff * scale, 'S'); doc.setTextColor(...GREY); doc.setFontSize(6); doc.text('Zijaanzicht', (svX0 + svX1) / 2, py(maxH) - 4, { align: 'center' }); doc.text('buiten', svX1 + 1.5, floorY - 1); doc.setTextColor(...BLUE); doc.setFontSize(5.5); doc.text(`${sT} ${frameD} ${sT}`, (svX0 + svX1) / 2, floorY + 5, { align: 'center' }); } return { drawSheet }; })();