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 { 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 { estimatePolyWarp } from './polywarp';
|
||||||
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
|
import type { Vec2, Segment, Alignment, PolyWarp } from '../types';
|
||||||
|
|
||||||
|
|
@ -37,4 +37,36 @@ describe('transform', () => {
|
||||||
const a = alignmentFromOrigin([7, 9], 0);
|
const a = alignmentFromOrigin([7, 9], 0);
|
||||||
expect(close(applyAlignment(a, [0, 0]), [7, 9])).toBe(true);
|
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 type { Vec2, Segment, Alignment, PolyWarp } from '../types';
|
||||||
import { applyPolyWarp, applyPolyWarpInverse } from './polywarp';
|
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). */
|
/** 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 {
|
||||||
const c = Math.cos(a.rot);
|
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]];
|
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) => ({
|
return segments.map((seg) => ({
|
||||||
kind: seg.kind,
|
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