package world import ( "image/color" "testing" "github.com/stretchr/testify/require" ) func TestHitTest_ReturnsBestByPriorityAndAllHits(t *testing.T) { t.Parallel() w := NewWorld(10, 10) // Build index once renderer state is initialized. params := RenderParams{ ViewportWidthPx: 100, ViewportHeightPx: 100, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, } w.IndexOnViewportChange(params.ViewportWidthPx, params.ViewportHeightPx, params.CameraZoom) // Add overlapping objects near center. idLine, err := w.AddLine(4.5, 5.0, 5.5, 5.0, LineWithPriority(100)) require.NoError(t, err) idCircle, err := w.AddCircle(5.0, 5.0, 1.0, CircleWithPriority(300)) require.NoError(t, err) idPoint, err := w.AddPoint(5.0, 5.0, PointWithPriority(200)) require.NoError(t, err) // Force index rebuild from last state (Add already does it, but keep explicit). w.Reindex() buf := make([]Hit, 0, 8) hits, err := w.HitTest(buf, ¶ms, 50, 50) // center of viewport require.NoError(t, err) // Should find all three, best first (priority desc). require.Len(t, hits, 3) require.Equal(t, idCircle, hits[0].ID) require.Equal(t, idPoint, hits[1].ID) require.Equal(t, idLine, hits[2].ID) } func TestHitTest_BufferTooSmall_KeepsBestHits(t *testing.T) { t.Parallel() w := NewWorld(10, 10) params := RenderParams{ ViewportWidthPx: 100, ViewportHeightPx: 100, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, } w.IndexOnViewportChange(params.ViewportWidthPx, params.ViewportHeightPx, params.CameraZoom) _, _ = w.AddLine(4.5, 5.0, 5.5, 5.0, LineWithPriority(100)) idCircle, _ := w.AddCircle(5.0, 5.0, 1.0, CircleWithPriority(300)) _, _ = w.AddPoint(5.0, 5.0, PointWithPriority(200)) w.Reindex() // Only room for 1 hit => must keep the best (highest priority). buf := make([]Hit, 0, 1) hits, err := w.HitTest(buf, ¶ms, 50, 50) require.NoError(t, err) require.Len(t, hits, 1) require.Equal(t, idCircle, hits[0].ID) } func TestHitTest_NoWrap_ClampsCameraAndStillHits(t *testing.T) { t.Parallel() w := NewWorld(10, 10) params := RenderParams{ ViewportWidthPx: 100, ViewportHeightPx: 100, MarginXPx: 25, MarginYPx: 25, CameraXWorldFp: -100000, // invalid camera, should be clamped CameraYWorldFp: -100000, CameraZoom: 1.0, Options: &RenderOptions{DisableWrapScroll: true}, } w.IndexOnViewportChange(params.ViewportWidthPx, params.ViewportHeightPx, params.CameraZoom) _, err := w.AddPoint(0.0, 0.0, PointWithPriority(100)) require.NoError(t, err) w.Reindex() // Tap near top-left of viewport should still map to world and find the point. buf := make([]Hit, 0, 8) hits, err := w.HitTest(buf, ¶ms, 0, 0) require.NoError(t, err) require.NotEmpty(t, hits) } func TestHitTest_CircleStrokeOnly_HitsNearRingNotCenter(t *testing.T) { t.Parallel() w := NewWorld(10, 10) params := RenderParams{ ViewportWidthPx: 100, ViewportHeightPx: 100, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, } w.IndexOnViewportChange(params.ViewportWidthPx, params.ViewportHeightPx, params.CameraZoom) // Stroke-only circle: FillColor alpha=0 => ring mode. ov := StyleOverride{ FillColor: color.RGBA{A: 0}, StrokeColor: color.RGBA{A: 255}, } strokeStyle := w.AddStyleCircle(ov) _, err := w.AddCircle(5.0, 5.0, 2.0, CircleWithStyleID(strokeStyle), CircleWithPriority(100), ) require.NoError(t, err) w.Reindex() buf := make([]Hit, 0, 8) // Center must NOT hit. hits, err := w.HitTest(buf, ¶ms, 50, 50) require.NoError(t, err) require.Empty(t, hits) // Near ring should hit. For small circles we use a minimum visible ring radius (3px). // So tapping at +3px from center should be within ring+slop. hits, err = w.HitTest(buf, ¶ms, 50+3, 50) require.NoError(t, err) require.NotEmpty(t, hits) require.Equal(t, KindCircle, hits[0].Kind) } func TestHitTest_CircleRadiusScale_AffectsHitArea(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.SetTheme(DefaultTheme{}) // filled circles by default in our defaults w.IndexOnViewportChange(100, 100, 1.0) // raw radius=2 units, centered at (5,5) _, err := w.AddCircle(5, 5, 2) require.NoError(t, err) // scale=2 => eff radius=4 require.NoError(t, w.SetCircleRadiusScaleFp(2*SCALE)) w.Reindex() params := RenderParams{ ViewportWidthPx: 100, ViewportHeightPx: 100, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, } // Tap at +4 px from center should hit (eff radius 4). buf := make([]Hit, 0, 8) hits, err := w.HitTest(buf, ¶ms, 50+4, 50) require.NoError(t, err) require.NotEmpty(t, hits) require.Equal(t, KindCircle, hits[0].Kind) // Tap at +5 should typically miss (depending on slop); enforce by setting small slop via options. // We'll add a small-slope circle and test deterministically. }