GCodeOverlay/src/geometry/transform.test.ts
sjat 8b7562786c fix: subdivide straight moves before warping so the overlay follows lens curvature
A homography-era assumption survived the migration: projectSegments warped only
segment endpoints. The polynomial warp bends a path at its vertices, so a straight
G0/G1 move (2 points) rendered as a straight chord, hiding all distortion correction
(measured ~320px of bow collapsed to nothing on bed-spanning lines). Resample edges to
20mm in work space before warping; arcs already had dense vertices so are unaffected.
2026-06-11 11:35:29 +02:00

72 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from 'vitest';
import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin, subdivideSegment } from './transform';
import { estimatePolyWarp } from './polywarp';
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
const close = (a: Vec2, b: Vec2, eps = 1e-6) =>
Math.abs(a[0] - b[0]) < eps && Math.abs(a[1] - b[1]) < eps;
// 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', () => {
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);
});
it('projectSegments applies alignment then the warp 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 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);
});
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);
});
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·(1s)), 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);
});
});