diff --git a/src/methods/clipping/sutherland_hodgman.jl b/src/methods/clipping/sutherland_hodgman.jl index b95c6320ef..c3260457a3 100644 --- a/src/methods/clipping/sutherland_hodgman.jl +++ b/src/methods/clipping/sutherland_hodgman.jl @@ -397,9 +397,11 @@ function _intersection_sutherland_hodgman( buf_in, buf_out = buf_out, buf_in end - # Handle empty result - check if clip polygon is fully inside the original subject + # Handle empty result - check if clip polygon is fully inside the original subject. + # "Fully inside" needs EVERY clip vertex inside the subject, not just the first: if isempty(buf_in) - if !isempty(clip_points) && _point_in_convex_spherical_polygon(clip_points[1], original_subject) + if !isempty(clip_points) && + all(p -> _point_in_convex_spherical_polygon(p, original_subject), clip_points) # Subject contains clip - return clip polygon (copied, so it doesn't alias the cache) result = copy(clip_points) push!(result, result[1]) diff --git a/test/methods/clipping/sutherland_hodgman.jl b/test/methods/clipping/sutherland_hodgman.jl index fea77c2886..4e1f5a5ab0 100644 --- a/test/methods/clipping/sutherland_hodgman.jl +++ b/test/methods/clipping/sutherland_hodgman.jl @@ -505,6 +505,39 @@ end @test a_oh ≈ a_ho rtol = 1e-6 @test 0 < a_oh < 0.1 * spherical_area(healpix) # sliver, not the whole cell end + + @testset "Grazing tiles are not handed the whole clip" begin + # The empty-output fallback returns the clip polygon when the subject + # contains it. "Contains" needs EVERY clip vertex inside the subject: + # `spherical_orient >= 0` counts the boundary as inside, so testing only + # `clip_points[1]` credits every subject that merely touches that one + # vertex with the clip's whole area. + # + # Reaching the fallback at all takes a non-convex clip -- with a convex + # clip a grazing subject survives every half-space test and leaves a + # 1-2 point output, which the degenerate branch already zeroes. So this + # is a graceful-degradation test for out-of-contract input (a cell that + # goes non-convex numerically, see #466): the answer may be wrong, but it + # must be wrong towards zero, never towards a whole fabricated cell. + clip = spherical_polygon([(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (10.0, 6.0), (0.0, 20.0)]) + + # Three convex, pairwise-disjoint tiles of the quadrants meeting at the + # clip ring's FIRST vertex (0, 0). None overlaps the clip's interior. + tiles = [ + spherical_polygon([(-5.0, -5.0), (0.0, -5.0), (0.0, 0.0), (-5.0, 0.0)]), # SW + spherical_polygon([(0.0, -5.0), (5.0, -5.0), (5.0, 0.0), (0.0, 0.0)]), # SE, shares the clip's south edge + spherical_polygon([(-5.0, 0.0), (0.0, 0.0), (0.0, 5.0), (-5.0, 5.0)]), # NW, shares the clip's west edge + ] + + alg = GO.ConvexConvexSutherlandHodgman(GO.Spherical()) + clip_a = spherical_area(clip) + for tile in tiles + @test spherical_area(GO.intersection(alg, tile, clip)) < 1e-9 * clip_a + end + # Summed over the disjoint tiles this is what breaks conservative + # regridding: the total was 3x the clip's own area. + @test sum(spherical_area(GO.intersection(alg, tile, clip)) for tile in tiles) < 1e-9 * clip_a + end end end