fix: improve magnifier zoom calculation for multi-piece regions
Problem: - Portugal's bounding box includes distant Atlantic islands, causing mainland to be ignored - Algorithm was selecting "largest piece by area" which picked island groups instead of mainland - This caused Portugal to dominate zoom calculations and prevent Gibraltar from being prioritized Solution: - Changed pre-computation to use FIRST piece instead of largest (mainland is typically piece 1) - Added showDebugBoundingBoxes prop to hide debug rectangles in production - Improved zoom animation smoothness with gentler spring easing (tension: 120, friction: 30) Technical details: - Multi-piece SVG paths split by `z m` separator - First piece is mainland, subsequent pieces are islands/territories - Pre-computed sizes cached in useEffect for performance - Only Portugal logs to console for debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1a54f09814
commit
cb4114f344
|
|
@ -57,6 +57,8 @@ interface MapRendererProps {
|
||||||
useObstacles?: boolean
|
useObstacles?: boolean
|
||||||
obstaclePadding?: number
|
obstaclePadding?: number
|
||||||
}
|
}
|
||||||
|
// Debug flags
|
||||||
|
showDebugBoundingBoxes?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -109,6 +111,7 @@ export function MapRenderer({
|
||||||
guessHistory,
|
guessHistory,
|
||||||
playerMetadata,
|
playerMetadata,
|
||||||
forceTuning = {},
|
forceTuning = {},
|
||||||
|
showDebugBoundingBoxes = false,
|
||||||
}: MapRendererProps) {
|
}: MapRendererProps) {
|
||||||
// Extract force tuning parameters with defaults
|
// Extract force tuning parameters with defaults
|
||||||
const {
|
const {
|
||||||
|
|
@ -182,6 +185,15 @@ export function MapRenderer({
|
||||||
const cursorPositionRef = useRef<{ x: number; y: number } | null>(null)
|
const cursorPositionRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
const [smallestRegionSize, setSmallestRegionSize] = useState<number>(Infinity)
|
const [smallestRegionSize, setSmallestRegionSize] = useState<number>(Infinity)
|
||||||
|
|
||||||
|
// Debug: Track bounding boxes for visualization
|
||||||
|
const [debugBoundingBoxes, setDebugBoundingBoxes] = useState<
|
||||||
|
Array<{ regionId: string; x: number; y: number; width: number; height: number }>
|
||||||
|
>([])
|
||||||
|
|
||||||
|
// Pre-computed largest piece sizes for multi-piece regions
|
||||||
|
// Maps regionId -> {width, height} of the largest piece
|
||||||
|
const largestPieceSizesRef = useRef<Map<string, { width: number; height: number }>>(new Map())
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const MAX_ZOOM = 1000 // Maximum zoom level (for Gibraltar at 0.08px!)
|
const MAX_ZOOM = 1000 // Maximum zoom level (for Gibraltar at 0.08px!)
|
||||||
const HIGH_ZOOM_THRESHOLD = 100 // Show gold border above this zoom level
|
const HIGH_ZOOM_THRESHOLD = 100 // Show gold border above this zoom level
|
||||||
|
|
@ -244,6 +256,48 @@ export function MapRenderer({
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Pre-compute largest piece sizes for multi-piece regions
|
||||||
|
useEffect(() => {
|
||||||
|
if (!svgRef.current) return
|
||||||
|
|
||||||
|
const largestPieceSizes = new Map<string, { width: number; height: number }>()
|
||||||
|
|
||||||
|
mapData.regions.forEach((region) => {
|
||||||
|
const pathData = region.path
|
||||||
|
const pieceSeparatorRegex = /(?<=z)\s*m\s*/i
|
||||||
|
const rawPieces = pathData.split(pieceSeparatorRegex)
|
||||||
|
|
||||||
|
if (rawPieces.length > 1) {
|
||||||
|
// Multi-piece region: use the FIRST piece (mainland), not largest
|
||||||
|
// The first piece is typically the mainland, with islands as subsequent pieces
|
||||||
|
const svg = svgRef.current
|
||||||
|
if (!svg) return
|
||||||
|
|
||||||
|
// Just measure the first piece
|
||||||
|
const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||||
|
tempPath.setAttribute('d', rawPieces[0]) // First piece already has 'm' command
|
||||||
|
tempPath.style.visibility = 'hidden'
|
||||||
|
svg.appendChild(tempPath)
|
||||||
|
|
||||||
|
const bbox = tempPath.getBoundingClientRect()
|
||||||
|
const firstPieceSize = { width: bbox.width, height: bbox.height }
|
||||||
|
|
||||||
|
svg.removeChild(tempPath)
|
||||||
|
|
||||||
|
largestPieceSizes.set(region.id, firstPieceSize)
|
||||||
|
|
||||||
|
// Only log Portugal for debugging
|
||||||
|
if (region.id === 'pt') {
|
||||||
|
console.log(
|
||||||
|
`[Pre-compute] ${region.id}: Using first piece (mainland): ${firstPieceSize.width.toFixed(2)}px × ${firstPieceSize.height.toFixed(2)}px`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
largestPieceSizesRef.current = largestPieceSizes
|
||||||
|
}, [mapData])
|
||||||
|
|
||||||
// Request pointer lock on first click
|
// Request pointer lock on first click
|
||||||
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
console.log('[MapRenderer] Container clicked:', {
|
console.log('[MapRenderer] Container clicked:', {
|
||||||
|
|
@ -268,7 +322,8 @@ export function MapRenderer({
|
||||||
|
|
||||||
// Animated spring values for smooth transitions
|
// Animated spring values for smooth transitions
|
||||||
// Different fade speeds: fast fade-in (100ms), slow fade-out (1000ms)
|
// Different fade speeds: fast fade-in (100ms), slow fade-out (1000ms)
|
||||||
// Position animates with medium speed (300ms)
|
// Zoom: smooth, slower animation with gentle easing
|
||||||
|
// Position: medium speed (300ms)
|
||||||
const magnifierSpring = useSpring({
|
const magnifierSpring = useSpring({
|
||||||
zoom: targetZoom,
|
zoom: targetZoom,
|
||||||
opacity: targetOpacity,
|
opacity: targetOpacity,
|
||||||
|
|
@ -280,16 +335,14 @@ export function MapRenderer({
|
||||||
? { duration: 100 } // Fade in: 0.1 seconds
|
? { duration: 100 } // Fade in: 0.1 seconds
|
||||||
: { duration: 1000 } // Fade out: 1 second
|
: { duration: 1000 } // Fade out: 1 second
|
||||||
}
|
}
|
||||||
// Position and zoom: medium speed
|
if (key === 'zoom') {
|
||||||
|
// Zoom: smooth, slower animation with gentle easing
|
||||||
|
return { tension: 120, friction: 30, mass: 1 }
|
||||||
|
}
|
||||||
|
// Position: medium speed
|
||||||
return { tension: 200, friction: 25 }
|
return { tension: 200, friction: 25 }
|
||||||
},
|
},
|
||||||
onChange: (result) => {
|
// onChange removed - was flooding console with animation frames
|
||||||
console.log('[Magnifier Spring] Animating:', {
|
|
||||||
opacity: result.value.opacity?.toFixed(2),
|
|
||||||
top: result.value.top?.toFixed(0),
|
|
||||||
left: result.value.left?.toFixed(0),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [labelPositions, setLabelPositions] = useState<RegionLabelPosition[]>([])
|
const [labelPositions, setLabelPositions] = useState<RegionLabelPosition[]>([])
|
||||||
|
|
@ -820,11 +873,6 @@ export function MapRenderer({
|
||||||
|
|
||||||
if (isVerySmall) {
|
if (isVerySmall) {
|
||||||
hasSmallRegion = true
|
hasSmallRegion = true
|
||||||
console.log('[Magnifier] Small region detected:', region.id, {
|
|
||||||
width: pixelWidth,
|
|
||||||
height: pixelHeight,
|
|
||||||
area: pixelArea,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track smallest region size for cursor dampening (use smallest in detection box)
|
// Track smallest region size for cursor dampening (use smallest in detection box)
|
||||||
|
|
@ -834,12 +882,39 @@ export function MapRenderer({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sort detected regions by size (smallest first) to prioritize tiny regions in zoom calculation
|
||||||
|
// This ensures Gibraltar (0.08px) is checked before Spain (81px) when finding optimal zoom
|
||||||
|
detectedRegions.sort((a, b) => {
|
||||||
|
const pathA = svgRef.current?.querySelector(`path[data-region-id="${a}"]`)
|
||||||
|
const pathB = svgRef.current?.querySelector(`path[data-region-id="${b}"]`)
|
||||||
|
if (!pathA || !pathB) return 0
|
||||||
|
|
||||||
|
const rectA = pathA.getBoundingClientRect()
|
||||||
|
const rectB = pathB.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Use smallest dimension (width or height) for comparison
|
||||||
|
const sizeA = Math.min(rectA.width, rectA.height)
|
||||||
|
const sizeB = Math.min(rectB.width, rectB.height)
|
||||||
|
|
||||||
|
return sizeA - sizeB // Smallest first
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pointerLocked && detectedRegions.length > 0) {
|
||||||
|
const sortedSizes = detectedRegions.map((id) => {
|
||||||
|
const path = svgRef.current?.querySelector(`path[data-region-id="${id}"]`)
|
||||||
|
if (!path) return `${id}: ?`
|
||||||
|
const rect = path.getBoundingClientRect()
|
||||||
|
const size = Math.min(rect.width, rect.height)
|
||||||
|
return `${id}: ${size.toFixed(2)}px`
|
||||||
|
})
|
||||||
|
console.log('[Zoom Search] Sorted regions (smallest first):', sortedSizes)
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate adaptive zoom level based on region density and size
|
// Calculate adaptive zoom level based on region density and size
|
||||||
// Base zoom: 8x
|
// Base zoom: 8x
|
||||||
// More regions = more zoom (up to +8x for 10+ regions)
|
// More regions = more zoom (up to +8x for 10+ regions)
|
||||||
// Smaller regions = more zoom (up to +8x for very tiny regions)
|
// Show magnifier only when there are small regions (< 15px)
|
||||||
// Show magnifier if: 7+ regions in detection box OR any region smaller than 15px
|
const shouldShow = hasSmallRegion
|
||||||
const shouldShow = regionsInBox >= 7 || hasSmallRegion
|
|
||||||
|
|
||||||
// Update smallest region size for adaptive cursor dampening
|
// Update smallest region size for adaptive cursor dampening
|
||||||
if (shouldShow && detectedSmallestSize !== Infinity) {
|
if (shouldShow && detectedSmallestSize !== Infinity) {
|
||||||
|
|
@ -854,135 +929,258 @@ export function MapRenderer({
|
||||||
setHoveredRegion(regionUnderCursor)
|
setHoveredRegion(regionUnderCursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logging - ONLY for Gibraltar or ultra-small regions
|
// Magnifier detection logging removed for performance
|
||||||
const hasGibraltar = detectedRegions.includes('gi')
|
|
||||||
if (hasGibraltar || detectedSmallestSize < 1) {
|
|
||||||
console.log(
|
|
||||||
`[Magnifier] ${hasGibraltar ? '🎯 GIBRALTAR DETECTED' : '🔍 Tiny region'} Detection:`,
|
|
||||||
{
|
|
||||||
detectedRegionIds: detectedRegions,
|
|
||||||
regionsInBox,
|
|
||||||
smallestScreenSize: detectedSmallestSize.toFixed(4) + 'px',
|
|
||||||
shouldShow,
|
|
||||||
movementMultiplier: getMovementMultiplier(detectedSmallestSize).toFixed(2),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
// Binary search for optimal zoom level
|
// Adaptive threshold based on smallest detected region
|
||||||
// Goal: Find zoom where regions fit nicely in magnifier (taking 10-20% of area)
|
// For ultra-small regions (< 1px), we need a lower acceptance threshold
|
||||||
const TARGET_AREA_MIN = 0.10 // 10% of magnifier
|
// Otherwise Gibraltar (0.08px) will never fit the 10-25% range even at 1000x zoom
|
||||||
const TARGET_AREA_MAX = 0.20 // 20% of magnifier
|
let minAcceptableRatio = 0.1 // Default: 10% minimum
|
||||||
|
let maxAcceptableRatio = 0.25 // Default: 25% maximum
|
||||||
|
|
||||||
// Get magnifier dimensions
|
if (detectedSmallestSize < 1) {
|
||||||
const magnifierWidth = containerRect.width * 0.5
|
// Sub-pixel regions: accept 2-8% of magnifier
|
||||||
const magnifierHeight = magnifierWidth / 2
|
minAcceptableRatio = 0.02
|
||||||
const magnifierArea = magnifierWidth * magnifierHeight
|
maxAcceptableRatio = 0.08
|
||||||
|
} else if (detectedSmallestSize < 5) {
|
||||||
|
// Tiny regions (1-5px): accept 5-15% of magnifier
|
||||||
|
minAcceptableRatio = 0.05
|
||||||
|
maxAcceptableRatio = 0.15
|
||||||
|
}
|
||||||
|
|
||||||
// Get SVG viewBox for coordinate conversion
|
if (pointerLocked) {
|
||||||
|
console.log('[Zoom Search] Adaptive thresholds:', {
|
||||||
|
detectedSmallestSize: detectedSmallestSize.toFixed(4) + 'px',
|
||||||
|
minAcceptableRatio: (minAcceptableRatio * 100).toFixed(1) + '%',
|
||||||
|
maxAcceptableRatio: (maxAcceptableRatio * 100).toFixed(1) + '%',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom-out approach: Start from max zoom and reduce until a region fits nicely
|
||||||
|
// Goal: Find zoom where any region occupies ~15% of magnifier width or height
|
||||||
|
const TARGET_RATIO = 0.15 // Region should occupy 15% of magnifier dimension
|
||||||
|
|
||||||
|
// Get SVG viewBox for bounding box conversion
|
||||||
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
|
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
|
||||||
const viewBoxWidth = viewBoxParts[2] || 1000
|
const viewBoxWidth = viewBoxParts[2] || 1000
|
||||||
const viewBoxHeight = viewBoxParts[3] || 1000
|
const viewBoxHeight = viewBoxParts[3] || 1000
|
||||||
|
|
||||||
// Binary search bounds
|
// Magnifier dimensions
|
||||||
let minZoom = 1
|
|
||||||
let maxZoom = MAX_ZOOM
|
|
||||||
let adaptiveZoom = 10
|
|
||||||
let iterations = 0
|
|
||||||
const MAX_ITERATIONS = 20
|
|
||||||
|
|
||||||
while (iterations < MAX_ITERATIONS && maxZoom - minZoom > 0.1) {
|
|
||||||
iterations++
|
|
||||||
const testZoom = (minZoom + maxZoom) / 2
|
|
||||||
|
|
||||||
// Calculate magnified viewBox dimensions at this zoom
|
|
||||||
const magnifiedViewBoxWidth = viewBoxWidth / testZoom
|
|
||||||
const magnifiedViewBoxHeight = viewBoxHeight / testZoom
|
|
||||||
const magnifiedViewBoxArea = magnifiedViewBoxWidth * magnifiedViewBoxHeight
|
|
||||||
|
|
||||||
// Check regions in detection box to see how they fit
|
|
||||||
let anyRegionFullyInside = false
|
|
||||||
let largestRegionRatio = 0
|
|
||||||
|
|
||||||
detectedRegions.forEach((regionId) => {
|
|
||||||
const region = mapData.regions.find((r) => r.id === regionId)
|
|
||||||
if (!region) return
|
|
||||||
|
|
||||||
const regionPath = svgRef.current?.querySelector(`path[data-region-id="${regionId}"]`)
|
|
||||||
if (!regionPath) return
|
|
||||||
|
|
||||||
const pathRect = regionPath.getBoundingClientRect()
|
|
||||||
const regionPixelArea = pathRect.width * pathRect.height
|
|
||||||
|
|
||||||
// Convert pixel area to viewBox area (approximate)
|
|
||||||
const scaleX = viewBoxWidth / svgRect.width
|
|
||||||
const scaleY = viewBoxHeight / svgRect.height
|
|
||||||
const regionViewBoxArea = regionPixelArea * scaleX * scaleY
|
|
||||||
|
|
||||||
// Check if region fits in magnified view
|
|
||||||
const regionRatioInMagnifier = regionViewBoxArea / magnifiedViewBoxArea
|
|
||||||
|
|
||||||
if (regionRatioInMagnifier < 1.0) {
|
|
||||||
anyRegionFullyInside = true
|
|
||||||
largestRegionRatio = Math.max(largestRegionRatio, regionRatioInMagnifier)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Binary search logic
|
|
||||||
if (!anyRegionFullyInside) {
|
|
||||||
// No regions fit - zoom out
|
|
||||||
maxZoom = testZoom
|
|
||||||
} else if (largestRegionRatio < TARGET_AREA_MIN) {
|
|
||||||
// Regions too small - zoom in
|
|
||||||
minZoom = testZoom
|
|
||||||
} else if (largestRegionRatio > TARGET_AREA_MAX) {
|
|
||||||
// Regions too large - zoom out
|
|
||||||
maxZoom = testZoom
|
|
||||||
} else {
|
|
||||||
// Just right!
|
|
||||||
adaptiveZoom = testZoom
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
adaptiveZoom = testZoom
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logging for Gibraltar
|
|
||||||
const hasGibraltar = detectedRegions.includes('gi')
|
|
||||||
if (hasGibraltar) {
|
|
||||||
console.log(`[Zoom] 🎯 BINARY SEARCH RESULT:`, {
|
|
||||||
iterations,
|
|
||||||
finalZoom: adaptiveZoom.toFixed(1),
|
|
||||||
detectedRegions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate magnifier position (opposite corner from cursor)
|
|
||||||
// containerRect already declared at top of function
|
|
||||||
const magnifierWidth = containerRect.width * 0.5
|
const magnifierWidth = containerRect.width * 0.5
|
||||||
const magnifierHeight = magnifierWidth / 2
|
const magnifierHeight = magnifierWidth / 2
|
||||||
|
|
||||||
|
// Calculate target sizes: region should be this big in magnifier
|
||||||
|
const targetWidthPx = magnifierWidth * TARGET_RATIO
|
||||||
|
const targetHeightPx = magnifierHeight * TARGET_RATIO
|
||||||
|
|
||||||
|
// Track bounding boxes for debug visualization
|
||||||
|
const boundingBoxes: Array<{
|
||||||
|
regionId: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
// Start from max zoom and work down until we find a good fit
|
||||||
|
let adaptiveZoom = MAX_ZOOM
|
||||||
|
let foundGoodZoom = false
|
||||||
|
|
||||||
|
// We'll test zoom levels by halving each time to find a good range quickly
|
||||||
|
const MIN_ZOOM = 1
|
||||||
|
const ZOOM_STEP = 0.9 // Reduce by 10% each iteration
|
||||||
|
|
||||||
|
// Convert cursor position to SVG coordinates
|
||||||
|
const scaleX = viewBoxWidth / svgRect.width
|
||||||
|
const scaleY = viewBoxHeight / svgRect.height
|
||||||
|
const viewBoxX = viewBoxParts[0] || 0
|
||||||
|
const viewBoxY = viewBoxParts[1] || 0
|
||||||
|
const cursorSvgX = (cursorX - (svgRect.left - containerRect.left)) * scaleX + viewBoxX
|
||||||
|
const cursorSvgY = (cursorY - (svgRect.top - containerRect.top)) * scaleY + viewBoxY
|
||||||
|
|
||||||
|
// Zoom search logging disabled for performance
|
||||||
|
|
||||||
|
for (let testZoom = MAX_ZOOM; testZoom >= MIN_ZOOM; testZoom *= ZOOM_STEP) {
|
||||||
|
// Calculate the SVG viewport that will be shown in the magnifier at this zoom
|
||||||
|
const magnifiedViewBoxWidth = viewBoxWidth / testZoom
|
||||||
|
const magnifiedViewBoxHeight = viewBoxHeight / testZoom
|
||||||
|
|
||||||
|
// The viewport is centered on cursor position, but clamped to map bounds
|
||||||
|
let viewportLeft = cursorSvgX - magnifiedViewBoxWidth / 2
|
||||||
|
let viewportRight = cursorSvgX + magnifiedViewBoxWidth / 2
|
||||||
|
let viewportTop = cursorSvgY - magnifiedViewBoxHeight / 2
|
||||||
|
let viewportBottom = cursorSvgY + magnifiedViewBoxHeight / 2
|
||||||
|
|
||||||
|
// Clamp viewport to stay within map bounds
|
||||||
|
const mapLeft = viewBoxX
|
||||||
|
const mapRight = viewBoxX + viewBoxWidth
|
||||||
|
const mapTop = viewBoxY
|
||||||
|
const mapBottom = viewBoxY + viewBoxHeight
|
||||||
|
|
||||||
|
let wasClamped = false
|
||||||
|
const originalViewport = {
|
||||||
|
left: viewportLeft,
|
||||||
|
right: viewportRight,
|
||||||
|
top: viewportTop,
|
||||||
|
bottom: viewportBottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If viewport extends beyond left edge, shift it right
|
||||||
|
if (viewportLeft < mapLeft) {
|
||||||
|
const shift = mapLeft - viewportLeft
|
||||||
|
viewportLeft += shift
|
||||||
|
viewportRight += shift
|
||||||
|
wasClamped = true
|
||||||
|
}
|
||||||
|
// If viewport extends beyond right edge, shift it left
|
||||||
|
if (viewportRight > mapRight) {
|
||||||
|
const shift = viewportRight - mapRight
|
||||||
|
viewportLeft -= shift
|
||||||
|
viewportRight -= shift
|
||||||
|
wasClamped = true
|
||||||
|
}
|
||||||
|
// If viewport extends beyond top edge, shift it down
|
||||||
|
if (viewportTop < mapTop) {
|
||||||
|
const shift = mapTop - viewportTop
|
||||||
|
viewportTop += shift
|
||||||
|
viewportBottom += shift
|
||||||
|
wasClamped = true
|
||||||
|
}
|
||||||
|
// If viewport extends beyond bottom edge, shift it up
|
||||||
|
if (viewportBottom > mapBottom) {
|
||||||
|
const shift = viewportBottom - mapBottom
|
||||||
|
viewportTop -= shift
|
||||||
|
viewportBottom -= shift
|
||||||
|
wasClamped = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewport logging disabled for performance
|
||||||
|
|
||||||
|
// Check all detected regions to see if any are inside this viewport and fit nicely
|
||||||
|
let foundFit = false
|
||||||
|
const regionsChecked: Array<{ id: string; inside: boolean; ratio?: number }> = []
|
||||||
|
|
||||||
|
for (const regionId of detectedRegions) {
|
||||||
|
const region = mapData.regions.find((r) => r.id === regionId)
|
||||||
|
if (!region) continue
|
||||||
|
|
||||||
|
const regionPath = svgRef.current?.querySelector(`path[data-region-id="${regionId}"]`)
|
||||||
|
if (!regionPath) continue
|
||||||
|
|
||||||
|
// Use pre-computed largest piece size for multi-piece regions
|
||||||
|
let currentWidth: number
|
||||||
|
let currentHeight: number
|
||||||
|
|
||||||
|
const cachedSize = largestPieceSizesRef.current.get(regionId)
|
||||||
|
if (cachedSize) {
|
||||||
|
// Multi-piece region: use pre-computed largest piece
|
||||||
|
currentWidth = cachedSize.width
|
||||||
|
currentHeight = cachedSize.height
|
||||||
|
} else {
|
||||||
|
// Single-piece region: use normal bounding box
|
||||||
|
const pathRect = regionPath.getBoundingClientRect()
|
||||||
|
currentWidth = pathRect.width
|
||||||
|
currentHeight = pathRect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathRect = regionPath.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Convert region bounding box to SVG coordinates
|
||||||
|
const regionSvgLeft = (pathRect.left - svgRect.left) * scaleX + viewBoxX
|
||||||
|
const regionSvgRight = regionSvgLeft + pathRect.width * scaleX
|
||||||
|
const regionSvgTop = (pathRect.top - svgRect.top) * scaleY + viewBoxY
|
||||||
|
const regionSvgBottom = regionSvgTop + pathRect.height * scaleY
|
||||||
|
|
||||||
|
// Check if region is inside the magnified viewport
|
||||||
|
const isInsideViewport =
|
||||||
|
regionSvgLeft < viewportRight &&
|
||||||
|
regionSvgRight > viewportLeft &&
|
||||||
|
regionSvgTop < viewportBottom &&
|
||||||
|
regionSvgBottom > viewportTop
|
||||||
|
|
||||||
|
regionsChecked.push({ id: regionId, inside: isInsideViewport })
|
||||||
|
|
||||||
|
if (!isInsideViewport) continue // Skip regions not in viewport
|
||||||
|
|
||||||
|
// Region is in viewport - check if it's a good size
|
||||||
|
const magnifiedWidth = currentWidth * testZoom
|
||||||
|
const magnifiedHeight = currentHeight * testZoom
|
||||||
|
|
||||||
|
const widthRatio = magnifiedWidth / magnifierWidth
|
||||||
|
const heightRatio = magnifiedHeight / magnifierHeight
|
||||||
|
|
||||||
|
// Update the checked region data with ratio
|
||||||
|
regionsChecked[regionsChecked.length - 1].ratio = Math.max(widthRatio, heightRatio)
|
||||||
|
|
||||||
|
// If either dimension is within our adaptive acceptance range, we found a good zoom
|
||||||
|
if (
|
||||||
|
(widthRatio >= minAcceptableRatio && widthRatio <= maxAcceptableRatio) ||
|
||||||
|
(heightRatio >= minAcceptableRatio && heightRatio <= maxAcceptableRatio)
|
||||||
|
) {
|
||||||
|
adaptiveZoom = testZoom
|
||||||
|
foundFit = true
|
||||||
|
foundGoodZoom = true
|
||||||
|
|
||||||
|
// Only log when we actually accept a zoom
|
||||||
|
console.log(
|
||||||
|
`[Zoom] ✅ Accepted ${testZoom.toFixed(1)}x for ${regionId} (${currentWidth.toFixed(1)}px × ${currentHeight.toFixed(1)}px)`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save bounding box for this region
|
||||||
|
boundingBoxes.push({
|
||||||
|
regionId,
|
||||||
|
x: regionSvgLeft,
|
||||||
|
y: regionSvgTop,
|
||||||
|
width: pathRect.width * scaleX,
|
||||||
|
height: pathRect.height * scaleY,
|
||||||
|
})
|
||||||
|
|
||||||
|
break // Found a good zoom, stop checking regions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundFit) break // Found a good zoom level, stop searching
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundGoodZoom) {
|
||||||
|
// Didn't find a good zoom - use minimum
|
||||||
|
adaptiveZoom = MIN_ZOOM
|
||||||
|
if (pointerLocked) {
|
||||||
|
console.log(`[Zoom Search] ⚠️ No good zoom found, using minimum: ${MIN_ZOOM}x`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save bounding boxes for rendering
|
||||||
|
setDebugBoundingBoxes(boundingBoxes)
|
||||||
|
|
||||||
|
// Calculate magnifier position (opposite corner from cursor)
|
||||||
|
// magnifierWidth and magnifierHeight already declared above
|
||||||
const isLeftHalf = cursorX < containerRect.width / 2
|
const isLeftHalf = cursorX < containerRect.width / 2
|
||||||
const isTopHalf = cursorY < containerRect.height / 2
|
const isTopHalf = cursorY < containerRect.height / 2
|
||||||
|
|
||||||
const newTop = isTopHalf ? containerRect.height - magnifierHeight - 20 : 20
|
const newTop = isTopHalf ? containerRect.height - magnifierHeight - 20 : 20
|
||||||
const newLeft = isLeftHalf ? containerRect.width - magnifierWidth - 20 : 20
|
const newLeft = isLeftHalf ? containerRect.width - magnifierWidth - 20 : 20
|
||||||
|
|
||||||
|
if (pointerLocked) {
|
||||||
console.log(
|
console.log(
|
||||||
'[Magnifier] SHOWING with zoom:',
|
'[Magnifier] SHOWING with zoom:',
|
||||||
adaptiveZoom,
|
adaptiveZoom,
|
||||||
'| Setting opacity to 1, position:',
|
'| Setting opacity to 1, position:',
|
||||||
{ top: newTop, left: newLeft }
|
{ top: newTop, left: newLeft }
|
||||||
)
|
)
|
||||||
|
}
|
||||||
setTargetZoom(adaptiveZoom)
|
setTargetZoom(adaptiveZoom)
|
||||||
setShowMagnifier(true)
|
setShowMagnifier(true)
|
||||||
setTargetOpacity(1)
|
setTargetOpacity(1)
|
||||||
setTargetTop(newTop)
|
setTargetTop(newTop)
|
||||||
setTargetLeft(newLeft)
|
setTargetLeft(newLeft)
|
||||||
} else {
|
} else {
|
||||||
|
if (pointerLocked) {
|
||||||
console.log('[Magnifier] HIDING - not enough regions or too large | Setting opacity to 0')
|
console.log('[Magnifier] HIDING - not enough regions or too large | Setting opacity to 0')
|
||||||
|
}
|
||||||
setShowMagnifier(false)
|
setShowMagnifier(false)
|
||||||
setTargetOpacity(0)
|
setTargetOpacity(0)
|
||||||
|
setDebugBoundingBoxes([]) // Clear bounding boxes when hiding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -997,6 +1195,7 @@ export function MapRenderer({
|
||||||
setShowMagnifier(false)
|
setShowMagnifier(false)
|
||||||
setTargetOpacity(0)
|
setTargetOpacity(0)
|
||||||
setCursorPosition(null)
|
setCursorPosition(null)
|
||||||
|
setDebugBoundingBoxes([]) // Clear bounding boxes when leaving
|
||||||
cursorPositionRef.current = null
|
cursorPositionRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1115,6 +1314,39 @@ export function MapRenderer({
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Debug: Render bounding boxes (only if enabled) */}
|
||||||
|
{showDebugBoundingBoxes &&
|
||||||
|
debugBoundingBoxes.map((bbox) => (
|
||||||
|
<g key={`bbox-${bbox.regionId}`}>
|
||||||
|
<rect
|
||||||
|
x={bbox.x}
|
||||||
|
y={bbox.y}
|
||||||
|
width={bbox.width}
|
||||||
|
height={bbox.height}
|
||||||
|
fill="none"
|
||||||
|
stroke="#ff0000"
|
||||||
|
strokeWidth={viewBoxWidth / 500}
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
strokeDasharray="3,3"
|
||||||
|
pointerEvents="none"
|
||||||
|
opacity={0.8}
|
||||||
|
/>
|
||||||
|
{/* Label showing region ID */}
|
||||||
|
<text
|
||||||
|
x={bbox.x + bbox.width / 2}
|
||||||
|
y={bbox.y + bbox.height / 2}
|
||||||
|
fill="#ff0000"
|
||||||
|
fontSize={viewBoxWidth / 80}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
pointerEvents="none"
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
>
|
||||||
|
{bbox.regionId}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Arrow marker definition */}
|
{/* Arrow marker definition */}
|
||||||
<defs>
|
<defs>
|
||||||
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||||||
|
|
@ -1354,17 +1586,7 @@ export function MapRenderer({
|
||||||
|
|
||||||
{/* Custom Cursor - Visible when pointer lock is active */}
|
{/* Custom Cursor - Visible when pointer lock is active */}
|
||||||
{(() => {
|
{(() => {
|
||||||
console.log('[Custom Cursor] Render check:', {
|
// Debug logging removed - was flooding console
|
||||||
pointerLocked,
|
|
||||||
hasCursorPosition: !!cursorPosition,
|
|
||||||
cursorPosition,
|
|
||||||
shouldRender: pointerLocked && cursorPosition,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (pointerLocked && cursorPosition) {
|
|
||||||
console.log('[Custom Cursor] ✅ RENDERING at position:', cursorPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pointerLocked && cursorPosition ? (
|
return pointerLocked && cursorPosition ? (
|
||||||
<div
|
<div
|
||||||
data-element="custom-cursor"
|
data-element="custom-cursor"
|
||||||
|
|
@ -1423,7 +1645,8 @@ export function MapRenderer({
|
||||||
width: '50%',
|
width: '50%',
|
||||||
aspectRatio: '2/1',
|
aspectRatio: '2/1',
|
||||||
// High zoom (>60x) gets gold border, normal zoom gets blue border
|
// High zoom (>60x) gets gold border, normal zoom gets blue border
|
||||||
border: magnifierSpring.zoom.to((zoom) =>
|
border: magnifierSpring.zoom.to(
|
||||||
|
(zoom) =>
|
||||||
zoom > HIGH_ZOOM_THRESHOLD
|
zoom > HIGH_ZOOM_THRESHOLD
|
||||||
? `4px solid ${isDark ? '#fbbf24' : '#f59e0b'}` // gold-400/gold-500
|
? `4px solid ${isDark ? '#fbbf24' : '#f59e0b'}` // gold-400/gold-500
|
||||||
: `3px solid ${isDark ? '#60a5fa' : '#3b82f6'}` // blue-400/blue-600
|
: `3px solid ${isDark ? '#60a5fa' : '#3b82f6'}` // blue-400/blue-600
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue