v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-06 18:40:11 +02:00
co-authored by Claude Opus 5.5
parent 8cdfa21f46
commit e1a688728c
30 changed files with 6667 additions and 868 deletions
+256 -23
View File
@@ -20,7 +20,7 @@ window.HousePdf = (() => {
const KIND_LABELS = {
'vloerbalk': 'Vloerbalk', 'dakbalk': 'Dakbalk', 'onderregel': 'Onderregel', 'bovenregel': 'Bovenregel',
'verticale balk': 'Verticale balk', 'kreupele stijl': 'Kreupele stijl',
'bovendorpel': 'Bovendorpel', 'onderdorpel': 'Onderdorpel'
'bovendorpel': 'Bovendorpel', 'onderdorpel': 'Onderdorpel', 'schuine regel': 'Schuine regel'
};
const PLAN_ELEMENT = 'PL-1';
const VIEWS_ELEMENT = 'AZ-1';
@@ -46,10 +46,13 @@ window.HousePdf = (() => {
list.push({ kind: 'floor', element: u.floorElement, label: `Unit ${u.id} vloer`, unit: u });
u.walls.forEach(w => w.pieces.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p })));
u.roof.planes.forEach(pl => list.push({ kind: 'roof', element: pl.element, label: pl.label, plane: pl }));
// Wolfsdak: het bovenste deel (wolfseinden, nok) op een eigen blad.
if (u.roof.unitRoof && u.roof.unitRoof.cap) list.push({ kind: 'cap', element: `${u.id}-D${u.roof.planes.length + 1}`, label: `Unit ${u.id} dak bovenste deel (wolfsdak)`, unit: u });
});
parts.interior.forEach(p => list.push({ kind: 'wall', element: p.element, label: p.label, piece: p }));
list.push({ kind: 'totals', element: 'TOT', label: 'Totalen' });
list.push({ kind: 'saw', element: 'ZAAG', label: 'Zaaglijst & zaagmachine' });
if (parts.elektra && parts.elektra.enabled) list.push({ kind: 'elektra', element: 'EL', label: 'Elektra' });
return list;
}
@@ -128,6 +131,9 @@ window.HousePdf = (() => {
doc.rect(px(l.x + l.normal.x * 80) - 0.9, py(l.z + l.normal.z * 80) - 0.9, 1.8, 1.8, 'FD');
});
// Elektra: meterkast, schakelaars (letters), wcd, leidingen (ElektraPdf).
ElektraPdf.drawPlan(doc, parts.elektra, px, py);
// Naden: gestippeld oranje
doc.setDrawColor(230, 130, 30);
doc.setLineWidth(0.4);
@@ -177,6 +183,7 @@ window.HousePdf = (() => {
doc.setFontSize(6);
doc.text('Plattegrond woning — oranje gestippeld = naad tussen units (wand vervalt), openingen gekruist, grijs gestippeld = dakrand, zwart gestippeld = ' +
'boeideel (o = spot), grijs = daktrim, geel blokje = gevellamp', drawX0, drawY1 + 4);
if (parts.elektra && parts.elektra.enabled) doc.text(ElektraPdf.PLAN_LEGEND, drawX0, drawY1 + 7.5);
}
// ------------------------------------------------------------
@@ -216,7 +223,7 @@ window.HousePdf = (() => {
const addWall = (p, y0) => {
const n = { x: p.place.normal.x, z: p.place.normal.y };
const zOut = p.place.thickness / 2 + WallModel.claddingOffset(p.buitenMateriaal, p.stone);
const color = p.stone ? (WallModel.STONE_COLORS[p.stone] || '#a0724f') : VIEW_COLORS.rabat;
const color = p.stone ? (WallModel.STONE_COLORS[p.stone] || '#a0724f') : WallModel.rabatHex(state.settings.facade.rabatColor);
faces.push({ pts: wallFace(p, y0).map(q => toWorld(p, q.x, q.y, zOut)), n, color, bias: 0 });
// Trasraam (strook onderaan, iets naar voren)
const tr = p.trasraam;
@@ -246,14 +253,7 @@ window.HousePdf = (() => {
if (sh.type === 'plat') return;
const cov = RoofModel.COVERINGS[r.covering] || RoofModel.COVERINGS.dakleer;
const ctr = sh.axis === 'x' ? u.rect.cx : u.rect.cz;
r.planes.forEach(pl => {
const a0 = ctr + pl.p0, a1 = ctr + pl.p1, mid = (a0 + a1) / 2;
// Hoogte net binnen het vlak (op de nok geeft topAt het hoogste vlak).
const inward = (t) => t + Math.sign(mid - t) * 0.5;
const corners = sh.axis === 'x'
? [[a0, r.rect.z0], [a1, r.rect.z0], [a1, r.rect.z1], [a0, r.rect.z1]]
: [[r.rect.x0, a0], [r.rect.x1, a0], [r.rect.x1, a1], [r.rect.x0, a1]];
const pts = corners.map(([x, z]) => ({ x, y: sh.axis === 'x' ? r.topAt(inward(x), z) : r.topAt(x, inward(z)), z }));
const pushRoof = (pts) => {
// Normaal (omhoog) uit het vlak.
const [A, B, C] = pts;
const ux = B.x - A.x, uy = B.y - A.y, uz = B.z - A.z, vx = C.x - A.x, vy = C.y - A.y, vz = C.z - A.z;
@@ -261,18 +261,45 @@ window.HousePdf = (() => {
if (ny < 0) { nx = -nx; ny = -ny; nz = -nz; }
const len = Math.hypot(nx, ny, nz) || 1;
faces.push({ pts, n: { x: nx / len, z: nz / len }, roof: true, color: cov.color || '#2a2a2a', bias: 0 });
};
// Wolfsdak: bovenste deel per vlak (RoofModel.capRegions, unit-lokaal (p, q)).
RoofModel.capRegions(r.unitRoof).forEach(rg => {
pushRoof(rg.poly.map(pt => {
const x = u.rect.cx + (sh.axis === 'x' ? pt.x : pt.z), z = u.rect.cz + (sh.axis === 'x' ? pt.z : pt.x);
return { x, y: u.layout.floorTop + rg.under(pt) + rg.vThick, z };
}));
});
r.planes.forEach(pl => {
const a0 = ctr + pl.p0, a1 = ctr + pl.p1, mid = (a0 + a1) / 2;
// Hoogte net binnen het vlak (op de nok geeft topAt het hoogste vlak).
const inward = (t) => t + Math.sign(mid - t) * 0.5;
const corners = sh.axis === 'x'
? [[a0, r.rect.z0], [a1, r.rect.z0], [a1, r.rect.z1], [a0, r.rect.z1]]
: [[r.rect.x0, a0], [r.rect.x1, a0], [r.rect.x1, a1], [r.rect.x0, a1]];
pushRoof(corners.map(([x, z]) => ({ x, y: sh.axis === 'x' ? r.topAt(inward(x), z) : r.topAt(x, inward(z)), z })));
});
});
// Dakrand: buitenvlak van elk boeideel / elke daktrim.
// Dakrand: elk boeideel / elke daktrim zoals je hem van buiten ziet: de
// bovenkant volgt het dak (sg.topAt, langs de ribbe bemonsterd, zodat
// knikken van een kopgevel - zadel, mansarde, wolfsdak - meekomen), en
// telt zowel de dakkant als de buitenkant mee (bij een hellend dak ligt
// de buitenkant lager, de bovenkant van het boeideel dekt de gevel af).
const edge = parts.roofEdge;
if (edge) {
const fc = (RoofModel.FASCIA_COLORS[edge.fasciaColor] || RoofModel.FASCIA_COLORS.antraciet).hex;
const lerp = (a, b, t) => ({ x: a.x + (b.x - a.x) * t, z: a.z + (b.z - a.z) * t });
edge.segments.forEach(sg => {
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
const A = sg.outerA, B = sg.outerB, tA = sg.topAt(A), tB = sg.topAt(B);
const pts = [{ x: A.x, y: tA, z: A.z }, { x: B.x, y: tB, z: B.z }, { x: B.x, y: tB - h, z: B.z }, { x: A.x, y: tA - h, z: A.z }];
faces.push({ pts, n: { x: sg.normal.x, z: sg.normal.z }, color: sg.fascia ? fc : 0xb8bcc0, bias: 3 });
const n = Math.max(1, Math.ceil(Math.hypot(sg.outerB.x - sg.outerA.x, sg.outerB.z - sg.outerA.z) / 100));
const top = [], bottom = [];
for (let k = 0; k <= n; k++) {
const t = k / n, I = lerp(sg.a, sg.b, t), O = lerp(sg.outerA, sg.outerB, t);
const tI = sg.topAt(I), tO = sg.topAt(O);
top.push({ x: O.x, y: Math.max(tI, tO), z: O.z });
bottom.push({ x: O.x, y: Math.min(tI, tO) - h, z: O.z });
}
faces.push({ pts: top.concat(bottom.reverse()), n: { x: sg.normal.x, z: sg.normal.z }, color: sg.fascia ? fc : 0xb8bcc0, bias: 3 });
});
}
return faces;
@@ -292,7 +319,7 @@ window.HousePdf = (() => {
seen.add(key);
items.push(fc.type === 'steen'
? { label: 'Gevel', value: `Steen ${fc.stone}`, thumb: WallModel.stoneThumbUrl(fc.stone, 64), color: WallModel.STONE_COLORS[fc.stone] }
: { label: 'Gevel', value: `Rabat (${(WallModel.CLADDING_DIRECTIONS[st.cladding] || st.cladding).toLowerCase()})`, color: VIEW_COLORS.rabat });
: { label: 'Gevel', value: `Rabat ${((WallModel.RABAT_COLORS[st.facade.rabatColor] || {}).label || '').toLowerCase()} (${(WallModel.CLADDING_DIRECTIONS[st.cladding] || st.cladding).toLowerCase()})`, color: WallModel.rabatHex(st.facade.rabatColor) });
}));
if (st.trasraam.enabled) {
const tf = st.trasraam.finish;
@@ -420,6 +447,80 @@ window.HousePdf = (() => {
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
// Blad voor het bovenste deel van een wolfsdak (RoofModel.computeCap):
// bovenaanzicht van het houtwerk (sporen, hoekkepers, nokbalk, kopbalken)
// op de dakvlakken, en de stuklijst.
const CAP_KINDS = { spoor: 'Spoor', hoekkeper: 'Hoekkeper', nokbalk: 'Nokbalk', kopbalk: 'Kopbalk' };
function drawCapSheet(doc, u, sheetOpts) {
const { RED, BLUE, BLACK, GREY } = PdfSheet.COLORS;
const ur = u.roof.unitRoof, c = ur.cap;
// (p, q) -> plattegrond (x, z), unit-lokaal mm.
const xz = (pt) => ur.alongX ? { x: pt.x, z: pt.z } : { x: pt.z, z: pt.x };
const band = [{ x: c.pA, z: c.q0 }, { x: c.pB, z: c.q0 }, { x: c.pB, z: c.q1 }, { x: c.pA, z: c.q1 }].map(xz);
const xs = band.map(p => p.x), zs = band.map(p => p.z);
const X0 = 20, X1 = 275, Y0 = 32, Y1 = 230;
const sc = Math.min((X1 - X0) / (Math.max(...xs) - Math.min(...xs)), (Y1 - Y0) / (Math.max(...zs) - Math.min(...zs)));
const ox = (X0 + X1) / 2 - (Math.min(...xs) + Math.max(...xs)) / 2 * sc, oz = (Y0 + Y1) / 2 - (Math.min(...zs) + Math.max(...zs)) / 2 * sc;
const P = (p) => [ox + p.x * sc, oz + p.z * sc];
const poly = (pts, style) => {
const q = pts.map(P);
doc.lines(q.slice(1).map((b, k) => [b[0] - q[k][0], b[1] - q[k][1]]), q[0][0], q[0][1], [1, 1], style, true);
};
doc.setTextColor(...BLACK);
doc.setFontSize(11);
doc.text('Bovenaanzicht bovenste deel (wolfseinden en nok)', X0, 20);
// Dakvlakken (grijs) en de band.
doc.setDrawColor(...GREY);
doc.setLineWidth(0.15);
c.regions.forEach(rg => poly(rg.poly.map(xz), 'S'));
// Balken (rood, plattegrond: rechthoek van balkbreedte rond de hartlijn).
doc.setDrawColor(...RED);
doc.setLineWidth(0.2);
c.beams.forEach(bm => poly(bm.poly.map(xz), 'S'));
// Maten: lengte en diepte van de band, diepte van een wolfseind.
doc.setDrawColor(...BLUE);
doc.setTextColor(...BLUE);
doc.setFontSize(7);
const bx0 = Math.min(...xs), bx1 = Math.max(...xs), bz0 = Math.min(...zs), bz1 = Math.max(...zs);
const [ax, ay] = P({ x: bx0, z: bz1 }), [bx, by] = P({ x: bx1, z: bz1 });
PdfSheet.dimLine(doc, ax, ay + 8, bx, by + 8);
doc.text(`${Math.round(bx1 - bx0)}`, (ax + bx) / 2, ay + 7, { align: 'center' });
const [cx0, cy0] = P({ x: bx1, z: bz0 }), [cx1, cy1] = P({ x: bx1, z: bz1 });
PdfSheet.dimLine(doc, cx0 + 8, cy0, cx1 + 8, cy1);
doc.text(`${Math.round(bz1 - bz0)}`, cx0 + 9.5, (cy0 + cy1) / 2, { angle: 90 });
// Stuklijst en kerngegevens.
const groups = new Map();
c.beams.forEach(bm => {
const key = `${bm.kind}|${Math.round(bm.length / 10) * 10}|${bm.angle}`;
const g = groups.get(key) || { kind: bm.kind, length: Math.round(bm.length / 10) * 10, angle: bm.angle, n: 0 };
g.n++;
groups.set(key, g);
});
const order = Object.keys(CAP_KINDS);
const rows = [...groups.values()].sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind) || b.length - a.length)
.map(g => [CAP_KINDS[g.kind] || g.kind, String(g.length), String(g.angle).replace('.', ','), String(g.n)]);
const H = (p) => RoofModel.profileHeight(ur.shape.profile, p);
const deg = (t) => String(Math.round(Math.atan(t) * 1800 / Math.PI) / 10).replace('.', ',');
const hip = c.regions.find(rg => rg.key === 'h1');
const facts = [
['Wolfseind (onderkant dak)', `${Math.round(c.hipH)} mm`],
['Nok (onderkant dak)', `${Math.round(H(c.pR))} mm`],
['Helling dakvlakken', `${deg((H(c.pR) - H(c.pA)) / (c.pR - c.pA))}°`],
['Helling wolfseinden', hip ? `${deg(Math.tan(Math.acos(hip.cos)))}°` : '-'],
['Balken', `${c.joistW} x ${c.joistH} mm`],
['Oppervlak (schuin)', `${fmt(c.specs.osbAreaM2, 2)} m²`],
['Hout', `${fmt(c.specs.joistWoodM3, 3)} m³`]
];
doc.setTextColor(...BLACK);
let y = PdfSheet.table(doc, 290, 20, [0, 52], ['Gegeven', 'Waarde'], facts, { title: 'Wolfsdak, bovenste deel', fontSize: 7, rowH: 4.6, lastColW: 40 });
PdfSheet.table(doc, 290, y + 6, [0, 32, 56, 78], ['Soort', 'Lengte (mm)', 'Hoek (°)', 'Aantal'], rows, { title: 'Stuklijst balken', fontSize: 7, rowH: 4.6, lastColW: 14, maxY: 240 });
doc.setFontSize(7);
doc.setTextColor(...GREY);
doc.text('Hoogtes vanaf bovenkant vloer. Lengte = langs de balk; hoek = helling van de balk (zaagsnede aan beide koppen). Grijs = dakvlakken, rood = houtwerk.', X0, 240);
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
function drawTotalsSheet(doc, totals, sheetOpts) {
const { BLACK } = PdfSheet.COLORS;
doc.setFont('helvetica', 'normal');
@@ -476,7 +577,7 @@ window.HousePdf = (() => {
}
// Eigen blad na de totalen: zaaglijst vuren (in kolommen) + zaagmachine.
function drawSawSheet(doc, totals, sheetOpts) {
function drawSawSheet(doc, totals, cost, sheetOpts) {
const { BLACK } = PdfSheet.COLORS;
doc.setFont('helvetica', 'normal');
doc.setTextColor(...BLACK);
@@ -496,10 +597,10 @@ window.HousePdf = (() => {
// TIJDELIJK (verwijderen voor productie): zaagmachine-overzicht.
const sw = totals.saw;
const hm = (h) => `${Math.floor(h)}:${String(Math.round((h % 1) * 60)).padStart(2, '0')}`;
PdfSheet.table(doc, 290, 30, [0, 55], ['Onderdeel', 'Waarde'], [
['Zaagmeters', fmt(sw.meters, 1) + ' m'],
[`Zaagtijd (${HouseModel.SAW_M_PER_H} m/u)`, hm(sw.hours) + ' u:mm'],
['Zaagtijd robot', CostModel.uur(cost.saw.robotSec / 3600) + ' uur'],
[`Zaagtijd handmatig (\u00d7${cost.settings.zagen.handFactor})`, CostModel.uur(cost.saw.handSec / 3600) + ' uur'],
['Zaagsneden', sw.cuts + ' st.'],
['Verschillende hoeken', String(sw.angles.length)]
].concat(sw.angles.map(a => [a.deg === 0 ? 'Haaks (0\u00b0)' : `Schuin ${fmt(a.deg, 1)}\u00b0`, a.count + ' sneden'])), tableOpts('Zaagmachine (tijdelijk)'));
@@ -507,12 +608,134 @@ window.HousePdf = (() => {
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
// ------------------------------------------------------------
// Kostenraming (CostModel): samenvatting + regels per element.
// ------------------------------------------------------------
const COST_ROW_H = 3.4;
// Kolommen van een detailblad: x, onderkant (rechts het titelblok).
const COST_COLS = [{ x: 15, yMax: 278 }, { x: 150, yMax: 278 }, { x: 285, yMax: 246 }];
const COST_Y0 = 30;
const clip = (t, n) => (t.length > n ? t.slice(0, n - 1) + '\u2026' : t);
// Platte regels voor de detailbladen: kop per element, dan de regels.
function costDetailRows(cost) {
return cost.elements.flatMap(el => elementCostRows(el));
}
// Kop + regels van één element.
function elementCostRows(el) {
const rows = [];
if (el.lines.length) {
rows.push({ head: true, label: `${el.element} ${el.label}`, mat: CostModel.eur(el.mat), time: CostModel.uur(el.sec / 3600) });
el.lines.forEach(l => rows.push({
label: l.label, qty: `${fmt(l.qty, ['st.', 'sneden', 'sectie'].includes(l.unit) ? 0 : 2)} ${l.unit}`,
mat: l.mat ? CostModel.eur(l.mat, 2) : '', time: l.sec ? CostModel.uur(l.sec / 3600) : ''
}));
}
return rows;
}
// Kostenblok op het blad van het onderdeel zelf (onder de hoofdtekening,
// links van het titelblok). Te veel regels: rest op het detailblad.
function drawElementCost(doc, el, extraLabel) {
if (!el || !el.lines.length) return;
const x = 132, y = 236, yMax = 287;
const rows = elementCostRows(el);
rows[0] = Object.assign({}, rows[0], { label: `Kosten ${el.element}${extraLabel ? ' (' + extraLabel + ')' : ''}` });
rows.push({ head: true, label: 'Kostprijs (incl. verlies, bezetting, indirect, QA, afschrijving)', mat: CostModel.eur(el.kostprijs), time: '' });
const fit = Math.floor((yMax - y - 5) / COST_ROW_H);
let shown = rows;
if (rows.length > fit) {
shown = rows.slice(0, fit - 2).concat([{ label: `+ ${rows.length - fit + 1} regels: zie kostenblad detail`, mat: '', time: '' }, rows[rows.length - 1]]);
}
drawCostTable(doc, x, y, shown);
}
// Verdeelt de regels over bladen (kolommen vullen van boven naar beneden).
function paginateCostRows(rows) {
const pages = [];
let i = 0;
while (i < rows.length) {
const cols = COST_COLS.map(c => {
const n = Math.floor((c.yMax - COST_Y0 - 6) / COST_ROW_H);
const chunk = rows.slice(i, i + n);
i += chunk.length;
return chunk;
});
pages.push(cols);
}
return pages;
}
function drawCostTable(doc, x, y, rows) {
const { BLACK, GREY } = PdfSheet.COLORS;
doc.setFontSize(6.5);
doc.setTextColor(...GREY);
doc.text('Omschrijving', x, y); doc.text('Hoeveelheid', x + 84, y, { align: 'right' });
doc.text('Materiaal', x + 104, y, { align: 'right' }); doc.text('Manuur', x + 124, y, { align: 'right' });
doc.setDrawColor(...GREY); doc.setLineWidth(0.15);
doc.line(x, y + 1.5, x + 126, y + 1.5);
y += 5;
rows.forEach(r => {
doc.setTextColor(...BLACK);
doc.setFont('helvetica', r.head ? 'bold' : 'normal');
doc.text(clip(r.label, r.head ? 60 : 52), r.head ? x : x + 2, y);
if (r.qty) doc.text(r.qty, x + 84, y, { align: 'right' });
doc.text(r.mat, x + 104, y, { align: 'right' });
doc.text(r.time, x + 124, y, { align: 'right' });
y += COST_ROW_H;
});
doc.setFont('helvetica', 'normal');
}
function drawCostSheet(doc, cost, sheetOpts) {
const { BLACK } = PdfSheet.COLORS;
const s = cost.summary, S = cost.settings, eur = (n) => CostModel.eur(n);
doc.setFont('helvetica', 'normal');
doc.setTextColor(...BLACK);
doc.setFontSize(14);
doc.text('Kostenraming (indicatief)', 15, 18);
const tableOpts = (title) => ({ title, fontSize: 7, rowH: 4.6, lastColW: 24 });
let y = PdfSheet.table(doc, 15, 30, [0, 82], ['Post', 'Bedrag'],
s.rows.map(r => [r.label, eur(r.amount)]).concat([['Totaal incl. btw', eur(s.inclBtw)]]), tableOpts('Samenvatting'));
y = PdfSheet.table(doc, 15, y + 6, [0, 82], ['Groep', 'Materiaal'],
cost.categories.filter(r => r.mat > 0).map(r => [r.name, eur(r.mat)]), tableOpts('Materiaal per groep'));
PdfSheet.table(doc, 15, y + 6, [0, 82], ['Werkzaamheid', 'Manuur'],
cost.activities.filter(r => r.sec > 0).map(r => [r.name, CostModel.uur(r.sec / 3600)])
.concat([[`Toeslag indirect ${S.arbeid.toeslagUren}%`, CostModel.uur(s.hoursIndirect)], [`QA ${S.arbeid.qa}%`, CostModel.uur(s.hoursQa)], ['Totaal', CostModel.uur(s.hours)]]),
tableOpts('Arbeid per werkzaamheid'));
PdfSheet.table(doc, 130, 30, [0, 72, 97, 122], ['Element / onderdeel', 'Materiaal', 'Manuur', 'Kostprijs*'],
cost.elements.filter(el => el.lines.length).map(el => [clip(`${el.element} ${el.label}`, 48), eur(el.mat), CostModel.uur(el.sec / 3600), eur(el.kostprijs)]),
Object.assign(tableOpts('Per element'), { maxY: 278 }));
PdfSheet.table(doc, 290, 30, [0, 40, 65, 88], ['Unit', 'Materiaal', 'Manuur', 'Kostprijs*'],
cost.units.map(u => [u.id ? `Unit ${u.id}` : 'Woning-breed', eur(u.mat), CostModel.uur(u.sec / 3600), eur(u.kostprijs)]), tableOpts('Per unit'));
doc.setFontSize(6.5);
doc.text('* Kostprijs: materiaal incl. verlies en klein materiaal + arbeid (manuren incl. bezetting, indirect en QA) + afschrijving/huisvesting', 290, 236);
doc.text(' (+ transport per unit, kraan bij woning-breed); zonder algemene kosten, marge en btw.', 290, 240);
doc.text('Schattingen op basis van Instellingen > Kosten & tijden; bedragen excl. btw tenzij vermeld.', 290, 244);
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
function drawCostDetailSheet(doc, cols, sheetOpts) {
doc.setFont('helvetica', 'normal');
doc.setTextColor(...PdfSheet.COLORS.BLACK);
doc.setFontSize(14);
doc.text('Kostenraming per element (detail)', 15, 18);
cols.forEach((rows, i) => { if (rows.length) drawCostTable(doc, COST_COLS[i].x, COST_Y0, rows); });
PdfSheet.titleBlock(doc, sheetOpts.project, sheetOpts);
}
function generate(state) {
const parts = HouseModel.computeParts(state, {
wallMaterials: WallModel.createMaterials(), floorMaterials: FloorModel.createMaterials(), roofMaterials: RoofModel.createMaterials()
});
const totals = HouseModel.computeTotals(parts);
const sheets = sheetList(parts);
const cost = CostModel.compute(parts, totals);
const costOf = (element) => cost.elements.find(el => el.element === element);
const drawWallWithCost = (sheet, opts) => {
WallPdf.drawSheet(doc, sheet.piece.config, sheet.piece.frame, Object.assign({ buitenMateriaal: sheet.piece.buitenMateriaal, cladding: sheet.piece.cladding, trasraam: sheet.piece.trasraam, skinExt: sheet.piece.skinExt, facade: sheet.piece.facade, skinFrom: sheet.piece.skinFrom, lamps: sheet.piece.lamps || [] }, opts));
drawElementCost(doc, costOf(sheet.element));
};
const detailPages = paginateCostRows(costDetailRows(cost));
sheets.push({ kind: 'cost', element: 'KOST', label: 'Kostenraming' });
detailPages.forEach((cols, i) => sheets.push({ kind: 'costdetail', element: `KOST-${i + 1}`, label: 'Kostenraming detail' + (detailPages.length > 1 ? ` (${i + 1})` : ''), cols }));
const project = state.project;
const doc = PdfSheet.createDoc();
@@ -521,14 +744,24 @@ window.HousePdf = (() => {
const opts = { project, titel: sheet.label, element: sheet.element, blad: `${i + 1} / ${sheets.length}` };
if (sheet.kind === 'views') drawViewsSheet(doc, parts, state, opts);
else if (sheet.kind === 'plan') drawPlanSheet(doc, parts, sheets, opts);
else if (sheet.kind === 'floor') FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, opts);
else if (sheet.kind === 'roof') FloorPdf.drawSheet(doc, sheet.plane.data.calcData, sheet.plane.data.resolvedOpenings, Object.assign({ roof: true }, opts));
else if (sheet.kind === 'wall') WallPdf.drawSheet(doc, sheet.piece.config, sheet.piece.frame, Object.assign({ buitenMateriaal: sheet.piece.buitenMateriaal, cladding: sheet.piece.cladding, trasraam: sheet.piece.trasraam, skinExt: sheet.piece.skinExt, facade: sheet.piece.facade, skinFrom: sheet.piece.skinFrom, lamps: sheet.piece.lamps || [] }, opts));
else if (sheet.kind === 'saw') drawSawSheet(doc, totals, opts);
else if (sheet.kind === 'floor') {
FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, opts);
drawElementCost(doc, costOf(sheet.element));
} else if (sheet.kind === 'roof') {
FloorPdf.drawSheet(doc, sheet.plane.data.calcData, sheet.plane.data.resolvedOpenings, Object.assign({ roof: true }, opts));
// Dakkosten per unit (alle dakvlakken samen): op het eerste dakvlak.
if (/-D1$/.test(sheet.element)) drawElementCost(doc, costOf(sheet.element.replace(/\d+$/, '')), 'alle dakvlakken');
}
else if (sheet.kind === 'cap') drawCapSheet(doc, sheet.unit, opts);
else if (sheet.kind === 'wall') drawWallWithCost(sheet, opts);
else if (sheet.kind === 'saw') drawSawSheet(doc, totals, cost, opts);
else if (sheet.kind === 'elektra') ElektraPdf.drawSheet(doc, parts.elektra, opts);
else if (sheet.kind === 'cost') drawCostSheet(doc, cost, opts);
else if (sheet.kind === 'costdetail') drawCostDetailSheet(doc, sheet.cols, opts);
else drawTotalsSheet(doc, totals, opts);
});
return doc;
}
return { generate, sheetList, materialThumbUrls };
return { generate, sheetList, materialThumbUrls, viewFaces, VIEWS };
})();