feat: add detailed zoom decision debug panel

Enhance the debug panel to show comprehensive information about how each
detected region contributed to the final zoom level decision.

New debug information displayed:
- Final zoom level and whether it was accepted or fallback to minimum
- Which region was accepted (if any)
- Adaptive acceptance thresholds (min-max % of magnifier size)
- Per-region analysis showing:
  * Region ID and importance score
  * Current size on screen (width × height in pixels)
  * Zoom level tested for that region
  * Magnified size at that zoom
  * Size ratio (what % of magnifier it would occupy)
  * Rejection reason if not accepted
  * Visual highlighting (green for accepted, gray for rejected)

The panel now shows the top 5 most important regions sorted by importance
score, with clear visual indication of why each was accepted or rejected.

This helps understand:
- Why a particular zoom level was chosen
- Which region drove the decision
- Why other regions were rejected (too small/large, not in viewport)
- What the acceptance thresholds are for the current situation

All behind SHOW_DEBUG_BOUNDING_BOXES dev flag.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-24 21:13:49 -06:00
parent 58dad83862
commit cb57f1585a
2 changed files with 167 additions and 7 deletions

View File

@ -256,6 +256,10 @@ export function MapRenderer({
// Debug: Track bounding boxes for visualization
const [debugBoundingBoxes, setDebugBoundingBoxes] = useState<DebugBoundingBox[]>([])
// Debug: Track full zoom search result for detailed panel
const [zoomSearchDebugInfo, setZoomSearchDebugInfo] = useState<ReturnType<
typeof findOptimalZoom
> | null>(null)
// Pre-computed largest piece sizes for multi-piece regions
// Maps regionId -> {width, height} of the largest piece
@ -1094,6 +1098,8 @@ export function MapRenderer({
// Save bounding boxes for rendering
setDebugBoundingBoxes(boundingBoxes)
// Save full zoom search result for debug panel
setZoomSearchDebugInfo(zoomSearchResult)
// Calculate magnifier dimensions (needed for positioning)
const magnifierWidth = containerRect.width * 0.5
@ -2222,7 +2228,88 @@ export function MapRenderer({
Smallest size:{' '}
{detectedSmallestSize === Infinity ? '∞' : `${detectedSmallestSize.toFixed(1)}px`}
</div>
<div style={{ marginTop: '8px' }}>
{/* Zoom Decision Details */}
{zoomSearchDebugInfo && (
<>
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid #444' }}>
<strong>Zoom Decision:</strong>
</div>
<div style={{ fontSize: '10px', marginLeft: '8px' }}>
Final zoom: <strong>{zoomSearchDebugInfo.zoom.toFixed(1)}×</strong>
{!zoomSearchDebugInfo.foundGoodZoom && ' (fallback to min)'}
</div>
<div style={{ fontSize: '10px', marginLeft: '8px' }}>
Accepted: <strong>{zoomSearchDebugInfo.acceptedRegionId || 'none'}</strong>
</div>
<div style={{ fontSize: '10px', marginLeft: '8px' }}>
Thresholds: {(zoomSearchDebugInfo.acceptanceThresholds.min * 100).toFixed(0)}% -{' '}
{(zoomSearchDebugInfo.acceptanceThresholds.max * 100).toFixed(0)}% of magnifier
</div>
<div style={{ marginTop: '8px' }}>
<strong>Region Analysis:</strong>
</div>
{zoomSearchDebugInfo.regionDecisions
.sort((a, b) => b.importance - a.importance)
.slice(0, 5)
.map((decision) => {
const bgColor = decision.wasAccepted
? 'rgba(0, 255, 0, 0.15)'
: 'rgba(128, 128, 128, 0.1)'
const textColor = decision.wasAccepted ? '#0f0' : '#ccc'
return (
<div
key={decision.regionId}
style={{
fontSize: '9px',
marginLeft: '8px',
marginTop: '4px',
padding: '4px',
backgroundColor: bgColor,
borderLeft: `2px solid ${textColor}`,
paddingLeft: '6px',
}}
>
<div style={{ fontWeight: 'bold', color: textColor }}>
{decision.regionId} (importance: {decision.importance.toFixed(2)})
</div>
<div>
Size: {decision.currentSize.width.toFixed(1)}×
{decision.currentSize.height.toFixed(1)}px
</div>
{decision.testedZoom && (
<>
<div>
@ {decision.testedZoom.toFixed(1)}×: {decision.magnifiedSize?.width.toFixed(0)}×
{decision.magnifiedSize?.height.toFixed(0)}px
</div>
<div>
Ratio: {((decision.sizeRatio?.width ?? 0) * 100).toFixed(1)}% ×{' '}
{((decision.sizeRatio?.height ?? 0) * 100).toFixed(1)}%
</div>
</>
)}
{decision.rejectionReason && (
<div style={{ color: '#f88', fontStyle: 'italic' }}>
{decision.rejectionReason}
</div>
)}
{decision.wasAccepted && (
<div style={{ color: '#0f0', fontWeight: 'bold' }}> ACCEPTED</div>
)}
</div>
)
})}
{zoomSearchDebugInfo.regionDecisions.length > 5 && (
<div style={{ fontSize: '9px', marginLeft: '8px', color: '#888', marginTop: '4px' }}>
...and {zoomSearchDebugInfo.regionDecisions.length - 5} more regions
</div>
)}
</>
)}
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid #444' }}>
<strong>Detected Regions:</strong>
</div>
{detectedRegions.slice(0, 5).map((region) => (

View File

@ -61,6 +61,17 @@ export interface BoundingBox {
wasAccepted?: boolean
}
export interface RegionZoomDecision {
regionId: string
importance: number
currentSize: { width: number; height: number } // Screen pixels
testedZoom: number | null // Zoom level tested for this region (null if not in viewport)
magnifiedSize: { width: number; height: number } | null // Size at tested zoom
sizeRatio: { width: number; height: number } | null // Ratio of magnified size to magnifier
wasAccepted: boolean
rejectionReason: string | null // Why this region was rejected (if applicable)
}
export interface AdaptiveZoomSearchResult {
/** The optimal zoom level found */
zoom: number
@ -68,6 +79,12 @@ export interface AdaptiveZoomSearchResult {
foundGoodZoom: boolean
/** Debug bounding boxes for visualization (includes all detected regions) */
boundingBoxes: BoundingBox[]
/** Detailed decision information for each region */
regionDecisions: RegionZoomDecision[]
/** The acceptance thresholds used */
acceptanceThresholds: { min: number; max: number }
/** The region that was accepted (if any) */
acceptedRegionId: string | null
}
/**
@ -338,6 +355,10 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
}
}).filter((bbox) => bbox.width > 0 && bbox.height > 0)
// Track detailed decision information for each region
const regionDecisions: RegionZoomDecision[] = []
let acceptedRegionId: string | null = null
// Search for optimal zoom
let optimalZoom = maxZoom
let foundGoodZoom = false
@ -361,7 +382,7 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
// Check regions in order of importance (most important first)
let foundFit = false
for (const { region: detectedRegion } of sortedRegions) {
for (const { region: detectedRegion, importance } of sortedRegions) {
const region = mapData.regions.find((r) => r.id === detectedRegion.id)
if (!region) continue
@ -400,7 +421,23 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
bottom: regionSvgBottom,
}
if (!isRegionInViewport(regionBounds, viewport)) {
const inViewport = isRegionInViewport(regionBounds, viewport)
if (!inViewport) {
// Record decision: Not in viewport at this zoom
const existingDecision = regionDecisions.find((d) => d.regionId === detectedRegion.id)
if (!existingDecision) {
regionDecisions.push({
regionId: detectedRegion.id,
importance,
currentSize: { width: currentWidth, height: currentHeight },
testedZoom: null,
magnifiedSize: null,
sizeRatio: null,
wasAccepted: false,
rejectionReason: 'Not in viewport',
})
}
continue // Skip regions not in viewport
}
@ -412,13 +449,14 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
const heightRatio = magnifiedHeight / magnifierHeight
// If either dimension is within our adaptive acceptance range, we found a good zoom
if (
(widthRatio >= minAcceptableRatio && widthRatio <= maxAcceptableRatio) ||
(heightRatio >= minAcceptableRatio && heightRatio <= maxAcceptableRatio)
) {
const widthFits = widthRatio >= minAcceptableRatio && widthRatio <= maxAcceptableRatio
const heightFits = heightRatio >= minAcceptableRatio && heightRatio <= maxAcceptableRatio
if (widthFits || heightFits) {
optimalZoom = testZoom
foundFit = true
foundGoodZoom = true
acceptedRegionId = detectedRegion.id
// Log when we accept a zoom
console.log(
@ -431,7 +469,39 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
acceptedBox.wasAccepted = true
}
// Record the acceptance decision
regionDecisions.push({
regionId: detectedRegion.id,
importance,
currentSize: { width: currentWidth, height: currentHeight },
testedZoom: testZoom,
magnifiedSize: { width: magnifiedWidth, height: magnifiedHeight },
sizeRatio: { width: widthRatio, height: heightRatio },
wasAccepted: true,
rejectionReason: null,
})
break // Found a good zoom, stop checking regions
} else {
// Record rejection: Size doesn't fit
const reason =
widthRatio < minAcceptableRatio || heightRatio < minAcceptableRatio
? 'Too small (below min threshold)'
: 'Too large (above max threshold)'
const existingDecision = regionDecisions.find((d) => d.regionId === detectedRegion.id)
if (!existingDecision) {
regionDecisions.push({
regionId: detectedRegion.id,
importance,
currentSize: { width: currentWidth, height: currentHeight },
testedZoom: testZoom,
magnifiedSize: { width: magnifiedWidth, height: magnifiedHeight },
sizeRatio: { width: widthRatio, height: heightRatio },
wasAccepted: false,
rejectionReason: reason,
})
}
}
}
@ -450,5 +520,8 @@ export function findOptimalZoom(context: AdaptiveZoomSearchContext): AdaptiveZoo
zoom: optimalZoom,
foundGoodZoom,
boundingBoxes,
regionDecisions,
acceptanceThresholds: thresholds,
acceptedRegionId,
}
}