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:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
+256
-23
@@ -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 };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user