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';
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue