feat: per-job alignment (numeric, click-origin, drag/rotate)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d1577dd569
commit
5d80273717
2 changed files with 126 additions and 0 deletions
117
src/app/alignment-ui.ts
Normal file
117
src/app/alignment-ui.ts
Normal file
|
|
@ -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 = `
|
||||||
|
<h2>Alignment</h2>
|
||||||
|
<div class="row">X <input id="al-x" type="number" step="1" /> Y <input id="al-y" type="number" step="1" /></div>
|
||||||
|
<div class="row">Rotation° <input id="al-rot" type="number" step="0.5" /></div>
|
||||||
|
<div class="row"><button id="al-origin">Set origin by clicking video</button></div>
|
||||||
|
<div class="row"><small>Or drag the toolpath to move; Shift-drag to rotate. Arrow keys nudge 1 mm.</small></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import { createState } from './app/state';
|
||||||
import { computeOverlayRect, syncOverlay } from './app/layout';
|
import { computeOverlayRect, syncOverlay } from './app/layout';
|
||||||
import { drawOverlay, clearCanvas } from './render/renderer';
|
import { drawOverlay, clearCanvas } from './render/renderer';
|
||||||
import { mountCalibration } from './app/calibration-ui';
|
import { mountCalibration } from './app/calibration-ui';
|
||||||
|
import { mountAlignment } from './app/alignment-ui';
|
||||||
import type { RenderStyle } from './types';
|
import type { RenderStyle } from './types';
|
||||||
|
|
||||||
const stage = document.getElementById('stage') as HTMLDivElement;
|
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<void> {
|
async function boot(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const cfg = await loadConfig('config.json');
|
const cfg = await loadConfig('config.json');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue