Files
Tectum/js/house/house-dxf.js
T

394 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*!
* Tectum
* Copyright © 2026 Machiel Heinen.
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md.
*/
// CAD-export van de woning (DXF): plattegrond en vier gevels in modelruimte, 1:1 in mm, met lagen per onderdeel.
// Plattegrond: noord boven, zuidwesthoek van de units = 0,0. Gevels in een 2×2-raster eronder, maaiveld als basis.
// Tekst, maatvoering en lijntypes zijn afgestemd op plotten op 1:50.
window.HouseDxf = (() => {
const SCALE = 50;
const CUT = 1500; // snijvlak plattegrond boven de vloer: openingen die lager beginnen zijn doorgesneden
const LAYERS = {
'PL-WAND-BUITEN': [7], 'PL-WAND-OPBOUW': [8], 'PL-GEVELBEKLEDING': [32], 'PL-WAND-BINNEN': [7],
'PL-KOZIJNEN': [4], 'PL-DRAAIRICHTING': [151], 'PL-SPARING-BOVEN': [4, 'STREEP'],
'PL-RUIMTE-GRENS': [3], 'PL-RUIMTE-TEKST': [7], 'PL-INRICHTING': [8], 'PL-DAKRAND': [8, 'STREEP'],
'PL-PALEN': [32], 'PL-SCHOORSTEEN': [8], 'PL-VERLICHTING': [2], 'PL-ELEKTRA': [6], 'PL-NADEN': [30, 'STREEPSTIP'],
'PL-ELEMENTCODES': [8],
'AZ-GEVEL': [7], 'AZ-TRASRAAM': [7], 'AZ-KOZIJNEN': [4], 'AZ-DAK': [7], 'AZ-DAKRAND': [8], 'AZ-SCHOORSTEEN': [8], 'AZ-MAAIVELD': [7],
'MAATVOERING': [5], 'TEKST': [7]
};
const FACE_LAYERS = { wand: 'AZ-GEVEL', opening: 'AZ-GEVEL', trasraam: 'AZ-TRASRAAM', dak: 'AZ-DAK', dakrand: 'AZ-DAKRAND', schoorsteen: 'AZ-SCHOORSTEEN' };
const mm = (paper) => paper * SCALE;
// Intervallen [a, b]: samenvoegen en aftrekken.
function mergeIv(list) {
const s = list.filter(([a, b]) => b > a).sort((p, q) => p[0] - q[0]);
const out = [];
s.forEach(([a, b]) => { const l = out[out.length - 1]; if (l && a <= l[1] + 1e-9) l[1] = Math.max(l[1], b); else out.push([a, b]); });
return out;
}
function subtractIv(from, cuts) {
let out = from.map(iv => iv.slice());
mergeIv(cuts).forEach(([c0, c1]) => {
out = out.flatMap(([a, b]) => {
if (c1 <= a || c0 >= b) return [[a, b]];
const r = [];
if (c0 > a) r.push([a, c0]);
if (c1 < b) r.push([c1, b]);
return r;
});
});
return out;
}
// Randen van een ruimte (RoomModel: as x/z, waarde v, s0..s1) tot gesloten lussen.
function roomLoops(edges) {
const key = (p) => Math.round(p.x) + ',' + Math.round(p.z);
const segs = edges.map(e => e.axis === 'x' ? [{ x: e.v, z: e.s0 }, { x: e.v, z: e.s1 }] : [{ x: e.s0, z: e.v }, { x: e.s1, z: e.v }]);
const at = new Map();
segs.forEach((s, i) => s.forEach(p => { const k = key(p); if (!at.has(k)) at.set(k, []); at.get(k).push(i); }));
const used = new Set(), out = [];
segs.forEach((s, i) => {
if (used.has(i)) return;
used.add(i);
const loop = [s[0], s[1]];
let cur = s[1];
while (key(cur) !== key(loop[0])) {
const j = (at.get(key(cur)) || []).find(n => !used.has(n));
if (j === undefined) break;
used.add(j);
cur = key(segs[j][0]) === key(cur) ? segs[j][1] : segs[j][0];
loop.push(cur);
}
const closed = key(loop[loop.length - 1]) === key(loop[0]);
if (closed) loop.pop();
const n = loop.length;
const pts = loop.filter((p, k) => {
if (!closed && (k === 0 || k === n - 1)) return true;
const a = loop[(k - 1 + n) % n], b = loop[(k + 1) % n];
return !((Math.abs(a.x - p.x) < 0.5 && Math.abs(b.x - p.x) < 0.5) || (Math.abs(a.z - p.z) < 0.5 && Math.abs(b.z - p.z) < 0.5));
});
out.push({ pts, closed });
});
return out;
}
// Cirkelboog door het eerste, middelste en laatste punt van een polylijn (draaicirkel van een deur).
function arcThrough(pts) {
const a = pts[0], b = pts[Math.floor(pts.length / 2)], c = pts[pts.length - 1];
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
if (Math.abs(d) < 1e-9) return null;
const s = (p) => p.x * p.x + p.y * p.y;
const ctr = { x: (s(a) * (b.y - c.y) + s(b) * (c.y - a.y) + s(c) * (a.y - b.y)) / d, y: (s(a) * (c.x - b.x) + s(b) * (a.x - c.x) + s(c) * (b.x - a.x)) / d };
const ang = (p) => Math.atan2(p.y - ctr.y, p.x - ctr.x);
const ccw = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0;
return { c: ctr, r: Math.hypot(a.x - ctr.x, a.y - ctr.y), a0: ccw ? ang(a) : ang(c), a1: ccw ? ang(c) : ang(a) };
}
// Tekstrichting langs een wand, altijd leesbaar (van onder of van rechts).
const readable = (deg) => { let a = ((deg % 360) + 360) % 360; if (a > 90.01 && a <= 270.01) a -= 180; return a; };
function drawPlan(dxf, parts, T) {
const W2 = (place, lx, lz) => T(UnitModel.localToWorld(place, lx, lz));
const ext = new Set();
parts.units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => ext.add(p))));
const P = KozijnModel.PROFILE;
HouseModel.allWallPieces(parts).forEach(p => {
const pl = p.place;
if (pl.baseY) return;
const isExt = ext.has(p);
const W = pl.width, zi = -pl.thickness / 2, zo = pl.thickness / 2;
const ops = (p.config.openings || []).filter(op => op.type !== 'schoorsteen');
const cutOps = ops.filter(op => op.y < CUT);
const holes = mergeIv(cutOps.map(op => [Math.max(-W / 2, -W / 2 + op.x), Math.min(W / 2, -W / 2 + op.x + op.w)]));
const solid = subtractIv([[-W / 2, W / 2]], holes);
const lay = isExt ? 'PL-WAND-BUITEN' : 'PL-WAND-BINNEN';
const box = (a, b, z0, z1, layer, eo) => dxf.polyline([W2(pl, a, z0), W2(pl, b, z0), W2(pl, b, z1), W2(pl, a, z1)], true, layer, eo);
solid.forEach(([a, b]) => box(a, b, zi, zo, lay));
const c = isExt ? WallModel.claddingOffset(p.buitenMateriaal, p.stone, p.stuc) : 0;
if (isExt) {
const sh = p.config.sheathingThick || 0;
if (sh > 0) solid.forEach(([a, b]) => [zi + sh, zo - sh].forEach(z => dxf.line(W2(pl, a, z), W2(pl, b, z), 'PL-WAND-OPBOUW')));
if (c > 0) {
// Hoek: buitenhoek loopt de bekleding door over de buurwand, binnenhoek stopt ze ervoor (skinExt < 0).
const e = p.skinExt || {};
const xl = -W / 2 - (e.left > 0 ? e.left + c : (e.left || 0)), xr = W / 2 + (e.right > 0 ? e.right + c : (e.right || 0));
subtractIv([[xl, xr]], holes).forEach(([a, b]) => box(a, b, zo, zo + c, 'PL-GEVELBEKLEDING'));
}
}
cutOps.forEach(op => {
const f = KozijnModel.normalize(op.fill);
if (!f) return;
const x0 = -W / 2 + op.x, x1 = x0 + op.w;
const exterior = p.buitenMateriaal === 'vochtdoek' || p.buitenMateriaal === 'cementplaat';
const kz1 = zo, kz0 = exterior ? Math.max(zi, zo - P.depth) : zi;
const a = x0 + P.gap, b = x1 - P.gap;
if (f.type === 'raam') {
box(a, b, kz0, kz1, 'PL-KOZIJNEN');
[a + P.kader, b - P.kader].forEach(x => dxf.line(W2(pl, x, kz0), W2(pl, x, kz1), 'PL-KOZIJNEN'));
dxf.line(W2(pl, a + P.kader, kz1 - 50), W2(pl, b - P.kader, kz1 - 50), 'PL-KOZIJNEN');
return;
}
box(a, a + P.kader, kz0, kz1, 'PL-KOZIJNEN');
box(b - P.kader, b, kz0, kz1, 'PL-KOZIJNEN');
KozijnModel.planSymbol(op, op.fill, { zIn: zi, zOut: zo }).forEach(sh => {
const pts = sh.pts.map(q => W2(pl, x0 + q.x, q.z));
if (sh.kind === 'arc') {
const arc = arcThrough(pts);
if (arc) dxf.arc(arc.c, arc.r, arc.a0, arc.a1, 'PL-DRAAIRICHTING');
} else if (sh.kind === 'rail') dxf.polyline(pts, !!sh.closed, 'PL-DRAAIRICHTING', { ltype: 'STIP' });
else dxf.polyline(pts, !!sh.closed, sh.kind === 'arrow' ? 'PL-DRAAIRICHTING' : 'PL-KOZIJNEN');
});
});
// Sparingen boven het snijvlak (bovenlichten, hoge ramen) gestreept, tenzij ze boven een doorgesneden opening liggen.
ops.filter(op => op.y >= CUT).forEach(op => {
const x0 = -W / 2 + op.x, x1 = x0 + op.w;
if (holes.some(([a, b]) => x0 >= a - 1 && x1 <= b + 1)) return;
box(x0, x1, zi, zo, 'PL-SPARING-BOVEN');
});
// Elementcode binnen langs de wand, verticaal gecentreerd op 2,5 mm (papier) van het binnenvlak.
const rot = readable(Math.atan2(W2(pl, 1, 0).y - W2(pl, 0, 0).y, W2(pl, 1, 0).x - W2(pl, 0, 0).x) * 180 / Math.PI) * Math.PI / 180;
const mid = W2(pl, 0, zi - mm(2.5)), th = mm(2);
dxf.text({ x: mid.x + Math.sin(rot) * th / 2, y: mid.y - Math.cos(rot) * th / 2 }, th, p.element, 'PL-ELEMENTCODES', { align: 'center', rot: rot * 180 / Math.PI });
if (isExt) {
const xs = [-W / 2, W / 2];
holes.forEach(([a, b]) => xs.push(a, b));
const ticks = [...new Set(xs.map(x => Math.round(x * 10) / 10))].sort((m, n) => m - n);
const zf = zo + c, row1 = zf + 700, row2 = row1 + 450;
for (let i = 0; i < ticks.length - 1; i++) {
dxf.dim(W2(pl, ticks[i], zf), W2(pl, ticks[i + 1], zf), W2(pl, (ticks[i] + ticks[i + 1]) / 2, row1), 'MAATVOERING');
}
if (ticks.length > 2) dxf.dim(W2(pl, -W / 2, zf), W2(pl, W / 2, zf), W2(pl, 0, row2), 'MAATVOERING');
}
});
const R = parts.rooms;
R.rooms.concat(R.free).forEach(r => {
roomLoops(r.edges || []).forEach(lp => dxf.polyline(lp.pts.map(T), lp.closed, 'PL-RUIMTE-GRENS'));
const named = !!r.name;
const lines = [[named ? r.name : 'Ruimte (geen functie)', 3.5], [`${r.areaM2.toLocaleString('nl-NL', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} m²`, 2.5]];
if (r.ceilingActive) lines.push([`plafond ${r.ceiling}`, 2]);
const c = T(r.label), step = mm(4.5);
lines.forEach(([t, size], i) => dxf.text({ x: c.x, y: c.y + ((lines.length - 1) / 2 - i) * step - mm(size) / 2 }, mm(size), t, 'PL-RUIMTE-TEKST', { align: 'center' }));
});
(parts.fixtures || []).forEach(it => FixtureModel.planShapes(it).forEach(sh => {
const eo = sh.kind === 'dash' ? { ltype: 'STREEP' } : null;
if (sh.circle) dxf.circle(T(sh.circle), sh.circle.r, 'PL-INRICHTING', eo);
else if (sh.text) { const q = T(sh.at); dxf.text({ x: q.x, y: q.y - mm(0.7) }, mm(1.4), sh.text, 'PL-INRICHTING', { align: 'center' }); }
else dxf.polyline(sh.pts.map(T), !!sh.closed, 'PL-INRICHTING', eo);
}));
if (parts.roofEdge) {
parts.roofEdge.segments.forEach(sg => dxf.line(T(sg.outerA), T(sg.outerB), 'PL-DAKRAND'));
parts.roofEdge.spots.forEach(sp => dxf.circle(T(sp), mm(0.8), 'PL-VERLICHTING'));
} else {
parts.units.forEach(u => {
const r = u.roof.rect;
dxf.polyline([{ x: r.x0, z: r.z0 }, { x: r.x1, z: r.z0 }, { x: r.x1, z: r.z1 }, { x: r.x0, z: r.z1 }].map(T), true, 'PL-DAKRAND');
});
}
parts.posts.posts.forEach(pp => {
const s = pp.size / 2;
dxf.polyline([{ x: pp.x - s, z: pp.z - s }, { x: pp.x + s, z: pp.z - s }, { x: pp.x + s, z: pp.z + s }, { x: pp.x - s, z: pp.z + s }].map(T), true, 'PL-PALEN');
});
parts.posts.walls.forEach(w => dxf.polyline([{ x: w.x0, z: w.z0 }, { x: w.x1, z: w.z0 }, { x: w.x1, z: w.z1 }, { x: w.x0, z: w.z1 }].map(T), true, 'PL-PALEN'));
(parts.chimneys || []).forEach(ch => {
dxf.polyline(ch.footprint.map(T), true, 'PL-SCHOORSTEEN');
dxf.polyline(ch.flueFootprint.map(T), true, 'PL-SCHOORSTEEN');
});
parts.lighting.lamps.forEach(l => {
const q = T({ x: l.x + l.normal.x * 80, z: l.z + l.normal.z * 80 }), s = mm(0.9);
dxf.polyline([{ x: q.x - s, y: q.y - s }, { x: q.x + s, y: q.y - s }, { x: q.x + s, y: q.y + s }, { x: q.x - s, y: q.y + s }], true, 'PL-VERLICHTING');
});
const o = T({ x: 0, z: 0 });
ElektraPdf.drawPlan(DxfWriter.pdfAdapter(dxf, 'PL-ELEKTRA'), parts.elektra, (x) => (x + o.x) / SCALE, (z) => (z - o.y) / SCALE);
parts.seams.forEach(s => dxf.line(T({ x: s.x0, z: s.z0 }), T({ x: s.x1, z: s.z1 }), 'PL-NADEN'));
parts.rects.forEach(r => {
const q = T({ x: r.x0 + 450, z: r.z0 + 450 });
dxf.text({ x: q.x, y: q.y - mm(8) }, mm(8), r.id, 'PL-ELEMENTCODES');
});
// Totaalmaten en noordpijl buiten alles.
const rects = parts.rects;
const bx0 = Math.min(...rects.map(r => r.x0)), bx1 = Math.max(...rects.map(r => r.x1));
const bz0 = Math.min(...rects.map(r => r.z0)), bz1 = Math.max(...rects.map(r => r.z1));
const e = dxf.extents();
dxf.dim(T({ x: bx0, z: bz0 }), T({ x: bx1, z: bz0 }), { x: 0, y: e.y1 + 700 }, 'MAATVOERING');
dxf.dim(T({ x: bx0, z: bz1 }), T({ x: bx0, z: bz0 }), { x: e.x0 - 700, y: 0 }, 'MAATVOERING');
const na = { x: e.x1 + mm(10), y: e.y1 - mm(4) }, ns = mm(4);
dxf.polyline([{ x: na.x, y: na.y + ns }, { x: na.x - ns * 0.4, y: na.y - ns * 0.6 }, { x: na.x, y: na.y - ns * 0.3 }, { x: na.x + ns * 0.4, y: na.y - ns * 0.6 }], true, 'TEKST');
dxf.text({ x: na.x, y: na.y - ns * 0.6 - mm(5) }, mm(3.5), 'N', 'TEKST', { align: 'center' });
}
function simplifyPoly(pts) {
let p = pts.filter((q, i) => { const r = pts[(i + 1) % pts.length]; return Math.hypot(r.x - q.x, r.y - q.y) > 0.5; });
let changed = true;
while (changed && p.length > 3) {
changed = false;
for (let i = 0; i < p.length; i++) {
const a = p[(i - 1 + p.length) % p.length], b = p[i], c = p[(i + 1) % p.length];
const len = Math.hypot(c.x - a.x, c.y - a.y);
if (len > 0 && Math.abs((c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)) / len < 0.5) { p.splice(i, 1); changed = true; break; }
}
}
return p;
}
const insidePoly = (q, poly) => {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const a = poly[i], b = poly[j];
if ((a.y > q.y) !== (b.y > q.y) && q.x < (b.x - a.x) * (q.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
}
return inside;
};
const distSeg = (q, a, b) => {
const dx = b.x - a.x, dy = b.y - a.y, l2 = dx * dx + dy * dy;
const t = l2 ? Math.max(0, Math.min(1, ((q.x - a.x) * dx + (q.y - a.y) * dy) / l2)) : 0;
return Math.hypot(q.x - a.x - t * dx, q.y - a.y - t * dy);
};
// Echt binnen (niet op de rand): lijnen die samenvallen met de rand van een vlak ervoor blijven zichtbaar.
const strictlyInside = (q, poly) => insidePoly(q, poly) && poly.every((a, i) => distSeg(q, a, poly[(i + 1) % poly.length]) > 0.5);
// Delen [t0, t1] van segment a-b die door poly worden afgedekt.
function coveredBy(a, b, poly) {
const ts = [0, 1], dx = b.x - a.x, dy = b.y - a.y;
poly.forEach((c, i) => {
const d = poly[(i + 1) % poly.length], ex = d.x - c.x, ey = d.y - c.y;
const den = dx * ey - dy * ex;
if (Math.abs(den) < 1e-9) return;
const t = ((c.x - a.x) * ey - (c.y - a.y) * ex) / den, s = ((c.x - a.x) * dy - (c.y - a.y) * dx) / den;
if (t > 0 && t < 1 && s >= -1e-9 && s <= 1 + 1e-9) ts.push(t);
});
ts.sort((m, n) => m - n);
const out = [];
for (let i = 0; i < ts.length - 1; i++) {
const t0 = ts[i], t1 = ts[i + 1];
if (t1 - t0 < 1e-7) continue;
const tm = (t0 + t1) / 2;
if (!strictlyInside({ x: a.x + dx * tm, y: a.y + dy * tm }, poly)) continue;
const l = out[out.length - 1];
if (l && Math.abs(l[1] - t0) < 1e-7) l[1] = t1; else out.push([t0, t1]);
}
return out;
}
// Verborgen lijnen weg: vlakken van achter naar voor, elke rand wordt afgedekt door de vlakken die later komen.
function hiddenLines(faces) {
const boxOf = (pts) => ({ x0: Math.min(...pts.map(p => p.x)), x1: Math.max(...pts.map(p => p.x)), y0: Math.min(...pts.map(p => p.y)), y1: Math.max(...pts.map(p => p.y)) });
const hit = (a, b) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
faces.forEach(f => { f.box = boxOf(f.poly); });
const out = [];
faces.forEach((f, i) => {
const segs = [];
const ring = (pts, layer) => pts.forEach((a, k) => segs.push({ a, b: pts[(k + 1) % pts.length], layer }));
ring(f.poly, f.layer);
f.detail.forEach(pl => ring(pl, 'AZ-KOZIJNEN'));
segs.forEach(sg => {
const len = Math.hypot(sg.b.x - sg.a.x, sg.b.y - sg.a.y);
if (len < 1) return;
const sb = boxOf([sg.a, sg.b]);
sb.x0 -= 1; sb.y0 -= 1; sb.x1 += 1; sb.y1 += 1;
let vis = [[0, 1]];
for (let j = i + 1; j < faces.length && vis.length; j++) {
if (!hit(sb, faces[j].box)) continue;
const cov = coveredBy(sg.a, sg.b, faces[j].poly);
if (cov.length) vis = subtractIv(vis, cov);
}
const at = (t) => ({ x: sg.a.x + (sg.b.x - sg.a.x) * t, y: sg.a.y + (sg.b.y - sg.a.y) * t });
vis.forEach(([t0, t1]) => { if ((t1 - t0) * len > 1) out.push({ a: at(t0), b: at(t1), layer: sg.layer }); });
});
});
return out;
}
// Samenvallende en in elkaars verlengde liggende stukken per laag samenvoegen (gedeelde randen maar één keer).
function mergeCollinear(segs) {
const groups = new Map();
segs.forEach(s => {
let dx = s.b.x - s.a.x, dy = s.b.y - s.a.y;
const len = Math.hypot(dx, dy);
dx /= len; dy /= len;
if (dx < -1e-9 || (Math.abs(dx) < 1e-9 && dy < 0)) { dx = -dx; dy = -dy; }
const off = -dy * s.a.x + dx * s.a.y;
const key = `${s.layer}|${Math.round(Math.atan2(dy, dx) * 2000)}|${Math.round(off)}`;
if (!groups.has(key)) groups.set(key, { dx, dy, off, layer: s.layer, iv: [] });
const g = groups.get(key);
const ta = s.a.x * dx + s.a.y * dy, tb = s.b.x * dx + s.b.y * dy;
g.iv.push([Math.min(ta, tb), Math.max(ta, tb)]);
});
const out = [];
groups.forEach(g => mergeIv(g.iv).forEach(([t0, t1]) => {
const p = (t) => ({ x: g.dx * t - g.dy * g.off, y: g.dy * t + g.dx * g.off });
out.push({ a: p(t0), b: p(t1), layer: g.layer });
}));
return out;
}
function drawViews(dxf, parts, state, planExt) {
const { groundY, views } = HousePdf.viewProjections(parts, state);
if (!views.length) return;
const floorTop = Math.max(...parts.units.map(u => u.layout.floorTop));
const maxW = Math.max(...views.map(w => w.u1 - w.u0)), maxH = Math.max(...views.map(w => w.v1 - groundY));
const gapX = 5000, cellH = maxH + 4500;
views.forEach((w, i) => {
const col = i % 2, row = Math.floor(i / 2);
const ox = planExt.x0 + col * (maxW + gapX) - w.u0;
const base = planExt.y0 - 3500 - maxH - row * cellH;
const P = (u, v) => ({ x: ox + u, y: base + (v - groundY) });
const faces = w.list.map(l => ({
poly: simplifyPoly(l.pts.map(p => ({ x: p.u, y: p.v }))),
layer: FACE_LAYERS[l.f.kind] || 'AZ-GEVEL',
detail: (l.f.detail || []).map(pl => simplifyPoly(pl.map(p => ({ x: w.v.u(p), y: p.y }))))
})).filter(f => f.poly.length >= 3);
mergeCollinear(hiddenLines(faces)).forEach(s => dxf.line(P(s.a.x, s.a.y), P(s.b.x, s.b.y), s.layer));
dxf.line(P(w.u0 - 1000, groundY), P(w.u1 + 1000, groundY), 'AZ-MAAIVELD');
const left = P(w.u0, groundY), xd = left.x - 900;
if (floorTop - groundY > 1) dxf.dim(P(w.u0, groundY), P(w.u0, floorTop), { x: xd, y: 0 }, 'MAATVOERING', { dir: { x: 0, y: 1 } });
dxf.dim(P(w.u0, floorTop), P(w.u0, w.v1), { x: xd, y: 0 }, 'MAATVOERING', { dir: { x: 0, y: 1 } });
const peil = P(w.u0, floorTop);
dxf.line({ x: xd - mm(10), y: peil.y }, { x: xd - mm(2), y: peil.y }, 'MAATVOERING');
dxf.text({ x: xd - mm(10), y: peil.y + mm(1) }, mm(2), 'peil', 'MAATVOERING');
dxf.dim(P(w.u0, groundY), P(w.u1, groundY), { x: 0, y: base - 700 }, 'MAATVOERING');
dxf.text(P((w.u0 + w.u1) / 2, groundY - 1300 - mm(5)), mm(5), w.v.title.toUpperCase(), 'TEKST', { align: 'center' });
});
}
function generate(state) {
const parts = HouseModel.computeParts(state, { skip3D: true });
if (!parts.units.length) throw new Error('geen units in het ontwerp');
const dxf = DxfWriter.create({ scale: SCALE });
Object.entries(LAYERS).forEach(([name, [color, ltype]]) => dxf.layer(name, color, ltype));
const bx0 = Math.min(...parts.rects.map(r => r.x0)), bz1 = Math.max(...parts.rects.map(r => r.z1));
const T = (p) => ({ x: p.x - bx0, y: bz1 - p.z });
drawPlan(dxf, parts, T);
const planExt = dxf.extents();
dxf.text({ x: planExt.x0, y: planExt.y0 - mm(12) }, mm(5), 'PLATTEGROND', 'TEKST');
drawViews(dxf, parts, state, planExt);
const pr = state.project || {};
const version = typeof APP_VERSION !== 'undefined' ? ` ${APP_VERSION}` : '';
const info = [
pr.naam, [pr.straat, pr.nummer].filter(Boolean).join(' '), pr.plaats,
`Tectum${version}, ${new Date().toLocaleDateString('nl-NL')}`,
'Maten in mm, peil = bovenkant vloer; tekst en maatvoering voor 1:50.'
].filter(Boolean);
info.forEach((t, i) => dxf.text({ x: planExt.x1 + mm(30), y: planExt.y1 - mm(5) - i * mm(4.5) }, mm(i === 0 && pr.naam ? 3.5 : 2.5), t, 'TEKST'));
return dxf;
}
return { generate, LAYERS };
})();