refactor: alignment UI and main wiring use PolyWarp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
103e96f097
commit
7614590b03
3 changed files with 22 additions and 19 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
import type { Vec2, Mat3, Alignment } from '../types';
|
import type { Vec2, PolyWarp, Alignment } from '../types';
|
||||||
import { imageToMachine, alignmentFromOrigin } from '../geometry/transform';
|
import { imageToMachine, alignmentFromOrigin } from '../geometry/transform';
|
||||||
|
|
||||||
export interface AlignmentDeps {
|
export interface AlignmentDeps {
|
||||||
panel: HTMLElement;
|
panel: HTMLElement;
|
||||||
overlay: HTMLCanvasElement;
|
overlay: HTMLCanvasElement;
|
||||||
getAlignment: () => Alignment;
|
getAlignment: () => Alignment;
|
||||||
getHomography: () => Mat3 | null;
|
getWarp: () => PolyWarp | null;
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,13 +61,13 @@ export function mountAlignment(deps: AlignmentDeps): void {
|
||||||
let lastMachine: Vec2 | null = null;
|
let lastMachine: Vec2 | null = null;
|
||||||
|
|
||||||
overlay.addEventListener('mousedown', (ev) => {
|
overlay.addEventListener('mousedown', (ev) => {
|
||||||
const H = deps.getHomography();
|
const warp = deps.getWarp();
|
||||||
if (!H) return;
|
if (!warp) return;
|
||||||
// Another module (e.g. calibration) has armed the overlay for this click; don't hijack it.
|
// 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 && overlay.classList.contains('interactive')) return;
|
||||||
if (armOrigin) {
|
if (armOrigin) {
|
||||||
const a = deps.getAlignment();
|
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));
|
Object.assign(a, alignmentFromOrigin(m, a.rot));
|
||||||
armOrigin = false;
|
armOrigin = false;
|
||||||
overlay.classList.remove('interactive');
|
overlay.classList.remove('interactive');
|
||||||
|
|
@ -77,15 +77,15 @@ export function mountAlignment(deps: AlignmentDeps): void {
|
||||||
}
|
}
|
||||||
dragging = true;
|
dragging = true;
|
||||||
overlay.classList.add('interactive');
|
overlay.classList.add('interactive');
|
||||||
lastMachine = imageToMachine(H, normFromEvent(overlay, ev));
|
lastMachine = imageToMachine(warp, normFromEvent(overlay, ev));
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('mousemove', (ev) => {
|
window.addEventListener('mousemove', (ev) => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
const H = deps.getHomography();
|
const warp = deps.getWarp();
|
||||||
if (!H || !lastMachine) return;
|
if (!warp || !lastMachine) return;
|
||||||
const a = deps.getAlignment();
|
const a = deps.getAlignment();
|
||||||
const cur = imageToMachine(H, normFromEvent(overlay, ev));
|
const cur = imageToMachine(warp, normFromEvent(overlay, ev));
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
// rotate about the work origin's current machine position (tx,ty)
|
// 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);
|
const angle = (p: Vec2) => Math.atan2(p[1] - a.ty, p[0] - a.tx);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { mountAlignment } from './alignment-ui';
|
import { mountAlignment } from './alignment-ui';
|
||||||
import { mountCalibration } from './calibration-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.
|
// 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 {
|
function makeOverlay(): HTMLCanvasElement {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
|
|
@ -39,7 +42,7 @@ describe('overlay interaction', () => {
|
||||||
panel,
|
panel,
|
||||||
overlay,
|
overlay,
|
||||||
getAlignment: () => alignment,
|
getAlignment: () => alignment,
|
||||||
getHomography: () => IDENT,
|
getWarp: () => IDENT,
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
});
|
});
|
||||||
overlay.dispatchEvent(mouse('mousedown', 50, 50));
|
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', () => {
|
it('while calibration is armed, a click is captured by calibration and does NOT drag the alignment', () => {
|
||||||
const calibPanel = document.createElement('div');
|
const calibPanel = document.createElement('div');
|
||||||
const alignPanel = document.createElement('div');
|
const alignPanel = document.createElement('div');
|
||||||
mountCalibration({ panel: calibPanel, overlay, onHomography: () => {} });
|
mountCalibration({ panel: calibPanel, overlay, onWarp: () => {} });
|
||||||
mountAlignment({
|
mountAlignment({
|
||||||
panel: alignPanel,
|
panel: alignPanel,
|
||||||
overlay,
|
overlay,
|
||||||
getAlignment: () => alignment,
|
getAlignment: () => alignment,
|
||||||
getHomography: () => IDENT,
|
getWarp: () => IDENT,
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
});
|
});
|
||||||
(calibPanel.querySelector('#cal-x') as HTMLInputElement).value = '10';
|
(calibPanel.querySelector('#cal-x') as HTMLInputElement).value = '10';
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ fileInput.addEventListener('change', async () => {
|
||||||
mountCalibration({
|
mountCalibration({
|
||||||
panel: document.getElementById('calib-panel') as HTMLElement,
|
panel: document.getElementById('calib-panel') as HTMLElement,
|
||||||
overlay,
|
overlay,
|
||||||
onHomography: (H) => {
|
onWarp: (w) => {
|
||||||
state.setHomography(H);
|
state.setWarp(w);
|
||||||
statusEl.textContent = 'Calibrated (unsaved — paste JSON into config.json to persist).';
|
statusEl.textContent = 'Calibrated (unsaved — paste JSON into config.json to persist).';
|
||||||
render();
|
render();
|
||||||
},
|
},
|
||||||
|
|
@ -62,7 +62,7 @@ mountAlignment({
|
||||||
panel: document.getElementById('align-panel') as HTMLElement,
|
panel: document.getElementById('align-panel') as HTMLElement,
|
||||||
overlay,
|
overlay,
|
||||||
getAlignment: () => state.alignment,
|
getAlignment: () => state.alignment,
|
||||||
getHomography: () => state.homography,
|
getWarp: () => state.warp,
|
||||||
onChange: render,
|
onChange: render,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ async function boot(): Promise<void> {
|
||||||
const cfg = await loadConfig('config.json');
|
const cfg = await loadConfig('config.json');
|
||||||
style = cfg.renderDefaults;
|
style = cfg.renderDefaults;
|
||||||
if (cfg.streamUrl) stream.src = cfg.streamUrl;
|
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.';
|
statusEl.textContent = cfg.calibration ? 'Ready. Open a G-code file.' : 'Not calibrated — calibrate first.';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = `Config error: ${(e as Error).message}`;
|
statusEl.textContent = `Config error: ${(e as Error).message}`;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue