Files
immortaly007andClaude Opus 5.5 41baa25a01 fix: drop glare holes before the margin and never auto-pick specks
Filtering holes after the margin let -2 mm grow 1 mm specks past the
3 mm cutoff, and could drop real holes shrunk by a positive margin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:07:16 +02:00

79 lines
3.3 KiB
TypeScript

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('keeps every hole, however small (speck filtering happens before the margin)', () => {
const shapePx = { outer: denseSquare(0, 0, 400), holes: [denseSquare(100, 100, 8), denseSquare(200, 200, 40)] };
expect(prepareForExport(shapePx, 4)!.holes).toHaveLength(2);
});
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');
});
});