feat: add visual debugging for zoom importance scoring
Add comprehensive visual debugging for the adaptive zoom algorithm: - Render bounding boxes for all detected regions (not just accepted one) - Color-code by importance: green (accepted), orange (high), yellow (medium), gray (low) - Display importance scores calculated from distance + size weighting - Use HTML overlays for text labels (always readable at any zoom level) - Enable automatically in development mode via SHOW_DEBUG_BOUNDING_BOXES flag This helps diagnose zoom behavior issues by showing: - Which region "won" the importance calculation (green box) - Exact importance scores (distance × size weighting) - Bounding box rectangles vs actual region shapes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0aee60d8d1
commit
e60a2c09c0
|
|
@ -83,7 +83,5 @@
|
||||||
"ask": []
|
"ask": []
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enableAllProjectMcpServers": true,
|
||||||
"enabledMcpjsonServers": [
|
"enabledMcpjsonServers": ["sqlite"]
|
||||||
"sqlite"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
||||||
import { useSpring, useSpringRef, animated } from '@react-spring/web'
|
import { useSpring, animated } from '@react-spring/web'
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
import type { MapData, MapRegion } from '../types'
|
import type { MapData, MapRegion } from '../types'
|
||||||
|
|
@ -19,7 +19,10 @@ import {
|
||||||
calculateMaxZoomAtThreshold,
|
calculateMaxZoomAtThreshold,
|
||||||
isAboveThreshold,
|
isAboveThreshold,
|
||||||
} from '../utils/screenPixelRatio'
|
} from '../utils/screenPixelRatio'
|
||||||
import { findOptimalZoom } from '../utils/adaptiveZoomSearch'
|
import {
|
||||||
|
findOptimalZoom,
|
||||||
|
type BoundingBox as DebugBoundingBox,
|
||||||
|
} from '../utils/adaptiveZoomSearch'
|
||||||
import { useRegionDetection } from '../hooks/useRegionDetection'
|
import { useRegionDetection } from '../hooks/useRegionDetection'
|
||||||
import { usePointerLock } from '../hooks/usePointerLock'
|
import { usePointerLock } from '../hooks/usePointerLock'
|
||||||
import { useMagnifierZoom } from '../hooks/useMagnifierZoom'
|
import { useMagnifierZoom } from '../hooks/useMagnifierZoom'
|
||||||
|
|
@ -27,6 +30,9 @@ import { useMagnifierZoom } from '../hooks/useMagnifierZoom'
|
||||||
// Debug flag: show technical info in magnifier (dev only)
|
// Debug flag: show technical info in magnifier (dev only)
|
||||||
const SHOW_MAGNIFIER_DEBUG_INFO = process.env.NODE_ENV === 'development'
|
const SHOW_MAGNIFIER_DEBUG_INFO = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
// Debug flag: show bounding boxes with importance scores (dev only)
|
||||||
|
const SHOW_DEBUG_BOUNDING_BOXES = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
// Precision mode threshold: screen pixel ratio that triggers pointer lock recommendation
|
// Precision mode threshold: screen pixel ratio that triggers pointer lock recommendation
|
||||||
const PRECISION_MODE_THRESHOLD = 20
|
const PRECISION_MODE_THRESHOLD = 20
|
||||||
|
|
||||||
|
|
@ -125,7 +131,7 @@ export function MapRenderer({
|
||||||
guessHistory,
|
guessHistory,
|
||||||
playerMetadata,
|
playerMetadata,
|
||||||
forceTuning = {},
|
forceTuning = {},
|
||||||
showDebugBoundingBoxes = false,
|
showDebugBoundingBoxes = SHOW_DEBUG_BOUNDING_BOXES,
|
||||||
}: MapRendererProps) {
|
}: MapRendererProps) {
|
||||||
// Extract force tuning parameters with defaults
|
// Extract force tuning parameters with defaults
|
||||||
const {
|
const {
|
||||||
|
|
@ -249,9 +255,7 @@ export function MapRenderer({
|
||||||
const [smallestRegionSize, setSmallestRegionSize] = useState<number>(Infinity)
|
const [smallestRegionSize, setSmallestRegionSize] = useState<number>(Infinity)
|
||||||
|
|
||||||
// Debug: Track bounding boxes for visualization
|
// Debug: Track bounding boxes for visualization
|
||||||
const [debugBoundingBoxes, setDebugBoundingBoxes] = useState<
|
const [debugBoundingBoxes, setDebugBoundingBoxes] = useState<DebugBoundingBox[]>([])
|
||||||
Array<{ regionId: string; x: number; y: number; width: number; height: number }>
|
|
||||||
>([])
|
|
||||||
|
|
||||||
// Pre-computed largest piece sizes for multi-piece regions
|
// Pre-computed largest piece sizes for multi-piece regions
|
||||||
// Maps regionId -> {width, height} of the largest piece
|
// Maps regionId -> {width, height} of the largest piece
|
||||||
|
|
@ -362,7 +366,6 @@ export function MapRenderer({
|
||||||
console.log('[CLICK] Using closest detected region:', {
|
console.log('[CLICK] Using closest detected region:', {
|
||||||
regionId: closestRegion.id,
|
regionId: closestRegion.id,
|
||||||
regionName: region.name,
|
regionName: region.name,
|
||||||
distance: closestRegion.distanceToCenter?.toFixed(2),
|
|
||||||
})
|
})
|
||||||
onRegionClick(closestRegion.id, region.name)
|
onRegionClick(closestRegion.id, region.name)
|
||||||
}
|
}
|
||||||
|
|
@ -1048,10 +1051,7 @@ export function MapRenderer({
|
||||||
totalRegionArea,
|
totalRegionArea,
|
||||||
} = detectionResult
|
} = detectionResult
|
||||||
|
|
||||||
// Extract region IDs for zoom search (already sorted smallest-first by hook)
|
if (pointerLocked && detectedRegionObjects.length > 0) {
|
||||||
const detectedRegions = detectedRegionObjects.map((r) => r.id)
|
|
||||||
|
|
||||||
if (pointerLocked && detectedRegions.length > 0) {
|
|
||||||
const sortedSizes = detectedRegionObjects.map((r) => `${r.id}: ${r.screenSize.toFixed(2)}px`)
|
const sortedSizes = detectedRegionObjects.map((r) => `${r.id}: ${r.screenSize.toFixed(2)}px`)
|
||||||
console.log('[Zoom Search] Sorted regions (smallest first):', sortedSizes)
|
console.log('[Zoom Search] Sorted regions (smallest first):', sortedSizes)
|
||||||
}
|
}
|
||||||
|
|
@ -1075,7 +1075,7 @@ export function MapRenderer({
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
// Use adaptive zoom search utility to find optimal zoom
|
// Use adaptive zoom search utility to find optimal zoom
|
||||||
const zoomSearchResult = findOptimalZoom({
|
const zoomSearchResult = findOptimalZoom({
|
||||||
detectedRegions,
|
detectedRegions: detectedRegionObjects,
|
||||||
detectedSmallestSize,
|
detectedSmallestSize,
|
||||||
cursorX,
|
cursorX,
|
||||||
cursorY,
|
cursorY,
|
||||||
|
|
@ -1272,36 +1272,42 @@ export function MapRenderer({
|
||||||
|
|
||||||
{/* Debug: Render bounding boxes (only if enabled) */}
|
{/* Debug: Render bounding boxes (only if enabled) */}
|
||||||
{showDebugBoundingBoxes &&
|
{showDebugBoundingBoxes &&
|
||||||
debugBoundingBoxes.map((bbox) => (
|
debugBoundingBoxes.map((bbox) => {
|
||||||
<g key={`bbox-${bbox.regionId}`}>
|
// Color based on acceptance and importance
|
||||||
<rect
|
// Green = accepted, Orange = high importance, Yellow = medium, Gray = low
|
||||||
x={bbox.x}
|
const importance = bbox.importance ?? 0
|
||||||
y={bbox.y}
|
let strokeColor = '#888888' // Default gray for low importance
|
||||||
width={bbox.width}
|
let fillColor = 'rgba(136, 136, 136, 0.1)'
|
||||||
height={bbox.height}
|
|
||||||
fill="none"
|
if (bbox.wasAccepted) {
|
||||||
stroke="#ff0000"
|
strokeColor = '#00ff00' // Green for accepted region
|
||||||
strokeWidth={viewBoxWidth / 500}
|
fillColor = 'rgba(0, 255, 0, 0.15)'
|
||||||
vectorEffect="non-scaling-stroke"
|
} else if (importance > 1.5) {
|
||||||
strokeDasharray="3,3"
|
strokeColor = '#ff6600' // Orange for high importance (2.0× boost + close)
|
||||||
pointerEvents="none"
|
fillColor = 'rgba(255, 102, 0, 0.1)'
|
||||||
opacity={0.8}
|
} else if (importance > 0.5) {
|
||||||
/>
|
strokeColor = '#ffcc00' // Yellow for medium importance
|
||||||
{/* Label showing region ID */}
|
fillColor = 'rgba(255, 204, 0, 0.1)'
|
||||||
<text
|
}
|
||||||
x={bbox.x + bbox.width / 2}
|
|
||||||
y={bbox.y + bbox.height / 2}
|
return (
|
||||||
fill="#ff0000"
|
<g key={`bbox-${bbox.regionId}`}>
|
||||||
fontSize={viewBoxWidth / 80}
|
<rect
|
||||||
textAnchor="middle"
|
x={bbox.x}
|
||||||
dominantBaseline="middle"
|
y={bbox.y}
|
||||||
pointerEvents="none"
|
width={bbox.width}
|
||||||
style={{ fontWeight: 'bold' }}
|
height={bbox.height}
|
||||||
>
|
fill={fillColor}
|
||||||
{bbox.regionId}
|
stroke={strokeColor}
|
||||||
</text>
|
strokeWidth={viewBoxWidth / 500}
|
||||||
</g>
|
vectorEffect="non-scaling-stroke"
|
||||||
))}
|
strokeDasharray="3,3"
|
||||||
|
pointerEvents="none"
|
||||||
|
opacity={0.9}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Arrow marker definition */}
|
{/* Arrow marker definition */}
|
||||||
<defs>
|
<defs>
|
||||||
|
|
@ -1540,6 +1546,64 @@ export function MapRenderer({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Debug: Bounding box labels as HTML overlays */}
|
||||||
|
{showDebugBoundingBoxes &&
|
||||||
|
containerRef.current &&
|
||||||
|
svgRef.current &&
|
||||||
|
debugBoundingBoxes.map((bbox) => {
|
||||||
|
const importance = bbox.importance ?? 0
|
||||||
|
let strokeColor = '#888888'
|
||||||
|
|
||||||
|
if (bbox.wasAccepted) {
|
||||||
|
strokeColor = '#00ff00'
|
||||||
|
} else if (importance > 1.5) {
|
||||||
|
strokeColor = '#ff6600'
|
||||||
|
} else if (importance > 0.5) {
|
||||||
|
strokeColor = '#ffcc00'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert SVG coordinates to pixel coordinates
|
||||||
|
const containerRect = containerRef.current!.getBoundingClientRect()
|
||||||
|
const svgRect = svgRef.current!.getBoundingClientRect()
|
||||||
|
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
|
||||||
|
const viewBoxX = viewBoxParts[0] || 0
|
||||||
|
const viewBoxY = viewBoxParts[1] || 0
|
||||||
|
const viewBoxWidth = viewBoxParts[2] || 1000
|
||||||
|
const viewBoxHeight = viewBoxParts[3] || 1000
|
||||||
|
|
||||||
|
const scaleX = svgRect.width / viewBoxWidth
|
||||||
|
const scaleY = svgRect.height / viewBoxHeight
|
||||||
|
const svgOffsetX = svgRect.left - containerRect.left
|
||||||
|
const svgOffsetY = svgRect.top - containerRect.top
|
||||||
|
|
||||||
|
// Convert bbox center from SVG coords to pixels
|
||||||
|
const centerX = (bbox.x + bbox.width / 2 - viewBoxX) * scaleX + svgOffsetX
|
||||||
|
const centerY = (bbox.y + bbox.height / 2 - viewBoxY) * scaleY + svgOffsetY
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`bbox-label-${bbox.regionId}`}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${centerX}px`,
|
||||||
|
top: `${centerY}px`,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 15,
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: strokeColor,
|
||||||
|
textAlign: 'center',
|
||||||
|
textShadow: '0 0 2px black, 0 0 2px black, 0 0 2px black',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{bbox.regionId}</div>
|
||||||
|
<div style={{ fontSize: '8px', fontWeight: 'normal' }}>{importance.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Custom Cursor - Visible when pointer lock is active */}
|
{/* Custom Cursor - Visible when pointer lock is active */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// Debug logging removed - was flooding console
|
// Debug logging removed - was flooding console
|
||||||
|
|
@ -2034,7 +2098,8 @@ export function MapRenderer({
|
||||||
<div>Regions detected: {detectedRegions.length}</div>
|
<div>Regions detected: {detectedRegions.length}</div>
|
||||||
<div>Has small region: {hasSmallRegion ? 'YES' : 'NO'}</div>
|
<div>Has small region: {hasSmallRegion ? 'YES' : 'NO'}</div>
|
||||||
<div>
|
<div>
|
||||||
Smallest size: {detectedSmallestSize === Infinity ? '∞' : `${detectedSmallestSize.toFixed(1)}px`}
|
Smallest size:{' '}
|
||||||
|
{detectedSmallestSize === Infinity ? '∞' : `${detectedSmallestSize.toFixed(1)}px`}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: '8px' }}>
|
<div style={{ marginTop: '8px' }}>
|
||||||
<strong>Detected Regions:</strong>
|
<strong>Detected Regions:</strong>
|
||||||
|
|
|
||||||
|
|
@ -281,9 +281,7 @@ describe('adaptiveZoomSearch', () => {
|
||||||
const region1: Bounds = { left: 150, right: 250, top: 150, bottom: 250 }
|
const region1: Bounds = { left: 150, right: 250, top: 150, bottom: 250 }
|
||||||
const region2: Bounds = { left: 100, right: 200, top: 100, bottom: 200 }
|
const region2: Bounds = { left: 100, right: 200, top: 100, bottom: 200 }
|
||||||
|
|
||||||
expect(isRegionInViewport(region1, region2)).toBe(
|
expect(isRegionInViewport(region1, region2)).toBe(isRegionInViewport(region2, region1))
|
||||||
isRegionInViewport(region2, region1)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MapData } from '../types'
|
import type { MapData } from '../types'
|
||||||
|
import type { DetectedRegion } from '../hooks/useRegionDetection'
|
||||||
|
|
||||||
export interface AdaptiveZoomSearchContext {
|
export interface AdaptiveZoomSearchContext {
|
||||||
/** Detected region IDs, sorted by size (smallest first) */
|
/** Detected region objects with size and metadata */
|
||||||
detectedRegions: string[]
|
detectedRegions: DetectedRegion[]
|
||||||
/** Size of the smallest detected region in pixels */
|
/** Size of the smallest detected region in pixels */
|
||||||
detectedSmallestSize: number
|
detectedSmallestSize: number
|
||||||
/** Cursor position in container coordinates */
|
/** Cursor position in container coordinates */
|
||||||
|
|
@ -43,12 +44,21 @@ export interface AdaptiveZoomSearchContext {
|
||||||
pointerLocked?: boolean
|
pointerLocked?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Bounds {
|
||||||
|
left: number
|
||||||
|
right: number
|
||||||
|
top: number
|
||||||
|
bottom: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface BoundingBox {
|
export interface BoundingBox {
|
||||||
regionId: string
|
regionId: string
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
|
importance?: number
|
||||||
|
wasAccepted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdaptiveZoomSearchResult {
|
export interface AdaptiveZoomSearchResult {
|
||||||
|
|
@ -56,7 +66,7 @@ export interface AdaptiveZoomSearchResult {
|
||||||
zoom: number
|
zoom: number
|
||||||
/** Whether a good zoom was found (false = using minimum zoom as fallback) */
|
/** Whether a good zoom was found (false = using minimum zoom as fallback) */
|
||||||
foundGoodZoom: boolean
|
foundGoodZoom: boolean
|
||||||
/** Debug bounding boxes for visualization */
|
/** Debug bounding boxes for visualization (includes all detected regions) */
|
||||||
boundingBoxes: BoundingBox[]
|
boundingBoxes: BoundingBox[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,9 +106,9 @@ export function calculateAdaptiveThresholds(smallestSize: number): {
|
||||||
* @returns Clamped viewport bounds
|
* @returns Clamped viewport bounds
|
||||||
*/
|
*/
|
||||||
export function clampViewportToMapBounds(
|
export function clampViewportToMapBounds(
|
||||||
viewport: { left: number; right: number; top: number; bottom: number },
|
viewport: Bounds,
|
||||||
mapBounds: { left: number; right: number; top: number; bottom: number }
|
mapBounds: Bounds
|
||||||
): { left: number; right: number; top: number; bottom: number; wasClamped: boolean } {
|
): Bounds & { wasClamped: boolean } {
|
||||||
let { left, right, top, bottom } = viewport
|
let { left, right, top, bottom } = viewport
|
||||||
let wasClamped = false
|
let wasClamped = false
|
||||||
|
|
||||||
|
|
@ -144,10 +154,7 @@ export function clampViewportToMapBounds(
|
||||||
* @param viewport - Viewport bounds in SVG coordinates
|
* @param viewport - Viewport bounds in SVG coordinates
|
||||||
* @returns True if region overlaps viewport
|
* @returns True if region overlaps viewport
|
||||||
*/
|
*/
|
||||||
export function isRegionInViewport(
|
export function isRegionInViewport(regionBounds: Bounds, viewport: Bounds): boolean {
|
||||||
regionBounds: { left: number; right: number; top: number; bottom: number },
|
|
||||||
viewport: { left: number; right: number; top: number; bottom: number }
|
|
||||||
): boolean {
|
|
||||||
return (
|
return (
|
||||||
regionBounds.left < viewport.right &&
|
regionBounds.left < viewport.right &&
|
||||||
regionBounds.right > viewport.left &&
|
regionBounds.right > viewport.left &&
|
||||||
|
|
@ -156,6 +163,48 @@ export function isRegionInViewport(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate importance score for a region based on distance from cursor and size.
|
||||||
|
*
|
||||||
|
* Smaller regions and regions closer to cursor get higher importance scores.
|
||||||
|
* This ensures we zoom appropriately for the region the user is actually targeting.
|
||||||
|
*
|
||||||
|
* @param region - Detected region with size metadata
|
||||||
|
* @param cursorX - Cursor X in container coordinates
|
||||||
|
* @param cursorY - Cursor Y in container coordinates
|
||||||
|
* @param regionCenterX - Region center X in screen coordinates
|
||||||
|
* @param regionCenterY - Region center Y in screen coordinates
|
||||||
|
* @param containerRect - Container bounding rect
|
||||||
|
* @returns Importance score (higher = more important)
|
||||||
|
*/
|
||||||
|
function calculateRegionImportance(
|
||||||
|
region: DetectedRegion,
|
||||||
|
cursorX: number,
|
||||||
|
cursorY: number,
|
||||||
|
regionCenterX: number,
|
||||||
|
regionCenterY: number,
|
||||||
|
containerRect: DOMRect
|
||||||
|
): number {
|
||||||
|
// 1. Distance factor: Closer to cursor = more important
|
||||||
|
const cursorClientX = containerRect.left + cursorX
|
||||||
|
const cursorClientY = containerRect.top + cursorY
|
||||||
|
const distanceToCursor = Math.sqrt(
|
||||||
|
(cursorClientX - regionCenterX) ** 2 + (cursorClientY - regionCenterY) ** 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Normalize distance to 0-1 range (0 = at cursor, 1 = 50px away or more)
|
||||||
|
// Use 50px as reference since that's our detection box size
|
||||||
|
const normalizedDistance = Math.min(distanceToCursor / 50, 1)
|
||||||
|
const distanceWeight = 1 - normalizedDistance // Invert: closer = higher weight
|
||||||
|
|
||||||
|
// 2. Size factor: Smaller regions get boosted importance
|
||||||
|
// This ensures San Marino can be targeted even when Italy is closer to cursor
|
||||||
|
const sizeWeight = region.isVerySmall ? 2.0 : 1.0
|
||||||
|
|
||||||
|
// Combined importance score
|
||||||
|
return distanceWeight * sizeWeight
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find optimal zoom level for the detected regions.
|
* Find optimal zoom level for the detected regions.
|
||||||
*
|
*
|
||||||
|
|
@ -225,8 +274,69 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
|
||||||
bottom: viewBoxY + viewBoxHeight,
|
bottom: viewBoxY + viewBoxHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track bounding boxes for debug visualization
|
// Calculate importance scores for all detected regions
|
||||||
const boundingBoxes: BoundingBox[] = []
|
// This weights regions by distance from cursor and size
|
||||||
|
const regionsWithScores = detectedRegions.map((detectedRegion) => {
|
||||||
|
const regionPath = svgElement.querySelector(`path[data-region-id="${detectedRegion.id}"]`)
|
||||||
|
if (!regionPath) {
|
||||||
|
return { region: detectedRegion, importance: 0, centerX: 0, centerY: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathRect = regionPath.getBoundingClientRect()
|
||||||
|
const regionCenterX = pathRect.left + pathRect.width / 2
|
||||||
|
const regionCenterY = pathRect.top + pathRect.height / 2
|
||||||
|
|
||||||
|
const importance = calculateRegionImportance(
|
||||||
|
detectedRegion,
|
||||||
|
cursorX,
|
||||||
|
cursorY,
|
||||||
|
regionCenterX,
|
||||||
|
regionCenterY,
|
||||||
|
containerRect
|
||||||
|
)
|
||||||
|
|
||||||
|
return { region: detectedRegion, importance, centerX: regionCenterX, centerY: regionCenterY }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort by importance (highest first)
|
||||||
|
const sortedRegions = regionsWithScores.sort((a, b) => b.importance - a.importance)
|
||||||
|
|
||||||
|
if (pointerLocked) {
|
||||||
|
console.log(
|
||||||
|
'[Zoom Search] Region importance scores:',
|
||||||
|
sortedRegions.map((r) => `${r.region.id}: ${r.importance.toFixed(2)}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track bounding boxes for debug visualization - add ALL detected regions upfront
|
||||||
|
const boundingBoxes: BoundingBox[] = sortedRegions.map(({ region: detectedRegion, importance }) => {
|
||||||
|
const regionPath = svgElement.querySelector(`path[data-region-id="${detectedRegion.id}"]`)
|
||||||
|
if (!regionPath) {
|
||||||
|
return {
|
||||||
|
regionId: detectedRegion.id,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
importance,
|
||||||
|
wasAccepted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathRect = regionPath.getBoundingClientRect()
|
||||||
|
const regionSvgLeft = (pathRect.left - svgRect.left) * scaleX + viewBoxX
|
||||||
|
const regionSvgTop = (pathRect.top - svgRect.top) * scaleY + viewBoxY
|
||||||
|
|
||||||
|
return {
|
||||||
|
regionId: detectedRegion.id,
|
||||||
|
x: regionSvgLeft,
|
||||||
|
y: regionSvgTop,
|
||||||
|
width: pathRect.width * scaleX,
|
||||||
|
height: pathRect.height * scaleY,
|
||||||
|
importance,
|
||||||
|
wasAccepted: false,
|
||||||
|
}
|
||||||
|
}).filter((bbox) => bbox.width > 0 && bbox.height > 0)
|
||||||
|
|
||||||
// Search for optimal zoom
|
// Search for optimal zoom
|
||||||
let optimalZoom = maxZoom
|
let optimalZoom = maxZoom
|
||||||
|
|
@ -248,21 +358,21 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
|
||||||
// Clamp viewport to stay within map bounds
|
// Clamp viewport to stay within map bounds
|
||||||
const viewport = clampViewportToMapBounds(initialViewport, mapBounds)
|
const viewport = clampViewportToMapBounds(initialViewport, mapBounds)
|
||||||
|
|
||||||
// Check all detected regions to see if any are inside this viewport and fit nicely
|
// Check regions in order of importance (most important first)
|
||||||
let foundFit = false
|
let foundFit = false
|
||||||
|
|
||||||
for (const regionId of detectedRegions) {
|
for (const { region: detectedRegion } of sortedRegions) {
|
||||||
const region = mapData.regions.find((r) => r.id === regionId)
|
const region = mapData.regions.find((r) => r.id === detectedRegion.id)
|
||||||
if (!region) continue
|
if (!region) continue
|
||||||
|
|
||||||
const regionPath = svgElement.querySelector(`path[data-region-id="${regionId}"]`)
|
const regionPath = svgElement.querySelector(`path[data-region-id="${detectedRegion.id}"]`)
|
||||||
if (!regionPath) continue
|
if (!regionPath) continue
|
||||||
|
|
||||||
// Use pre-computed largest piece size for multi-piece regions
|
// Use pre-computed largest piece size for multi-piece regions
|
||||||
let currentWidth: number
|
let currentWidth: number
|
||||||
let currentHeight: number
|
let currentHeight: number
|
||||||
|
|
||||||
const cachedSize = largestPieceSizesCache.get(regionId)
|
const cachedSize = largestPieceSizesCache.get(detectedRegion.id)
|
||||||
if (cachedSize) {
|
if (cachedSize) {
|
||||||
// Multi-piece region: use pre-computed largest piece
|
// Multi-piece region: use pre-computed largest piece
|
||||||
currentWidth = cachedSize.width
|
currentWidth = cachedSize.width
|
||||||
|
|
@ -312,17 +422,14 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
|
||||||
|
|
||||||
// Log when we accept a zoom
|
// Log when we accept a zoom
|
||||||
console.log(
|
console.log(
|
||||||
`[Zoom] ✅ Accepted ${testZoom.toFixed(1)}x for ${regionId} (${currentWidth.toFixed(1)}px × ${currentHeight.toFixed(1)}px)`
|
`[Zoom] ✅ Accepted ${testZoom.toFixed(1)}x for ${detectedRegion.id} (${currentWidth.toFixed(1)}px × ${currentHeight.toFixed(1)}px)`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Save bounding box for this region
|
// Mark this region's bounding box as accepted
|
||||||
boundingBoxes.push({
|
const acceptedBox = boundingBoxes.find((bbox) => bbox.regionId === detectedRegion.id)
|
||||||
regionId,
|
if (acceptedBox) {
|
||||||
x: regionSvgLeft,
|
acceptedBox.wasAccepted = true
|
||||||
y: regionSvgTop,
|
}
|
||||||
width: pathRect.width * scaleX,
|
|
||||||
height: pathRect.height * scaleY,
|
|
||||||
})
|
|
||||||
|
|
||||||
break // Found a good zoom, stop checking regions
|
break // Found a good zoom, stop checking regions
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue