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 { 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<void> {
|
||||
try {
|
||||
const cfg = await loadConfig('config.json');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue