feat: hit on primitives
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user