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.
This commit is contained in:
parent
669815f64d
commit
8b7562786c
2 changed files with 77 additions and 4 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin } from './transform';
|
||||
import { applyAlignment, projectSegments, imageToMachine, alignmentFromOrigin, subdivideSegment } from './transform';
|
||||
import { estimatePolyWarp } from './polywarp';
|
||||
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
|
||||
|
||||
|
|
@ -37,4 +37,36 @@ describe('transform', () => {
|
|||
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·(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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
|
||||
import { applyPolyWarp, applyPolyWarpInverse } from './polywarp';
|
||||
|
||||
/**
|
||||
* Max length (machine mm) of a sub-edge before the warp is applied. The polynomial warp
|
||||
* bends a path only at its vertices, so a straight 2-point move would render as a straight
|
||||
* chord — hiding the lens curvature. Resampling long edges to this step makes the warped
|
||||
* polyline follow the curve. 20 mm keeps the per-sub-edge deviation well under a pixel even
|
||||
* on the most strongly-bowed lines, while leaving already-short moves (most toolpaths) untouched.
|
||||
*/
|
||||
const SUBDIVIDE_STEP_MM = 20;
|
||||
|
||||
/** 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);
|
||||
|
|
@ -8,11 +17,43 @@ 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]];
|
||||
}
|
||||
|
||||
/** Project work-coordinate segments to image: work → (alignment) → machine → (warp) → image. */
|
||||
export function projectSegments(segments: Segment[], a: Alignment, warp: PolyWarp): Segment[] {
|
||||
/**
|
||||
* Resample a polyline (work mm) so no sub-edge exceeds `stepMm`. Endpoints and existing
|
||||
* vertices are preserved; edges already shorter than the step are left as-is. This is what
|
||||
* lets the nonlinear warp curve a straight machine move instead of drawing a chord.
|
||||
*/
|
||||
export function subdivideSegment(seg: Segment, stepMm: number = SUBDIVIDE_STEP_MM): Segment {
|
||||
const src = seg.points;
|
||||
if (src.length < 2) return seg;
|
||||
const out: Vec2[] = [];
|
||||
for (let i = 0; i < src.length - 1; i++) {
|
||||
const a = src[i]!;
|
||||
const b = src[i + 1]!;
|
||||
const len = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
||||
const n = Math.max(1, Math.ceil(len / stepMm));
|
||||
for (let k = 0; k < n; k++) {
|
||||
const t = k / n;
|
||||
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
||||
}
|
||||
}
|
||||
out.push(src[src.length - 1]!);
|
||||
return { kind: seg.kind, points: out };
|
||||
}
|
||||
|
||||
/**
|
||||
* Project work-coordinate segments to image: work → (subdivide) → (alignment) → machine →
|
||||
* (warp) → image. Subdivision happens in work space so the warp can bend each straight move
|
||||
* along the camera's distortion instead of collapsing it to a chord.
|
||||
*/
|
||||
export function projectSegments(
|
||||
segments: Segment[],
|
||||
a: Alignment,
|
||||
warp: PolyWarp,
|
||||
stepMm: number = SUBDIVIDE_STEP_MM,
|
||||
): Segment[] {
|
||||
return segments.map((seg) => ({
|
||||
kind: seg.kind,
|
||||
points: seg.points.map((p) => applyPolyWarp(warp, applyAlignment(a, p))),
|
||||
points: subdivideSegment(seg, stepMm).points.map((p) => applyPolyWarp(warp, applyAlignment(a, p))),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue