fix: use actual SVG path geometry for region detection instead of bounding boxes
CRITICAL FIX: Replace bounding box intersection tests with actual SVG path geometry using isPointInFill(). This affects three critical systems: 1. **Region detection for auto-zoom** - No longer triggers on empty space within bounding boxes of irregularly shaped regions 2. **Hover highlighting** - Only highlights when cursor is actually over the region shape, not just its bounding box 3. **Click detection** - Only registers clicks on actual region geometry, preventing false positives Implementation: - Fast rejection: Check bounding box overlap first (performance optimization) - Precise detection: Sample 5×5 grid (25 points) within detection box - Use SVGGeometryElement.isPointInFill() for each sample point - Direct cursor test: Check if cursor point is inside region fill - Reuse SVGPoint object to avoid allocations in hot path This fixes issues with regions like Italy (boot shape), Croatia (complex coastline), and Malta (tiny region with large bounding box due to nearby islands). Benefits: - No false positives from empty space in bounding boxes - More precise hover highlighting - More accurate click detection - Better auto-zoom targeting for small regions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3887dd9ab2
commit
e255ce2c6f
|
|
@ -1,12 +1,18 @@
|
|||
/**
|
||||
* Region Detection Hook
|
||||
*
|
||||
* Detects which regions are near the cursor using a detection box.
|
||||
* Detects which regions are near the cursor using actual SVG path geometry.
|
||||
* Uses isPointInFill() to test if sample points within the detection box
|
||||
* intersect with the actual region shapes (not just bounding boxes).
|
||||
*
|
||||
* Returns information about detected regions including:
|
||||
* - Which regions overlap with the detection box
|
||||
* - Which region is directly under the cursor
|
||||
* - Which regions overlap with the detection box (using actual geometry)
|
||||
* - Which region is directly under the cursor (using actual geometry)
|
||||
* - Size information for adaptive cursor dampening
|
||||
* - Whether there are small regions requiring magnifier zoom
|
||||
*
|
||||
* CRITICAL: All detection uses SVG path geometry via isPointInFill(), not
|
||||
* bounding boxes. This prevents false positives from irregularly shaped regions.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, type RefObject } from 'react'
|
||||
|
|
@ -141,31 +147,62 @@ export function useRegionDetection(options: UseRegionDetectionOptions): UseRegio
|
|||
|
||||
mapData.regions.forEach((region) => {
|
||||
const regionPath = svgElement.querySelector(`path[data-region-id="${region.id}"]`)
|
||||
if (!regionPath) return
|
||||
if (!regionPath || !(regionPath instanceof SVGGeometryElement)) return
|
||||
|
||||
const pathRect = regionPath.getBoundingClientRect()
|
||||
|
||||
// CRITICAL: Use actual SVG path geometry, not bounding box
|
||||
// Sample multiple points within the detection box to check for intersection
|
||||
// This prevents false positives from irregularly shaped regions
|
||||
|
||||
// First quick check: does bounding box even overlap? (fast rejection)
|
||||
const regionLeft = pathRect.left
|
||||
const regionRight = pathRect.right
|
||||
const regionTop = pathRect.top
|
||||
const regionBottom = pathRect.bottom
|
||||
|
||||
// Check if region overlaps with detection box
|
||||
const overlaps =
|
||||
const boundingBoxOverlaps =
|
||||
regionLeft < boxRight &&
|
||||
regionRight > boxLeft &&
|
||||
regionTop < boxBottom &&
|
||||
regionBottom > boxTop
|
||||
|
||||
// Check if cursor is directly over this region
|
||||
const cursorInRegion =
|
||||
cursorClientX >= regionLeft &&
|
||||
cursorClientX <= regionRight &&
|
||||
cursorClientY >= regionTop &&
|
||||
cursorClientY <= regionBottom
|
||||
if (!boundingBoxOverlaps) {
|
||||
// Bounding box doesn't overlap, so actual path definitely doesn't
|
||||
return
|
||||
}
|
||||
|
||||
// Bounding box overlaps - now check actual path geometry
|
||||
// Sample a grid of points within the detection box
|
||||
const samplesPerSide = 5 // 5×5 = 25 sample points
|
||||
const sampleStep = detectionBoxSize / (samplesPerSide - 1)
|
||||
|
||||
let overlaps = false
|
||||
let cursorInRegion = false
|
||||
|
||||
// Create SVG point for reuse
|
||||
const svgPoint = svgElement.createSVGPoint()
|
||||
|
||||
// Check if cursor point is inside the actual region path
|
||||
svgPoint.x = cursorClientX
|
||||
svgPoint.y = cursorClientY
|
||||
cursorInRegion = regionPath.isPointInFill(svgPoint)
|
||||
|
||||
// Sample points in detection box to check for overlap
|
||||
for (let i = 0; i < samplesPerSide && !overlaps; i++) {
|
||||
for (let j = 0; j < samplesPerSide && !overlaps; j++) {
|
||||
svgPoint.x = boxLeft + i * sampleStep
|
||||
svgPoint.y = boxTop + j * sampleStep
|
||||
if (regionPath.isPointInFill(svgPoint)) {
|
||||
overlaps = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If cursor is inside region, track it as region under cursor
|
||||
if (cursorInRegion) {
|
||||
// Calculate distance from cursor to region center
|
||||
// Calculate distance from cursor to region center (using bounding box center as approximation)
|
||||
const regionCenterX = (regionLeft + regionRight) / 2
|
||||
const regionCenterY = (regionTop + regionBottom) / 2
|
||||
const distanceToCenter = Math.sqrt(
|
||||
|
|
@ -179,6 +216,7 @@ export function useRegionDetection(options: UseRegionDetectionOptions): UseRegio
|
|||
}
|
||||
}
|
||||
|
||||
// If detection box overlaps with actual path geometry, add to detected regions
|
||||
if (overlaps) {
|
||||
const pixelWidth = pathRect.width
|
||||
const pixelHeight = pathRect.height
|
||||
|
|
|
|||
Loading…
Reference in New Issue