313 lines
15 KiB
JavaScript
313 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;
|
|
const x0 = px(opXWorld), y0 = py(op.y + op.h), x1 = px(opXWorld + op.w), y1 = py(op.y);
|
|
doc.rect(x0, y0, x1 - x0, y1 - y0, 'S');
|
|
doc.line(x0, y0, x1, y1);
|
|
doc.line(x0, y1, x1, y0);
|
|
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.w}x${op.h}`, (x0 + x1) / 2, y0 + 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 });
|
|
if (tl.peak) {
|
|
// Nokhoogte: stippellijn van vloer tot nok.
|
|
doc.setLineDashPattern([1, 1], 0);
|
|
doc.line(px(tl.peak.x), py(0), px(tl.peak.x), py(tl.peak.y));
|
|
doc.setLineDashPattern([], 0);
|
|
doc.text('nok ' + Math.round(tl.peak.y), px(tl.peak.x) + 1.5, py(tl.peak.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 };
|
|
})();
|