feat: shared types and arc flattening
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
959c1fa44e
commit
7ddaee1454
3 changed files with 116 additions and 0 deletions
35
src/geometry/arc.test.ts
Normal file
35
src/geometry/arc.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { flattenArc } from './arc';
|
||||||
|
|
||||||
|
const onCircle = (p: [number, number], c: [number, number], r: number) =>
|
||||||
|
Math.abs(Math.hypot(p[0] - c[0], p[1] - c[1]) - r) < 1e-6;
|
||||||
|
|
||||||
|
describe('flattenArc', () => {
|
||||||
|
it('flattens a CCW quarter circle, endpoints exact, all points on the circle', () => {
|
||||||
|
// centre (0,0), radius 10, from (10,0) CCW to (0,10)
|
||||||
|
const pts = flattenArc([10, 0], [0, 10], [0, 0], false, 0.1);
|
||||||
|
expect(pts[0]).toEqual([10, 0]);
|
||||||
|
expect(pts[pts.length - 1]).toEqual([0, 10]);
|
||||||
|
expect(pts.length).toBeGreaterThan(2);
|
||||||
|
for (const p of pts) expect(onCircle(p, [0, 0], 10)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CW arc sweeps the other way (midpoint has negative y)', () => {
|
||||||
|
// centre (0,0), radius 10, from (10,0) CW to (0,10) → goes the long way through y<0
|
||||||
|
const pts = flattenArc([10, 0], [0, 10], [0, 0], true, 0.1);
|
||||||
|
const mid = pts[Math.floor(pts.length / 2)];
|
||||||
|
expect(mid[1]).toBeLessThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats start==end as a full circle', () => {
|
||||||
|
const pts = flattenArc([10, 0], [10, 0], [0, 0], false, 0.5);
|
||||||
|
// a point roughly opposite the start should exist
|
||||||
|
expect(pts.some((p) => p[0] < -9)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects chord tolerance (tighter tolerance → more points)', () => {
|
||||||
|
const coarse = flattenArc([10, 0], [0, 10], [0, 0], false, 1.0);
|
||||||
|
const fine = flattenArc([10, 0], [0, 10], [0, 0], false, 0.01);
|
||||||
|
expect(fine.length).toBeGreaterThan(coarse.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
41
src/geometry/arc.ts
Normal file
41
src/geometry/arc.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { Vec2 } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flatten a circular arc into a polyline.
|
||||||
|
* @param start arc start point (mm)
|
||||||
|
* @param end arc end point (mm)
|
||||||
|
* @param center arc centre (mm)
|
||||||
|
* @param clockwise true for G2, false for G3 (in a right-handed XY frame, Y up)
|
||||||
|
* @param tolerance max chord deviation (mm)
|
||||||
|
* @returns points including both start and end
|
||||||
|
*/
|
||||||
|
export function flattenArc(
|
||||||
|
start: Vec2,
|
||||||
|
end: Vec2,
|
||||||
|
center: Vec2,
|
||||||
|
clockwise: boolean,
|
||||||
|
tolerance = 0.2,
|
||||||
|
): Vec2[] {
|
||||||
|
const radius = Math.hypot(start[0] - center[0], start[1] - center[1]);
|
||||||
|
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
|
||||||
|
const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
|
||||||
|
|
||||||
|
let sweep = endAngle - startAngle;
|
||||||
|
if (clockwise) {
|
||||||
|
while (sweep >= 0) sweep -= 2 * Math.PI; // sweep in [-2π, 0)
|
||||||
|
} else {
|
||||||
|
while (sweep <= 0) sweep += 2 * Math.PI; // sweep in (0, 2π]
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxStep = radius > tolerance ? 2 * Math.acos(1 - tolerance / radius) : Math.PI / 8;
|
||||||
|
const steps = Math.max(1, Math.ceil(Math.abs(sweep) / maxStep));
|
||||||
|
|
||||||
|
const pts: Vec2[] = [];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const a = startAngle + (sweep * i) / steps;
|
||||||
|
pts.push([center[0] + radius * Math.cos(a), center[1] + radius * Math.sin(a)]);
|
||||||
|
}
|
||||||
|
pts[0] = [start[0], start[1]];
|
||||||
|
pts[pts.length - 1] = [end[0], end[1]]; // pin endpoints exactly
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
40
src/types.ts
Normal file
40
src/types.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
export type Vec2 = [number, number];
|
||||||
|
|
||||||
|
/** Row-major 3×3 matrix. */
|
||||||
|
export type Mat3 = [number, number, number, number, number, number, number, number, number];
|
||||||
|
|
||||||
|
export type MoveKind = 'cut' | 'rapid';
|
||||||
|
|
||||||
|
/** A polyline in millimetres (machine coordinates before per-job alignment). */
|
||||||
|
export interface Segment {
|
||||||
|
kind: MoveKind;
|
||||||
|
points: Vec2[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Per-job placement: rotate the work coordinates by `rot`, then translate by (tx,ty). mm / radians. */
|
||||||
|
export interface Alignment {
|
||||||
|
tx: number;
|
||||||
|
ty: number;
|
||||||
|
rot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Calibration {
|
||||||
|
/** Clicked points in the camera image, pixels. */
|
||||||
|
imagePoints: Vec2[];
|
||||||
|
/** Corresponding machine coordinates, mm. */
|
||||||
|
machinePoints: Vec2[];
|
||||||
|
/** machine-mm → image-px. */
|
||||||
|
homography: Mat3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderStyle {
|
||||||
|
cutColor: string;
|
||||||
|
rapidColor: string;
|
||||||
|
lineWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
streamUrl: string;
|
||||||
|
calibration: Calibration | null;
|
||||||
|
renderDefaults: RenderStyle;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue