refactor: transform layer uses PolyWarp
This commit is contained in:
parent
fcfa3a0d80
commit
dc59f5ed63
2 changed files with 21 additions and 17 deletions
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue