Files

285 lines
14 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.
*/
// 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 };
})();