From 4a554e2057a29aea2e60ba59343548a509fe81f3 Mon Sep 17 00:00:00 2001 From: sjat Date: Mon, 8 Jun 2026 22:18:42 +0200 Subject: [PATCH] feat: alignment + projection pipeline Co-Authored-By: Claude Opus 4.8 (1M context) --- src/geometry/transform.test.ts | 36 ++++++++++++++++++++++++++++++++++ src/geometry/transform.ts | 27 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/geometry/transform.test.ts create mode 100644 src/geometry/transform.ts diff --git a/src/geometry/transform.test.ts b/src/geometry/transform.test.ts new file mode 100644 index 0000000..13ebe32 --- /dev/null +++ b/src/geometry/transform.test.ts @@ -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); + }); +}); diff --git a/src/geometry/transform.ts b/src/geometry/transform.ts new file mode 100644 index 0000000..b3711ff --- /dev/null +++ b/src/geometry/transform.ts @@ -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 }; +}