feat(know-your-world): wire interaction state machine to MapRenderer

Add state machine integration with sync effects for incremental migration:
- Call useInteractionStateMachine hook in MapRenderer
- Add sync effects to drive state machine from existing boolean state:
  - Precision mode sync (pointerLocked, isReleasingPointerLock)
  - Magnifier visibility sync (showMagnifier)
  - Magnifier expanded sync (isMagnifierExpanded)
- Add debug logging to compare machine state vs boolean state
- Clean up unused imports (CompassCrosshair, FeedbackType)
- Organize imports per Biome rules

The state machine now runs in parallel with existing state. Next steps:
1. Test sync effects work correctly in dev console
2. Migrate handlers to dispatch events directly to state machine
3. Replace rendering conditionals with state machine checks
4. Remove old boolean state once migration is complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-12-03 09:26:13 -06:00
parent e4d6748d70
commit 7e55953eee
14 changed files with 764 additions and 753 deletions

View File

@ -6,11 +6,11 @@
*/
export type {
InteractionState,
InteractionEvent,
InteractionContext,
TouchPoint,
InteractionEvent,
InteractionState,
MachineState,
TouchPoint,
UseInteractionStateMachineReturn,
} from './useInteractionStateMachine'

View File

@ -10,7 +10,7 @@
'use client'
import { useReducer, useCallback, useRef, useMemo } from 'react'
import { useCallback, useMemo, useReducer, useRef } from 'react'
// ============================================================================
// State Types
@ -680,10 +680,7 @@ export function useInteractionStateMachine(): UseInteractionStateMachineReturn {
]
)
const showCursor = useMemo(
() => isHovering || showMagnifier,
[isHovering, showMagnifier]
)
const showCursor = useMemo(() => isHovering || showMagnifier, [isHovering, showMagnifier])
const isAnyPanning = useMemo(
() => isMagnifierPanning || isMapPanningMobile || isMapPanningDesktop,

View File

@ -9,7 +9,7 @@
*/
import { forceCollide, forceSimulation, forceX, forceY, type SimulationNodeDatum } from 'd3-force'
import { RefObject, useEffect, useState } from 'react'
import { type RefObject, useEffect, useState } from 'react'
import type { MapData, MapRegion } from '../../types'
import { getArrowStartPoint, getRenderedViewport } from './labelUtils'

View File

@ -90,15 +90,7 @@ export const LetterDisplay = memo(function LetterDisplay({
const isSpace = char === ' '
if (isSpace) {
return (
<StyledLetter
key={index}
char={char}
status="space"
isDark={isDark}
index={index}
/>
)
return <StyledLetter key={index} char={char} status="space" isDark={isDark} index={index} />
}
// Get current index before incrementing
@ -113,15 +105,7 @@ export const LetterDisplay = memo(function LetterDisplay({
isComplete
)
return (
<StyledLetter
key={index}
char={char}
status={status}
isDark={isDark}
index={index}
/>
)
return <StyledLetter key={index} char={char} status={status} isDark={isDark} index={index} />
})
}, [regionName, confirmedCount, requiredLetters, isComplete, isDark])

View File

@ -89,10 +89,7 @@ export function getLetterStatus(
* @param isDark - Whether dark mode is active
* @returns CSS properties for the letter
*/
export function getLetterStyles(
status: LetterStatus,
isDark: boolean
): React.CSSProperties {
export function getLetterStyles(status: LetterStatus, isDark: boolean): React.CSSProperties {
const baseStyles: React.CSSProperties = {
transition: 'all 0.15s ease-out',
}
@ -129,10 +126,7 @@ export function getLetterStyles(
* @param requiredLetters - Number of letters required
* @returns Progress value (0 = none, 1 = complete)
*/
export function calculateProgress(
confirmedCount: number,
requiredLetters: number
): number {
export function calculateProgress(confirmedCount: number, requiredLetters: number): number {
if (requiredLetters === 0) return 1
return Math.min(1, confirmedCount / requiredLetters)
}

View File

@ -91,12 +91,7 @@ export function useLetterConfirmation({
// Get letter status for display
const getLetterStatus = useCallback(
(nonSpaceIndex: number): LetterStatus => {
return getLetterStatusUtil(
nonSpaceIndex,
confirmedCount,
requiredLetters,
isComplete
)
return getLetterStatusUtil(nonSpaceIndex, confirmedCount, requiredLetters, isComplete)
},
[confirmedCount, requiredLetters, isComplete]
)
@ -109,10 +104,7 @@ export function useLetterConfirmation({
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in an input or textarea
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return
}

View File

@ -211,11 +211,7 @@ export const MagnifierControls = memo(function MagnifierControls({
<>
{/* Expand button - top-right corner (when not expanded and not pointer locked) */}
{!isExpanded && !pointerLocked && (
<ControlButton
position="top-right"
style="icon"
onClick={onExpand}
>
<ControlButton position="top-right" style="icon" onClick={onExpand}>
<ExpandIcon isDark={isDark} />
</ControlButton>
)}
@ -234,11 +230,7 @@ export const MagnifierControls = memo(function MagnifierControls({
{/* Full Map button - bottom-left corner (when expanded) */}
{isExpanded && showSelectButton && (
<ControlButton
position="bottom-left"
style="secondary"
onClick={onExitExpanded}
>
<ControlButton position="bottom-left" style="secondary" onClick={onExitExpanded}>
Full Map
</ControlButton>
)}

View File

@ -50,7 +50,12 @@ export interface MagnifierRegionsProps {
/** Get player ID who found a region */
getPlayerWhoFoundRegion: (regionId: string) => string | null
/** Get region fill color */
getRegionColor: (regionId: string, isFound: boolean, isHovered: boolean, isDark: boolean) => string
getRegionColor: (
regionId: string,
isFound: boolean,
isHovered: boolean,
isDark: boolean
) => string
/** Get region stroke color */
getRegionStroke: (isFound: boolean, isDark: boolean) => string
/** Whether to show region outline */
@ -80,13 +85,8 @@ export const MagnifierRegions = memo(function MagnifierRegions({
getRegionStroke,
showOutline,
}: MagnifierRegionsProps) {
const {
regionsFound,
hoveredRegion,
celebrationRegionId,
giveUpRegionId,
isGiveUpAnimating,
} = regionState
const { regionsFound, hoveredRegion, celebrationRegionId, giveUpRegionId, isGiveUpAnimating } =
regionState
const { celebrationFlash, giveUpFlash } = flashProgress
return (

View File

@ -16,7 +16,10 @@
import { memo, useMemo } from 'react'
import { getRenderedViewport } from '../labels'
import { getAdjustedMagnifiedDimensions, getMagnifierDimensions } from '../../utils/magnifierDimensions'
import {
getAdjustedMagnifiedDimensions,
getMagnifierDimensions,
} from '../../utils/magnifierDimensions'
// ============================================================================
// Types
@ -124,12 +127,7 @@ export const ZoomLines = memo(function ZoomLines({
isDark,
}: ZoomLinesProps) {
// Memoize all the calculated values
const {
paths,
visibleCorners,
lineColor,
glowColor,
} = useMemo(() => {
const { paths, visibleCorners, lineColor, glowColor } = useMemo(() => {
// Calculate leftover rectangle dimensions (area not covered by UI elements)
const leftoverWidth = containerRect.width - safeZoneMargins.left - safeZoneMargins.right
const leftoverHeight = containerRect.height - safeZoneMargins.top - safeZoneMargins.bottom
@ -210,14 +208,7 @@ export const ZoomLines = memo(function ZoomLines({
magTop + magnifierHeight
)
// Check if line passes through indicator
const passesThroughInd = linePassesThroughRect(
from,
to,
indTL.x,
indTL.y,
indBR.x,
indBR.y
)
const passesThroughInd = linePassesThroughRect(from, to, indTL.x, indTL.y, indBR.x, indBR.y)
return !passesThroughMag && !passesThroughInd
})
@ -233,9 +224,7 @@ export const ZoomLines = memo(function ZoomLines({
: isDark
? '#60a5fa'
: '#3b82f6' // blue
const calculatedGlowColor = isHighZoom
? 'rgba(251, 191, 36, 0.6)'
: 'rgba(96, 165, 250, 0.6)'
const calculatedGlowColor = isHighZoom ? 'rgba(251, 191, 36, 0.6)' : 'rgba(96, 165, 250, 0.6)'
return {
paths: calculatedPaths,

View File

@ -12,7 +12,12 @@
'use client'
import React, { useCallback, useRef, type RefObject, type TouchEvent as ReactTouchEvent } from 'react'
import React, {
useCallback,
useRef,
type RefObject,
type TouchEvent as ReactTouchEvent,
} from 'react'
import type { UseMagnifierStateReturn } from './useMagnifierState'
@ -223,8 +228,10 @@ export function useMagnifierTouch(options: UseMagnifierTouchOptions): UseMagnifi
totalDeltaRef.current.y += deltaY
// Track if user has moved significantly
if (Math.abs(totalDeltaRef.current.x) > moveThreshold ||
Math.abs(totalDeltaRef.current.y) > moveThreshold) {
if (
Math.abs(totalDeltaRef.current.x) > moveThreshold ||
Math.abs(totalDeltaRef.current.y) > moveThreshold
) {
magnifierState.didMoveRef.current = true
}

View File

@ -103,9 +103,7 @@ export function MapRendererProvider({ children, value }: MapRendererProviderProp
]
)
return (
<MapRendererContext.Provider value={memoizedValue}>{children}</MapRendererContext.Provider>
)
return <MapRendererContext.Provider value={memoizedValue}>{children}</MapRendererContext.Provider>
}
// ============================================================================

View File

@ -177,14 +177,7 @@ export interface FlashProgress {
/**
* Hot/cold feedback type for visual indicators
*/
export type HotColdFeedbackType =
| 'freezing'
| 'cold'
| 'cool'
| 'warm'
| 'hot'
| 'burning'
| null
export type HotColdFeedbackType = 'freezing' | 'cold' | 'cool' | 'warm' | 'hot' | 'burning' | null
/**
* Heat-based styling for borders and glows

View File

@ -96,13 +96,7 @@ export function screenToSVG(
svgRect: DOMRect,
viewBox: ViewBoxComponents
): SVGPosition {
const viewport = getRenderedViewport(
svgRect,
viewBox.x,
viewBox.y,
viewBox.width,
viewBox.height
)
const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height)
// Calculate offset from container origin to SVG rendered content
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX
@ -130,13 +124,7 @@ export function svgToScreen(
svgRect: DOMRect,
viewBox: ViewBoxComponents
): CursorPosition {
const viewport = getRenderedViewport(
svgRect,
viewBox.x,
viewBox.y,
viewBox.width,
viewBox.height
)
const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height)
// Calculate offset from container origin to SVG rendered content
const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX