refactor: alignment UI and main wiring use PolyWarp

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sjat 2026-06-11 09:32:54 +02:00
parent 103e96f097
commit 7614590b03
3 changed files with 22 additions and 19 deletions

View file

@ -1,11 +1,11 @@
import type { Vec2, Mat3, Alignment } from '../types';
import type { Vec2, PolyWarp, Alignment } from '../types';
import { imageToMachine, alignmentFromOrigin } from '../geometry/transform';
export interface AlignmentDeps {
panel: HTMLElement;
overlay: HTMLCanvasElement;
getAlignment: () => Alignment;
getHomography: () => Mat3 | null;
getWarp: () => PolyWarp | null;
onChange: () => void;
}
@ -61,13 +61,13 @@ export function mountAlignment(deps: AlignmentDeps): void {
let lastMachine: Vec2 | null = null;
overlay.addEventListener('mousedown', (ev) => {
const H = deps.getHomography();
if (!H) return;
const warp = deps.getWarp();
if (!warp) return;
// Another module (e.g. calibration) has armed the overlay for this click; don't hijack it.
if (!armOrigin && overlay.classList.contains('interactive')) return;
if (armOrigin) {
const a = deps.getAlignment();
const m = imageToMachine(H, normFromEvent(overlay, ev));
const m = imageToMachine(warp, normFromEvent(overlay, ev));
Object.assign(a, alignmentFromOrigin(m, a.rot));
armOrigin = false;
overlay.classList.remove('interactive');
@ -77,15 +77,15 @@ export function mountAlignment(deps: AlignmentDeps): void {
}
dragging = true;
overlay.classList.add('interactive');
lastMachine = imageToMachine(H, normFromEvent(overlay, ev));
lastMachine = imageToMachine(warp, normFromEvent(overlay, ev));
});
window.addEventListener('mousemove', (ev) => {
if (!dragging) return;
const H = deps.getHomography();
if (!H || !lastMachine) return;
const warp = deps.getWarp();
if (!warp || !lastMachine) return;
const a = deps.getAlignment();
const cur = imageToMachine(H, normFromEvent(overlay, ev));
const cur = imageToMachine(warp, normFromEvent(overlay, ev));
if (ev.shiftKey) {
// rotate about the work origin's current machine position (tx,ty)
const angle = (p: Vec2) => Math.atan2(p[1] - a.ty, p[0] - a.tx);

View file

@ -2,11 +2,14 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { mountAlignment } from './alignment-ui';
import { mountCalibration } from './calibration-ui';
import type { Mat3, Alignment } from '../types';
import type { Vec2, PolyWarp, Alignment } from '../types';
import { estimatePolyWarp } from '../geometry/polywarp';
// Identity homography: machine coords == normalized coords for this test, so a
// Identity warp: machine coords == normalized coords for this test, so a
// normalized click at fraction f over the 100px box maps to machine value f.
const IDENT: Mat3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];
// We use degree 1 fitted on a unit square so the map is a near-perfect identity.
const square: Vec2[] = [[0, 0], [1, 0], [1, 1], [0, 1]];
const IDENT: PolyWarp = estimatePolyWarp(square, square, 1);
function makeOverlay(): HTMLCanvasElement {
const c = document.createElement('canvas');
@ -39,7 +42,7 @@ describe('overlay interaction', () => {
panel,
overlay,
getAlignment: () => alignment,
getHomography: () => IDENT,
getWarp: () => IDENT,
onChange: () => {},
});
overlay.dispatchEvent(mouse('mousedown', 50, 50));
@ -53,12 +56,12 @@ describe('overlay interaction', () => {
it('while calibration is armed, a click is captured by calibration and does NOT drag the alignment', () => {
const calibPanel = document.createElement('div');
const alignPanel = document.createElement('div');
mountCalibration({ panel: calibPanel, overlay, onHomography: () => {} });
mountCalibration({ panel: calibPanel, overlay, onWarp: () => {} });
mountAlignment({
panel: alignPanel,
overlay,
getAlignment: () => alignment,
getHomography: () => IDENT,
getWarp: () => IDENT,
onChange: () => {},
});
(calibPanel.querySelector('#cal-x') as HTMLInputElement).value = '10';

View file

@ -51,8 +51,8 @@ fileInput.addEventListener('change', async () => {
mountCalibration({
panel: document.getElementById('calib-panel') as HTMLElement,
overlay,
onHomography: (H) => {
state.setHomography(H);
onWarp: (w) => {
state.setWarp(w);
statusEl.textContent = 'Calibrated (unsaved — paste JSON into config.json to persist).';
render();
},
@ -62,7 +62,7 @@ mountAlignment({
panel: document.getElementById('align-panel') as HTMLElement,
overlay,
getAlignment: () => state.alignment,
getHomography: () => state.homography,
getWarp: () => state.warp,
onChange: render,
});
@ -71,7 +71,7 @@ async function boot(): Promise<void> {
const cfg = await loadConfig('config.json');
style = cfg.renderDefaults;
if (cfg.streamUrl) stream.src = cfg.streamUrl;
if (cfg.calibration) state.setHomography(cfg.calibration.homography);
if (cfg.calibration) state.setWarp(cfg.calibration.warp);
statusEl.textContent = cfg.calibration ? 'Ready. Open a G-code file.' : 'Not calibrated — calibrate first.';
} catch (e) {
statusEl.textContent = `Config error: ${(e as Error).message}`;