fix(annotations): use pixel-space viewBox so handles stay square on non-square annotations
ResizeObserver binds actual SVG pixel dimensions; viewBox matches them so 16px handle squares and 44px hit areas are physically correct regardless of the annotation's aspect ratio. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,21 @@ $effect(() => {
|
||||
|
||||
let svgEl = $state<SVGSVGElement | null>(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<DragState | null>(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<Array<{ id: HandleId; cx: number; cy: number; cursor: string }>>([
|
||||
{ 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);
|
||||
</script>
|
||||
|
||||
<div aria-live="polite" class="sr-only">
|
||||
@@ -195,8 +212,7 @@ let previewH = $derived((liveHeight / annotation.height) * 100);
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<svg
|
||||
bind:this={svgEl}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
viewBox="0 0 {svgWidth} {svgHeight}"
|
||||
role="application"
|
||||
aria-label="Annotation resize handles"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; touch-action: none; overflow: visible;"
|
||||
@@ -209,8 +225,8 @@ let previewH = $derived((liveHeight / annotation.height) * 100);
|
||||
role="none"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100"
|
||||
height="100"
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
fill="transparent"
|
||||
style="cursor: move; pointer-events: all;"
|
||||
onpointerdown={(e) => handlePointerDown(e, 'move')}
|
||||
|
||||
Reference in New Issue
Block a user