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

321 lines
15 KiB
JavaScript

/*!
* 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);
});
// --- Extra laag van de aanroeper (bijv. elektra, ElektraPdf):
// overlay(doc, pu, py), pu = wand-x (0..breedte) -> blad-mm.
if (opts.overlay) opts.overlay(doc, (u) => px(-wallWidth / 2 + u), py);
// --- 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 };
})();