feat: add geometry helpers for mm conversion, simplification and export prep
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a39122c504
commit
b142c4f766
2 files changed
+180
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
bounds, formatMm, prepareForExport, scrapFilename, shapeSize, simplifyRing,
|
||||
} from '../src/lib/geometry';
|
||||
import type { Point } from '../src/lib/types';
|
||||
|
||||
/** Square ring from (x0,y0) with side `size`, one point every `step` units. */
|
||||
function denseSquare(x0: number, y0: number, size: number, step = 1): Point[] {
|
||||
const pts: Point[] = [];
|
||||
for (let i = 0; i < size; i += step) pts.push({ x: x0 + i, y: y0 });
|
||||
for (let i = 0; i < size; i += step) pts.push({ x: x0 + size, y: y0 + i });
|
||||
for (let i = 0; i < size; i += step) pts.push({ x: x0 + size - i, y: y0 + size });
|
||||
for (let i = 0; i < size; i += step) pts.push({ x: x0, y: y0 + size - i });
|
||||
return pts;
|
||||
}
|
||||
|
||||
describe('bounds / shapeSize', () => {
|
||||
it('measures the outer ring', () => {
|
||||
const shape = { outer: denseSquare(5, 10, 20), holes: [] };
|
||||
expect(bounds(shape.outer)).toEqual({ minX: 5, minY: 10, maxX: 25, maxY: 30 });
|
||||
expect(shapeSize(shape)).toEqual({ width: 20, height: 20 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplifyRing', () => {
|
||||
it('reduces collinear points of a square to its 4 corners', () => {
|
||||
const result = simplifyRing(denseSquare(0, 0, 10), 0.1);
|
||||
expect(result).toHaveLength(4);
|
||||
expect(bounds(result)).toEqual({ minX: 0, minY: 0, maxX: 10, maxY: 10 });
|
||||
});
|
||||
|
||||
it('removes wiggles below tolerance but keeps bumps above it', () => {
|
||||
const ring = denseSquare(0, 0, 100);
|
||||
ring[150] = { x: 100.3, y: 50 }; // on the right edge, 0.3 out
|
||||
expect(simplifyRing(ring, 0.5)).toHaveLength(4);
|
||||
ring[150] = { x: 102, y: 50 }; // 2 out
|
||||
expect(simplifyRing(ring, 0.5).some((p) => p.x === 102)).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves tiny rings alone', () => {
|
||||
const tri = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }];
|
||||
expect(simplifyRing(tri, 5)).toEqual(tri);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareForExport', () => {
|
||||
it('converts px to mm and moves the outer bounding box to the origin', () => {
|
||||
const shapePx = { outer: denseSquare(100, 50, 200), holes: [denseSquare(150, 100, 40)] };
|
||||
const mm = prepareForExport(shapePx, 4)!; // 4 px per mm
|
||||
expect(bounds(mm.outer)).toEqual({ minX: 0, minY: 0, maxX: 50, maxY: 50 });
|
||||
expect(mm.holes).toHaveLength(1);
|
||||
expect(bounds(mm.holes[0])).toEqual({ minX: 12.5, minY: 12.5, maxX: 22.5, maxY: 22.5 });
|
||||
expect(mm.outer).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('drops holes smaller than 3 mm in both directions (glare/speckle)', () => {
|
||||
const shapePx = {
|
||||
outer: denseSquare(0, 0, 400),
|
||||
holes: [denseSquare(100, 100, 8), denseSquare(200, 200, 40)], // 2 mm and 10 mm at 4 px/mm
|
||||
};
|
||||
const mm = prepareForExport(shapePx, 4)!;
|
||||
expect(mm.holes).toHaveLength(1);
|
||||
expect(shapeSize({ outer: mm.holes[0], holes: [] }).width).toBeCloseTo(10);
|
||||
});
|
||||
|
||||
it('keeps a long thin hole that is under 3 mm in only one direction', () => {
|
||||
const slot: Point[] = [{ x: 100, y: 100 }, { x: 300, y: 100 }, { x: 300, y: 104 }, { x: 100, y: 104 }];
|
||||
const mm = prepareForExport({ outer: denseSquare(0, 0, 400), holes: [slot] }, 4)!;
|
||||
expect(mm.holes).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns null when the outer ring degenerates', () => {
|
||||
expect(prepareForExport({ outer: [{ x: 0, y: 0 }, { x: 1, y: 1 }], holes: [] }, 4)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrapFilename / formatMm', () => {
|
||||
it('names the file after the rounded size', () => {
|
||||
const shape = { outer: [{ x: 0, y: 0 }, { x: 412.4, y: 0 }, { x: 412.4, y: 286.6 }, { x: 0, y: 286.6 }], holes: [] };
|
||||
expect(scrapFilename(shape, 'dxf')).toBe('scrap-412x287mm.dxf');
|
||||
expect(scrapFilename(shape, 'svg')).toBe('scrap-412x287mm.svg');
|
||||
});
|
||||
|
||||
it('formats numbers compactly', () => {
|
||||
expect(formatMm(1.23456)).toBe('1.235');
|
||||
expect(formatMm(2)).toBe('2');
|
||||
expect(formatMm(-0.0001)).toBe('0');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user