feat: add DXF (R12) and SVG exporters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
immortaly007andClaude Opus 5.5 committed 2026-10-15 14:39:29 +02:00
1 parent b142c4f766
commit 8d597e856e
4 files changed
+119

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { toDxf } from '../src/lib/export-dxf';
const shape = {
outer: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 50 }, { x: 0, y: 50 }],
holes: [[{ x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 20 }]],
};
/** Split DXF text into [groupCode, value] pairs. */
function pairs(dxf: string): [string, string][] {
const lines = dxf.split('\r\n');
if (lines[lines.length - 1] === '') lines.pop();
const out: [string, string][] = [];
for (let i = 0; i < lines.length; i += 2) out.push([lines[i].trim(), lines[i + 1]]);
return out;
}
describe('toDxf', () => {
it('is a well-formed R12 file in mm with CRLF line endings', () => {
const dxf = toDxf(shape);
expect(dxf).not.toMatch(/[^\r]\n/);
const p = pairs(dxf);
expect(p[0]).toEqual(['0', 'SECTION']);
expect(p).toContainEqual(['1', 'AC1009']);
const unitsIdx = p.findIndex(([c, v]) => c === '9' && v === '$INSUNITS');
expect(p[unitsIdx + 1]).toEqual(['70', '4']);
expect(p[p.length - 1]).toEqual(['0', 'EOF']);
});
it('writes one closed POLYLINE per ring with all vertices', () => {
const p = pairs(toDxf(shape));
const entities = p.filter(([c]) => c === '0').map(([, v]) => v);
expect(entities.filter((e) => e === 'POLYLINE')).toHaveLength(2);
expect(entities.filter((e) => e === 'VERTEX')).toHaveLength(7);
expect(entities.filter((e) => e === 'SEQEND')).toHaveLength(2);
const polyIdx = p.findIndex(([c, v]) => c === '0' && v === 'POLYLINE');
const flags = p.slice(polyIdx, polyIdx + 8).find(([c]) => c === '70');
expect(flags).toEqual(['70', '1']);
});
it('flips Y so the piece is not mirrored', () => {
const p = pairs(toDxf(shape));
const firstVertex = p.findIndex(([c, v]) => c === '0' && v === 'VERTEX');
const x = p.slice(firstVertex).find(([c]) => c === '10')![1];
const y = p.slice(firstVertex).find(([c]) => c === '20')![1];
expect([x, y]).toEqual(['0', '50']); // (0,0) top-left → (0,50) in Y-up space
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { toSvg } from '../src/lib/export-svg';
const shape = {
outer: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 50 }, { x: 0, y: 50 }],
holes: [[{ x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 20.5 }]],
};
describe('toSvg', () => {
it('declares its size in mm with a matching viewBox', () => {
const svg = toSvg(shape);
expect(svg).toContain('width="100mm"');
expect(svg).toContain('height="50mm"');
expect(svg).toContain('viewBox="0 0 100 50"');
});
it('writes one closed subpath per ring in a single evenodd path', () => {
const svg = toSvg(shape);
const d = svg.match(/ d="([^"]+)"/)![1];
expect(d.match(/M/g)).toHaveLength(2);
expect(d.match(/Z/g)).toHaveLength(2);
expect(d).toContain('M0,0 L100,0 L100,50 L0,50 Z');
expect(d).toContain('20,20.5');
expect(svg).toContain('fill-rule="evenodd"');
expect(svg).toContain('fill="none"');
});
});