diff --git a/frontend/src/lib/person/genealogy/panZoom.test.ts b/frontend/src/lib/person/genealogy/panZoom.test.ts index 4e1a8fd6..42282a65 100644 --- a/frontend/src/lib/person/genealogy/panZoom.test.ts +++ b/frontend/src/lib/person/genealogy/panZoom.test.ts @@ -3,6 +3,7 @@ import { clampZoom, parsePanZoomParams, serializePanZoomParams, + screenDeltaToSvg, DEFAULT_VIEW, DEFAULT_ZOOM, MIN_ZOOM, @@ -75,3 +76,18 @@ describe('serializePanZoomParams', () => { expect(parsePanZoomParams(serializePanZoomParams(state))).toEqual(state); }); }); + +describe('screenDeltaToSvg', () => { + it('scales a pixel delta by the viewBox-to-element ratio per axis', () => { + // viewBox is 2x the element in width, 2x in height → 1px == 2 SVG units. + expect(screenDeltaToSvg(10, 5, 1000, 800, 500, 400)).toEqual({ dx: 20, dy: 10 }); + }); + + it('is identity when the viewBox matches the element pixel size', () => { + expect(screenDeltaToSvg(7, -3, 600, 600, 600, 600)).toEqual({ dx: 7, dy: -3 }); + }); + + it('returns zero when the element has no measured size', () => { + expect(screenDeltaToSvg(10, 10, 1000, 800, 0, 0)).toEqual({ dx: 0, dy: 0 }); + }); +}); diff --git a/frontend/src/lib/person/genealogy/panZoom.ts b/frontend/src/lib/person/genealogy/panZoom.ts index 714764f5..07a934d7 100644 --- a/frontend/src/lib/person/genealogy/panZoom.ts +++ b/frontend/src/lib/person/genealogy/panZoom.ts @@ -61,3 +61,23 @@ export function parsePanZoomParams(raw: { export function serializePanZoomParams(state: PanZoomState): { cx: string; cy: string; z: string } { return { cx: String(state.x), cy: String(state.y), z: String(state.z) }; } + +/** + * Convert a pointer delta in CSS pixels into SVG user units, using the current + * viewBox-to-element ratio per axis. This is the distance the pointer traversed + * expressed in the tree's coordinate space; the gesture handler subtracts it + * from the pan offset so the canvas follows the finger (US-PAN-001). + */ +export function screenDeltaToSvg( + dxPx: number, + dyPx: number, + viewBoxW: number, + viewBoxH: number, + elPxW: number, + elPxH: number +): { dx: number; dy: number } { + return { + dx: elPxW > 0 ? dxPx * (viewBoxW / elPxW) : 0, + dy: elPxH > 0 ? dyPx * (viewBoxH / elPxH) : 0 + }; +}