feat: alignment + projection pipeline

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sjat 2026-06-08 22:18:42 +02:00
parent 87d579eb47
commit 4a554e2057
2 changed files with 63 additions and 0 deletions

View file

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin } from './transform';
import type { Vec2, Mat3, Segment, Alignment } from '../types';
const close = (a: Vec2, b: Vec2, eps = 1e-9) =>
Math.abs(a[0] - b[0]) < eps && Math.abs(a[1] - b[1]) < eps;
const IDENT: Mat3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];
describe('transform', () => {
it('applyAlignment rotates then translates', () => {
const a: Alignment = { tx: 5, ty: 1, rot: Math.PI / 2 };
// (1,0) rotated 90° → (0,1), then +(5,1) → (5,2)
expect(close(applyAlignment(a, [1, 0]), [5, 2])).toBe(true);
});
it('projectSegments applies alignment then homography to every point', () => {
const segs: Segment[] = [{ kind: 'cut', points: [[0, 0], [10, 0]] }];
const a: Alignment = { tx: 2, ty: 3, rot: 0 };
const out = projectSegments(segs, a, IDENT);
expect(out[0]!.kind).toBe('cut');
expect(close(out[0]!.points[0]!, [2, 3])).toBe(true);
expect(close(out[0]!.points[1]!, [12, 3])).toBe(true);
});
it('imageToMachine is the inverse of the homography', () => {
const H: Mat3 = [2, 0, 10, 0, 2, 20, 0, 0, 1];
// machine (5,5) → image (20,30); invert should return (5,5)
expect(close(imageToMachine(H, [20, 30]), [5, 5])).toBe(true);
});
it('alignmentFromOrigin places work-origin at the given machine point', () => {
const a = alignmentFromOrigin([7, 9], 0);
expect(close(applyAlignment(a, [0, 0]), [7, 9])).toBe(true);
});
});

27
src/geometry/transform.ts Normal file
View file

@ -0,0 +1,27 @@
import type { Vec2, Mat3, Segment, Alignment } from '../types';
import { applyHomography, invertMat3 } from './homography';
/** Apply per-job alignment to a point: rotate by rot, then translate by (tx,ty). */
export function applyAlignment(a: Alignment, p: Vec2): Vec2 {
const c = Math.cos(a.rot);
const s = Math.sin(a.rot);
return [a.tx + c * p[0] - s * p[1], a.ty + s * p[0] + c * p[1]];
}
/** Project work-coordinate segments to image pixels: work → (alignment) → machine → (H) → image. */
export function projectSegments(segments: Segment[], a: Alignment, H: Mat3): Segment[] {
return segments.map((seg) => ({
kind: seg.kind,
points: seg.points.map((p) => applyHomography(H, applyAlignment(a, p))),
}));
}
/** Convert an image-pixel point to machine mm using the inverse homography. */
export function imageToMachine(H: Mat3, imagePoint: Vec2): Vec2 {
return applyHomography(invertMat3(H), imagePoint);
}
/** Build an alignment that places the work origin (0,0) at the given machine point. */
export function alignmentFromOrigin(machinePoint: Vec2, rot: number): Alignment {
return { tx: machinePoint[0], ty: machinePoint[1], rot };
}