refactor: integrate usePointerLock hook into MapRenderer (Phase 1)

Replace pointer lock state and event listeners with usePointerLock hook:
- Removed local pointerLocked state
- Removed 134 lines of pointer lock event listeners and cleanup logic
- Added usePointerLock hook with onLockAcquired and onLockReleased callbacks
- Callbacks handle: cursor position saving, zoom updates, squish reset, zoom recalculation
- Updated handleContainerClick to use hook's requestPointerLock()

Benefits:
- Pointer lock logic now encapsulated in reusable hook
- Event listeners automatically managed by hook
- Cleaner component code with callbacks instead of inline effects
- Reduced MapRenderer from 2148 → 2083 lines (-65 lines, -3.0%)

Total reduction so far: 2430 → 2083 lines (-347 lines, -14.3%)

Part of Phase 1: Integrate usePointerLock hook (low risk).
No behavior changes - purely structural refactoring.
This commit is contained in:
Thomas Hallock
2025-11-24 07:14:46 -06:00
parent 12aba01b73
commit fb55a1ce53

View File

@@ -21,6 +21,7 @@ import {
} from '../utils/screenPixelRatio'
import { findOptimalZoom } from '../utils/adaptiveZoomSearch'
import { useRegionDetection } from '../hooks/useRegionDetection'
import { usePointerLock } from '../hooks/usePointerLock'
// Debug flag: show technical info in magnifier (dev only)
const SHOW_MAGNIFIER_DEBUG_INFO = process.env.NODE_ENV === 'development'
@@ -191,28 +192,105 @@ export function MapRenderer({
smallRegionThreshold: 15,
smallRegionAreaThreshold: 200,
})
// State that needs to be available for pointer lock callbacks
const [targetZoom, setTargetZoom] = useState(10)
const uncappedAdaptiveZoomRef = useRef<number | null>(null)
const cursorPositionRef = useRef<{ x: number; y: number } | null>(null)
const initialCapturePositionRef = useRef<{ x: number; y: number } | null>(null)
const [cursorSquish, setCursorSquish] = useState({ x: 1, y: 1 })
const [isReleasingPointerLock, setIsReleasingPointerLock] = useState(false)
// Pointer lock hook
const { pointerLocked, requestPointerLock, exitPointerLock } = usePointerLock({
containerRef,
onLockAcquired: () => {
// Save initial cursor position
if (cursorPositionRef.current) {
initialCapturePositionRef.current = { ...cursorPositionRef.current }
console.log(
'[Pointer Lock] 📍 Saved initial capture position:',
initialCapturePositionRef.current
)
}
// Update target zoom to uncapped value
if (uncappedAdaptiveZoomRef.current !== null) {
console.log(
`[Pointer Lock] Updating target zoom to uncapped value: ${uncappedAdaptiveZoomRef.current.toFixed(1)}×`
)
setTargetZoom(uncappedAdaptiveZoomRef.current)
}
},
onLockReleased: () => {
console.log('[Pointer Lock] 🔓 RELEASED - Starting cleanup and zoom recalculation')
// Reset cursor squish
setCursorSquish({ x: 1, y: 1 })
setIsReleasingPointerLock(false)
// Recalculate zoom with capping
if (uncappedAdaptiveZoomRef.current !== null && containerRef.current && svgRef.current) {
const containerRect = containerRef.current.getBoundingClientRect()
const svgRect = svgRef.current.getBoundingClientRect()
const magnifierWidth = containerRect.width * 0.5
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
const viewBoxWidth = viewBoxParts[2]
if (viewBoxWidth && !Number.isNaN(viewBoxWidth)) {
const uncappedZoom = uncappedAdaptiveZoomRef.current
const screenPixelRatio = calculateScreenPixelRatio({
magnifierWidth,
viewBoxWidth,
svgWidth: svgRect.width,
zoom: uncappedZoom,
})
console.log('[Pointer Lock] Screen pixel ratio check:', {
uncappedZoom: uncappedZoom.toFixed(1),
screenPixelRatio: screenPixelRatio.toFixed(1),
threshold: PRECISION_MODE_THRESHOLD,
exceedsThreshold: isAboveThreshold(screenPixelRatio, PRECISION_MODE_THRESHOLD),
})
// If it exceeds threshold, cap the zoom to stay at threshold
if (isAboveThreshold(screenPixelRatio, PRECISION_MODE_THRESHOLD)) {
const maxZoom = calculateMaxZoomAtThreshold(
PRECISION_MODE_THRESHOLD,
magnifierWidth,
svgRect.width
)
const cappedZoom = Math.min(uncappedZoom, maxZoom)
console.log(
`[Pointer Lock] ✅ Capping zoom: ${uncappedZoom.toFixed(1)}×${cappedZoom.toFixed(1)}× (threshold: ${PRECISION_MODE_THRESHOLD} px/px)`
)
setTargetZoom(cappedZoom)
} else {
console.log(
`[Pointer Lock] No capping needed - zoom ${uncappedZoom.toFixed(1)}× is below threshold`
)
}
} else {
console.log('[Pointer Lock] ⚠️ Cannot recalculate zoom - invalid viewBoxWidth')
}
} else {
console.log('[Pointer Lock] ⚠️ Cannot recalculate zoom - missing refs:', {
hasContainer: !!containerRef.current,
hasSvg: !!svgRef.current,
hasUncappedZoom: uncappedAdaptiveZoomRef.current !== null,
})
}
},
})
const [svgDimensions, setSvgDimensions] = useState({ width: 1000, height: 500 })
const [cursorPosition, setCursorPosition] = useState<{ x: number; y: number } | null>(null)
const [showMagnifier, setShowMagnifier] = useState(false)
const [targetZoom, setTargetZoom] = useState(10)
const [targetOpacity, setTargetOpacity] = useState(0)
const [targetTop, setTargetTop] = useState(20)
const [targetLeft, setTargetLeft] = useState(20)
// Pointer lock management
const [pointerLocked, setPointerLocked] = useState(false)
// Cursor position tracking (container-relative coordinates)
const cursorPositionRef = useRef<{ x: number; y: number } | null>(null)
const initialCapturePositionRef = useRef<{ x: number; y: number } | null>(null)
const [smallestRegionSize, setSmallestRegionSize] = useState<number>(Infinity)
// Cursor distortion at boundaries (for squish effect)
const [cursorSquish, setCursorSquish] = useState({ x: 1, y: 1 }) // Scale factors
// Track if we're animating back to release position
const [isReleasingPointerLock, setIsReleasingPointerLock] = useState(false)
// Debug: Track bounding boxes for visualization
const [debugBoundingBoxes, setDebugBoundingBoxes] = useState<
Array<{ regionId: string; x: number; y: number; width: number; height: number }>
@@ -222,9 +300,6 @@ export function MapRenderer({
// Maps regionId -> {width, height} of the largest piece
const largestPieceSizesRef = useRef<Map<string, { width: number; height: number }>>(new Map())
// Store the uncapped adaptive zoom for use when pointer lock activates
const uncappedAdaptiveZoomRef = useRef<number | null>(null)
// Configuration
const MAX_ZOOM = 1000 // Maximum zoom level (for Gibraltar at 0.08px!)
const HIGH_ZOOM_THRESHOLD = 100 // Show gold border above this zoom level
@@ -241,142 +316,6 @@ export function MapRenderer({
return 1.0 // Normal speed for larger regions
}
// Set up pointer lock event listeners
useEffect(() => {
const handlePointerLockChange = () => {
const isLocked = document.pointerLockElement === containerRef.current
console.log('[MapRenderer] Pointer lock change event:', {
isLocked,
pointerLockElement: document.pointerLockElement,
containerElement: containerRef.current,
elementsMatch: document.pointerLockElement === containerRef.current,
})
setPointerLocked(isLocked)
// When acquiring pointer lock, save the initial cursor position
if (isLocked && cursorPositionRef.current) {
initialCapturePositionRef.current = { ...cursorPositionRef.current }
console.log(
'[Pointer Lock] 📍 Saved initial capture position:',
initialCapturePositionRef.current
)
}
// Reset cursor squish when lock state changes
if (!isLocked) {
console.log('[Pointer Lock] 🔓 RELEASED - Starting cleanup and zoom recalculation')
// Get current zoom state before any changes
const currentZoom = magnifierSpring.zoom.get()
const currentTargetZoom = targetZoom
console.log('[Pointer Lock] Current zoom state:', {
currentZoom: currentZoom.toFixed(1),
targetZoom: currentTargetZoom.toFixed(1),
uncappedZoom: uncappedAdaptiveZoomRef.current?.toFixed(1) || 'null',
})
setCursorSquish({ x: 1, y: 1 })
setIsReleasingPointerLock(false)
initialCapturePositionRef.current = null
// When releasing pointer lock, recalculate zoom with capping applied
// The current zoom may be above the threshold (uncapped), so we need to cap it
if (containerRef.current && svgRef.current && uncappedAdaptiveZoomRef.current !== null) {
const containerRect = containerRef.current.getBoundingClientRect()
const svgRect = svgRef.current.getBoundingClientRect()
const magnifierWidth = containerRect.width * 0.5
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
const viewBoxWidth = viewBoxParts[2]
console.log('[Pointer Lock] Zoom recalculation context:', {
magnifierWidth: magnifierWidth.toFixed(1),
viewBoxWidth: viewBoxWidth?.toFixed(1) || 'undefined',
svgWidth: svgRect.width.toFixed(1),
})
if (viewBoxWidth && !Number.isNaN(viewBoxWidth)) {
// Calculate what the screen pixel ratio would be at the uncapped zoom
const uncappedZoom = uncappedAdaptiveZoomRef.current
const screenPixelRatio = calculateScreenPixelRatio({
magnifierWidth,
viewBoxWidth,
svgWidth: svgRect.width,
zoom: uncappedZoom,
})
console.log('[Pointer Lock] Screen pixel ratio check:', {
uncappedZoom: uncappedZoom.toFixed(1),
screenPixelRatio: screenPixelRatio.toFixed(1),
threshold: PRECISION_MODE_THRESHOLD,
exceedsThreshold: isAboveThreshold(screenPixelRatio, PRECISION_MODE_THRESHOLD),
})
// If it exceeds threshold, cap the zoom to stay at threshold
if (isAboveThreshold(screenPixelRatio, PRECISION_MODE_THRESHOLD)) {
const maxZoom = calculateMaxZoomAtThreshold(
PRECISION_MODE_THRESHOLD,
magnifierWidth,
svgRect.width
)
const cappedZoom = Math.min(uncappedZoom, maxZoom)
console.log(
`[Pointer Lock] ✅ Capping zoom: ${uncappedZoom.toFixed(1)}×${cappedZoom.toFixed(1)}× (threshold: ${PRECISION_MODE_THRESHOLD} px/px)`
)
setTargetZoom(cappedZoom)
} else {
console.log(
`[Pointer Lock] No capping needed - zoom ${uncappedZoom.toFixed(1)}× is below threshold`
)
}
} else {
console.log('[Pointer Lock] ⚠️ Cannot recalculate zoom - invalid viewBoxWidth')
}
} else {
console.log('[Pointer Lock] ⚠️ Cannot recalculate zoom - missing refs:', {
hasContainer: !!containerRef.current,
hasSvg: !!svgRef.current,
hasUncappedZoom: uncappedAdaptiveZoomRef.current !== null,
})
}
}
// When pointer lock activates, update target zoom to the uncapped value
// This allows the zoom animation to resume immediately
if (isLocked && uncappedAdaptiveZoomRef.current !== null) {
console.log(
`[Pointer Lock] Updating target zoom to uncapped value: ${uncappedAdaptiveZoomRef.current.toFixed(1)}×`
)
setTargetZoom(uncappedAdaptiveZoomRef.current)
}
}
const handlePointerLockError = () => {
console.error('[Pointer Lock] ❌ Failed to acquire pointer lock')
setPointerLocked(false)
}
document.addEventListener('pointerlockchange', handlePointerLockChange)
document.addEventListener('pointerlockerror', handlePointerLockError)
console.log('[MapRenderer] Pointer lock listeners attached')
return () => {
document.removeEventListener('pointerlockchange', handlePointerLockChange)
document.removeEventListener('pointerlockerror', handlePointerLockError)
console.log('[MapRenderer] Pointer lock listeners removed')
}
}, [])
// Release pointer lock when component unmounts
useEffect(() => {
return () => {
if (document.pointerLockElement) {
console.log('[Pointer Lock] 🔓 RELEASING (MapRenderer unmount)')
document.exitPointerLock()
}
}
}, [])
// Pre-compute largest piece sizes for multi-piece regions
useEffect(() => {
if (!svgRef.current) return
@@ -423,13 +362,9 @@ export function MapRenderer({
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>) => {
// Silently request pointer lock if not already locked
// This makes the first gameplay click also enable precision mode
if (!pointerLocked && containerRef.current) {
try {
containerRef.current.requestPointerLock()
console.log('[Pointer Lock] 🔒 Silently requested (user clicked map)')
} catch (error) {
console.error('[Pointer Lock] Request failed:', error)
}
if (!pointerLocked) {
requestPointerLock()
console.log('[Pointer Lock] 🔒 Silently requested (user clicked map)')
}
// Let region clicks still work (they have their own onClick handlers)