From 39886e859ca64967ca073e8172c4c8c8873b6102 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 4 Dec 2025 14:27:20 -0600 Subject: [PATCH] feat(know-your-world): implement empirical scale measurement for 1:1 magnifier tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace calculated touch multiplier with empirical measurement approach: - Add invisible probe circles in magnifier SVG at known SVG coordinates (100 units apart) - Measure actual screen pixel distance between probes via getBoundingClientRect() - Calculate pixelsPerSvgUnit from measured distance / known SVG distance - Use empirical scale for touch multiplier: viewportScale / pixelsPerSvgUnit - Falls back to calculated method if measurement fails This approach is robust to rendering pipeline changes since it measures what's actually on screen rather than calculating through transform layers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/MapRenderer.tsx | 17 +- .../features/magnifier/MagnifierContext.tsx | 8 +- .../features/magnifier/MagnifierControls.tsx | 2 +- .../features/magnifier/MagnifierOverlay.tsx | 46 ++++- .../MagnifierOverlayWithHandlers.tsx | 2 +- .../features/magnifier/index.ts | 5 + .../features/magnifier/useEmpiricalScale.ts | 142 +++++++++++++ .../features/magnifier/useMagnifierState.ts | 2 +- .../magnifier/useMagnifierTouchHandlers.ts | 195 +++++++++++++++--- 9 files changed, 369 insertions(+), 50 deletions(-) create mode 100644 apps/web/src/arcade-games/know-your-world/features/magnifier/useEmpiricalScale.ts diff --git a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx index dab4d5df..56009756 100644 --- a/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx +++ b/apps/web/src/arcade-games/know-your-world/components/MapRenderer.tsx @@ -19,18 +19,11 @@ import { } from '../features/interaction' import { getRenderedViewport, LabelLayer, useD3ForceLabels } from '../features/labels' import { - applyPanDelta, - calculateTouchMultiplier, - clampToSvgBounds, getAdjustedMagnifiedDimensions, getMagnifierDimensions, type MagnifierContextValue, - MagnifierCrosshair, MagnifierOverlayWithHandlers, - MagnifierPixelGrid, MagnifierProvider, - MagnifierRegions, - parseViewBoxDimensions, type UseMagnifierTouchHandlersOptions, useMagnifierState, useMagnifierStyle, @@ -51,7 +44,6 @@ import { usePointerLock } from '../hooks/usePointerLock' import { useRegionDetection } from '../hooks/useRegionDetection' import { useHasRegionHint, useRegionHint } from '../hooks/useRegionHint' import { useSpeakHint } from '../hooks/useSpeakHint' -import { getRegionColor, getRegionStroke } from '../mapColors' import { ASSISTANCE_LEVELS, calculateFitCropViewBox, @@ -67,7 +59,7 @@ import type { HintMap } from '../messages' import { useKnowYourWorld } from '../Provider' import type { MapData, MapRegion } from '../types' import { type BoundingBox as DebugBoundingBox, findOptimalZoom } from '../utils/adaptiveZoomSearch' -import { CELEBRATION_TIMING, classifyCelebration } from '../utils/celebration' +import { classifyCelebration } from '../utils/celebration' import { calculateMaxZoomAtThreshold, calculateScreenPixelRatio, @@ -512,6 +504,9 @@ export function MapRenderer({ // Ref to magnifier element for tap position calculation const magnifierRef = useRef(null) + // Refs for scale probe elements (for empirical 1:1 tracking measurement) + const scaleProbe1Ref = useRef(null) + const scaleProbe2Ref = useRef(null) // Where user tapped on magnifier const magnifierTapPositionRef = useRef<{ x: number; y: number } | null>(null) @@ -2359,6 +2354,8 @@ export function MapRenderer({ svgRef, magnifierRef, cursorPositionRef, + scaleProbe1Ref, + scaleProbe2Ref, // Position & Animation (cursorPosition comes from state machine) cursorPosition, zoomSpring, @@ -2397,6 +2394,8 @@ export function MapRenderer({ svgRef, magnifierRef, cursorPositionRef, + scaleProbe1Ref, + scaleProbe2Ref, cursorPosition, zoomSpring, magnifierSpring, diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierContext.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierContext.tsx index 32fe98c5..131c1892 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierContext.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierContext.tsx @@ -15,8 +15,8 @@ 'use client' -import { createContext, useContext, useMemo, type ReactNode, type RefObject } from 'react' import type { SpringValue } from '@react-spring/web' +import { createContext, type ReactNode, type RefObject, useContext, useMemo } from 'react' import type { UseInteractionStateMachineReturn } from '../interaction' @@ -66,6 +66,10 @@ export interface MagnifierContextValue { magnifierRef: RefObject /** Cursor position ref (mutable) */ cursorPositionRef: React.MutableRefObject<{ x: number; y: number } | null> + /** Scale probe 1 ref (for empirical scale measurement) */ + scaleProbe1Ref: RefObject + /** Scale probe 2 ref (for empirical scale measurement) */ + scaleProbe2Ref: RefObject // ------------------------------------------------------------------------- // Position & Animation @@ -187,6 +191,8 @@ export function MagnifierProvider({ children, value }: MagnifierProviderProps) { value.svgRef, value.magnifierRef, value.cursorPositionRef, + value.scaleProbe1Ref, + value.scaleProbe2Ref, // Position & Animation value.cursorPosition, value.zoomSpring, diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx index 329adf35..a8b68eca 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierControls.tsx @@ -9,7 +9,7 @@ 'use client' -import { memo, type TouchEvent as ReactTouchEvent, type MouseEvent as ReactMouseEvent } from 'react' +import { memo, type MouseEvent as ReactMouseEvent, type TouchEvent as ReactTouchEvent } from 'react' // ============================================================================ // Types diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlay.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlay.tsx index 564d1600..9c3ef466 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlay.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlay.tsx @@ -35,7 +35,6 @@ import { MagnifierControls } from './MagnifierControls' import { MagnifierCrosshair } from './MagnifierCrosshair' import { MagnifierPixelGrid } from './MagnifierPixelGrid' import { MagnifierRegions } from './MagnifierRegions' -import type { CrosshairStyle as HeatCrosshairStyle } from './types' // ============================================================================ // Types @@ -89,8 +88,13 @@ export function MagnifierOverlay({ precisionCalcs, getCurrentZoom, highZoomThreshold, + scaleProbe1Ref, + scaleProbe2Ref, } = useMagnifierContext() + // Distance between scale probes in SVG units (must match useEmpiricalScale.ts) + const SCALE_PROBE_DISTANCE = 100 + const { mapData, regionsFound, @@ -261,7 +265,7 @@ export function MagnifierOverlay({ showOutline={showOutline} /> - {/* Crosshair at center position */} + {/* Crosshair at center position + Scale probes for empirical measurement */} {(() => { const viewport = getRenderedViewport( svgRect, @@ -276,13 +280,37 @@ export function MagnifierOverlay({ const cursorSvgY = (cursorPosition.y - svgOffsetY) / viewport.scale + viewBoxY return ( - + <> + + {/* Scale probe circles for empirical 1:1 tracking measurement */} + {/* These are invisible but their screen positions are measured via getBoundingClientRect */} + + + ) })()} diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlayWithHandlers.tsx b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlayWithHandlers.tsx index a49ba68d..8ee581e8 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlayWithHandlers.tsx +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/MagnifierOverlayWithHandlers.tsx @@ -15,8 +15,8 @@ import type { SpringValue } from '@react-spring/web' import { MagnifierOverlay } from './MagnifierOverlay' import { - useMagnifierTouchHandlers, type UseMagnifierTouchHandlersOptions, + useMagnifierTouchHandlers, } from './useMagnifierTouchHandlers' // ============================================================================ diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/index.ts b/apps/web/src/arcade-games/know-your-world/features/magnifier/index.ts index 9a2ba5bf..c847dff7 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/index.ts +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/index.ts @@ -38,6 +38,11 @@ // State Management Hooks // ============================================================================ +export type { + EmpiricalScaleResult, + UseEmpiricalScaleReturn, +} from './useEmpiricalScale' +export { useEmpiricalScale } from './useEmpiricalScale' export type { UseMagnifierStateOptions, UseMagnifierStateReturn, diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/useEmpiricalScale.ts b/apps/web/src/arcade-games/know-your-world/features/magnifier/useEmpiricalScale.ts new file mode 100644 index 00000000..ef7901e9 --- /dev/null +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/useEmpiricalScale.ts @@ -0,0 +1,142 @@ +/** + * Empirical Scale Measurement for 1:1 Touch Tracking + * + * Instead of calculating the scale through all the transform layers, + * this hook empirically measures the actual pixel-to-SVG ratio by + * placing probe elements and comparing their screen positions. + * + * This approach is robust to any changes in the rendering pipeline + * because it measures what's actually happening on screen. + */ + +'use client' + +import { useCallback, useRef } from 'react' + +// ============================================================================ +// Types +// ============================================================================ + +export interface EmpiricalScaleResult { + /** Pixels per SVG unit (measured empirically) */ + pixelsPerSvgUnit: number + /** Whether measurement was successful */ + isValid: boolean + /** Debug info about the measurement */ + debug?: { + probe1Screen: { x: number; y: number } + probe2Screen: { x: number; y: number } + pixelDistance: number + svgDistance: number + } +} + +export interface UseEmpiricalScaleReturn { + /** Ref for first probe element (circle at known SVG coords) */ + probe1Ref: React.RefObject + /** Ref for second probe element (circle at known SVG coords) */ + probe2Ref: React.RefObject + /** Measure the current scale empirically */ + measureScale: () => EmpiricalScaleResult + /** The fixed SVG distance between probes (for reference) */ + probeSvgDistance: number +} + +// ============================================================================ +// Constants +// ============================================================================ + +/** + * Fixed distance between probes in SVG units. + * This should be large enough to get accurate measurements + * but small enough to fit within the magnifier view at all zoom levels. + */ +const PROBE_SVG_DISTANCE = 100 + +// ============================================================================ +// Hook Implementation +// ============================================================================ + +/** + * Hook for empirically measuring the pixel-to-SVG scale in the magnifier. + * + * Usage: + * 1. Render two probe circles inside the magnifier SVG using probe1Ref and probe2Ref + * 2. Position them at (cursorSvgX - 50, cursorSvgY) and (cursorSvgX + 50, cursorSvgY) + * 3. Call measureScale() during touch move to get the actual pixels-per-SVG-unit + * 4. Use: touchMultiplier = 1 / pixelsPerSvgUnit + * + * @example + * ```tsx + * const { probe1Ref, probe2Ref, measureScale, probeSvgDistance } = useEmpiricalScale() + * + * // In magnifier SVG, at cursor position: + * + * + * + * // In touch handler: + * const { pixelsPerSvgUnit, isValid } = measureScale() + * if (isValid) { + * const touchMultiplier = 1 / pixelsPerSvgUnit + * // Apply to cursor movement... + * } + * ``` + */ +export function useEmpiricalScale(): UseEmpiricalScaleReturn { + const probe1Ref = useRef(null) + const probe2Ref = useRef(null) + + const measureScale = useCallback((): EmpiricalScaleResult => { + const probe1 = probe1Ref.current + const probe2 = probe2Ref.current + + if (!probe1 || !probe2) { + return { pixelsPerSvgUnit: 1, isValid: false } + } + + // Get screen positions of the probes + const rect1 = probe1.getBoundingClientRect() + const rect2 = probe2.getBoundingClientRect() + + // Use center of each probe + const screen1 = { + x: rect1.left + rect1.width / 2, + y: rect1.top + rect1.height / 2, + } + const screen2 = { + x: rect2.left + rect2.width / 2, + y: rect2.top + rect2.height / 2, + } + + // Calculate pixel distance between probes + const dx = screen2.x - screen1.x + const dy = screen2.y - screen1.y + const pixelDistance = Math.sqrt(dx * dx + dy * dy) + + // Guard against zero/invalid measurements + if (pixelDistance < 1 || !Number.isFinite(pixelDistance)) { + return { pixelsPerSvgUnit: 1, isValid: false } + } + + // Calculate pixels per SVG unit + const pixelsPerSvgUnit = pixelDistance / PROBE_SVG_DISTANCE + + return { + pixelsPerSvgUnit, + isValid: true, + debug: { + probe1Screen: screen1, + probe2Screen: screen2, + pixelDistance, + svgDistance: PROBE_SVG_DISTANCE, + }, + } + }, []) + + return { + probe1Ref, + probe2Ref, + measureScale, + probeSvgDistance: PROBE_SVG_DISTANCE, + } +} diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierState.ts b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierState.ts index c890e531..b1133acc 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierState.ts +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierState.ts @@ -22,7 +22,7 @@ 'use client' -import { useState, useCallback, useRef } from 'react' +import { useCallback, useRef, useState } from 'react' // ============================================================================ // Types diff --git a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouchHandlers.ts b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouchHandlers.ts index 9f310937..0a1f67ff 100644 --- a/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouchHandlers.ts +++ b/apps/web/src/arcade-games/know-your-world/features/magnifier/useMagnifierTouchHandlers.ts @@ -128,6 +128,8 @@ export function useMagnifierTouchHandlers( svgRef, containerRef, cursorPositionRef, + scaleProbe1Ref, + scaleProbe2Ref, isMagnifierExpanded, setIsMagnifierExpanded, getCurrentZoom, @@ -137,6 +139,9 @@ export function useMagnifierTouchHandlers( parsedViewBox, } = useMagnifierContext() + // Fixed distance between scale probes in SVG units (must match MagnifierOverlay) + const SCALE_PROBE_SVG_DISTANCE = 100 + const { mapData, currentPrompt, @@ -150,6 +155,47 @@ export function useMagnifierTouchHandlers( // Get isPinching from state machine const isPinchingFromMachine = interaction.isPinching + // ------------------------------------------------------------------------- + // Empirical Scale Measurement + // ------------------------------------------------------------------------- + /** + * Measure the actual pixels-per-SVG-unit by comparing screen positions + * of the two probe circles. This is robust to any transform pipeline changes. + */ + const measureEmpiricalScale = useCallback((): { pixelsPerSvgUnit: number; isValid: boolean } => { + const probe1 = scaleProbe1Ref.current + const probe2 = scaleProbe2Ref.current + + if (!probe1 || !probe2) { + return { pixelsPerSvgUnit: 1, isValid: false } + } + + // Get screen positions of the probes + const rect1 = probe1.getBoundingClientRect() + const rect2 = probe2.getBoundingClientRect() + + // Use center of each probe + const x1 = rect1.left + rect1.width / 2 + const y1 = rect1.top + rect1.height / 2 + const x2 = rect2.left + rect2.width / 2 + const y2 = rect2.top + rect2.height / 2 + + // Calculate pixel distance between probes + const dx = x2 - x1 + const dy = y2 - y1 + const pixelDistance = Math.sqrt(dx * dx + dy * dy) + + // Guard against zero/invalid measurements + if (pixelDistance < 1 || !Number.isFinite(pixelDistance)) { + return { pixelsPerSvgUnit: 1, isValid: false } + } + + // Calculate pixels per SVG unit + const pixelsPerSvgUnit = pixelDistance / SCALE_PROBE_SVG_DISTANCE + + return { pixelsPerSvgUnit, isValid: true } + }, [scaleProbe1Ref, scaleProbe2Ref, SCALE_PROBE_SVG_DISTANCE]) + // ------------------------------------------------------------------------- // Refs for touch tracking (internal to this hook) // ------------------------------------------------------------------------- @@ -204,15 +250,42 @@ export function useMagnifierTouchHandlers( x: touch.clientX - magnifierRect.left, y: touch.clientY - magnifierRect.top, } + + console.log('[MagnifierTouchStart] Touch started on magnifier:', { + touchClient: { x: touch.clientX, y: touch.clientY }, + magnifierRect: { + left: magnifierRect.left.toFixed(0), + top: magnifierRect.top.toFixed(0), + width: magnifierRect.width.toFixed(0), + height: magnifierRect.height.toFixed(0), + }, + tapPositionInMagnifier: magnifierTapPositionRef.current, + cursorPosition: cursorPositionRef.current, + currentZoom: getCurrentZoom().toFixed(2), + isMagnifierExpanded, + }) } // State machine handles dragging state via TOUCH_MOVE (transitions to magnifierPanning) // Note: touchAction: 'none' CSS prevents scrolling } }, - [getTouchDistance, getCurrentZoom, interaction, setIsMagnifierExpanded, magnifierRef] + [ + getTouchDistance, + getCurrentZoom, + interaction, + setIsMagnifierExpanded, + magnifierRef, + cursorPositionRef, + isMagnifierExpanded, + ] ) + // ------------------------------------------------------------------------- + // Ref to throttle logging (don't spam console on every move) + // ------------------------------------------------------------------------- + const lastLogTimeRef = useRef(0) + // ------------------------------------------------------------------------- // Touch Move Handler // ------------------------------------------------------------------------- @@ -273,32 +346,94 @@ export function useMagnifierTouchHandlers( // Update start position for next move (keep in client coords for delta calculation) magnifierTouchStartRef.current = { x: touch.clientX, y: touch.clientY } - // Parse viewBox and get magnifier dimensions - const viewBox = parseViewBoxDimensions(displayViewBox) - const leftoverWidth = containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right - const leftoverHeight = containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom - const { width: magnifierWidth, height: magnifierHeight } = getMagnifierDimensions( - leftoverWidth, - leftoverHeight - ) - const actualMagnifierWidth = isMagnifierExpanded ? leftoverWidth : magnifierWidth - const actualMagnifierHeight = isMagnifierExpanded ? leftoverHeight : magnifierHeight - const currentZoom = getCurrentZoom() + // ========================================================================= + // EMPIRICAL SCALE MEASUREMENT for 1:1 Touch Tracking + // ========================================================================= + // Instead of calculating through all transform layers, we measure the actual + // pixel-to-SVG ratio by comparing screen positions of probe elements. + // This is robust to any rendering pipeline changes. - // Calculate touch multiplier for 1:1 panning using extracted utility - const { multiplier: touchMultiplier } = calculateTouchMultiplier( - { - viewBoxWidth: viewBox.width, - viewBoxHeight: viewBox.height, - svgWidth: svgRect.width, - svgHeight: svgRect.height, - }, - { - width: actualMagnifierWidth, - height: actualMagnifierHeight, - zoom: currentZoom, - } - ) + const empiricalScale = measureEmpiricalScale() + const currentZoom = getCurrentZoom() + const viewBox = parseViewBoxDimensions(displayViewBox) + + // touchMultiplier = how many container pixels to move per touch pixel + // For 1:1: when finger moves N pixels, content should move N pixels in magnifier + // Since we measure pixelsPerSvgUnit (magnifier pixels per SVG unit), + // and cursor position is in container coords (main SVG scale), + // we need: touchMultiplier = viewportScale / pixelsPerSvgUnit + const viewportScale = + svgRect.width / viewBox.width > svgRect.height / viewBox.height + ? svgRect.height / viewBox.height + : svgRect.width / viewBox.width + + // Default to calculated value if empirical measurement fails + let touchMultiplier: number + if (empiricalScale.isValid) { + // Empirical: finger moves in screen pixels, we need cursor delta in container pixels + // pixelsPerSvgUnit = screen pixels per SVG unit in magnifier + // viewportScale = container pixels per SVG unit in main map + // touchMultiplier = viewportScale / pixelsPerSvgUnit + touchMultiplier = viewportScale / empiricalScale.pixelsPerSvgUnit + } else { + // Fallback to old calculation + const leftoverWidth = containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right + const leftoverHeight = + containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom + const { width: magnifierWidth, height: magnifierHeight } = getMagnifierDimensions( + leftoverWidth, + leftoverHeight + ) + const actualMagnifierWidth = isMagnifierExpanded ? leftoverWidth : magnifierWidth + const actualMagnifierHeight = isMagnifierExpanded ? leftoverHeight : magnifierHeight + const touchMultiplierResult = calculateTouchMultiplier( + { + viewBoxWidth: viewBox.width, + viewBoxHeight: viewBox.height, + svgWidth: svgRect.width, + svgHeight: svgRect.height, + }, + { + width: actualMagnifierWidth, + height: actualMagnifierHeight, + zoom: currentZoom, + } + ) + touchMultiplier = touchMultiplierResult.multiplier + } + + // DEBUG: Log empirical scale measurement (throttled to once per 500ms) + const now = performance.now() + if (now - lastLogTimeRef.current > 500) { + lastLogTimeRef.current = now + + console.log('[MagnifierPan] EMPIRICAL 1:1 tracking:', { + // Touch delta + touchDelta: { x: deltaX.toFixed(1), y: deltaY.toFixed(1) }, + + // Empirical measurement + empirical: { + isValid: empiricalScale.isValid, + pixelsPerSvgUnit: empiricalScale.pixelsPerSvgUnit.toFixed(4), + }, + + // Multiplier being used + touchMultiplier: touchMultiplier.toFixed(4), + viewportScale: viewportScale.toFixed(4), + + // Current zoom + zoom: currentZoom.toFixed(2), + + // Cursor movement (in container pixels) + cursorMovement: { + expected: { x: (-deltaX).toFixed(1), y: (-deltaY).toFixed(1) }, + actual: { + x: (-deltaX * touchMultiplier).toFixed(1), + y: (-deltaY * touchMultiplier).toFixed(1), + }, + }, + }) + } // Apply pan delta and clamp to SVG bounds const svgOffsetX = svgRect.left - containerRect.left @@ -407,6 +542,7 @@ export function useMagnifierTouchHandlers( isMagnifierExpanded, getTouchDistance, setTargetZoom, + measureEmpiricalScale, detectRegions, onCursorUpdate, gameMode, @@ -427,6 +563,7 @@ export function useMagnifierTouchHandlers( hotColdEnabledRef, largestPieceSizesRef, parsedViewBox, + interaction, ] ) @@ -473,8 +610,10 @@ export function useMagnifierTouchHandlers( type: 'TOUCH_END', touchCount: e.touches.length, // Number of fingers still touching }) - console.log('[handleMagnifierTouchEnd] After dispatch, new phase:', - interaction.state.mode === 'mobile' ? interaction.state.phase : 'N/A') + console.log( + '[handleMagnifierTouchEnd] After dispatch, new phase:', + interaction.state.mode === 'mobile' ? interaction.state.phase : 'N/A' + ) // State machine is authoritative for dragging state (magnifierPanning phase) magnifierTouchStartRef.current = null