package world import ( "image/color" "testing" "github.com/stretchr/testify/require" ) func TestHitTest_Circle_StrictThresholds_WithRadiusScale_Table(t *testing.T) { t.Parallel() type tc struct { name string fillVisible bool rawRadius int // world units (not fixed); zoom=1 => 1px per unit scaleFp int hitSlopPx int cursorDxPx int // offset from center in pixels along X axis wantHit bool wantKind PrimitiveKind } // Common settings: world 20x20, viewport 200x200, camera at center (10,10). params := RenderParams{ ViewportWidthPx: 200, ViewportHeightPx: 200, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 10 * SCALE, CameraYWorldFp: 10 * SCALE, CameraZoom: 1.0, } tests := []tc{ { name: "filled: on boundary hits (R=4, S=1, dx=4)", fillVisible: true, rawRadius: 2, scaleFp: 2 * SCALE, // eff radius = 4 hitSlopPx: 1, cursorDxPx: 4, wantHit: true, wantKind: KindCircle, }, { name: "filled: outside beyond slop misses (R=4, S=1, dx=6)", fillVisible: true, rawRadius: 2, scaleFp: 2 * SCALE, hitSlopPx: 1, cursorDxPx: 6, // 6 > R+S = 5 wantHit: false, }, { name: "filled: just inside slop hits (R=4, S=1, dx=5)", fillVisible: true, rawRadius: 2, scaleFp: 2 * SCALE, hitSlopPx: 1, cursorDxPx: 5, // == R+S wantHit: true, wantKind: KindCircle, }, { name: "stroke-only: center must miss even if slop would cover", fillVisible: false, rawRadius: 2, scaleFp: 2 * SCALE, // eff radius = 4 hitSlopPx: 10, // huge, would normally include center without our rule cursorDxPx: 0, wantHit: false, }, { name: "stroke-only: on ring hits (R=4, S=1, dx=4)", fillVisible: false, rawRadius: 2, scaleFp: 2 * SCALE, hitSlopPx: 1, cursorDxPx: 4, wantHit: true, wantKind: KindCircle, }, { name: "stroke-only: inside ring beyond slop misses (R=4, S=1, dx=2)", fillVisible: false, rawRadius: 2, scaleFp: 2 * SCALE, hitSlopPx: 1, cursorDxPx: 2, // 2 < R-S = 3 wantHit: false, }, { name: "stroke-only: outside ring beyond slop misses (R=4, S=1, dx=6)", fillVisible: false, rawRadius: 2, scaleFp: 2 * SCALE, hitSlopPx: 1, cursorDxPx: 6, // 6 > R+S = 5 wantHit: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() w := NewWorld(20, 20) w.IndexOnViewportChange(params.ViewportWidthPx, params.ViewportHeightPx, params.CameraZoom) require.NoError(t, w.SetCircleRadiusScaleFp(tt.scaleFp)) // Build a stroke-only circle style if needed. var opts []CircleOpt opts = append(opts, CircleWithHitSlopPx(tt.hitSlopPx)) if !tt.fillVisible { // Force fill alpha=0 => stroke-only for hit-test and rendering. sw := 1.0 styleID := w.AddStyleCircle(StyleOverride{ FillColor: color.RGBA{A: 0}, StrokeColor: color.RGBA{A: 255}, StrokeWidthPx: &sw, }) opts = append(opts, CircleWithStyleID(styleID)) } _, err := w.AddCircle(10, 10, float64(tt.rawRadius), opts...) require.NoError(t, err) w.Reindex() // Cursor at viewport center +/- dx along X. At zoom=1, 1px == 1 world unit. cx := params.ViewportWidthPx/2 + tt.cursorDxPx cy := params.ViewportHeightPx / 2 buf := make([]Hit, 0, 8) hits, err := w.HitTest(buf, ¶ms, cx, cy) require.NoError(t, err) if !tt.wantHit { require.Empty(t, hits) return } require.NotEmpty(t, hits) require.Equal(t, tt.wantKind, hits[0].Kind) }) } }