feat(know-your-world): implement empirical scale measurement for 1:1 magnifier tracking
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 <noreply@anthropic.com>
This commit is contained in:
parent
e712fcbcb7
commit
39886e859c
|
|
@ -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<HTMLDivElement>(null)
|
||||
// Refs for scale probe elements (for empirical 1:1 tracking measurement)
|
||||
const scaleProbe1Ref = useRef<SVGCircleElement>(null)
|
||||
const scaleProbe2Ref = useRef<SVGCircleElement>(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,
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>
|
||||
/** Cursor position ref (mutable) */
|
||||
cursorPositionRef: React.MutableRefObject<{ x: number; y: number } | null>
|
||||
/** Scale probe 1 ref (for empirical scale measurement) */
|
||||
scaleProbe1Ref: RefObject<SVGCircleElement>
|
||||
/** Scale probe 2 ref (for empirical scale measurement) */
|
||||
scaleProbe2Ref: RefObject<SVGCircleElement>
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<MagnifierCrosshair
|
||||
cursorSvgX={cursorSvgX}
|
||||
cursorSvgY={cursorSvgY}
|
||||
viewBoxWidth={viewBoxWidth}
|
||||
rotationAngle={rotationAngle}
|
||||
heatStyle={crosshairHeatStyle}
|
||||
/>
|
||||
<>
|
||||
<MagnifierCrosshair
|
||||
cursorSvgX={cursorSvgX}
|
||||
cursorSvgY={cursorSvgY}
|
||||
viewBoxWidth={viewBoxWidth}
|
||||
rotationAngle={rotationAngle}
|
||||
heatStyle={crosshairHeatStyle}
|
||||
/>
|
||||
{/* Scale probe circles for empirical 1:1 tracking measurement */}
|
||||
{/* These are invisible but their screen positions are measured via getBoundingClientRect */}
|
||||
<circle
|
||||
ref={scaleProbe1Ref}
|
||||
cx={cursorSvgX - SCALE_PROBE_DISTANCE / 2}
|
||||
cy={cursorSvgY}
|
||||
r={0.5}
|
||||
fill="transparent"
|
||||
stroke="none"
|
||||
pointerEvents="none"
|
||||
data-scale-probe="1"
|
||||
/>
|
||||
<circle
|
||||
ref={scaleProbe2Ref}
|
||||
cx={cursorSvgX + SCALE_PROBE_DISTANCE / 2}
|
||||
cy={cursorSvgY}
|
||||
r={0.5}
|
||||
fill="transparent"
|
||||
stroke="none"
|
||||
pointerEvents="none"
|
||||
data-scale-probe="2"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import type { SpringValue } from '@react-spring/web'
|
|||
|
||||
import { MagnifierOverlay } from './MagnifierOverlay'
|
||||
import {
|
||||
useMagnifierTouchHandlers,
|
||||
type UseMagnifierTouchHandlersOptions,
|
||||
useMagnifierTouchHandlers,
|
||||
} from './useMagnifierTouchHandlers'
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@
|
|||
// State Management Hooks
|
||||
// ============================================================================
|
||||
|
||||
export type {
|
||||
EmpiricalScaleResult,
|
||||
UseEmpiricalScaleReturn,
|
||||
} from './useEmpiricalScale'
|
||||
export { useEmpiricalScale } from './useEmpiricalScale'
|
||||
export type {
|
||||
UseMagnifierStateOptions,
|
||||
UseMagnifierStateReturn,
|
||||
|
|
|
|||
|
|
@ -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<SVGCircleElement | null>
|
||||
/** Ref for second probe element (circle at known SVG coords) */
|
||||
probe2Ref: React.RefObject<SVGCircleElement | null>
|
||||
/** 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:
|
||||
* <circle ref={probe1Ref} cx={cursorSvgX - probeSvgDistance/2} cy={cursorSvgY} r={0.1} opacity={0} />
|
||||
* <circle ref={probe2Ref} cx={cursorSvgX + probeSvgDistance/2} cy={cursorSvgY} r={0.1} opacity={0} />
|
||||
*
|
||||
* // In touch handler:
|
||||
* const { pixelsPerSvgUnit, isValid } = measureScale()
|
||||
* if (isValid) {
|
||||
* const touchMultiplier = 1 / pixelsPerSvgUnit
|
||||
* // Apply to cursor movement...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useEmpiricalScale(): UseEmpiricalScaleReturn {
|
||||
const probe1Ref = useRef<SVGCircleElement | null>(null)
|
||||
const probe2Ref = useRef<SVGCircleElement | null>(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,
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue