refactor: transform layer uses PolyWarp

This commit is contained in:
sjat 2026-06-11 09:22:02 +02:00
parent fcfa3a0d80
commit dc59f5ed63
2 changed files with 21 additions and 17 deletions

View file

@ -1,20 +1,22 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin } from './transform'; import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin } from './transform';
import type { Vec2, Mat3, Segment, Alignment } from '../types'; import { estimatePolyWarp } from './polywarp';
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
const close = (a: Vec2, b: Vec2, eps = 1e-9) => const close = (a: Vec2, b: Vec2, eps = 1e-6) =>
Math.abs(a[0] - b[0]) < eps && Math.abs(a[1] - b[1]) < eps; 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]; // Identity warp: image == machine. A degree-1 fit on a non-degenerate quad is exact.
const square: Vec2[] = [[0, 0], [10, 0], [10, 10], [0, 10]];
const IDENT: PolyWarp = estimatePolyWarp(square, square, 1);
describe('transform', () => { describe('transform', () => {
it('applyAlignment rotates then translates', () => { it('applyAlignment rotates then translates', () => {
const a: Alignment = { tx: 5, ty: 1, rot: Math.PI / 2 }; 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); expect(close(applyAlignment(a, [1, 0]), [5, 2])).toBe(true);
}); });
it('projectSegments applies alignment then homography to every point', () => { it('projectSegments applies alignment then the warp to every point', () => {
const segs: Segment[] = [{ kind: 'cut', points: [[0, 0], [10, 0]] }]; const segs: Segment[] = [{ kind: 'cut', points: [[0, 0], [10, 0]] }];
const a: Alignment = { tx: 2, ty: 3, rot: 0 }; const a: Alignment = { tx: 2, ty: 3, rot: 0 };
const out = projectSegments(segs, a, IDENT); const out = projectSegments(segs, a, IDENT);
@ -23,10 +25,12 @@ describe('transform', () => {
expect(close(out[0]!.points[1]!, [12, 3])).toBe(true); expect(close(out[0]!.points[1]!, [12, 3])).toBe(true);
}); });
it('imageToMachine is the inverse of the homography', () => { it('imageToMachine inverts the warp', () => {
const H: Mat3 = [2, 0, 10, 0, 2, 20, 0, 0, 1]; // Warp scales machine→image by 0.5 (offset 0): machine (4,8) → image (2,4).
// machine (5,5) → image (20,30); invert should return (5,5) const m: Vec2[] = [[0, 0], [10, 0], [10, 10], [0, 10]];
expect(close(imageToMachine(H, [20, 30]), [5, 5])).toBe(true); const img: Vec2[] = m.map((p): Vec2 => [p[0] * 0.5, p[1] * 0.5]);
const w = estimatePolyWarp(m, img, 1);
expect(close(imageToMachine(w, [2, 4]), [4, 8], 1e-4)).toBe(true);
}); });
it('alignmentFromOrigin places work-origin at the given machine point', () => { it('alignmentFromOrigin places work-origin at the given machine point', () => {

View file

@ -1,5 +1,5 @@
import type { Vec2, Mat3, Segment, Alignment } from '../types'; import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
import { applyHomography, invertMat3 } from './homography'; import { applyPolyWarp, applyPolyWarpInverse } from './polywarp';
/** Apply per-job alignment to a point: rotate by rot, then translate by (tx,ty). */ /** Apply per-job alignment to a point: rotate by rot, then translate by (tx,ty). */
export function applyAlignment(a: Alignment, p: Vec2): Vec2 { export function applyAlignment(a: Alignment, p: Vec2): Vec2 {
@ -8,17 +8,17 @@ export function applyAlignment(a: Alignment, p: Vec2): Vec2 {
return [a.tx + c * p[0] - s * p[1], a.ty + s * p[0] + c * p[1]]; 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. */ /** Project work-coordinate segments to image: work → (alignment) → machine → (warp) → image. */
export function projectSegments(segments: Segment[], a: Alignment, H: Mat3): Segment[] { export function projectSegments(segments: Segment[], a: Alignment, warp: PolyWarp): Segment[] {
return segments.map((seg) => ({ return segments.map((seg) => ({
kind: seg.kind, kind: seg.kind,
points: seg.points.map((p) => applyHomography(H, applyAlignment(a, p))), points: seg.points.map((p) => applyPolyWarp(warp, applyAlignment(a, p))),
})); }));
} }
/** Convert an image-pixel point to machine mm using the inverse homography. */ /** Convert a normalized image point to machine mm using the warp's inverse map. */
export function imageToMachine(H: Mat3, imagePoint: Vec2): Vec2 { export function imageToMachine(warp: PolyWarp, imagePoint: Vec2): Vec2 {
return applyHomography(invertMat3(H), imagePoint); return applyPolyWarpInverse(warp, imagePoint);
} }
/** Build an alignment that places the work origin (0,0) at the given machine point. */ /** Build an alignment that places the work origin (0,0) at the given machine point. */