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>
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import type { Point, Shape } from './types';
|
|
|
|
export type Bounds = { minX: number; minY: number; maxX: number; maxY: number };
|
|
|
|
export function bounds(points: Point[]): Bounds {
|
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
for (const p of points) {
|
|
if (p.x < minX) minX = p.x;
|
|
if (p.y < minY) minY = p.y;
|
|
if (p.x > maxX) maxX = p.x;
|
|
if (p.y > maxY) maxY = p.y;
|
|
}
|
|
return { minX, minY, maxX, maxY };
|
|
}
|
|
|
|
export function shapeSize(shape: Shape): { width: number; height: number } {
|
|
const b = bounds(shape.outer);
|
|
return { width: b.maxX - b.minX, height: b.maxY - b.minY };
|
|
}
|
|
|
|
export function simplifyRing(ring: Point[], tolerance: number): Point[] {
|
|
if (ring.length <= 3) return ring.slice();
|
|
// Split at the vertex farthest from the start so each half is an open polyline.
|
|
let far = 0;
|
|
let farDist = -1;
|
|
for (let i = 1; i < ring.length; i++) {
|
|
const d = Math.hypot(ring[i].x - ring[0].x, ring[i].y - ring[0].y);
|
|
if (d > farDist) {
|
|
farDist = d;
|
|
far = i;
|
|
}
|
|
}
|
|
const a = douglasPeucker(ring.slice(0, far + 1), tolerance);
|
|
const b = douglasPeucker([...ring.slice(far), ring[0]], tolerance);
|
|
return [...a.slice(0, -1), ...b.slice(0, -1)];
|
|
}
|
|
|
|
function douglasPeucker(points: Point[], tolerance: number): Point[] {
|
|
if (points.length < 3) return points;
|
|
const first = points[0];
|
|
const last = points[points.length - 1];
|
|
let index = 0;
|
|
let maxDist = 0;
|
|
for (let i = 1; i < points.length - 1; i++) {
|
|
const d = distanceToSegment(points[i], first, last);
|
|
if (d > maxDist) {
|
|
maxDist = d;
|
|
index = i;
|
|
}
|
|
}
|
|
if (maxDist <= tolerance) return [first, last];
|
|
const left = douglasPeucker(points.slice(0, index + 1), tolerance);
|
|
const right = douglasPeucker(points.slice(index), tolerance);
|
|
return [...left.slice(0, -1), ...right];
|
|
}
|
|
|
|
function distanceToSegment(p: Point, a: Point, b: Point): number {
|
|
const dx = b.x - a.x;
|
|
const dy = b.y - a.y;
|
|
const len2 = dx * dx + dy * dy;
|
|
if (len2 === 0) return Math.hypot(p.x - a.x, p.y - a.y);
|
|
const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));
|
|
return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
|
|
}
|
|
|
|
export function prepareForExport(shapePx: Shape, pxPerMm: number, toleranceMm = 0.5): Shape | null {
|
|
const toMm = (ring: Point[]) =>
|
|
simplifyRing(ring.map((p) => ({ x: p.x / pxPerMm, y: p.y / pxPerMm })), toleranceMm);
|
|
const outer = toMm(shapePx.outer);
|
|
if (outer.length < 3) return null;
|
|
const holes = shapePx.holes.map(toMm).filter((hole) => hole.length >= 3);
|
|
const { minX, minY } = bounds(outer);
|
|
const shift = (ring: Point[]) => ring.map((p) => ({ x: p.x - minX, y: p.y - minY }));
|
|
return { outer: shift(outer), holes: holes.map(shift) };
|
|
}
|
|
|
|
export function scrapFilename(shapeMm: Shape, ext: 'dxf' | 'svg'): string {
|
|
const { width, height } = shapeSize(shapeMm);
|
|
return `scrap-${Math.round(width)}x${Math.round(height)}mm.${ext}`;
|
|
}
|
|
|
|
export function formatMm(n: number): string {
|
|
const rounded = Number(n.toFixed(3));
|
|
return String(rounded === 0 ? 0 : rounded);
|
|
}
|