fix(know-your-world): stabilize mobile magnifier 1:1 touch tracking
Use empirical scale measurement with direct delta tracking instead of closed-loop feedback approach. The feedback loop was causing instability due to one-frame lag between error measurement and cursor updates. The empirical approach measures actual pixels-per-SVG-unit using probe circles, then applies frame-by-frame deltas with that scale factor. This automatically accounts for zoom, aspect ratio, and letterboxing without accumulating error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
39886e859c
commit
ab30adda25
|
|
@ -507,6 +507,10 @@ export function MapRenderer({
|
||||||
// Refs for scale probe elements (for empirical 1:1 tracking measurement)
|
// Refs for scale probe elements (for empirical 1:1 tracking measurement)
|
||||||
const scaleProbe1Ref = useRef<SVGCircleElement>(null)
|
const scaleProbe1Ref = useRef<SVGCircleElement>(null)
|
||||||
const scaleProbe2Ref = useRef<SVGCircleElement>(null)
|
const scaleProbe2Ref = useRef<SVGCircleElement>(null)
|
||||||
|
// Refs for closed-loop anchor probe tracking (1:1 touch tracking)
|
||||||
|
const anchorProbeRef = useRef<SVGCircleElement>(null)
|
||||||
|
const anchorSvgPositionRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
const fingerStartRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
// Where user tapped on magnifier
|
// Where user tapped on magnifier
|
||||||
const magnifierTapPositionRef = useRef<{ x: number; y: number } | null>(null)
|
const magnifierTapPositionRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
|
|
@ -2356,6 +2360,9 @@ export function MapRenderer({
|
||||||
cursorPositionRef,
|
cursorPositionRef,
|
||||||
scaleProbe1Ref,
|
scaleProbe1Ref,
|
||||||
scaleProbe2Ref,
|
scaleProbe2Ref,
|
||||||
|
anchorProbeRef,
|
||||||
|
anchorSvgPositionRef,
|
||||||
|
fingerStartRef,
|
||||||
// Position & Animation (cursorPosition comes from state machine)
|
// Position & Animation (cursorPosition comes from state machine)
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
zoomSpring,
|
zoomSpring,
|
||||||
|
|
@ -2396,6 +2403,9 @@ export function MapRenderer({
|
||||||
cursorPositionRef,
|
cursorPositionRef,
|
||||||
scaleProbe1Ref,
|
scaleProbe1Ref,
|
||||||
scaleProbe2Ref,
|
scaleProbe2Ref,
|
||||||
|
anchorProbeRef,
|
||||||
|
anchorSvgPositionRef,
|
||||||
|
fingerStartRef,
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
zoomSpring,
|
zoomSpring,
|
||||||
magnifierSpring,
|
magnifierSpring,
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,12 @@ export interface MagnifierContextValue {
|
||||||
scaleProbe1Ref: RefObject<SVGCircleElement>
|
scaleProbe1Ref: RefObject<SVGCircleElement>
|
||||||
/** Scale probe 2 ref (for empirical scale measurement) */
|
/** Scale probe 2 ref (for empirical scale measurement) */
|
||||||
scaleProbe2Ref: RefObject<SVGCircleElement>
|
scaleProbe2Ref: RefObject<SVGCircleElement>
|
||||||
|
/** Anchor probe ref (for closed-loop 1:1 tracking) */
|
||||||
|
anchorProbeRef: RefObject<SVGCircleElement>
|
||||||
|
/** Anchor SVG position - set on touch start, probe stays at this SVG coord */
|
||||||
|
anchorSvgPositionRef: React.MutableRefObject<{ x: number; y: number } | null>
|
||||||
|
/** Finger start position in screen coords - where finger was when anchor was placed */
|
||||||
|
fingerStartRef: React.MutableRefObject<{ x: number; y: number } | null>
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Position & Animation
|
// Position & Animation
|
||||||
|
|
@ -193,6 +199,9 @@ export function MagnifierProvider({ children, value }: MagnifierProviderProps) {
|
||||||
value.cursorPositionRef,
|
value.cursorPositionRef,
|
||||||
value.scaleProbe1Ref,
|
value.scaleProbe1Ref,
|
||||||
value.scaleProbe2Ref,
|
value.scaleProbe2Ref,
|
||||||
|
value.anchorProbeRef,
|
||||||
|
value.anchorSvgPositionRef,
|
||||||
|
value.fingerStartRef,
|
||||||
// Position & Animation
|
// Position & Animation
|
||||||
value.cursorPosition,
|
value.cursorPosition,
|
||||||
value.zoomSpring,
|
value.zoomSpring,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,8 @@ export function MagnifierOverlay({
|
||||||
highZoomThreshold,
|
highZoomThreshold,
|
||||||
scaleProbe1Ref,
|
scaleProbe1Ref,
|
||||||
scaleProbe2Ref,
|
scaleProbe2Ref,
|
||||||
|
anchorProbeRef,
|
||||||
|
anchorSvgPositionRef,
|
||||||
} = useMagnifierContext()
|
} = useMagnifierContext()
|
||||||
|
|
||||||
// Distance between scale probes in SVG units (must match useEmpiricalScale.ts)
|
// Distance between scale probes in SVG units (must match useEmpiricalScale.ts)
|
||||||
|
|
@ -310,6 +312,21 @@ export function MagnifierOverlay({
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
data-scale-probe="2"
|
data-scale-probe="2"
|
||||||
/>
|
/>
|
||||||
|
{/* Anchor probe for closed-loop 1:1 tracking */}
|
||||||
|
{/* Position is set on touch start and stays fixed in SVG coords */}
|
||||||
|
{/* We measure its screen position to solve for cursor movement */}
|
||||||
|
{anchorSvgPositionRef.current && (
|
||||||
|
<circle
|
||||||
|
ref={anchorProbeRef}
|
||||||
|
cx={anchorSvgPositionRef.current.x}
|
||||||
|
cy={anchorSvgPositionRef.current.y}
|
||||||
|
r={0.5}
|
||||||
|
fill="transparent"
|
||||||
|
stroke="none"
|
||||||
|
pointerEvents="none"
|
||||||
|
data-anchor-probe="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,7 @@ import { getMagnifierDimensions } from '../../utils/magnifierDimensions'
|
||||||
import { useMapGameContext } from '../game'
|
import { useMapGameContext } from '../game'
|
||||||
import { getRenderedViewport } from '../labels'
|
import { getRenderedViewport } from '../labels'
|
||||||
import { useMagnifierContext } from './MagnifierContext'
|
import { useMagnifierContext } from './MagnifierContext'
|
||||||
import {
|
import { calculateTouchMultiplier, clampToSvgBounds, parseViewBoxDimensions } from './panningMath'
|
||||||
applyPanDelta,
|
|
||||||
calculateTouchMultiplier,
|
|
||||||
clampToSvgBounds,
|
|
||||||
parseViewBoxDimensions,
|
|
||||||
} from './panningMath'
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Constants
|
// Constants
|
||||||
|
|
@ -130,6 +125,7 @@ export function useMagnifierTouchHandlers(
|
||||||
cursorPositionRef,
|
cursorPositionRef,
|
||||||
scaleProbe1Ref,
|
scaleProbe1Ref,
|
||||||
scaleProbe2Ref,
|
scaleProbe2Ref,
|
||||||
|
anchorSvgPositionRef,
|
||||||
isMagnifierExpanded,
|
isMagnifierExpanded,
|
||||||
setIsMagnifierExpanded,
|
setIsMagnifierExpanded,
|
||||||
getCurrentZoom,
|
getCurrentZoom,
|
||||||
|
|
@ -196,6 +192,20 @@ export function useMagnifierTouchHandlers(
|
||||||
return { pixelsPerSvgUnit, isValid: true }
|
return { pixelsPerSvgUnit, isValid: true }
|
||||||
}, [scaleProbe1Ref, scaleProbe2Ref, SCALE_PROBE_SVG_DISTANCE])
|
}, [scaleProbe1Ref, scaleProbe2Ref, SCALE_PROBE_SVG_DISTANCE])
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Direct Anchor Tracking
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* Direct approach to 1:1 tracking:
|
||||||
|
* 1. On touch start, record anchor's INITIAL screen position
|
||||||
|
* 2. On touch move, measure finger delta from its start position
|
||||||
|
* 3. Apply inverse delta to cursor (in container coords via empirical scale)
|
||||||
|
*
|
||||||
|
* This avoids the feedback loop issue where measuring anchor error after
|
||||||
|
* cursor update causes instability.
|
||||||
|
*/
|
||||||
|
const anchorInitialScreenRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Refs for touch tracking (internal to this hook)
|
// Refs for touch tracking (internal to this hook)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -204,6 +214,8 @@ export function useMagnifierTouchHandlers(
|
||||||
const magnifierTapPositionRef = useRef<{ x: number; y: number } | null>(null)
|
const magnifierTapPositionRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
const pinchStartDistanceRef = useRef<number | null>(null)
|
const pinchStartDistanceRef = useRef<number | null>(null)
|
||||||
const pinchStartZoomRef = useRef<number | null>(null)
|
const pinchStartZoomRef = useRef<number | null>(null)
|
||||||
|
// Track last zoom level to detect zoom changes and re-anchor
|
||||||
|
const lastZoomRef = useRef<number | null>(null)
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Helper: Calculate distance between two touch points
|
// Helper: Calculate distance between two touch points
|
||||||
|
|
@ -243,27 +255,83 @@ export function useMagnifierTouchHandlers(
|
||||||
magnifierTouchStartRef.current = { x: touch.clientX, y: touch.clientY }
|
magnifierTouchStartRef.current = { x: touch.clientX, y: touch.clientY }
|
||||||
magnifierDidMoveRef.current = false // Reset movement tracking
|
magnifierDidMoveRef.current = false // Reset movement tracking
|
||||||
|
|
||||||
// Record tap position relative to magnifier for tap-to-select
|
// =======================================================================
|
||||||
if (magnifierRef.current) {
|
// ANCHOR TRACKING SETUP (for empirical scale measurement)
|
||||||
|
// =======================================================================
|
||||||
|
// Calculate where the finger touch is in SVG coordinates
|
||||||
|
// This becomes the anchor position - a fixed point in SVG space that we'll
|
||||||
|
// try to keep under the finger as it moves
|
||||||
|
if (
|
||||||
|
magnifierRef.current &&
|
||||||
|
svgRef.current &&
|
||||||
|
containerRef.current &&
|
||||||
|
cursorPositionRef.current
|
||||||
|
) {
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect()
|
||||||
|
const svgRect = svgRef.current.getBoundingClientRect()
|
||||||
const magnifierRect = magnifierRef.current.getBoundingClientRect()
|
const magnifierRect = magnifierRef.current.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Get viewport info for coordinate conversion
|
||||||
|
const viewBox = parseViewBoxDimensions(displayViewBox)
|
||||||
|
const viewport = getRenderedViewport(
|
||||||
|
svgRect,
|
||||||
|
parsedViewBox.x,
|
||||||
|
parsedViewBox.y,
|
||||||
|
parsedViewBox.width,
|
||||||
|
parsedViewBox.height
|
||||||
|
)
|
||||||
|
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
|
||||||
|
const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY
|
||||||
|
|
||||||
|
// Current cursor position in SVG coordinates (center of magnifier view)
|
||||||
|
const cursorSvgX =
|
||||||
|
(cursorPositionRef.current.x - svgOffsetX) / viewport.scale + parsedViewBox.x
|
||||||
|
const cursorSvgY =
|
||||||
|
(cursorPositionRef.current.y - svgOffsetY) / viewport.scale + parsedViewBox.y
|
||||||
|
|
||||||
|
// Calculate magnified viewBox dimensions
|
||||||
|
const currentZoom = getCurrentZoom()
|
||||||
|
const magnifiedWidth = parsedViewBox.width / currentZoom
|
||||||
|
const magnifiedHeight = parsedViewBox.height / currentZoom
|
||||||
|
|
||||||
|
// Where did the finger touch within the magnifier? (0-1 normalized)
|
||||||
|
const touchInMagnifierX = (touch.clientX - magnifierRect.left) / magnifierRect.width
|
||||||
|
const touchInMagnifierY = (touch.clientY - magnifierRect.top) / magnifierRect.height
|
||||||
|
|
||||||
|
// Convert to SVG coordinates within the magnified view
|
||||||
|
const touchSvgX = cursorSvgX - magnifiedWidth / 2 + touchInMagnifierX * magnifiedWidth
|
||||||
|
const touchSvgY = cursorSvgY - magnifiedHeight / 2 + touchInMagnifierY * magnifiedHeight
|
||||||
|
|
||||||
|
// Set anchor position - this SVG coordinate should stay under the finger
|
||||||
|
anchorSvgPositionRef.current = { x: touchSvgX, y: touchSvgY }
|
||||||
|
|
||||||
|
// Record the finger's initial screen position for delta tracking
|
||||||
|
// We'll use this to track how far the finger has moved from start
|
||||||
|
anchorInitialScreenRef.current = { x: touch.clientX, y: touch.clientY }
|
||||||
|
|
||||||
|
console.log('[MagnifierTouchStart] ANCHOR SETUP:', {
|
||||||
|
fingerScreen: { x: touch.clientX, y: touch.clientY },
|
||||||
|
touchInMagnifier: {
|
||||||
|
x: touchInMagnifierX.toFixed(2),
|
||||||
|
y: touchInMagnifierY.toFixed(2),
|
||||||
|
},
|
||||||
|
cursorSvg: { x: cursorSvgX.toFixed(1), y: cursorSvgY.toFixed(1) },
|
||||||
|
anchorSvg: { x: touchSvgX.toFixed(1), y: touchSvgY.toFixed(1) },
|
||||||
|
magnifiedSize: {
|
||||||
|
w: magnifiedWidth.toFixed(1),
|
||||||
|
h: magnifiedHeight.toFixed(1),
|
||||||
|
},
|
||||||
|
zoom: currentZoom.toFixed(2),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Record tap position relative to magnifier for tap-to-select
|
||||||
magnifierTapPositionRef.current = {
|
magnifierTapPositionRef.current = {
|
||||||
x: touch.clientX - magnifierRect.left,
|
x: touch.clientX - magnifierRect.left,
|
||||||
y: touch.clientY - magnifierRect.top,
|
y: touch.clientY - magnifierRect.top,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[MagnifierTouchStart] Touch started on magnifier:', {
|
// Initialize lastZoomRef for zoom change detection
|
||||||
touchClient: { x: touch.clientX, y: touch.clientY },
|
lastZoomRef.current = currentZoom
|
||||||
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)
|
// State machine handles dragging state via TOUCH_MOVE (transitions to magnifierPanning)
|
||||||
|
|
@ -276,8 +344,12 @@ export function useMagnifierTouchHandlers(
|
||||||
interaction,
|
interaction,
|
||||||
setIsMagnifierExpanded,
|
setIsMagnifierExpanded,
|
||||||
magnifierRef,
|
magnifierRef,
|
||||||
|
svgRef,
|
||||||
|
containerRef,
|
||||||
cursorPositionRef,
|
cursorPositionRef,
|
||||||
isMagnifierExpanded,
|
anchorSvgPositionRef,
|
||||||
|
parsedViewBox,
|
||||||
|
displayViewBox,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -347,36 +419,69 @@ export function useMagnifierTouchHandlers(
|
||||||
magnifierTouchStartRef.current = { x: touch.clientX, y: touch.clientY }
|
magnifierTouchStartRef.current = { x: touch.clientX, y: touch.clientY }
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// EMPIRICAL SCALE MEASUREMENT for 1:1 Touch Tracking
|
// DIRECT DELTA TRACKING for True 1:1 Touch Tracking
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Instead of calculating through all transform layers, we measure the actual
|
// Simple approach that avoids feedback loops:
|
||||||
// pixel-to-SVG ratio by comparing screen positions of probe elements.
|
// 1. On touch start, record finger's initial screen position
|
||||||
// This is robust to any rendering pipeline changes.
|
// 2. On each move, calculate how far finger moved from START
|
||||||
|
// 3. Convert that screen delta to cursor delta using empirical scale
|
||||||
|
// 4. Apply inverse delta to cursor (dragging right moves view left)
|
||||||
|
//
|
||||||
|
// This uses the same "screen pixels per SVG unit" measurement but applies
|
||||||
|
// it to the delta from start, not frame-by-frame deltas. This is more
|
||||||
|
// stable because we're not accumulating errors.
|
||||||
|
|
||||||
const empiricalScale = measureEmpiricalScale()
|
|
||||||
const currentZoom = getCurrentZoom()
|
const currentZoom = getCurrentZoom()
|
||||||
|
const anchorInitialScreen = anchorInitialScreenRef.current
|
||||||
|
|
||||||
|
let cursorDeltaX: number
|
||||||
|
let cursorDeltaY: number
|
||||||
|
|
||||||
|
// Get empirical scale - this tells us how many screen pixels = 1 SVG unit in magnifier
|
||||||
|
const empiricalScale = measureEmpiricalScale()
|
||||||
const viewBox = parseViewBoxDimensions(displayViewBox)
|
const viewBox = parseViewBoxDimensions(displayViewBox)
|
||||||
|
|
||||||
// touchMultiplier = how many container pixels to move per touch pixel
|
// viewport scale = container pixels per SVG unit (for the main map, not magnifier)
|
||||||
// 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 =
|
const viewportScale =
|
||||||
svgRect.width / viewBox.width > svgRect.height / viewBox.height
|
svgRect.width / viewBox.width > svgRect.height / viewBox.height
|
||||||
? svgRect.height / viewBox.height
|
? svgRect.height / viewBox.height
|
||||||
: svgRect.width / viewBox.width
|
: svgRect.width / viewBox.width
|
||||||
|
|
||||||
// Default to calculated value if empirical measurement fails
|
if (empiricalScale.isValid && anchorInitialScreen) {
|
||||||
let touchMultiplier: number
|
// EMPIRICAL APPROACH: Use measured scale for accurate conversion
|
||||||
if (empiricalScale.isValid) {
|
//
|
||||||
// Empirical: finger moves in screen pixels, we need cursor delta in container pixels
|
// The user drags in screen pixels. We need to move the cursor in container pixels.
|
||||||
// pixelsPerSvgUnit = screen pixels per SVG unit in magnifier
|
// - In the magnifier: 1 SVG unit = empiricalScale.pixelsPerSvgUnit screen pixels
|
||||||
// viewportScale = container pixels per SVG unit in main map
|
// - In the main map: 1 SVG unit = viewportScale container pixels
|
||||||
// touchMultiplier = viewportScale / pixelsPerSvgUnit
|
//
|
||||||
touchMultiplier = viewportScale / empiricalScale.pixelsPerSvgUnit
|
// So: touchMultiplier = viewportScale / empiricalScale.pixelsPerSvgUnit
|
||||||
|
//
|
||||||
|
// When finger moves 1 screen pixel in magnifier:
|
||||||
|
// - That's (1 / pixelsPerSvgUnit) SVG units
|
||||||
|
// - Which is (viewportScale / pixelsPerSvgUnit) container pixels of cursor movement
|
||||||
|
|
||||||
|
const touchMultiplier = viewportScale / empiricalScale.pixelsPerSvgUnit
|
||||||
|
|
||||||
|
// Apply delta from this frame (deltaX, deltaY already calculated above)
|
||||||
|
// Inverted because dragging right should move cursor left (panning view)
|
||||||
|
cursorDeltaX = -deltaX * touchMultiplier
|
||||||
|
cursorDeltaY = -deltaY * touchMultiplier
|
||||||
|
|
||||||
|
// DEBUG: Log tracking (throttled)
|
||||||
|
const now = performance.now()
|
||||||
|
if (now - lastLogTimeRef.current > 200) {
|
||||||
|
lastLogTimeRef.current = now
|
||||||
|
console.log('[MagnifierPan] DIRECT delta tracking:', {
|
||||||
|
frameDelta: { x: deltaX.toFixed(1), y: deltaY.toFixed(1) },
|
||||||
|
touchMultiplier: touchMultiplier.toFixed(4),
|
||||||
|
cursorDelta: { x: cursorDeltaX.toFixed(2), y: cursorDeltaY.toFixed(2) },
|
||||||
|
empiricalPxPerSvg: empiricalScale.pixelsPerSvgUnit.toFixed(2),
|
||||||
|
viewportScale: viewportScale.toFixed(2),
|
||||||
|
zoom: currentZoom.toFixed(2),
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to old calculation
|
// FALLBACK: Calculate touch multiplier from dimensions
|
||||||
const leftoverWidth = containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
|
const leftoverWidth = containerRect.width - SAFE_ZONE_MARGINS.left - SAFE_ZONE_MARGINS.right
|
||||||
const leftoverHeight =
|
const leftoverHeight =
|
||||||
containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
|
containerRect.height - SAFE_ZONE_MARGINS.top - SAFE_ZONE_MARGINS.bottom
|
||||||
|
|
@ -399,50 +504,30 @@ export function useMagnifierTouchHandlers(
|
||||||
zoom: currentZoom,
|
zoom: currentZoom,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
touchMultiplier = touchMultiplierResult.multiplier
|
const touchMultiplier = touchMultiplierResult.multiplier
|
||||||
|
|
||||||
|
// Apply delta-based calculation (inverted because dragging right should move view left)
|
||||||
|
cursorDeltaX = -deltaX * touchMultiplier
|
||||||
|
cursorDeltaY = -deltaY * touchMultiplier
|
||||||
|
|
||||||
|
const now = performance.now()
|
||||||
|
if (now - lastLogTimeRef.current > 500) {
|
||||||
|
lastLogTimeRef.current = now
|
||||||
|
console.log('[MagnifierPan] FALLBACK calculated tracking:', {
|
||||||
|
touchDelta: { x: deltaX.toFixed(1), y: deltaY.toFixed(1) },
|
||||||
|
touchMultiplier: touchMultiplier.toFixed(4),
|
||||||
|
zoom: currentZoom.toFixed(2),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG: Log empirical scale measurement (throttled to once per 500ms)
|
// Apply cursor delta and clamp to SVG bounds
|
||||||
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
|
const svgOffsetX = svgRect.left - containerRect.left
|
||||||
const svgOffsetY = svgRect.top - containerRect.top
|
const svgOffsetY = svgRect.top - containerRect.top
|
||||||
const newCursor = applyPanDelta(
|
const newCursor = {
|
||||||
cursorPositionRef.current,
|
x: cursorPositionRef.current.x + cursorDeltaX,
|
||||||
{ x: deltaX, y: deltaY },
|
y: cursorPositionRef.current.y + cursorDeltaY,
|
||||||
touchMultiplier
|
}
|
||||||
)
|
|
||||||
const clamped = clampToSvgBounds(newCursor, {
|
const clamped = clampToSvgBounds(newCursor, {
|
||||||
left: svgOffsetX,
|
left: svgOffsetX,
|
||||||
top: svgOffsetY,
|
top: svgOffsetY,
|
||||||
|
|
@ -516,6 +601,7 @@ export function useMagnifierTouchHandlers(
|
||||||
pointerLocked: false, // Mobile never uses pointer lock
|
pointerLocked: false, // Mobile never uses pointer lock
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set the target zoom (no re-anchoring needed with direct delta tracking)
|
||||||
setTargetZoom(zoomSearchResult.zoom)
|
setTargetZoom(zoomSearchResult.zoom)
|
||||||
|
|
||||||
// Broadcast cursor update to other players (if in multiplayer)
|
// Broadcast cursor update to other players (if in multiplayer)
|
||||||
|
|
@ -559,11 +645,13 @@ export function useMagnifierTouchHandlers(
|
||||||
svgRef,
|
svgRef,
|
||||||
containerRef,
|
containerRef,
|
||||||
cursorPositionRef,
|
cursorPositionRef,
|
||||||
|
anchorSvgPositionRef,
|
||||||
// Note: hoveredRegion and setHoveredRegion removed - state machine is authoritative
|
// Note: hoveredRegion and setHoveredRegion removed - state machine is authoritative
|
||||||
hotColdEnabledRef,
|
hotColdEnabledRef,
|
||||||
largestPieceSizesRef,
|
largestPieceSizesRef,
|
||||||
parsedViewBox,
|
parsedViewBox,
|
||||||
interaction,
|
interaction,
|
||||||
|
magnifierRef,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -620,6 +708,10 @@ export function useMagnifierTouchHandlers(
|
||||||
magnifierDidMoveRef.current = false
|
magnifierDidMoveRef.current = false
|
||||||
magnifierTapPositionRef.current = null
|
magnifierTapPositionRef.current = null
|
||||||
|
|
||||||
|
// Clear anchor tracking refs
|
||||||
|
anchorSvgPositionRef.current = null
|
||||||
|
anchorInitialScreenRef.current = null
|
||||||
|
|
||||||
// If there was a changed touch that ended and it wasn't a drag, check for tap-to-select
|
// If there was a changed touch that ended and it wasn't a drag, check for tap-to-select
|
||||||
if (e.changedTouches.length === 1 && !didMove && tapPosition) {
|
if (e.changedTouches.length === 1 && !didMove && tapPosition) {
|
||||||
// Convert tap position on magnifier to SVG coordinates
|
// Convert tap position on magnifier to SVG coordinates
|
||||||
|
|
@ -691,6 +783,7 @@ export function useMagnifierTouchHandlers(
|
||||||
containerRef,
|
containerRef,
|
||||||
cursorPositionRef,
|
cursorPositionRef,
|
||||||
parsedViewBox,
|
parsedViewBox,
|
||||||
|
anchorSvgPositionRef,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue