/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // DXF R12 (AC1009): het formaat dat elk CAD-pakket opent; DWG maakt het CAD-pakket zelf (Opslaan als). // Eenheid mm, tekst in ANSI_1252. Maten zijn echte DIMENSION-entiteiten (met blok), dus aanpasbaar in CAD. window.DxfWriter = (() => { const CP1252 = { 0x20ac: 0x80, 0x201a: 0x82, 0x0192: 0x83, 0x201e: 0x84, 0x2026: 0x85, 0x2020: 0x86, 0x2021: 0x87, 0x02c6: 0x88, 0x2030: 0x89, 0x0160: 0x8a, 0x2039: 0x8b, 0x0152: 0x8c, 0x017d: 0x8e, 0x2018: 0x91, 0x2019: 0x92, 0x201c: 0x93, 0x201d: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97, 0x02dc: 0x98, 0x2122: 0x99, 0x0161: 0x9a, 0x203a: 0x9b, 0x0153: 0x9c, 0x017e: 0x9e, 0x0178: 0x9f }; const LTYPES = { CONTINUOUS: { desc: 'Solid line', dashes: [] }, STREEP: { desc: '__ __ __', dashes: [4, -2] }, STIP: { desc: '. . . .', dashes: [0.8, -0.8] }, STREEPSTIP: { desc: '__ . __ .', dashes: [6, -1.5, 0.5, -1.5] } }; const num = (v) => { const r = Math.round(v * 1000) / 1000; return Object.is(r, -0) || !isFinite(r) ? '0' : String(r); }; const deg = (rad) => ((rad * 180 / Math.PI) % 360 + 360) % 360; // Een groepswaarde mag geen regeleinde bevatten. const clean = (s) => String(s == null ? '' : s).replace(/[\r\n]+/g, ' '); function create(opts) { const o = opts || {}; const scale = o.scale || 50; // plotschaal: tekst/maatvoering/lijntypes in papier-mm × scale const layers = new Map([['0', { color: 7, ltype: 'CONTINUOUS' }]]); const entities = []; const blocks = []; let dimCount = 0; const ext = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity }; const grow = (x, y) => { if (x < ext.x0) ext.x0 = x; if (x > ext.x1) ext.x1 = x; if (y < ext.y0) ext.y0 = y; if (y > ext.y1) ext.y1 = y; }; function layer(name, color, ltype) { if (!layers.has(name)) layers.set(name, { color: color || 7, ltype: LTYPES[ltype] ? ltype : 'CONTINUOUS' }); return name; } function common(out, lay, eo) { out.push(8, lay || '0'); if (eo && eo.ltype && LTYPES[eo.ltype]) out.push(6, eo.ltype); if (eo && eo.color !== undefined && eo.color !== null) out.push(62, eo.color); } function emit(target, type, lay, eo, body) { const out = [0, type]; common(out, lay, eo); target.push(out.concat(body)); } function line(a, b, lay, eo, target) { if (Math.hypot(b.x - a.x, b.y - a.y) < 0.01) return; emit(target || entities, 'LINE', lay, eo, [10, num(a.x), 20, num(a.y), 30, 0, 11, num(b.x), 21, num(b.y), 31, 0]); if (!target) { grow(a.x, a.y); grow(b.x, b.y); } } function polyline(pts, closed, lay, eo, target) { const p = pts.filter((q, i) => i === 0 || Math.hypot(q.x - pts[i - 1].x, q.y - pts[i - 1].y) > 0.01); if (closed && p.length > 2 && Math.hypot(p[0].x - p[p.length - 1].x, p[0].y - p[p.length - 1].y) < 0.01) p.pop(); if (p.length < 2) return; if (p.length === 2 && !closed) { line(p[0], p[1], lay, eo, target); return; } const t = target || entities; emit(t, 'POLYLINE', lay, eo, [66, 1, 10, 0, 20, 0, 30, 0, 70, closed ? 1 : 0]); p.forEach(q => { emit(t, 'VERTEX', lay, null, [10, num(q.x), 20, num(q.y), 30, 0]); if (!target) grow(q.x, q.y); }); emit(t, 'SEQEND', lay, null, []); } function circle(c, r, lay, eo, target) { emit(target || entities, 'CIRCLE', lay, eo, [10, num(c.x), 20, num(c.y), 30, 0, 40, num(r)]); if (!target) { grow(c.x - r, c.y - r); grow(c.x + r, c.y + r); } } // Hoeken in radialen, tegen de klok in van a0 naar a1. function arc(c, r, a0, a1, lay, eo, target) { emit(target || entities, 'ARC', lay, eo, [10, num(c.x), 20, num(c.y), 30, 0, 40, num(r), 50, num(deg(a0)), 51, num(deg(a1))]); if (!target) { grow(c.x - r, c.y - r); grow(c.x + r, c.y + r); } } // h = teksthoogte in model-mm; align: left|center|right; rot in graden. function text(p, h, str, lay, to, target) { const s = clean(str); if (!s) return; const t = to || {}; const hj = t.align === 'center' ? 1 : t.align === 'right' ? 2 : 0; const body = [10, num(p.x), 20, num(p.y), 30, 0, 40, num(h), 1, s]; if (t.rot) body.push(50, num(t.rot)); body.push(7, 'STANDARD'); if (hj) body.push(72, hj, 11, num(p.x), 21, num(p.y), 31, 0); emit(target || entities, 'TEXT', lay, t, body); if (!target) { grow(p.x - s.length * h * 0.35, p.y); grow(p.x + s.length * h * 0.35, p.y + h); } } // Lineaire maat tussen p1 en p2, maatlijn door 'at' (evenwijdig aan p1-p2 tenzij dir gegeven). const DIM = { txt: 2.5, exo: 1.5, exe: 1.5, tsz: 1.25, gap: 0.8, dle: 1.5 }; function dim(p1, p2, at, lay, dopts) { const d = dopts || {}; let ux = p2.x - p1.x, uy = p2.y - p1.y; if (d.dir) { ux = d.dir.x; uy = d.dir.y; } const ul = Math.hypot(ux, uy); if (ul < 1e-9) return; ux /= ul; uy /= ul; const nx = -uy, ny = ux; const off = (at.x - p1.x) * nx + (at.y - p1.y) * ny; const s1 = (p1.x - at.x) * ux + (p1.y - at.y) * uy, s2 = (p2.x - at.x) * ux + (p2.y - at.y) * uy; const measured = Math.abs(s2 - s1); if (measured < 1) return; const q1 = { x: at.x + ux * s1, y: at.y + uy * s1 }, q2 = { x: at.x + ux * s2, y: at.y + uy * s2 }; const sg = Math.sign(s2 - s1) || 1, k = scale; const name = `*D${++dimCount}`; const be = []; const BB = { color: 0 }; // Maatlijn voorbij de streepjes, hulplijnen met afstand tot het object. line({ x: q1.x - ux * sg * DIM.dle * k, y: q1.y - uy * sg * DIM.dle * k }, { x: q2.x + ux * sg * DIM.dle * k, y: q2.y + uy * sg * DIM.dle * k }, '0', BB, be); [[p1, q1], [p2, q2]].forEach(([p, q]) => { const side = Math.sign(off) || 1; const a = { x: p.x + nx * side * DIM.exo * k, y: p.y + ny * side * DIM.exo * k }; const b = { x: q.x + nx * side * DIM.exe * k, y: q.y + ny * side * DIM.exe * k }; if (Math.abs(off) > DIM.exo * k) line(a, b, '0', BB, be); const t = DIM.tsz * k * Math.SQRT1_2; line({ x: q.x - (ux + nx) * t, y: q.y - (uy + ny) * t }, { x: q.x + (ux + nx) * t, y: q.y + (uy + ny) * t }, '0', BB, be); }); let ang = Math.atan2(uy, ux) * 180 / Math.PI; // Tekst leesbaar: van onder of van rechts. let tnx = nx, tny = ny; if (ang > 90.01 || ang <= -89.99) { ang += 180; tnx = -nx; tny = -ny; } ang = (ang + 360) % 360; const mid = { x: (q1.x + q2.x) / 2, y: (q1.y + q2.y) / 2 }; const label = d.text !== undefined ? d.text : String(Math.round(measured)); const tp = { x: mid.x + tnx * DIM.gap * k, y: mid.y + tny * DIM.gap * k }; text(tp, DIM.txt * k, label, '0', { align: 'center', rot: ang, color: 0 }, be); blocks.push({ name, entities: be }); const tm = { x: mid.x + tnx * (DIM.gap + DIM.txt / 2) * k, y: mid.y + tny * (DIM.gap + DIM.txt / 2) * k }; const body = [2, name, 3, 'TECTUM', 10, num(q2.x), 20, num(q2.y), 30, 0, 11, num(tm.x), 21, num(tm.y), 31, 0, 70, 0]; if (d.text !== undefined) body.push(1, clean(d.text)); body.push(13, num(p1.x), 23, num(p1.y), 33, 0, 14, num(p2.x), 24, num(p2.y), 34, 0, 50, num(deg(Math.atan2(uy, ux)))); emit(entities, 'DIMENSION', lay, null, body); [p1, p2, q1, q2, tm].forEach(p => grow(p.x, p.y)); } function header() { const has = isFinite(ext.x0); const e = has ? ext : { x0: 0, y0: 0, x1: 10000, y1: 10000 }; const v = [ ['$ACADVER', 1, 'AC1009'], ['$DWGCODEPAGE', 3, 'ANSI_1252'], ['$INSBASE', 10, 0, 20, 0, 30, 0], ['$EXTMIN', 10, num(e.x0), 20, num(e.y0), 30, 0], ['$EXTMAX', 10, num(e.x1), 20, num(e.y1), 30, 0], ['$LIMMIN', 10, num(e.x0), 20, num(e.y0)], ['$LIMMAX', 10, num(e.x1), 20, num(e.y1)], ['$LTSCALE', 40, scale], ['$TEXTSIZE', 40, 2.5 * scale], ['$TEXTSTYLE', 7, 'STANDARD'], ['$CLAYER', 8, '0'], ['$LUNITS', 70, 2], ['$LUPREC', 70, 0], ['$AUNITS', 70, 0], ['$AUPREC', 70, 0], ['$DIMSTYLE', 2, 'TECTUM'], ['$DIMSCALE', 40, scale], ['$DIMTXT', 40, DIM.txt], ['$DIMTSZ', 40, DIM.tsz], ['$DIMASZ', 40, 2.5], ['$DIMEXO', 40, DIM.exo], ['$DIMEXE', 40, DIM.exe], ['$DIMDLE', 40, DIM.dle], ['$DIMGAP', 40, DIM.gap], ['$DIMTAD', 70, 1], ['$DIMTIH', 70, 0], ['$DIMTOH', 70, 0], ['$DIMZIN', 70, 0] ]; const out = [0, 'SECTION', 2, 'HEADER']; v.forEach(([name, ...rest]) => out.push(9, name, ...rest)); out.push(0, 'ENDSEC'); return out; } function table(name, rows) { const out = [0, 'TABLE', 2, name, 70, rows.length]; rows.forEach(r => out.push(...r)); out.push(0, 'ENDTAB'); return out; } function tables() { const has = isFinite(ext.x0); const e = has ? ext : { x0: 0, y0: 0, x1: 10000, y1: 10000 }; const w = Math.max(1000, e.x1 - e.x0), h = Math.max(1000, e.y1 - e.y0); const vport = [0, 'VPORT', 2, '*ACTIVE', 70, 0, 10, 0, 20, 0, 11, 1, 21, 1, 12, num((e.x0 + e.x1) / 2), 22, num((e.y0 + e.y1) / 2), 13, 0, 23, 0, 14, 10, 24, 10, 15, 100, 25, 100, 16, 0, 26, 0, 36, 1, 17, 0, 27, 0, 37, 0, 40, num(Math.max(h, w / 1.6) * 1.08), 41, 1.6, 42, 50, 43, 0, 44, 0, 50, 0, 51, 0, 71, 0, 72, 100, 73, 1, 74, 3, 75, 0, 76, 0, 77, 0, 78, 0]; const ltypes = Object.entries(LTYPES).map(([name, lt]) => { const total = lt.dashes.reduce((a, d) => a + Math.abs(d), 0); const r = [0, 'LTYPE', 2, name, 70, 0, 3, lt.desc, 72, 65, 73, lt.dashes.length, 40, num(total)]; lt.dashes.forEach(d => r.push(49, num(d))); return r; }); const lay = [...layers.entries()].map(([name, l]) => [0, 'LAYER', 2, name, 70, 0, 62, l.color, 6, l.ltype]); const style = [[0, 'STYLE', 2, 'STANDARD', 70, 0, 40, 0, 41, 1, 50, 0, 71, 0, 42, 2.5 * scale, 3, 'arial.ttf', 4, '']]; const dimstyle = [[0, 'DIMSTYLE', 2, 'TECTUM', 70, 0, 3, '', 4, '', 5, '', 6, '', 7, '', 40, scale, 41, 2.5, 42, DIM.exo, 43, 7, 44, DIM.exe, 45, 0, 46, DIM.dle, 47, 0, 48, 0, 140, DIM.txt, 141, 0, 142, DIM.tsz, 143, 25.4, 144, 1, 145, 0, 146, 1, 147, DIM.gap, 71, 0, 72, 0, 73, 0, 74, 0, 75, 0, 76, 0, 77, 1, 78, 0, 170, 0, 171, 2, 172, 1, 173, 0, 174, 0, 175, 0, 176, 0, 177, 0, 178, 0]]; return [0, 'SECTION', 2, 'TABLES', ...table('VPORT', [vport]), ...table('LTYPE', ltypes), ...table('LAYER', lay), ...table('STYLE', style), ...table('VIEW', []), ...table('UCS', []), ...table('APPID', [[0, 'APPID', 2, 'ACAD', 70, 0]]), ...table('DIMSTYLE', dimstyle), 0, 'ENDSEC']; } function toString() { const out = [...header(), ...tables(), 0, 'SECTION', 2, 'BLOCKS']; blocks.forEach(b => { out.push(0, 'BLOCK', 8, '0', 2, b.name, 70, 1, 10, 0, 20, 0, 30, 0, 3, b.name); b.entities.forEach(en => out.push(...en)); out.push(0, 'ENDBLK', 8, '0'); }); out.push(0, 'ENDSEC', 0, 'SECTION', 2, 'ENTITIES'); entities.forEach(en => out.push(...en)); out.push(0, 'ENDSEC', 0, 'EOF'); const lines = []; for (let i = 0; i < out.length; i += 2) lines.push(String(out[i]).padStart(3, ' '), String(out[i + 1])); return lines.join('\r\n') + '\r\n'; } function toBytes() { const s = toString(); const bytes = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); bytes[i] = c < 0x80 || (c >= 0xa0 && c <= 0xff) ? c : (CP1252[c] || 0x3f); } return bytes; } function save(filename) { const blob = new Blob([toBytes()], { type: 'application/dxf' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } return { scale, layer, line, polyline, circle, arc, text, dim, toString, toBytes, save, extents: () => Object.assign({}, ext) }; } // jsPDF-achtige tekenlaag voor bestaande PDF-tekencode (papier-mm, y omlaag) op schaal 1:scale in een laag. // Kleur zwart = BYLAYER, anders de dichtstbijzijnde ACI-kleur; vullingen worden alleen omtrek. const ACI = [[1, 255, 0, 0], [2, 255, 255, 0], [3, 0, 255, 0], [4, 0, 255, 255], [5, 0, 0, 255], [6, 255, 0, 255], [8, 128, 128, 128], [9, 192, 192, 192], [30, 255, 127, 0], [40, 255, 191, 0], [32, 165, 82, 0], [94, 0, 127, 0], [12, 165, 0, 0]]; function aciOf(r, g, b) { if (r + g + b < 60) return null; let best = null, bd = Infinity; ACI.forEach(([i, R, G, B]) => { const d = (r - R) ** 2 + (g - G) ** 2 + (b - B) ** 2; if (d < bd) { bd = d; best = i; } }); return best; } function pdfAdapter(dxf, lay, map) { const k = dxf.scale; const P = map || ((x, y) => ({ x: x * k, y: -y * k })); let draw = null, txt = null, fontPt = 10, dashed = false; const eo = (c) => ({ color: c, ltype: dashed ? 'STIP' : undefined }); const ptToMm = 0.3528 * 0.72; const doc = { setDrawColor(r, g, b) { draw = aciOf(r, g === undefined ? r : g, b === undefined ? r : b); }, setTextColor(r, g, b) { txt = aciOf(r, g === undefined ? r : g, b === undefined ? r : b); }, setFillColor() {}, setLineWidth() {}, setFont() {}, setFontSize(pt) { fontPt = pt; }, setLineDashPattern(p) { dashed = !!(p && p.length); }, getTextWidth(s) { return String(s).length * fontPt * 0.3528 * 0.5; }, line(x1, y1, x2, y2) { dxf.line(P(x1, y1), P(x2, y2), lay, eo(draw)); }, rect(x, y, w, h) { dxf.polyline([P(x, y), P(x + w, y), P(x + w, y + h), P(x, y + h)], true, lay, eo(draw)); }, circle(x, y, r) { const c = P(x, y); dxf.circle(c, r * k, lay, eo(draw)); }, triangle(x1, y1, x2, y2, x3, y3) { dxf.polyline([P(x1, y1), P(x2, y2), P(x3, y3)], true, lay, eo(draw)); }, lines(segs, x, y, sc, style, closed) { const pts = [P(x, y)]; let cx = x, cy = y; segs.forEach(([dx, dy]) => { cx += dx; cy += dy; pts.push(P(cx, cy)); }); dxf.polyline(pts, !!closed, lay, eo(draw)); }, text(str, x, y, to) { const t = to || {}; dxf.text(P(x, y), fontPt * ptToMm * k, Array.isArray(str) ? str.join(' ') : str, lay, { align: t.align, rot: t.angle || 0, color: txt }); } }; return doc; } return { create, pdfAdapter, LTYPES }; })();