From 7614590b0307bc9f95817ffcfa6a0d2406fbd322 Mon Sep 17 00:00:00 2001 From: sjat Date: Thu, 11 Jun 2026 09:32:54 +0200 Subject: [PATCH] refactor: alignment UI and main wiring use PolyWarp Co-Authored-By: Claude Sonnet 4.6 --- src/app/alignment-ui.ts | 18 +++++++++--------- src/app/overlay-interaction.test.ts | 15 +++++++++------ src/main.ts | 8 ++++---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/app/alignment-ui.ts b/src/app/alignment-ui.ts index dd5d437..fa4f83e 100644 --- a/src/app/alignment-ui.ts +++ b/src/app/alignment-ui.ts @@ -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); diff --git a/src/app/overlay-interaction.test.ts b/src/app/overlay-interaction.test.ts index 5567ae0..5500082 100644 --- a/src/app/overlay-interaction.test.ts +++ b/src/app/overlay-interaction.test.ts @@ -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'; diff --git a/src/main.ts b/src/main.ts index ed4472b..40640d0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { 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}`;