2026-06-08 22:18:42 +02:00
|
|
|
|
import { describe, it, expect } from 'vitest';
|
2026-06-11 11:35:29 +02:00
|
|
|
|
import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin, subdivideSegment } from './transform';
|
2026-06-11 09:22:02 +02:00
|
|
|
|
import { estimatePolyWarp } from './polywarp';
|
|
|
|
|
|
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
|
2026-06-08 22:18:42 +02:00
|
|
|
|
|
2026-06-11 09:22:02 +02:00
|
|
|
|
const close = (a: Vec2, b: Vec2, eps = 1e-6) =>
|
2026-06-08 22:18:42 +02:00
|
|
|
|
Math.abs(a[0] - b[0]) < eps && Math.abs(a[1] - b[1]) < eps;
|
|
|
|
|
|
|
2026-06-11 09:22:02 +02:00
|
|
|
|
// 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);
|
2026-06-08 22:18:42 +02:00
|
|
|
|
|
|
|
|
|
|
describe('transform', () => {
|
|
|
|
|
|
it('applyAlignment rotates then translates', () => {
|
|
|
|
|
|
const a: Alignment = { tx: 5, ty: 1, rot: Math.PI / 2 };
|
|
|
|
|
|
expect(close(applyAlignment(a, [1, 0]), [5, 2])).toBe(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-11 09:22:02 +02:00
|
|
|
|
it('projectSegments applies alignment then the warp to every point', () => {
|
2026-06-08 22:18:42 +02:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-11 09:22:02 +02:00
|
|
|
|
it('imageToMachine inverts the warp', () => {
|
|
|
|
|
|
// Warp scales machine→image by 0.5 (offset 0): machine (4,8) → image (2,4).
|
|
|
|
|
|
const m: Vec2[] = [[0, 0], [10, 0], [10, 10], [0, 10]];
|
|
|
|
|
|
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);
|
2026-06-08 22:18:42 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
2026-06-11 11:35:29 +02:00
|
|
|
|
|
|
|
|
|
|
it('subdivideSegment splits a long edge into <=stepMm pieces, preserving endpoints', () => {
|
|
|
|
|
|
const seg: Segment = { kind: 'cut', points: [[0, 0], [100, 0]] };
|
|
|
|
|
|
const out = subdivideSegment(seg, 25);
|
|
|
|
|
|
expect(out.points.length).toBe(5); // 0,25,50,75,100
|
|
|
|
|
|
expect(close(out.points[0]!, [0, 0])).toBe(true);
|
|
|
|
|
|
expect(close(out.points[4]!, [100, 0])).toBe(true);
|
|
|
|
|
|
expect(close(out.points[2]!, [50, 0])).toBe(true);
|
|
|
|
|
|
expect(out.kind).toBe('cut');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('subdivideSegment leaves a short edge as just its endpoints', () => {
|
|
|
|
|
|
const out = subdivideSegment({ kind: 'cut', points: [[0, 0], [5, 0]] }, 25);
|
|
|
|
|
|
expect(out.points.length).toBe(2);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('projectSegments subdivides straight moves so they follow the warp curvature', () => {
|
|
|
|
|
|
// A warp where straight machine lines bow in image-y: image = (x/100, y/100 + 0.3·s·(1−s)), s=x/100.
|
|
|
|
|
|
const grid: Vec2[] = [[0, 0], [100, 0], [100, 100], [0, 100], [50, 50], [50, 0], [0, 50]];
|
|
|
|
|
|
const curved = (p: Vec2): Vec2 => {
|
|
|
|
|
|
const s = p[0] / 100;
|
|
|
|
|
|
return [s, p[1] / 100 + 0.3 * s * (1 - s)];
|
|
|
|
|
|
};
|
|
|
|
|
|
const warp = estimatePolyWarp(grid, grid.map(curved), 2);
|
|
|
|
|
|
const a: Alignment = { tx: 0, ty: 0, rot: 0 };
|
|
|
|
|
|
const out = projectSegments([{ kind: 'cut', points: [[0, 0], [100, 0]] }], a, warp);
|
|
|
|
|
|
// The single straight move must become a multi-vertex polyline...
|
|
|
|
|
|
expect(out[0]!.points.length).toBeGreaterThan(2);
|
|
|
|
|
|
// ...whose interior bows well away from the straight chord (endpoints both have y≈0).
|
|
|
|
|
|
const maxY = Math.max(...out[0]!.points.map((p) => Math.abs(p[1])));
|
|
|
|
|
|
expect(maxY).toBeGreaterThan(0.05);
|
|
|
|
|
|
});
|
2026-06-08 22:18:42 +02:00
|
|
|
|
});
|