v0.5.3: elektra-buizen min. 50 mm uit elkaar (gebundeld alleen naar de meterkast), vloer-wcd met leiding door de vloer, elektra op de PDF-bladen per wand/vloer/dakvlak, kosten elektra (buis + draad per meter, boorgaten 90 s geteld op stijlen/regels/balken, dozen, groepenkast, keuring), dakbeschot OSB 18 mm bovenop de sporen, felsplaten als dakbedekking

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-06 20:54:49 +02:00
co-authored by Claude Opus 5.5
parent e1a688728c
commit b5510d56c7
13 changed files with 668 additions and 64 deletions
+3 -1
View File
@@ -934,7 +934,7 @@ window.HouseModel = (() => {
const gevelOpen = ext.reduce((a, p) => a + openingsMM2(p), 0);
const glas = ext.reduce((a, p) => a + (p.config.openings || []).filter(op => op.type === 'raam').reduce((b, op) => b + WallModel.openingArea(op), 0), 0);
const binnen = all.reduce((a, p) => a + ((p.stats && p.stats.spaanplaatAreaM2) || 0) + ((p.stats && p.stats.spaanplaatBuitenAreaM2) || 0), 0);
const dakM2 = parts.units.reduce((a, u) => a + (u.roof.specs.dakleerM2 || 0) + (u.roof.specs.rietM2 || 0), 0);
const dakM2 = parts.units.reduce((a, u) => a + (u.roof.specs.dakleerM2 || 0) + (u.roof.specs.rietM2 || 0) + (u.roof.specs.felsM2 || 0), 0);
const dakPlanMM2 = parts.units.reduce((a, u) => a + u.roof.rect.W * u.roof.rect.D, 0);
// Inhoud (bruto): per unit gemiddelde hoogte bovenkant dak boven de
// onderkant vloer, bemonsterd op een raster.
@@ -1089,7 +1089,9 @@ window.HouseModel = (() => {
},
{ naam: 'OSB-plaat (vloeren + plafonds dak)', m2: sumU(u => u.floorSpecs.osbAreaM2 + u.roof.specs.osbAreaM2), m3: sumU(u => u.floorSpecs.osbVolM3 + u.roof.specs.osbVolM3), kg: sumU(u => u.floorSpecs.osbKg + u.roof.specs.osbKg) },
{ naam: 'Dakleer (EPDM)', m2: sumU(u => u.roof.specs.dakleerM2), m3: null, kg: sumU(u => u.roof.specs.dakleerKg) },
{ naam: `Dakbeschot OSB ${RoofModel.BESCHOT_MM} mm (bovenop de sporen)`, m2: sumU(u => u.roof.specs.beschotM2 || 0), m3: sumU(u => u.roof.specs.beschotVolM3 || 0), kg: sumU(u => u.roof.specs.beschotKg || 0) },
{ naam: `Riet (${RoofModel.RIET.thickMM} mm)`, m2: sumU(u => u.roof.specs.rietM2 || 0), m3: null, kg: sumU(u => u.roof.specs.rietKg || 0) },
{ naam: `Felsplaten (staande naad, ${RoofModel.FELS.thickMM} mm)`, m2: sumU(u => u.roof.specs.felsM2 || 0), m3: null, kg: sumU(u => u.roof.specs.felsKg || 0) },
{
naam: 'Cementplaat (buitenzijde)', m2: byMat('cementplaat').reduce((a, p) => a + p.stats.cementplaatAreaM2, 0),
m3: byMat('cementplaat').reduce((a, p) => a + p.stats.cementplaatVolumeM3, 0), kg: byMat('cementplaat').reduce((a, p) => a + p.specs.buitenKg, 0)
+5 -3
View File
@@ -730,7 +730,8 @@ window.HousePdf = (() => {
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));
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 || [],
overlay: ElektraPdf.wallOverlay(parts.elektra, sheet.piece) }, opts));
drawElementCost(doc, costOf(sheet.element));
};
const detailPages = paginateCostRows(costDetailRows(cost));
@@ -745,10 +746,11 @@ window.HousePdf = (() => {
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);
FloorPdf.drawSheet(doc, sheet.unit.floorData.calcData, sheet.unit.floorData.resolvedOpenings, Object.assign({ overlay: ElektraPdf.floorOverlay(parts.elektra, sheet.unit) }, 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));
const pu = parts.units.find(u => u.roof.planes.includes(sheet.plane));
FloorPdf.drawSheet(doc, sheet.plane.data.calcData, sheet.plane.data.resolvedOpenings, Object.assign({ roof: true, overlay: pu && ElektraPdf.roofOverlay(parts.elektra, pu, sheet.plane) }, 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');
}
+62 -5
View File
@@ -582,7 +582,10 @@ function mountHouseEditor(container, callbacks) {
canvas.style.cursor = 'grabbing';
const r = canvas.getBoundingClientRect();
const w = toWorld(e.clientX - r.left, e.clientY - r.top);
elDrag.snap = ElektraModel.snapToWall(parts.elektra, w.x, w.z);
// Vloer-wcd: vrij in een unit (10 mm raster); de rest langs de wanden.
elDrag.snap = elDrag.key.startsWith('vwcd:')
? (parts.rects.some(rc => w.x > rc.x0 && w.x < rc.x1 && w.z > rc.z0 && w.z < rc.z1) ? { floor: true, x: Math.round(w.x / 10) * 10, z: Math.round(w.z / 10) * 10 } : null)
: ElektraModel.snapToWall(parts.elektra, w.x, w.z);
drawPlan();
});
function onElektraDrop() {
@@ -592,8 +595,14 @@ function mountHouseEditor(container, callbacks) {
canvas.style.cursor = tool === 'iw' ? 'crosshair' : 'default';
if (!d.moved || !d.snap) return;
suppressClick = true;
const pl = state.settings.elektra.place, prev = pl[d.key] || {};
pl[d.key] = { element: d.snap.element, side: d.snap.side, u: d.snap.u, y: prev.y };
if (d.snap.floor) {
const rc = parts.rects.find(q => d.snap.x > q.x0 && d.snap.x < q.x1 && d.snap.z > q.z0 && d.snap.z < q.z1);
const vw = state.settings.elektra.vloerWcd.find(o => 'vwcd:' + o.id === d.key);
if (rc && vw) Object.assign(vw, { unitId: rc.id, x: Math.round(d.snap.x - rc.x0), z: Math.round(d.snap.z - rc.z0) });
} else {
const pl = state.settings.elektra.place, prev = pl[d.key] || {};
pl[d.key] = { element: d.snap.element, side: d.snap.side, u: d.snap.u, y: prev.y };
}
selection = { type: 'elektra', id: d.key };
refresh();
}
@@ -635,6 +644,8 @@ function mountHouseEditor(container, callbacks) {
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
if (e.key === 'Escape') {
if (drawStart) { drawStart = null; updateHint(); drawPlan(); } else if (tool === 'iw') setTool('select');
} else if (e.key === 'Delete' && selection && selection.type === 'elektra' && String(selection.id).startsWith('vwcd:')) {
removeSelectedFloorWcd();
} else if (e.key === 'Delete' && selection && selection.type === 'iw') {
state.interiorWalls = state.interiorWalls.filter(w => w.id !== selection.id);
selection = null;
@@ -834,7 +845,16 @@ function mountHouseEditor(container, callbacks) {
const q = toPx(x.marker.x, x.marker.z);
ctx.fillStyle = '#4da6ff'; ctx.beginPath(); ctx.arc(q.x + 10, q.y - 9, 2.5, 0, Math.PI * 2); ctx.fill();
});
if (elDrag && elDrag.snap) {
el.floorWcds.forEach(w => {
const q = toPx(w.x, w.z);
ctx.fillStyle = '#fff'; ctx.strokeStyle = rgb(RC.wcd, 1); ctx.lineWidth = 1.5;
ctx.fillRect(q.x - 7, q.y - 7, 14, 14); ctx.strokeRect(q.x - 7, q.y - 7, 14, 14);
ctx.fillStyle = rgb(RC.wcd, 1); ctx.font = 'bold 8px sans-serif'; ctx.fillText('V' + w.group, q.x, q.y + 0.5);
});
if (elDrag && elDrag.snap && elDrag.snap.floor) {
const q = toPx(elDrag.snap.x, elDrag.snap.z);
ctx.strokeStyle = '#4da6ff'; ctx.lineWidth = 2; ctx.strokeRect(q.x - 9, q.y - 9, 18, 18);
} else if (elDrag && elDrag.snap) {
const sn = elDrag.snap, a = toPx(sn.x, sn.z), b = toPx(sn.x + sn.n.x * 200, sn.z + sn.n.z * 200);
ctx.strokeStyle = '#4da6ff'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
@@ -1131,6 +1151,8 @@ function mountHouseEditor(container, callbacks) {
groups.push({ label: 'Apparaten', html: Object.entries(ElektraModel.APPLIANCES).map(([k, A]) =>
check(`he-app-${k}`, `${A.label} (${A.code})`, !!e.apparaten[k])).join('') });
const nManual = Object.keys(e.place).length;
groups.push({ label: 'Vloer', html: big('Vloer-wcd', 'plus-square', 'data-he-add-vw title="Vloerdoos met dubbele wcd in de geselecteerde (of eerste) unit"') +
note(`${e.vloerWcd.length} vloer-wcd('s); leiding door de vloer.`) });
groups.push({ label: 'Plaatsing', html: note('Sleep meterkast, schakelaars, wcd en apparaten in de plattegrond; selecteer er een voor positie en hoogte.') +
(nManual ? big('Alles automatisch', 'arrow-counterclockwise', `data-he-reset title="${nManual} handmatige plek(ken) wissen"`) : '') });
groups.push({ label: 'Leidingen', html: num('he-slack', 'Marge %', e.slack, { step: 1 }) + num('he-res', 'Reserve', e.reserve, { step: 50, hint: 'Per leiding (lussen in de dozen), mm' }) +
@@ -1158,6 +1180,15 @@ function mountHouseEditor(container, callbacks) {
const cb = document.getElementById(`he-app-${k}`);
if (cb) cb.addEventListener('change', () => { e.apparaten[k] = cb.checked; refresh(); });
});
const av = body.querySelector('[data-he-add-vw]');
if (av) av.addEventListener('click', () => {
const uid = selection && selection.type === 'unit' ? selection.id : state.units[0].id;
const rc = parts && parts.rects.find(q => q.id === uid);
const id = e.vloerWcd.reduce((m, o) => Math.max(m, o.id || 0), 0) + 1;
e.vloerWcd.push({ id, unitId: uid, x: rc ? Math.round(rc.W / 2) : 1000, z: rc ? Math.round(rc.D / 2) : 1000 });
selection = { type: 'elektra', id: 'vwcd:' + id };
refresh();
});
const rs = body.querySelector('[data-he-reset]');
if (rs) rs.addEventListener('click', () => { e.place = {}; refresh(); });
const show = () => {
@@ -1220,10 +1251,36 @@ function mountHouseEditor(container, callbacks) {
return null;
}
function removeSelectedFloorWcd() {
const e = state.settings.elektra;
e.vloerWcd = e.vloerWcd.filter(o => 'vwcd:' + o.id !== selection.id);
selection = null;
refresh();
}
// Geselecteerd elektra-onderdeel: plek langs de wand en hoogte; slepen in
// de plattegrond of hier invullen maakt de plek handmatig.
function elektraItemTab(it) {
const e = state.settings.elektra;
if (it.kind === 'vwcd') {
const vw = e.vloerWcd.find(o => 'vwcd:' + o.id === it.key);
return {
key: 'sel', label: it.label, icon: 'plug', contextual: true, order: 100,
groups: () => [
{ label: 'Vloer-wcd', html: kv('Unit', it.unitId) + note('Vloerdoos met dubbele wcd; leiding door de vloer naar de meterkast.') },
{ label: 'Plaats', html: num('he-vw-x', 'Vanaf west', Math.round(it.x), { step: 10, hint: 'Hart, vanaf de westrand van de unit' }) +
num('he-vw-z', 'Vanaf noord', Math.round(it.z), { step: 10, hint: 'Hart, vanaf de noordrand van de unit' }) },
{ label: 'Bewerken', html: note('Sleep de vloer-wcd in de plattegrond (ook naar een andere unit).') + big('Verwijderen', 'trash', 'data-he-vw-del title="Vloer-wcd verwijderen (Del)"') }
],
wire: (body) => {
live = [];
UI.onNum('he-vw-x', v => { if (vw) vw.x = v; }, after);
UI.onNum('he-vw-z', v => { if (vw) vw.z = v; }, after);
const b = body.querySelector('[data-he-vw-del]');
if (b) b.addEventListener('click', removeSelectedFloorWcd);
}
};
}
const setPlace = (patch) => {
const cur = e.place[it.key] || { element: it.element, side: it.side, u: Math.round(it.u) };
e.place[it.key] = Object.assign({}, cur, patch);
@@ -1299,7 +1356,7 @@ function mountHouseEditor(container, callbacks) {
if (form.type === 'wolf') groups.push({ label: 'Wolfsdak', html:
UI.range('hp-roof-cut', 'Wolfseind', form.cut, { min: 2100, max: LIM.ridge, step: 10, hint: 'Hoogte waarop de kopgevels stoppen; daarboven driehoekige wolfseinden met de dakhelling tot de nok' }) });
groups.push({ label: 'Dak', html:
pair(sel('hp-roof-cover', 'Bedekking', covOpts, covNow, { hint: shapeNow === 'plat' ? 'Riet kan alleen bij een hellend dak' : `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²` }),
pair(sel('hp-roof-cover', 'Bedekking', covOpts, covNow, { hint: shapeNow === 'plat' ? 'Riet en felsplaten alleen bij een hellend dak' : `${c.thickMM} mm, ${String(c.kgM2).replace('.', ',')} kg/m²` }),
thumb(RoofModel.coveringThumbUrl(covNow, 64), c.label)) +
toggle('hp-fascia', 'Boeidelen', u.roof.fascia !== false, { hint: 'Uit = daktrim' }) +
toggle('hp-roof-follow', 'Maat volgt vloer', u.roof.followFloor) });