Ejemplo n.º 1
0
    public void Test_S2LatLngRect_GetVertex()
    {
        S2LatLngRect r1 = new(new R1Interval(0, S2.M_PI_2), new S1Interval(-Math.PI, 0));

        Assert.Equal(r1.Vertex(0), S2LatLng.FromRadians(0, Math.PI));
        Assert.Equal(r1.Vertex(1), S2LatLng.FromRadians(0, 0));
        Assert.Equal(r1.Vertex(2), S2LatLng.FromRadians(S2.M_PI_2, 0));
        Assert.Equal(r1.Vertex(3), S2LatLng.FromRadians(S2.M_PI_2, Math.PI));

        // Make sure that GetVertex() returns vertices in CCW order.
        for (int i = 0; i < 4; ++i)
        {
            double       lat = S2.M_PI_4 * (i - 2);
            double       lng = S2.M_PI_2 * (i - 2) + 0.2;
            S2LatLngRect r   = new(
                new R1Interval(lat, lat + S2.M_PI_4),
                new S1Interval(
                    Math.IEEERemainder(lng, S2.M_2_PI),
                    Math.IEEERemainder(lng + S2.M_PI_2, S2.M_2_PI)));
            for (int k = 0; k < 4; ++k)
            {
                Assert.True(S2Pred.Sign(
                                r.Vertex(k - 1).ToPoint(),
                                r.Vertex(k).ToPoint(),
                                r.Vertex(k + 1).ToPoint()) > 0);
            }
        }
    }
Ejemplo n.º 2
0
 public void Test_S1ChordAngle_GetS2PointConstructorMaxError()
 {
     // Check that the error bound returned by GetS2PointConstructorMaxError() is
     // large enough.
     for (var iter = 0; iter < 100000; ++iter)
     {
         S2Testing.Random.Reset(iter);  // Easier to reproduce a specific case.
         var x = S2Testing.RandomPoint();
         var y = S2Testing.RandomPoint();
         if (S2Testing.Random.OneIn(10))
         {
             // Occasionally test a point pair that is nearly identical or antipodal.
             var r = S1Angle.FromRadians(S2.DoubleError * S2Testing.Random.RandDouble());
             y = S2.GetPointOnLine(x, y, r);
             if (S2Testing.Random.OneIn(2))
             {
                 y = -y;
             }
         }
         S1ChordAngle dist  = new(x, y);
         var          error = dist.GetS2PointConstructorMaxError();
         var          er1   = S2Pred.CompareDistance(x, y, dist.PlusError(error));
         if (er1 > 0)
         {
         }
         Assert.True(er1 <= 0);
         var er2 = S2Pred.CompareDistance(x, y, dist.PlusError(-error));
         if (er2 < 0)
         {
         }
         Assert.True(er2 >= 0);
     }
 }
Ejemplo n.º 3
0
    // Given a point X and an edge AB, check that the distance from X to AB is
    // "distance_radians" and the closest point on AB is "expected_closest".
    private static void CheckDistance(S2Point x, S2Point a, S2Point b, double distance_radians, S2Point expected_closest)
    {
        x = x.Normalize();
        a = a.Normalize();
        b = b.Normalize();
        expected_closest = expected_closest.Normalize();
        Assert2.Near(distance_radians, S2.GetDistance(x, a, b).Radians, S2.DoubleError);
        S2Point closest = S2.Project(x, a, b);

        Assert.True(S2Pred.CompareEdgeDistance(
                        closest, a, b, new S1ChordAngle(S2.kProjectPerpendicularErrorS1Angle)) < 0);

        // If X is perpendicular to AB then there is nothing further we can expect.
        if (distance_radians != S2.M_PI_2)
        {
            if (expected_closest == new S2Point())
            {
                // This special value says that the result should be A or B.
                Assert.True(closest == a || closest == b);
            }
            else
            {
                Assert.True(S2.ApproxEquals(closest, expected_closest));
            }
        }
        S1ChordAngle min_distance = S1ChordAngle.Zero;

        Assert.False(S2.UpdateMinDistance(x, a, b, ref min_distance));
        min_distance = S1ChordAngle.Infinity;
        Assert.True(S2.UpdateMinDistance(x, a, b, ref min_distance));
        Assert2.Near(distance_radians, min_distance.ToAngle().Radians, S2.DoubleError);
    }
Ejemplo n.º 4
0
        public void Test_S2ClosestEdgeQuery_TrueDistanceLessThanS1ChordAngleDistance()
        {
            // Tests that IsConservativeDistanceLessOrEqual returns points where the
            // true distance is slightly less than the one computed by S1ChordAngle.
            //
            // The points below had the worst error from among 100,000 random pairs.
            S2Point p0 = new(0.78516762584829192, -0.50200400690845970, -0.36263449417782678);
            S2Point p1 = new(0.78563011732429433, -0.50187655940493503, -0.36180828883938054);

            // The S1ChordAngle distance is ~4 ulps greater than the true distance.
            Distance dist1 = new(p0, p1);
            var      limit = dist1.Predecessor().Predecessor().Predecessor().Predecessor();

            Assert.True(S2Pred.CompareDistance(p0, p1, limit) < 0);

            // Verify that IsConservativeDistanceLessOrEqual() still returns "p1".
            S2Point[]           index_points = new[] { p0 };
            MutableS2ShapeIndex index        = new();

            index.Add(new S2PointVectorShape(index_points));
            S2ClosestEdgeQuery query = new(index);

            S2ClosestEdgeQuery.PointTarget target1 = new(p1);
            Assert.False(query.IsDistanceLess(target1, limit));
            Assert.False(query.IsDistanceLessOrEqual(target1, limit));
            Assert.True(query.IsConservativeDistanceLessOrEqual(target1, limit));
        }
Ejemplo n.º 5
0
        public void Test_S2ClosestEdgeQuery_IsConservativeDistanceLessOrEqual()
        {
            // Test
            int num_tested = 0;
            int num_conservative_needed = 0;

            for (int iter = 0; iter < 1000; ++iter)
            {
                S2Testing.Random.Reset(iter + 1);  // Easier to reproduce a specific case.
                S2Point  x     = S2Testing.RandomPoint();
                S2Point  dir   = S2Testing.RandomPoint();
                S1Angle  r     = S1Angle.FromRadians(Math.PI * Math.Pow(1e-30, S2Testing.Random.RandDouble()));
                S2Point  y     = S2.InterpolateAtDistance(r, x, dir);
                Distance limit = new(r);
                if (S2Pred.CompareDistance(x, y, limit) <= 0)
                {
                    MutableS2ShapeIndex index = new();
                    index.Add(new S2PointVectorShape(new S2Point[] { x }));
                    S2ClosestEdgeQuery             query  = new(index);
                    S2ClosestEdgeQuery.PointTarget target = new(y);
                    Assert.True(query.IsConservativeDistanceLessOrEqual(target, limit));
                    ++num_tested;
                    if (!query.IsDistanceLess(target, limit))
                    {
                        ++num_conservative_needed;
                    }
                }
            }
            // Verify that in most test cases, the distance between the target points
            // was close to the desired value.  Also verify that at least in some test
            // cases, the conservative distance test was actually necessary.
            Assert.True(num_tested >= 300);
            Assert.True(num_tested <= 700);
            Assert.True(num_conservative_needed >= 25);
        }
Ejemplo n.º 6
0
 /// <summary>
 /// Returns true if wedge A contains wedge B.  Equivalent to but faster than
 /// GetWedgeRelation() == WEDGE_PROPERLY_CONTAINS || WEDGE_EQUALS.
 /// REQUIRES: A and B are non-empty.
 /// </summary>
 public static bool WedgeContains(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2)
 {
     // For A to contain B (where each loop interior is defined to be its left
     // side), the CCW edge order around ab1 must be a2 b2 b0 a0.  We split
     // this test into two parts that test three vertices each.
     return(
         S2Pred.OrderedCCW(a2, b2, b0, ab1) &&
         S2Pred.OrderedCCW(b0, a0, a2, ab1));
 }
Ejemplo n.º 7
0
 /// <summary>
 /// Returns true if wedge A intersects wedge B.  Equivalent to but faster
 /// than GetWedgeRelation() != WEDGE_IS_DISJOINT.
 /// REQUIRES: A and B are non-empty.
 /// </summary>
 /// <returns></returns>
 public static bool WedgeIntersects(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2)
 {
     // For A not to intersect B (where each loop interior is defined to be
     // its left side), the CCW edge order around ab1 must be a0 b2 b0 a2.
     // Note that it's important to write these conditions as negatives
     // (!OrderedCCW(a,b,c,o) rather than Ordered(c,b,a,o)) to get correct
     // results when two vertices are the same.
     return(!(
                S2Pred.OrderedCCW(a0, b2, b0, ab1) &&
                S2Pred.OrderedCCW(b0, a2, a0, ab1)));
 }
Ejemplo n.º 8
0
    // Return the exterior angle at vertex B in the triangle ABC.  The return
    // value is positive if ABC is counterclockwise and negative otherwise.  If
    // you imagine a constant walking from A to B to C, this is the angle that the
    // constant turns at vertex B (positive = left = CCW, negative = right = CW).
    // This quantity is also known as the "geodesic curvature" at B.
    //
    // Ensures that TurnAngle(a,b,c) == -TurnAngle(c,b,a) for all distinct
    // a,b,c. The result is undefined if (a == b || b == c), but is either
    // -Pi or Pi if (a == c).  All points should be normalized.
    public static double TurnAngle(S2Point a, S2Point b, S2Point c)
    {
        // We use RobustCrossProd() to get good accuracy when two points are very
        // close together, and Sign() to ensure that the sign is correct for
        // turns that are close to 180 degrees.
        //
        // Unfortunately we can't save RobustCrossProd(a, b) and pass it as the
        // optional 4th argument to Sign(), because Sign() requires a.CrossProd(b)
        // exactly (the robust version differs in magnitude).
        double angle = S2.RobustCrossProd(a, b).Angle(S2.RobustCrossProd(b, c));

        // Don't return Sign() * angle because it is legal to have (a == c).
        return((S2Pred.Sign(a, b, c) > 0) ? angle : -angle);
    }
Ejemplo n.º 9
0
 public void Test_S2_ProjectError()
 {
     for (int iter = 0; iter < 1000; ++iter)
     {
         S2Testing.Random.Reset(iter + 1);  // Easier to reproduce a specific case.
         S2Point a = ChoosePoint();
         S2Point b = ChoosePoint();
         S2Point n = S2.RobustCrossProd(a, b).Normalize();
         S2Point x = S2Testing.SamplePoint(new S2Cap(n, S1Angle.FromRadians(1e-15)));
         S2Point p = S2.Project(x, a, b);
         Assert.True(S2Pred.CompareEdgeDistance(
                         p, a, b, new S1ChordAngle(S2.kProjectPerpendicularErrorS1Angle)) < 0);
     }
 }
Ejemplo n.º 10
0
    /// <summary>
    /// Returns the relation from wedge A to B.
    /// REQUIRES: A and B are non-empty.
    /// </summary>
    public static WedgeRelation GetWedgeRelation(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2)
    {
        // There are 6 possible edge orderings at a shared vertex (all
        // of these orderings are circular, i.e. abcd == bcda):
        //
        //  (1) a2 b2 b0 a0: A contains B
        //  (2) a2 a0 b0 b2: B contains A
        //  (3) a2 a0 b2 b0: A and B are disjoint
        //  (4) a2 b0 a0 b2: A and B intersect in one wedge
        //  (5) a2 b2 a0 b0: A and B intersect in one wedge
        //  (6) a2 b0 b2 a0: A and B intersect in two wedges
        //
        // We do not distinguish between 4, 5, and 6.
        // We pay extra attention when some of the edges overlap.  When edges
        // overlap, several of these orderings can be satisfied, and we take
        // the most specific.
        if (a0 == b0 && a2 == b2)
        {
            return(WedgeRelation.WEDGE_EQUALS);
        }

        if (S2Pred.OrderedCCW(a0, a2, b2, ab1))
        {
            // The cases with this vertex ordering are 1, 5, and 6,
            // although case 2 is also possible if a2 == b2.
            if (S2Pred.OrderedCCW(b2, b0, a0, ab1))
            {
                return(WedgeRelation.WEDGE_PROPERLY_CONTAINS);
            }

            // We are in case 5 or 6, or case 2 if a2 == b2.
            return((a2 == b2)
                ? WedgeRelation.WEDGE_IS_PROPERLY_CONTAINED
                : WedgeRelation.WEDGE_PROPERLY_OVERLAPS);
        }

        // We are in case 2, 3, or 4.
        if (S2Pred.OrderedCCW(a0, b0, b2, ab1))
        {
            return(WedgeRelation.WEDGE_IS_PROPERLY_CONTAINED);
        }

        return(S2Pred.OrderedCCW(a0, b0, a2, ab1)
            ? WedgeRelation.WEDGE_IS_DISJOINT : WedgeRelation.WEDGE_PROPERLY_OVERLAPS);
    }
Ejemplo n.º 11
0
    // Given a point P, return the minimum level at which an edge of some S2Cell
    // parent of P is nearly collinear with S2.Origin.  This is the minimum
    // level for which Sign() may need to resort to expensive calculations in
    // order to determine which side of an edge the origin lies on.
    private static int GetMinExpensiveLevel(S2Point p)
    {
        S2CellId id = new(p);

        for (int level = 0; level <= S2.kMaxCellLevel; ++level)
        {
            S2Cell cell = new(id.Parent(level));
            for (int k = 0; k < 4; ++k)
            {
                S2Point a = cell.Vertex(k);
                S2Point b = cell.Vertex(k + 1);
                if (S2Pred.TriageSign(a, b, S2.Origin, a.CrossProd(b)) == 0)
                {
                    return(level);
                }
            }
        }
        return(S2.kMaxCellLevel + 1);
    }
Ejemplo n.º 12
0
    // A slightly more efficient version of Project() where the cross product of
    // the two endpoints has been precomputed.  The cross product does not need to
    // be normalized, but should be computed using S2.RobustCrossProd() for the
    // most accurate results.  Requires that x, a, and b have unit length.
    public static S2Point Project(S2Point x, S2Point a, S2Point b, S2Point a_cross_b)
    {
        System.Diagnostics.Debug.Assert(a.IsUnitLength());
        System.Diagnostics.Debug.Assert(b.IsUnitLength());
        System.Diagnostics.Debug.Assert(x.IsUnitLength());

        // TODO(ericv): When X is nearly perpendicular to the plane containing AB,
        // the result is guaranteed to be close to the edge AB but may be far from
        // the true projected result.  This could be fixed by computing the product
        // (A x B) x X x (A x B) using methods similar to S2::RobustCrossProd() and
        // S2::GetIntersection().  However note that the error tolerance would need
        // to be significantly larger in order for this calculation to succeed in
        // double precision most of the time.  For example to avoid higher precision
        // when X is within 60 degrees of AB the minimum error would be 18 * DBL_ERR,
        // and to avoid higher precision when X is within 87 degrees of AB the
        // minimum error would be 120 * DBL_ERR.

        // The following is not necessary to meet accuracy guarantees but helps
        // to avoid unexpected results in unit tests.
        if (x == a || x == b)
        {
            return(x);
        }

        // Find the closest point to X along the great circle through AB.  Note that
        // we use "n" rather than a_cross_b in the final cross product in order to
        // avoid the possibility of underflow.
        S2Point n = a_cross_b.Normalize();
        S2Point p = S2.RobustCrossProd(n, x).CrossProd(n).Normalize();

        // If this point is on the edge AB, then it's the closest point.
        S2Point pn = p.CrossProd(n);

        if (S2Pred.Sign(p, n, a, pn) > 0 && S2Pred.Sign(p, n, b, pn) < 0)
        {
            return(p);
        }

        // Otherwise, the closest point is either A or B.
        return(((x - a).Norm2() <= (x - b).Norm2()) ? a : b);
    }
Ejemplo n.º 13
0
    public void Test_S2_GetUpdateMinInteriorDistanceMaxError()
    {
        // Check that the error bound returned by
        // GetUpdateMinInteriorDistanceMaxError() is large enough.
        for (int iter = 0; iter < 10000; ++iter)
        {
            S2Point a0         = S2Testing.RandomPoint();
            var     lenRadians = Math.PI * Math.Pow(1e-20, S2Testing.Random.RandDouble());
            S1Angle len        = S1Angle.FromRadians(lenRadians);
            if (S2Testing.Random.OneIn(4))
            {
                len = S1Angle.FromRadians(S2.M_PI) - len;
            }
            S2Point a1 = S2.GetPointOnLine(a0, S2Testing.RandomPoint(), len);

            // TODO(ericv): The error bound holds for antipodal points, but the S2
            // predicates used to test the error do not support antipodal points yet.
            if (a1 == -a0)
            {
                continue;
            }
            S2Point n        = S2.RobustCrossProd(a0, a1).Normalize();
            double  f        = Math.Pow(1e-20, S2Testing.Random.RandDouble());
            S2Point a        = ((1 - f) * a0 + f * a1).Normalize();
            var     rRadians = S2.M_PI_2 * Math.Pow(1e-20, S2Testing.Random.RandDouble());
            S1Angle r        = S1Angle.FromRadians(rRadians);
            if (S2Testing.Random.OneIn(2))
            {
                r = S1Angle.FromRadians(S2.M_PI_2) - r;
            }
            S2Point      x        = S2.GetPointOnLine(a, n, r);
            S1ChordAngle min_dist = S1ChordAngle.Infinity;
            if (!S2.UpdateMinInteriorDistance(x, a0, a1, ref min_dist))
            {
                --iter; continue;
            }
            double error = S2.GetUpdateMinDistanceMaxError(min_dist);
            Assert.True(S2Pred.CompareEdgeDistance(x, a0, a1, min_dist.PlusError(error)) <= 0);
            Assert.True(S2Pred.CompareEdgeDistance(x, a0, a1, min_dist.PlusError(-error)) >= 0);
        }
    }
Ejemplo n.º 14
0
 // Like Area(), but returns a positive value for counterclockwise triangles
 // and a negative value otherwise.
 public static double SignedArea(S2Point a, S2Point b, S2Point c)
 {
     return(S2Pred.Sign(a, b, c) * Area(a, b, c));
 }
Ejemplo n.º 15
0
    // Compute the convex hull of the input geometry provided.
    //
    // If there is no geometry, this method returns an empty loop containing no
    // points (see S2Loop.IsEmpty).
    //
    // If the geometry spans more than half of the sphere, this method returns a
    // full loop containing the entire sphere (see S2Loop.IsFull).
    //
    // If the geometry contains 1 or 2 points, or a single edge, this method
    // returns a very small loop consisting of three vertices (which are a
    // superset of the input vertices).
    //
    // Note that this method does not clear the geometry; you can continue
    // adding to it and call this method again if desired.
    public S2Loop GetConvexHull()
    {
        // Test whether the bounding cap is convex.  We need this to proceed with
        // the algorithm below in order to construct a point "origin" that is
        // definitely outside the convex hull.
        S2Cap cap = GetCapBound();

        if (cap.Height() >= 1 - 10 * S2Pred.DBL_ERR)
        {
            return(S2Loop.kFull);
        }
        // This code implements Andrew's monotone chain algorithm, which is a simple
        // variant of the Graham scan.  Rather than sorting by x-coordinate, instead
        // we sort the points in CCW order around an origin O such that all points
        // are guaranteed to be on one side of some geodesic through O.  This
        // ensures that as we scan through the points, each new point can only
        // belong at the end of the chain (i.e., the chain is monotone in terms of
        // the angle around O from the starting point).
        S2Point origin = cap.Center.Ortho();

        points_.Sort(new OrderedCcwAround(origin));

        // Remove duplicates.  We need to do this before checking whether there are
        // fewer than 3 points.
        var tmp = points_.Distinct().ToList();

        points_.Clear();
        points_.AddRange(tmp);

        // Special cases for fewer than 3 points.
        if (points_.Count < 3)
        {
            if (!points_.Any())
            {
                return(S2Loop.kEmpty);
            }
            else if (points_.Count == 1)
            {
                return(GetSinglePointLoop(points_[0]));
            }
            else
            {
                return(GetSingleEdgeLoop(points_[0], points_[1]));
            }
        }

        // Verify that all points lie within a 180 degree span around the origin.
        System.Diagnostics.Debug.Assert(S2Pred.Sign(origin, points_.First(), points_.Last()) >= 0);

        // Generate the lower and upper halves of the convex hull.  Each half
        // consists of the maximal subset of vertices such that the edge chain makes
        // only left (CCW) turns.
        var lower = new List <S2Point>();
        var upper = new List <S2Point>();

        GetMonotoneChain(lower);
        points_.Reverse();
        GetMonotoneChain(upper);

        // Remove the duplicate vertices and combine the chains.
        System.Diagnostics.Debug.Assert(lower.First() == upper.Last());
        System.Diagnostics.Debug.Assert(lower.Last() == upper.First());
        lower.RemoveAt(lower.Count - 1);
        upper.RemoveAt(lower.Count - 1);
        lower.AddRange(upper);
        return(new S2Loop(lower));
    }
Ejemplo n.º 16
0
    private static void TestCrossing(S2Point a, S2Point b, S2Point c, S2Point d,
                                     int crossing_sign, int signed_crossing_sign)
    {
        // For degenerate edges, CrossingSign() is documented to return 0 if two
        // vertices from different edges are the same and -1 otherwise.  The
        // TestCrossings() function below uses various argument permutations that
        // can sometimes create this case, so we fix it now if necessary.
        if (a == c || a == d || b == c || b == d)
        {
            crossing_sign = 0;
        }

        // As a sanity check, make sure that the expected value of
        // "signed_crossing_sign" is consistent with its documented properties.
        if (crossing_sign == 1)
        {
            Assert.Equal(signed_crossing_sign, S2Pred.Sign(a, b, c));
        }
        else if (crossing_sign == 0 && signed_crossing_sign != 0)
        {
            Assert.Equal(signed_crossing_sign, (a == c || b == d) ? 1 : -1);
        }

        Assert.Equal(crossing_sign, S2.CrossingSign(a, b, c, d));

        S2EdgeCrosser crosser = new(a, b, c);

        Assert.Equal(crossing_sign, crosser.CrossingSign(d));
        Assert.Equal(crossing_sign, crosser.CrossingSign(c));
        Assert.Equal(crossing_sign, crosser.CrossingSign(d, c));
        Assert.Equal(crossing_sign, crosser.CrossingSign(c, d));

        Assert.Equal(signed_crossing_sign != 0, S2.EdgeOrVertexCrossing(a, b, c, d));
        crosser.RestartAt(c);
        Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(d));
        Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(c));
        Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(d, c));
        Assert.Equal(signed_crossing_sign != 0, crosser.EdgeOrVertexCrossing(c, d));

        crosser.RestartAt(c);
        Assert.Equal(signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(d));
        Assert.Equal(-signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(c));
        Assert.Equal(-signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(d, c));
        Assert.Equal(signed_crossing_sign, crosser.SignedEdgeOrVertexCrossing(c, d));

        // Check that the crosser can be re-used.
        crosser.Init(c, d);
        crosser.RestartAt(a);
        Assert.Equal(crossing_sign, crosser.CrossingSign(b));
        Assert.Equal(crossing_sign, crosser.CrossingSign(a));

        // Now try all the same tests with CopyingEdgeCrosser.
        S2CopyingEdgeCrosser crosser2 = new(a, b, c);

        Assert.Equal(crossing_sign, crosser2.CrossingSign(d));
        Assert.Equal(crossing_sign, crosser2.CrossingSign(c));
        Assert.Equal(crossing_sign, crosser2.CrossingSign(d, c));
        Assert.Equal(crossing_sign, crosser2.CrossingSign(c, d));

        crosser2.RestartAt(c);
        Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(d));
        Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(c));
        Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(d, c));
        Assert.Equal(signed_crossing_sign != 0, crosser2.EdgeOrVertexCrossing(c, d));

        crosser2.RestartAt(c);
        Assert.Equal(signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(d));
        Assert.Equal(-signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(c));
        Assert.Equal(-signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(d, c));
        Assert.Equal(signed_crossing_sign, crosser2.SignedEdgeOrVertexCrossing(c, d));

        // Check that the crosser can be re-used.
        crosser2.Init(c, d);
        crosser2.RestartAt(a);
        Assert.Equal(crossing_sign, crosser2.CrossingSign(b));
        Assert.Equal(crossing_sign, crosser2.CrossingSign(a));
    }