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,91 @@
|
||||
import type { Point, Shape } from './types';
|
||||
|
||||
export type Bounds = { minX: number; minY: number; maxX: number; maxY: number };
|
||||
|
||||
const MIN_HOLE_MM = 3;
|
||||
|
||||
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) => {
|
||||
if (hole.length < 3) return false;
|
||||
const { width, height } = shapeSize({ outer: hole, holes: [] });
|
||||
return width >= MIN_HOLE_MM || height >= MIN_HOLE_MM;
|
||||
});
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user