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:
parent
58dad83862
commit
cb57f1585a
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue