From 910effec99119d185529243430588cbde291b7bd Mon Sep 17 00:00:00 2001 From: panmari Date: Sat, 2 Aug 2025 21:19:54 +0200 Subject: [PATCH] Return early in surfaceIntegral if there are less than 3 vertices. There's no area to integrate in these cases. Mirrors the logic from cpp at https://github.com/google/s2geometry/blob/9a43f6ac20950e59a182f498bcd3e9aa8fbe55ec/src/s2/s2loop_measures.h#L298 --- s2/loop.go | 8 ++++++ s2/loop_test.go | 71 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/s2/loop.go b/s2/loop.go index 0b8f2ed..3421505 100644 --- a/s2/loop.go +++ b/s2/loop.go @@ -1005,6 +1005,10 @@ func (l *Loop) ContainsNested(other *Loop) bool { // // Any changes to this method may need corresponding changes to surfaceIntegralPoint as well. func (l *Loop) surfaceIntegralFloat64(f func(a, b, c Point) float64) float64 { + if len(l.vertices) < 3 { + // If the loop has less than 3 vertices, there's no interior. + return 0 + } // We sum f over a collection T of oriented triangles, possibly // overlapping. Let the sign of a triangle be +1 if it is CCW and -1 // otherwise, and let the sign of a point x be the sum of the signs of the @@ -1092,6 +1096,10 @@ func (l *Loop) surfaceIntegralFloat64(f func(a, b, c Point) float64) float64 { func (l *Loop) surfaceIntegralPoint(f func(a, b, c Point) Point) Point { const maxLength = math.Pi - 1e-5 var sum r3.Vector + if len(l.vertices) < 3 { + // If the loop has less than 3 vertices, there's no interior. + return Point{sum} + } origin := l.Vertex(0) for i := 1; i+1 < len(l.vertices); i++ { diff --git a/s2/loop_test.go b/s2/loop_test.go index 986863c..4677c70 100644 --- a/s2/loop_test.go +++ b/s2/loop_test.go @@ -1584,28 +1584,59 @@ func TestLoopTurningAngle(t *testing.T) { } func TestLoopAreaAndCentroid(t *testing.T) { - var p Point - - if got, want := EmptyLoop().Area(), 0.0; got != want { - t.Errorf("EmptyLoop.Area() = %v, want %v", got, want) - } - if got, want := FullLoop().Area(), 4*math.Pi; got != want { - t.Errorf("FullLoop.Area() = %v, want %v", got, want) - } - if got := EmptyLoop().Centroid(); !p.ApproxEqual(got) { - t.Errorf("EmptyLoop.Centroid() = %v, want %v", got, p) - } - if got := FullLoop().Centroid(); !p.ApproxEqual(got) { - t.Errorf("FullLoop.Centroid() = %v, want %v", got, p) - } - - if got, want := northHemi.Area(), 2*math.Pi; !float64Eq(got, want) { - t.Errorf("northHemi.Area() = %v, want %v", got, want) + tests := []struct { + name string + loop *Loop + wantArea float64 + wantCentroid Point + }{ + { + name: "EmptyLoop", + loop: EmptyLoop(), + wantArea: 0.0, + wantCentroid: Point{}, + }, + { + name: "FullLoop", + loop: FullLoop(), + wantArea: 4 * math.Pi, + wantCentroid: Point{}, + }, + { + name: "northHemi", + loop: northHemi, + wantArea: 2 * math.Pi, + wantCentroid: Point{}, // Centroid of a hemisphere is (0,0,0) + }, + { + name: "eastHemi", + loop: eastHemi, + wantArea: 2 * math.Pi, + wantCentroid: Point{}, // Centroid of a hemisphere is (0,0,0) + }, + { + name: "lineTriangle", + loop: lineTriangle, + wantArea: 0, + wantCentroid: Point{}, + }, + { + name: "twoPoints", + loop: LoopFromPoints([]Point{PointFromLatLng(LatLngFromDegrees(0, 0)), PointFromLatLng(LatLngFromDegrees(0, 1))}), + wantArea: 0, + wantCentroid: Point{}, + }, } - eastHemiArea := eastHemi.Area() - if eastHemiArea < 2*math.Pi-1e-12 || eastHemiArea > 2*math.Pi+1e-12 { - t.Errorf("eastHemi.Area() = %v, want between [%v, %v]", eastHemiArea, 2*math.Pi-1e-12, 2*math.Pi+1e-12) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := test.loop.Area(); !float64Near(got, test.wantArea, epsilon) { + t.Errorf("Area() = %v, want %v", got, test.wantArea) + } + if got := test.loop.Centroid(); !got.ApproxEqual(test.wantCentroid) { + t.Errorf("Centroid() = %v, want %v", got, test.wantCentroid) + } + }) } // Construct spherical caps of random height, and approximate their boundary