diff --git a/frontend/src/lib/components/AnnotationEditOverlay.svelte b/frontend/src/lib/components/AnnotationEditOverlay.svelte index 45605f13..93f1403c 100644 --- a/frontend/src/lib/components/AnnotationEditOverlay.svelte +++ b/frontend/src/lib/components/AnnotationEditOverlay.svelte @@ -27,6 +27,21 @@ $effect(() => { let svgEl = $state(null); +// Actual rendered pixel dimensions of the SVG — updated by ResizeObserver. +// Used as the viewBox so handles are always physically 16×16px regardless of annotation aspect ratio. +let svgWidth = $state(1); +let svgHeight = $state(1); + +$effect(() => { + if (!svgEl) return; + const ro = new ResizeObserver(([entry]) => { + svgWidth = entry.contentRect.width || 1; + svgHeight = entry.contentRect.height || 1; + }); + ro.observe(svgEl); + return () => ro.disconnect(); +}); + type HandleId = 'nw' | 'n' | 'ne' | 'w' | 'e' | 'sw' | 's' | 'se'; type DragState = { @@ -42,16 +57,17 @@ type DragState = { let dragState = $state(null); -const handles: Array<{ id: HandleId; cx: number; cy: number; cursor: string }> = [ +// Handle positions in pixel space — always physically square regardless of annotation shape. +const handles = $derived>([ { id: 'nw', cx: 0, cy: 0, cursor: 'nwse-resize' }, - { id: 'n', cx: 50, cy: 0, cursor: 'ns-resize' }, - { id: 'ne', cx: 100, cy: 0, cursor: 'nesw-resize' }, - { id: 'w', cx: 0, cy: 50, cursor: 'ew-resize' }, - { id: 'e', cx: 100, cy: 50, cursor: 'ew-resize' }, - { id: 'sw', cx: 0, cy: 100, cursor: 'nesw-resize' }, - { id: 's', cx: 50, cy: 100, cursor: 'ns-resize' }, - { id: 'se', cx: 100, cy: 100, cursor: 'nwse-resize' } -]; + { id: 'n', cx: svgWidth / 2, cy: 0, cursor: 'ns-resize' }, + { id: 'ne', cx: svgWidth, cy: 0, cursor: 'nesw-resize' }, + { id: 'w', cx: 0, cy: svgHeight / 2, cursor: 'ew-resize' }, + { id: 'e', cx: svgWidth, cy: svgHeight / 2, cursor: 'ew-resize' }, + { id: 'sw', cx: 0, cy: svgHeight, cursor: 'nesw-resize' }, + { id: 's', cx: svgWidth / 2, cy: svgHeight, cursor: 'ns-resize' }, + { id: 'se', cx: svgWidth, cy: svgHeight, cursor: 'nwse-resize' } +]); function pixelToNorm(dx: number, dy: number): { nx: number; ny: number } { if (!svgEl) return { nx: 0, ny: 0 }; @@ -182,10 +198,11 @@ function handleKeyDown(event: KeyboardEvent): void { }, 300); } -let previewX = $derived(((liveX - annotation.x) / annotation.width) * 100); -let previewY = $derived(((liveY - annotation.y) / annotation.height) * 100); -let previewW = $derived((liveWidth / annotation.width) * 100); -let previewH = $derived((liveHeight / annotation.height) * 100); +// Preview rect in pixel space (maps live normalized coords back to SVG pixel coordinates) +let previewX = $derived(((liveX - annotation.x) / annotation.width) * svgWidth); +let previewY = $derived(((liveY - annotation.y) / annotation.height) * svgHeight); +let previewW = $derived((liveWidth / annotation.width) * svgWidth); +let previewH = $derived((liveHeight / annotation.height) * svgHeight);
@@ -195,8 +212,7 @@ let previewH = $derived((liveHeight / annotation.height) * 100); handlePointerDown(e, 'move')}