diff --git a/src/app/alignment-ui.ts b/src/app/alignment-ui.ts new file mode 100644 index 0000000..e891ce2 --- /dev/null +++ b/src/app/alignment-ui.ts @@ -0,0 +1,117 @@ +import type { Vec2, Mat3, Alignment } from '../types'; +import { imageToMachine, alignmentFromOrigin } from '../geometry/transform'; + +export interface AlignmentDeps { + panel: HTMLElement; + overlay: HTMLCanvasElement; + getAlignment: () => Alignment; + getHomography: () => Mat3 | null; + onChange: () => void; +} + +/** Convert a mouse event to normalized [0,1] coords over the overlay canvas box. */ +function normFromEvent(overlay: HTMLCanvasElement, ev: MouseEvent): Vec2 { + const rect = overlay.getBoundingClientRect(); + return [ + (ev.clientX - rect.left) / rect.width, + (ev.clientY - rect.top) / rect.height, + ]; +} + +export function mountAlignment(deps: AlignmentDeps): void { + const { panel, overlay } = deps; + panel.innerHTML = ` +

Alignment

+
X Y
+
Rotation°
+
+
Or drag the toolpath to move; Shift-drag to rotate. Arrow keys nudge 1 mm.
+ `; + + const xEl = panel.querySelector('#al-x') as HTMLInputElement; + const yEl = panel.querySelector('#al-y') as HTMLInputElement; + const rotEl = panel.querySelector('#al-rot') as HTMLInputElement; + + const sync = () => { + const a = deps.getAlignment(); + xEl.value = a.tx.toFixed(1); + yEl.value = a.ty.toFixed(1); + rotEl.value = ((a.rot * 180) / Math.PI).toFixed(1); + }; + sync(); + + const commitNumeric = () => { + const a = deps.getAlignment(); + a.tx = parseFloat(xEl.value) || 0; + a.ty = parseFloat(yEl.value) || 0; + a.rot = ((parseFloat(rotEl.value) || 0) * Math.PI) / 180; + deps.onChange(); + }; + for (const el of [xEl, yEl, rotEl]) el.addEventListener('change', commitNumeric); + + // Click-to-set-origin + let armOrigin = false; + (panel.querySelector('#al-origin') as HTMLButtonElement).addEventListener('click', () => { + armOrigin = true; + overlay.classList.add('interactive'); + }); + + // Drag to move / Shift-drag to rotate + let dragging = false; + let lastMachine: Vec2 | null = null; + + overlay.addEventListener('mousedown', (ev) => { + const H = deps.getHomography(); + if (!H) return; + if (armOrigin) { + const a = deps.getAlignment(); + const m = imageToMachine(H, normFromEvent(overlay, ev)); + Object.assign(a, alignmentFromOrigin(m, a.rot)); + armOrigin = false; + overlay.classList.remove('interactive'); + sync(); + deps.onChange(); + return; + } + dragging = true; + overlay.classList.add('interactive'); + lastMachine = imageToMachine(H, normFromEvent(overlay, ev)); + }); + + window.addEventListener('mousemove', (ev) => { + if (!dragging) return; + const H = deps.getHomography(); + if (!H || !lastMachine) return; + const a = deps.getAlignment(); + const cur = imageToMachine(H, 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); + a.rot += angle(cur) - angle(lastMachine); + } else { + a.tx += cur[0] - lastMachine[0]; + a.ty += cur[1] - lastMachine[1]; + } + lastMachine = cur; + sync(); + deps.onChange(); + }); + + window.addEventListener('mouseup', () => { + dragging = false; + overlay.classList.remove('interactive'); + }); + + window.addEventListener('keydown', (ev) => { + const a = deps.getAlignment(); + const step = 1; + if (ev.key === 'ArrowLeft') a.tx -= step; + else if (ev.key === 'ArrowRight') a.tx += step; + else if (ev.key === 'ArrowUp') a.ty -= step; + else if (ev.key === 'ArrowDown') a.ty += step; + else return; + ev.preventDefault(); + sync(); + deps.onChange(); + }); +} diff --git a/src/main.ts b/src/main.ts index 1fd429e..36f6f2c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { createState } from './app/state'; import { computeOverlayRect, syncOverlay } from './app/layout'; import { drawOverlay, clearCanvas } from './render/renderer'; import { mountCalibration } from './app/calibration-ui'; +import { mountAlignment } from './app/alignment-ui'; import type { RenderStyle } from './types'; const stage = document.getElementById('stage') as HTMLDivElement; @@ -59,6 +60,14 @@ mountCalibration({ }, }); +mountAlignment({ + panel: document.getElementById('align-panel') as HTMLElement, + overlay, + getAlignment: () => state.alignment, + getHomography: () => state.homography, + onChange: render, +}); + async function boot(): Promise { try { const cfg = await loadConfig('config.json');