From 9226618bee50c1492a27aac4c00711985de853ea Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 21:28:02 +0200 Subject: [PATCH 01/52] fix(Intersection): handle degenerated segments as points segment_triangle_distance and the segment_{plane,sphere,cylinder,ellipse} intersections, as well as 2D segment_segment and segment_line intersections, built an InfiniteLine from the segment, which throws from Segment::direction() when the segment length is below GLOBAL_EPSILON. Such segments are now treated as their barycenter: the result is the point-to-object distance, or a single intersection point when the point lies on the object. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 7 ++ src/geode/geometry/intersection.cpp | 105 +++++++++++++++++++++++++++ tests/geometry/test-distance.cpp | 23 ++++++ tests/geometry/test-intersection.cpp | 105 +++++++++++++++++++++++++++ 4 files changed, 240 insertions(+) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index 2258a180a..3688ed9f2 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -1139,6 +1139,13 @@ namespace geode std::tuple< double, Point3D, Point3D > segment_triangle_distance( const Segment3D& segment, const Triangle3D& triangle ) { + if( segment.is_degenerated() ) + { + const auto point = segment.barycenter(); + const auto [distance, closest_on_triangle] = + point_triangle_distance( point, triangle ); + return { distance, point, closest_on_triangle }; + } const InfiniteLine3D line{ segment }; const auto [_, closest_on_line, closest_on_triangle] = line_triangle_distance( line, triangle ); diff --git a/src/geode/geometry/intersection.cpp b/src/geode/geometry/intersection.cpp index 5b614c305..2c163f90a 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -95,6 +95,57 @@ namespace size_t numIntersections; std::array< double, 2 > parameter; }; + + /* + * Intersection result of a degenerated segment (considered as a point) + * with an object, given the distance from the point to the object and + * the projection of the point on the object. + */ + template < typename Intersection, typename PointType > + geode::IntersectionResult< Intersection > degenerated_segment_intersection( + const PointType& point, + double distance_to_object, + const PointType& projection_on_object ) + { + if( distance_to_object > geode::GLOBAL_EPSILON ) + { + return { geode::INTERSECTION_TYPE::none }; + } + typename geode::CorrectnessInfo< Intersection >::Correctness + first_correctness{ true, Intersection{ point } }; + typename geode::CorrectnessInfo< Intersection >::Correctness + second_correctness{ true, Intersection{ projection_on_object } }; + return { Intersection{ point }, + { first_correctness, second_correctness } }; + } + + double point_cylinder_distance( + const geode::Point3D& point, const geode::Cylinder& cylinder ) + { + const auto half_height = 0.5 * cylinder.axis().length(); + const geode::Vector3D center_to_point{ cylinder.axis().barycenter(), + point }; + const auto axial = + std::fabs( center_to_point.dot( cylinder.axis().direction() ) ); + const auto radial = std::sqrt( + std::max( center_to_point.length2() - axial * axial, 0. ) ); + const auto radial_distance = radial - cylinder.radius(); + const auto axial_distance = axial - half_height; + if( axial_distance <= 0 ) + { + if( radial_distance >= 0 ) + { + return radial_distance; + } + return std::min( -radial_distance, -axial_distance ); + } + if( radial_distance <= 0 ) + { + return axial_distance; + } + return std::sqrt( axial_distance * axial_distance + + radial_distance * radial_distance ); + } } // namespace namespace geode @@ -209,6 +260,15 @@ namespace geode segment_sphere_intersection( const Segment< dimension >& segment, const Sphere< dimension >& sphere ) { + if( segment.is_degenerated() ) + { + const auto point = segment.barycenter(); + const auto [distance, projection] = + point_sphere_distance( point, sphere ); + return degenerated_segment_intersection< + absl::InlinedVector< Point< dimension >, 2 > >( + point, distance, projection ); + } auto line_intersections = line_sphere_intersection( InfiniteLine< dimension >{ segment }, sphere ); if( line_intersections ) @@ -236,6 +296,14 @@ namespace geode IntersectionResult< Point3D > segment_plane_intersection( const Segment3D& segment, const Plane& plane ) { + if( segment.is_degenerated() ) + { + const auto point = segment.barycenter(); + const auto [distance, projection] = + point_plane_distance( point, plane ); + return degenerated_segment_intersection< Point3D >( + point, distance, projection ); + } auto line_plane_result = line_plane_intersection( InfiniteLine3D{ segment }, plane ); if( line_plane_result ) @@ -453,6 +521,20 @@ namespace geode IntersectionResult< Point2D > segment_segment_intersection( const Segment2D& segment0, const Segment2D& segment1 ) { + if( segment0.is_degenerated() ) + { + const auto point = segment0.barycenter(); + return degenerated_segment_intersection< Point2D >( point, + point_segment_distance( point, segment1 ), + point_segment_projection( point, segment1 ) ); + } + if( segment1.is_degenerated() ) + { + const auto point = segment1.barycenter(); + return degenerated_segment_intersection< Point2D >( + point_segment_projection( point, segment0 ), + point_segment_distance( point, segment0 ), point ); + } auto line_intersection_result = line_line_intersection( InfiniteLine2D{ segment0 }, InfiniteLine2D{ segment1 } ); if( line_intersection_result ) @@ -504,6 +586,13 @@ namespace geode IntersectionResult< Point2D > segment_line_intersection( const Segment2D& segment, const InfiniteLine2D& line ) { + if( segment.is_degenerated() ) + { + const auto point = segment.barycenter(); + return degenerated_segment_intersection< Point2D >( point, + point_line_distance( point, line ), + point_line_projection( point, line ) ); + } auto line_intersection_result = line_line_intersection( InfiniteLine2D{ segment }, line ); if( line_intersection_result ) @@ -824,6 +913,13 @@ namespace geode opengeode_geometry_api segment_cylinder_intersection( const Segment3D& segment, const Cylinder& cylinder ) { + if( segment.is_degenerated() ) + { + const auto point = segment.barycenter(); + return degenerated_segment_intersection< + absl::InlinedVector< Point3D, 2 > >( + point, point_cylinder_distance( point, cylinder ), point ); + } auto line_intersections = line_cylinder_intersection( InfiniteLine3D{ segment }, cylinder ); if( line_intersections ) @@ -1105,6 +1201,15 @@ namespace geode segment_ellipse_intersection( const Segment< dimension >& segment, const Ellipse< dimension >& ellipse ) { + if( segment.is_degenerated() ) + { + const auto point = segment.barycenter(); + const auto [distance, projection] = + point_ellipse_distance( point, ellipse ); + return degenerated_segment_intersection< + absl::InlinedVector< Point< dimension >, 2 > >( + point, distance, projection ); + } auto line_intersections = line_ellipse_intersection< dimension >( InfiniteLine< dimension >{ segment }, ellipse ); if( line_intersections ) diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index 72908d9a4..f763fd132 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1301,6 +1301,28 @@ void test_triangle_triangle_distance() "epsilon (non conformal parts only)" ); } +void test_degenerated_segment_triangle_distance() +{ + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; + const geode::Point3D c{ { 0.0, 1.0, 0.0 } }; + const geode::Triangle3D triangle{ a, b, c }; + for( const auto length : { 4e-7, 1e-7 } ) + { + const geode::Point3D p0{ { 0.2, 0.2, 1.0 } }; + const geode::Point3D p1{ { 0.2, 0.2 + length, 1.0 } }; + const auto [distance, closest_on_segment, closest_on_triangle] = + geode::segment_triangle_distance( + geode::Segment3D{ p0, p1 }, triangle ); + geode::OpenGeodeGeometryException::test( + std::fabs( distance - 1. ) <= geode::GLOBAL_EPSILON + && closest_on_triangle.inexact_equal( + geode::Point3D{ { 0.2, 0.2, 0.0 } } ), + "Wrong result for segment_triangle_distance with degenerated " + "segment" ); + } +} + void test() { test_point_segment_distance(); @@ -1316,6 +1338,7 @@ void test() test_segment_triangle_distance(); test_triangle_triangle_distance(); test_point_ellipse_distance(); + test_degenerated_segment_triangle_distance(); } OPENGEODE_TEST( "distance" ) diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index 2edb4aef7..c02e77067 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -995,6 +995,110 @@ void test_segment_ellipse_intersection() test_segment_ellipse_intersection_3D(); } +void test_degenerated_segment_intersections() +{ + for( const auto length : { 4e-7, 1e-7 } ) + { + const geode::Point3D origin{ { 0, 0, 0 } }; + const geode::Point3D x_axis{ { 1, 0, 0 } }; + const geode::Sphere3D sphere{ origin, 1 }; + const geode::Point3D on_sphere{ { 1, 0, 0 } }; + const geode::Point3D on_sphere2{ { 1 + length, 0, 0 } }; + const auto sphere_result = geode::segment_sphere_intersection( + geode::Segment3D{ on_sphere, on_sphere2 }, sphere ); + geode::OpenGeodeGeometryException::test( + sphere_result && sphere_result.result->size() == 1, + "Wrong degenerated segment_sphere_intersection" ); + const geode::Point3D far_sphere{ { 5, 0, 0 } }; + const geode::Point3D far_sphere2{ { 5 + length, 0, 0 } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_sphere_intersection( + geode::Segment3D{ far_sphere, far_sphere2 }, sphere ), + "Wrong degenerated segment_sphere_intersection (far)" ); + + const geode::Plane plane{ geode::Vector3D{ { 0, 0, 1 } }, origin }; + const geode::Point3D on_plane{ { 0.3, 0.2, 0 } }; + const geode::Point3D on_plane2{ { 0.3, 0.2, length } }; + const auto plane_result = geode::segment_plane_intersection( + geode::Segment3D{ on_plane, on_plane2 }, plane ); + geode::OpenGeodeGeometryException::test( + plane_result + && plane_result.result->inexact_equal( + geode::Point3D{ { 0.3, 0.2, 0 } } ), + "Wrong degenerated segment_plane_intersection" ); + const geode::Point3D off_plane{ { 0.3, 0.2, 1 } }; + const geode::Point3D off_plane2{ { 0.3, 0.2, 1 + length } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_plane_intersection( + geode::Segment3D{ off_plane, off_plane2 }, plane ), + "Wrong degenerated segment_plane_intersection (far)" ); + + const geode::Cylinder cylinder{ geode::Segment3D{ origin, x_axis }, + 0.2 }; + const geode::Point3D on_cylinder{ { 0.5, 0.2, 0 } }; + const geode::Point3D on_cylinder2{ { 0.5, 0.2 + length, 0 } }; + const auto cylinder_result = geode::segment_cylinder_intersection( + geode::Segment3D{ on_cylinder, on_cylinder2 }, cylinder ); + geode::OpenGeodeGeometryException::test( + cylinder_result && cylinder_result.result->size() == 1, + "Wrong degenerated segment_cylinder_intersection" ); + const geode::Point3D in_cylinder{ { 0.5, 0, 0 } }; + const geode::Point3D in_cylinder2{ { 0.5, length, 0 } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_cylinder_intersection( + geode::Segment3D{ in_cylinder, in_cylinder2 }, cylinder ), + "Wrong degenerated segment_cylinder_intersection (inside)" ); + + const geode::Frame2D frame{ { geode::Vector2D{ { 3.0, 0.0 } }, + geode::Vector2D{ { 0.0, 2.0 } } } }; + const geode::Point2D origin2d{ { 0, 0 } }; + const geode::Point2D x_axis2d{ { 1, 0 } }; + const geode::Ellipse2D ellipse{ origin2d, frame }; + const geode::Point2D on_ellipse{ { 3, 0 } }; + const geode::Point2D on_ellipse2{ { 3, length } }; + const auto ellipse_result = geode::segment_ellipse_intersection( + geode::Segment2D{ on_ellipse, on_ellipse2 }, ellipse ); + geode::OpenGeodeGeometryException::test( + ellipse_result && ellipse_result.result->size() == 1, + "Wrong degenerated segment_ellipse_intersection" ); + const geode::Point2D in_ellipse{ { 1, 0 } }; + const geode::Point2D in_ellipse2{ { 1, length } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_ellipse_intersection( + geode::Segment2D{ in_ellipse, in_ellipse2 }, ellipse ), + "Wrong degenerated segment_ellipse_intersection (inside)" ); + + const geode::Segment2D segment{ origin2d, x_axis2d }; + const geode::Point2D on_segment{ { 0.5, 0 } }; + const geode::Point2D on_segment2{ { 0.5, length } }; + const geode::Segment2D small_on{ on_segment, on_segment2 }; + const auto segment_result0 = + geode::segment_segment_intersection( small_on, segment ); + const auto segment_result1 = + geode::segment_segment_intersection( segment, small_on ); + geode::OpenGeodeGeometryException::test( + segment_result0 && segment_result1 + && segment_result0.result->inexact_equal( + segment_result1.result.value() ), + "Wrong degenerated segment_segment_intersection" ); + const geode::Point2D off_segment{ { 0.5, 1 } }; + const geode::Point2D off_segment2{ { 0.5, 1 + length } }; + const geode::Segment2D small_off{ off_segment, off_segment2 }; + geode::OpenGeodeGeometryException::test( + !geode::segment_segment_intersection( small_off, segment ) + && !geode::segment_segment_intersection( segment, small_off ), + "Wrong degenerated segment_segment_intersection (far)" ); + + const geode::InfiniteLine2D line{ segment }; + geode::OpenGeodeGeometryException::test( + geode::segment_line_intersection( small_on, line ), + "Wrong degenerated segment_line_intersection" ); + geode::OpenGeodeGeometryException::test( + !geode::segment_line_intersection( small_off, line ), + "Wrong degenerated segment_line_intersection (far)" ); + } +} + void test() { test_line_sphere_intersection(); @@ -1011,6 +1115,7 @@ void test() test_triangle_circle_intersection(); test_line_ellipse_intersection(); test_segment_ellipse_intersection(); + test_degenerated_segment_intersections(); } OPENGEODE_TEST( "intersection" ) From f0170a30789549926e47c3a49bdaea0adcdf135f Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 21:35:45 +0200 Subject: [PATCH 02/52] fix(Distance): skip degenerated edges in triangle_triangle_distance test_close_triangles printed leftover DEBUG/SDEBUG output when two vertices of a triangle were colocated, then still computed the distance from that zero-length edge (which used to throw from Segment::direction()). Degenerated edges are now skipped: their vertices are covered by the other edges of the triangle. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 11 +++-------- tests/geometry/test-distance.cpp | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index 3688ed9f2..b1672c342 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -451,14 +451,9 @@ namespace base_vertices[vertex1].get() ) <= geode::GLOBAL_EPSILON ) { - DEBUG( vertex0 ); - DEBUG( vertex1 ); - SDEBUG( base_vertices[0].get() ); - SDEBUG( base_vertices[1].get() ); - SDEBUG( base_vertices[2].get() ); - SDEBUG( other_triangle.vertices()[0].get() ); - SDEBUG( other_triangle.vertices()[1].get() ); - SDEBUG( other_triangle.vertices()[2].get() ); + // Colocated vertices: the edge is degenerated, its + // vertices are tested through the other edges + continue; } const geode::Segment3D edge{ base_vertices[vertex0], base_vertices[vertex1] }; diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index f763fd132..f4f3b6818 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1323,6 +1323,28 @@ void test_degenerated_segment_triangle_distance() } } +void test_degenerated_triangle_triangle_distance() +{ + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; + const geode::Point3D c{ { 0.0, 0.0, 1.0 } }; + const geode::Point3D d{ { 1.0, 0.0, 1.0 } }; + const geode::Point3D e{ { 0.0, 1.0, 1.0 } }; + const geode::Triangle3D degenerated{ a, b, b }; + const geode::Triangle3D triangle{ c, d, e }; + const auto result0 = + geode::triangle_triangle_distance( degenerated, triangle ); + geode::OpenGeodeGeometryException::test( + std::fabs( std::get< 0 >( result0 ) - 1. ) <= geode::GLOBAL_EPSILON, + "Wrong triangle_triangle_distance with degenerated triangle" ); + const auto result1 = + geode::triangle_triangle_distance( triangle, degenerated ); + geode::OpenGeodeGeometryException::test( + std::fabs( std::get< 0 >( result1 ) - 1. ) <= geode::GLOBAL_EPSILON, + "Wrong triangle_triangle_distance with degenerated triangle " + "(reversed)" ); +} + void test() { test_point_segment_distance(); @@ -1339,6 +1361,7 @@ void test() test_triangle_triangle_distance(); test_point_ellipse_distance(); test_degenerated_segment_triangle_distance(); + test_degenerated_triangle_triangle_distance(); } OPENGEODE_TEST( "distance" ) From 0cb2680749866dc8f8fcc1b6b54a09284c69f276 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 21:41:59 +0200 Subject: [PATCH 03/52] perf(Distance): test each triangle edge once in triangle_triangle_distance test_close_triangles iterated over all ordered vertex pairs, computing segment_triangle_distance twice per edge. Only unordered pairs are now tested, halving the work. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 10 +++--- tests/geometry/test-distance.cpp | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index b1672c342..d8f86ee68 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -439,14 +439,12 @@ namespace double min_distance{ std::numeric_limits< double >::max() }; geode::Point3D point0; geode::Point3D point1; - for( const auto vertex0 : non_colocated_points ) + for( const auto i : geode::Indices{ non_colocated_points } ) { - for( const auto vertex1 : non_colocated_points ) + const auto vertex0 = non_colocated_points[i]; + for( auto j = i + 1; j < non_colocated_points.size(); j++ ) { - if( vertex0 == vertex1 ) - { - continue; - } + const auto vertex1 = non_colocated_points[j]; if( geode::point_point_distance( base_vertices[vertex0].get(), base_vertices[vertex1].get() ) <= geode::GLOBAL_EPSILON ) diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index f4f3b6818..3b7ff2f1f 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -21,6 +21,8 @@ * */ +#include + #include #include @@ -1345,6 +1347,60 @@ void test_degenerated_triangle_triangle_distance() "(reversed)" ); } +void test_random_triangle_triangle_distance() +{ + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -1., 1. }; + const auto random_point = [&generator, &distribution] { + return geode::Point3D{ { distribution( generator ), + distribution( generator ), distribution( generator ) } }; + }; + for( const auto trial : geode::Range{ 500 } ) + { + geode_unused( trial ); + const auto a = random_point(); + const auto b = random_point(); + const auto c = random_point(); + const geode::Vector3D shift{ { 0, 0, 1. } }; + const auto d = random_point() + shift; + const auto e = random_point() + shift; + const auto f = random_point() + shift; + const geode::Triangle3D triangle0{ a, b, c }; + const geode::Triangle3D triangle1{ d, e, f }; + double reference{ std::numeric_limits< double >::max() }; + for( const auto& [base, other] : + { std::make_pair( &triangle0, &triangle1 ), + std::make_pair( &triangle1, &triangle0 ) } ) + { + const auto& vertices = base->vertices(); + for( const auto v0 : geode::LRange{ 3 } ) + { + for( const auto v1 : geode::LRange{ 3 } ) + { + if( v0 == v1 ) + { + continue; + } + reference = std::min( reference, + std::get< 0 >( geode::segment_triangle_distance( + { vertices[v0], vertices[v1] }, *other ) ) ); + } + } + } + const auto [distance, point0, point1] = + geode::triangle_triangle_distance( triangle0, triangle1 ); + geode::OpenGeodeGeometryException::test( + std::fabs( distance - reference ) <= 1e-12, + "Wrong triangle_triangle_distance on random triangles: ", distance, + " vs ", reference ); + geode::OpenGeodeGeometryException::test( + std::fabs( + geode::point_point_distance( point0, point1 ) - distance ) + <= 1e-12, + "Wrong triangle_triangle_distance closest points" ); + } +} + void test() { test_point_segment_distance(); @@ -1362,6 +1418,7 @@ void test() test_point_ellipse_distance(); test_degenerated_segment_triangle_distance(); test_degenerated_triangle_triangle_distance(); + test_random_triangle_triangle_distance(); } OPENGEODE_TEST( "distance" ) From 7520d0f1e0d720400e863c6e6f643cd6ea510835 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 21:46:34 +0200 Subject: [PATCH 04/52] fix(Distance): avoid out-of-bounds access on NaN in point_triangle_distance For degenerated triangles, no_pivot_point_triangle_distance initialized the selected edge to NO_LID and only updated it on a strictly smaller distance. With NaN coordinates no comparison succeeds and the edge array was read out of bounds. The selected edge now defaults to the first one. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 2 +- tests/geometry/test-distance.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index d8f86ee68..b9f5cda06 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -176,7 +176,7 @@ namespace { std::array< std::tuple< double, geode::Point3D >, 3 > edge_distances; auto min_distance = std::numeric_limits< double >::max(); - geode::local_index_t selected_edge{ geode::NO_LID }; + geode::local_index_t selected_edge{ 0 }; const auto& vertices = triangle.vertices(); for( const auto e : geode::LRange{ 3 } ) { diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index 3b7ff2f1f..daf07c1a5 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1401,6 +1401,23 @@ void test_random_triangle_triangle_distance() } } +void test_nan_point_triangle_distance() +{ + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; + const geode::Triangle3D degenerated{ a, b, b }; + const auto nan = std::numeric_limits< double >::quiet_NaN(); + const geode::Point3D point{ { nan, nan, nan } }; + const auto [distance, closest] = + geode::point_triangle_distance( point, degenerated ); + geode::OpenGeodeGeometryException::test( std::isnan( distance ), + "Wrong point_triangle_distance with NaN point" ); + const auto [signed_distance, signed_closest] = + geode::point_triangle_signed_distance( point, degenerated ); + geode::OpenGeodeGeometryException::test( std::isnan( signed_distance ), + "Wrong point_triangle_signed_distance with NaN point" ); +} + void test() { test_point_segment_distance(); @@ -1419,6 +1436,7 @@ void test() test_degenerated_segment_triangle_distance(); test_degenerated_triangle_triangle_distance(); test_random_triangle_triangle_distance(); + test_nan_point_triangle_distance(); } OPENGEODE_TEST( "distance" ) From 9954a5a7f340cf37e422eae1b2e6f161158723ca Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 21:52:30 +0200 Subject: [PATCH 05/52] fix(Distance): handle flat triangles in 2D point_triangle_distance The in-triangle test relies on the signs of three signed areas, which are all zero for any point on the supporting line of a collinear (or point) triangle, so such points were reported at distance 0. Triangles whose minimum height is below GLOBAL_EPSILON (compared with squared quantities) now skip the inside test and use the edge distances. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 20 +++++++++++++++++++- tests/geometry/test-distance.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index b9f5cda06..f0e26da56 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -68,6 +68,23 @@ namespace && signed_area_3 >= 0 ); } + /* + * A triangle is flat if its minimum height (twice its area divided by its + * longest edge length) is below GLOBAL_EPSILON + */ + bool is_triangle_flat( const geode::Triangle2D& triangle ) + { + const auto& vertices = triangle.vertices(); + const geode::Vector2D edge01{ vertices[0], vertices[1] }; + const geode::Vector2D edge02{ vertices[0], vertices[2] }; + const geode::Vector2D edge12{ vertices[1], vertices[2] }; + const auto double_area = geode::dot_perpendicular( edge01, edge02 ); + const auto max_length2 = std::max( + { edge01.length2(), edge02.length2(), edge12.length2() } ); + return double_area * double_area + <= geode::GLOBAL_EPSILON * geode::GLOBAL_EPSILON * max_length2; + } + std::optional< double > compute_point_line_distance( double segment_length, double point_to_v0_length, double point_to_v1_length ) @@ -996,7 +1013,8 @@ namespace geode opengeode_geometry_api point_triangle_distance( const Point2D& point, const Triangle2D& triangle ) { - if( may_point_be_in_triangle( point, triangle ) ) + if( !is_triangle_flat( triangle ) + && may_point_be_in_triangle( point, triangle ) ) { return { 0.0, point }; } diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index daf07c1a5..8679a954a 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1418,6 +1418,35 @@ void test_nan_point_triangle_distance() "Wrong point_triangle_signed_distance with NaN point" ); } +void test_degenerated_point_triangle_distance_2d() +{ + const geode::Point2D a{ { 0.0, 0.0 } }; + const geode::Point2D b{ { 1.0, 0.0 } }; + const geode::Point2D c{ { 2.0, 0.0 } }; + const geode::Triangle2D flat{ a, b, c }; + const geode::Point2D q{ { 5.0, 0.0 } }; + const auto [distance, closest] = geode::point_triangle_distance( q, flat ); + geode::OpenGeodeGeometryException::test( + std::fabs( distance - 3. ) <= geode::GLOBAL_EPSILON + && closest.inexact_equal( c ), + "Wrong point_triangle_distance 2D with flat triangle: ", distance ); + const geode::Point2D on{ { 1.5, 0.0 } }; + geode::OpenGeodeGeometryException::test( + std::get< 0 >( geode::point_triangle_distance( on, flat ) ) + <= geode::GLOBAL_EPSILON, + "Wrong point_triangle_distance 2D with flat triangle (on)" ); + + const geode::Point2D p{ { 1.0, 1.0 } }; + const geode::Triangle2D point_triangle{ p, p, p }; + const geode::Point2D r{ { 7.0, 3.0 } }; + const auto [distance2, closest2] = + geode::point_triangle_distance( r, point_triangle ); + geode::OpenGeodeGeometryException::test( + std::fabs( distance2 - std::sqrt( 40. ) ) <= geode::GLOBAL_EPSILON + && closest2.inexact_equal( p ), + "Wrong point_triangle_distance 2D with point triangle: ", distance2 ); +} + void test() { test_point_segment_distance(); @@ -1437,6 +1466,7 @@ void test() test_degenerated_triangle_triangle_distance(); test_random_triangle_triangle_distance(); test_nan_point_triangle_distance(); + test_degenerated_point_triangle_distance_2d(); } OPENGEODE_TEST( "distance" ) From 277d418c7435618770b798eebbce766ad64fe3bd Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 21:59:51 +0200 Subject: [PATCH 06/52] fix(Triangle): base is_degenerated on the minimum height Triangle::is_degenerated only measured the distance from the third vertex to the line of the first edge, so a triangle whose minimum height is below GLOBAL_EPSILON could be reported valid while normal() returns nullopt. Tetrahedron::is_degenerated then built a Plane from that triangle and threw bad_optional_access. Both now compare their minimum height (2*area / longest edge for triangles, 3*volume / largest facet area for tetrahedra) to GLOBAL_EPSILON, using squared quantities to avoid square roots. Co-Authored-By: Claude Opus 5.5 --- .../geometry/basic_objects/tetrahedron.cpp | 33 +++++++---- src/geode/geometry/basic_objects/triangle.cpp | 37 ++++++++---- tests/geometry/test-triangle.cpp | 59 +++++++++++++++++++ 3 files changed, 107 insertions(+), 22 deletions(-) diff --git a/src/geode/geometry/basic_objects/tetrahedron.cpp b/src/geode/geometry/basic_objects/tetrahedron.cpp index 82c95c590..dcd4a0b60 100644 --- a/src/geode/geometry/basic_objects/tetrahedron.cpp +++ b/src/geode/geometry/basic_objects/tetrahedron.cpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace geode { @@ -85,18 +86,26 @@ namespace geode template < typename PointType > bool GenericTetrahedron< PointType >::is_degenerated() const { - const Point3D& point0 = vertices_.at( 0 ); - const Point3D& point1 = vertices_.at( 1 ); - const Point3D& point2 = vertices_.at( 2 ); - const Triangle3D triangle{ point0, point1, point2 }; - if( triangle.is_degenerated() ) - { - return true; - } - const Point3D& point3 = vertices_.at( 3 ); - return std::get< 0 >( - point_plane_distance( point3, Plane{ triangle } ) ) - <= GLOBAL_EPSILON; + // The tetrahedron is degenerated if its minimum height, i.e. three + // times its volume divided by its largest facet area, is below + // GLOBAL_EPSILON. + const Point3D& point0 = vertices_[0]; + const Point3D& point1 = vertices_[1]; + const Point3D& point2 = vertices_[2]; + const Point3D& point3 = vertices_[3]; + const Vector3D edge01{ point0, point1 }; + const Vector3D edge02{ point0, point2 }; + const Vector3D edge03{ point0, point3 }; + const Vector3D edge12{ point1, point2 }; + const Vector3D edge13{ point1, point3 }; + const auto six_volume = edge01.dot( edge02.cross( edge03 ) ); + const auto max_double_area2 = + std::max( { edge01.cross( edge02 ).length2(), + edge01.cross( edge03 ).length2(), + edge02.cross( edge03 ).length2(), + edge12.cross( edge13 ).length2() } ); + return six_volume * six_volume + <= GLOBAL_EPSILON * GLOBAL_EPSILON * max_double_area2; } template < typename PointType > diff --git a/src/geode/geometry/basic_objects/triangle.cpp b/src/geode/geometry/basic_objects/triangle.cpp index b5727ff01..10b926635 100644 --- a/src/geode/geometry/basic_objects/triangle.cpp +++ b/src/geode/geometry/basic_objects/triangle.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include namespace { @@ -73,6 +75,19 @@ namespace } return result; } + + double squared_double_area( + const geode::Vector2D& edge0, const geode::Vector2D& edge1 ) + { + const auto double_area = geode::dot_perpendicular( edge0, edge1 ); + return double_area * double_area; + } + + double squared_double_area( + const geode::Vector3D& edge0, const geode::Vector3D& edge1 ) + { + return edge0.cross( edge1 ).length2(); + } } // namespace namespace geode @@ -294,16 +309,18 @@ namespace geode template < typename PointType, index_t dimension > bool GenericTriangle< PointType, dimension >::is_degenerated() const { - const Point< dimension >& point0 = vertices_.at( 0 ); - const Point< dimension >& point1 = vertices_.at( 1 ); - const Segment< dimension > edge{ point0, point1 }; - if( edge.is_degenerated() ) - { - return true; - } - const Point< dimension >& point2 = vertices_.at( 2 ); - return point_line_distance( point2, InfiniteLine< dimension >{ edge } ) - <= GLOBAL_EPSILON; + // The triangle is degenerated if its minimum height, i.e. twice its + // area divided by its longest edge length, is below GLOBAL_EPSILON. + const Point< dimension >& point0 = vertices_[0]; + const Point< dimension >& point1 = vertices_[1]; + const Point< dimension >& point2 = vertices_[2]; + const Vector< dimension > edge01{ point0, point1 }; + const Vector< dimension > edge02{ point0, point2 }; + const Vector< dimension > edge12{ point1, point2 }; + const auto max_length2 = std::max( + { edge01.length2(), edge02.length2(), edge12.length2() } ); + return squared_double_area( edge01, edge02 ) + <= GLOBAL_EPSILON * GLOBAL_EPSILON * max_length2; } template < typename PointType, index_t dimension > diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index bf3c28653..ee377f42b 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -21,17 +21,75 @@ * */ +#include + #include #include #include #include +#include #include #include #include +void test_degenerated_consistency() +{ + const geode::Point3D p0{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D p1{ { 1.0, 0.0, 0.0 } }; + const geode::Point3D p2{ { 3.0, 2.5e-6, 0.0 } }; + const geode::Point3D p3{ { 0.0, 0.0, 1.0 } }; + const geode::Triangle3D triangle{ p0, p1, p2 }; + geode::OpenGeodeGeometryException::test( triangle.is_degenerated(), + "Triangle with minimum height below epsilon should be degenerated" ); + const geode::Tetrahedron tetra{ p0, p1, p2, p3 }; + geode::OpenGeodeGeometryException::test( tetra.is_degenerated(), + "Tetrahedron with a degenerated facet should be degenerated" ); + const geode::Tetrahedron tetra2{ p3, p0, p1, p2 }; + geode::OpenGeodeGeometryException::test( tetra2.is_degenerated(), + "Tetrahedron with a degenerated facet should be degenerated" ); + + const geode::Point3D q2{ { 3.0, 1e-3, 0.0 } }; + const geode::Triangle3D triangle2{ p0, p1, q2 }; + geode::OpenGeodeGeometryException::test( !triangle2.is_degenerated(), + "Flat but valid triangle should not be degenerated" ); + const geode::Point3D q3{ { 1.0, 1.0, 5e-7 } }; + const geode::Point3D q4{ { 0.0, 1.0, 0.0 } }; + const geode::Tetrahedron flat_tetra{ p0, p1, q3, q4 }; + geode::OpenGeodeGeometryException::test( + flat_tetra.is_degenerated(), "Flat tetrahedron should be degenerated" ); + const geode::Tetrahedron tetra3{ p0, p1, q4, p3 }; + geode::OpenGeodeGeometryException::test( !tetra3.is_degenerated(), + "Valid tetrahedron should not be degenerated" ); + + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -1., 1. }; + std::uniform_real_distribution< double > exponent{ -9., -4. }; + for( const auto trial : geode::Range{ 1000 } ) + { + geode_unused( trial ); + const geode::Point3D a{ { distribution( generator ), + distribution( generator ), distribution( generator ) } }; + const geode::Point3D b{ { distribution( generator ), + distribution( generator ), distribution( generator ) } }; + const auto height = std::pow( 10., exponent( generator ) ); + const auto t = distribution( generator ) * 2; + const geode::Vector3D ab{ a, b }; + const auto offset = ab.cross( geode::Vector3D{ { 0.3, 0.5, 0.7 } } ); + const geode::Point3D c = a + ab * t + offset.normalize() * height; + const geode::Triangle3D random_triangle{ a, b, c }; + if( !random_triangle.normal() ) + { + geode::OpenGeodeGeometryException::test( + random_triangle.is_degenerated(), + "Triangle without normal should be degenerated: ", + random_triangle.string() ); + } + } +} + void test() { const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; @@ -60,6 +118,7 @@ void test() const auto pivot2 = triangle2.pivot(); geode::OpenGeodeGeometryException::test( !pivot2, "Pivot of triangle2 should not be calculated" ); + test_degenerated_consistency(); } OPENGEODE_TEST( "distance" ) \ No newline at end of file From 11e0f3f23957239f60d17dfc4807c6960e14e1c3 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:06:39 +0200 Subject: [PATCH 07/52] fix(Polygon): use the farthest vertex in minimum_height minimum_height measured the distance from the longest edge line to an arbitrary vertex (the last one not on that edge), so a valid polygon with one vertex near the longest edge line, e.g. (0,0),(10,0),(5,5), (-1,0), was reported with height 0 and as degenerated. The height is now the maximum distance over all vertices not on the longest edge. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/basic_objects/polygon.cpp | 9 +++++---- tests/geometry/test-basic-objects.cpp | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/geode/geometry/basic_objects/polygon.cpp b/src/geode/geometry/basic_objects/polygon.cpp index 125da5b26..b20212872 100644 --- a/src/geode/geometry/basic_objects/polygon.cpp +++ b/src/geode/geometry/basic_objects/polygon.cpp @@ -299,17 +299,18 @@ namespace geode geode::Segment< dimension >{ vertices_[max_length_edge], vertices_[next] } }; - index_t opposite_vertex{ 0 }; + double max_distance{ 0. }; for( const auto vertex : geode::Range{ nb_vertices } ) { if( vertex == max_length_edge || vertex == next ) { continue; } - opposite_vertex = vertex; + const geode::Point< dimension >& point = vertices_[vertex]; + max_distance = std::max( + max_distance, geode::point_line_distance( point, line ) ); } - const geode::Point< dimension >& point = vertices_[opposite_vertex]; - return geode::point_line_distance( point, line ); + return max_distance; } template < typename PointType, index_t dimension > diff --git a/tests/geometry/test-basic-objects.cpp b/tests/geometry/test-basic-objects.cpp index 4b9e8f6cb..c74ed1743 100644 --- a/tests/geometry/test-basic-objects.cpp +++ b/tests/geometry/test-basic-objects.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -293,6 +294,23 @@ void test_circle() "Wrong result for circle5" ); } +void test_polygon_minimum_height() +{ + const geode::OwnerPolygon2D quad{ { geode::Point2D{ { 0, 0 } }, + geode::Point2D{ { 10, 0 } }, geode::Point2D{ { 5, 5 } }, + geode::Point2D{ { -1, 0 } } } }; + geode::OpenGeodeGeometryException::test( + std::fabs( quad.minimum_height() - 5 ) <= geode::GLOBAL_EPSILON, + "Wrong polygon minimum height: ", quad.minimum_height() ); + geode::OpenGeodeGeometryException::test( + !quad.is_degenerated(), "Polygon should not be degenerated" ); + const geode::OwnerPolygon2D flat{ { geode::Point2D{ { 0, 0 } }, + geode::Point2D{ { 10, 0 } }, geode::Point2D{ { 5, 0 } }, + geode::Point2D{ { -1, 0 } } } }; + geode::OpenGeodeGeometryException::test( + flat.is_degenerated(), "Flat polygon should be degenerated" ); +} + void test() { test_segment(); @@ -302,6 +320,7 @@ void test() test_tetrahedron(); test_sphere(); test_circle(); + test_polygon_minimum_height(); } OPENGEODE_TEST( "basic-object" ) From 0b5c33124668808b6ef4814a2c155ec7358d1b5e Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:11:25 +0200 Subject: [PATCH 08/52] fix(NNSearch): handle empty point sets and zero neighbor requests closest_neighbor called front() on an empty result when the search had no point, and neighbors(point, 0) passed &vector[0] of empty vectors to nanoflann, whose k-NN result set writes at index k-1. closest_neighbor now returns NO_ID on an empty set (as AABBTree::closest_element_box does), and neighbors returns an empty vector for k = 0 or an empty set. Co-Authored-By: Claude Opus 5.5 --- include/geode/geometry/nn_search.hpp | 2 +- src/geode/geometry/nn_search.cpp | 20 +++++++++++++++----- tests/geometry/test-nnsearch.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/include/geode/geometry/nn_search.hpp b/include/geode/geometry/nn_search.hpp index 346c07f1c..5b98fc1f6 100644 --- a/include/geode/geometry/nn_search.hpp +++ b/include/geode/geometry/nn_search.hpp @@ -106,7 +106,7 @@ namespace geode /*! * Get the closest neighbor from the given point * @param[in] point The requested point - * @return the index of the closest point + * @return the index of the closest point, NO_ID if there is no point */ [[nodiscard]] index_t closest_neighbor( const Point< dimension >& point ) const; diff --git a/src/geode/geometry/nn_search.cpp b/src/geode/geometry/nn_search.cpp index 2016396a7..3878a5aec 100644 --- a/src/geode/geometry/nn_search.cpp +++ b/src/geode/geometry/nn_search.cpp @@ -66,7 +66,7 @@ namespace geode std::vector< nanoflann::ResultItem< index_t, double > > results; nanoflann::SearchParameters params; params.sorted = true; - const auto nb_results = nn_tree_.radiusSearch( ©( point )[0], + const auto nb_results = nn_tree_.radiusSearch( copy( point ).data(), threshold_distance * threshold_distance, results, params ); std::vector< index_t > indices; indices.reserve( nb_results ); @@ -89,7 +89,7 @@ namespace geode epsilons_frame.direction( max_elongation_direction ).length(); const auto radius = max_elongation * max_elongation; const auto nb_results = nn_tree_.radiusSearch( - ©( point )[0], radius, results, params ); + copy( point ).data(), radius, results, params ); std::vector< index_t > indices; indices.reserve( nb_results ); for( auto&& result : results ) @@ -116,10 +116,15 @@ namespace geode std::vector< index_t > nearest_vertices( const Point< dimension >& point, const index_t nb_neighbors ) const { + if( nb_neighbors == 0 || nb_points() == 0 ) + { + return {}; + } std::vector< index_t > results( nb_neighbors ); std::vector< double > distances( nb_neighbors ); - const auto new_nb_neighbors = nn_tree_.knnSearch( - ©( point )[0], nb_neighbors, &results[0], &distances[0] ); + const auto new_nb_neighbors = + nn_tree_.knnSearch( copy( point ).data(), nb_neighbors, + results.data(), distances.data() ); results.resize( new_nb_neighbors ); return results; } @@ -266,7 +271,12 @@ namespace geode index_t NNSearch< dimension >::closest_neighbor( const Point< dimension >& point ) const { - return impl_->nearest_vertices( point, 1 ).front(); + const auto closest = impl_->nearest_vertices( point, 1 ); + if( closest.empty() ) + { + return NO_ID; + } + return closest.front(); } template < index_t dimension > diff --git a/tests/geometry/test-nnsearch.cpp b/tests/geometry/test-nnsearch.cpp index 4bf980147..07e9f0c12 100644 --- a/tests/geometry/test-nnsearch.cpp +++ b/tests/geometry/test-nnsearch.cpp @@ -152,10 +152,35 @@ void second_test() "[Test 2] Should be 2 unique points" ); } +void test_empty_search() +{ + const geode::NNSearch3D empty_search{ std::vector< geode::Point3D >{} }; + const geode::Point3D query{ { 1, 2, 3 } }; + geode::OpenGeodeGeometryException::test( + empty_search.closest_neighbor( query ) == geode::NO_ID, + "Closest neighbor in empty search should be NO_ID" ); + geode::OpenGeodeGeometryException::test( + empty_search.neighbors( query, 3 ).empty(), + "Neighbors in empty search should be empty" ); + geode::OpenGeodeGeometryException::test( + empty_search.radius_neighbors( query, 10 ).empty(), + "Radius neighbors in empty search should be empty" ); + + const geode::NNSearch3D search{ { geode::Point3D{ { 0, 0, 0 } }, + geode::Point3D{ { 1, 0, 0 } } } }; + geode::OpenGeodeGeometryException::test( + search.neighbors( query, 0 ).empty(), + "Zero neighbors request should be empty" ); + geode::OpenGeodeGeometryException::test( + search.neighbors( query, 5 ).size() == 2, + "Too many neighbors request should return all points" ); +} + void test() { first_test(); second_test(); + test_empty_search(); } OPENGEODE_TEST( "nnsearch" ) \ No newline at end of file From 31d436f274f4cdc6bd91ec39a83bce4b2a93a98f Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:16:44 +0200 Subject: [PATCH 09/52] fix(SquareMatrix): implement 1x1 determinant and inverse SquareMatrix<1> is explicitly instantiated but determinant() and inverse() threw "Not implemented", so CoordinateSystem1D::coordinates always threw. Both are now implemented for dimension 1. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/square_matrix.cpp | 17 +++++++++++++++++ tests/geometry/test-coordinate-system.cpp | 10 ++++++++++ tests/geometry/test-square-matrix.cpp | 10 ++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/geode/geometry/square_matrix.cpp b/src/geode/geometry/square_matrix.cpp index 3a602e061..fda3eb141 100644 --- a/src/geode/geometry/square_matrix.cpp +++ b/src/geode/geometry/square_matrix.cpp @@ -154,6 +154,12 @@ namespace geode dimension }; } + template <> + double opengeode_geometry_api SquareMatrix< 1 >::determinant() const + { + return value( 0, 0 ); + } + template <> double opengeode_geometry_api SquareMatrix< 2 >::determinant() const { @@ -197,6 +203,17 @@ namespace geode dimension }; } + template <> + SquareMatrix< 1 > opengeode_geometry_api SquareMatrix< 1 >::inverse() const + { + const auto det = determinant(); + OpenGeodeGeometryException::check_exception( det != 0, nullptr, + OpenGeodeBasicException::TYPE::data, + "[SquareMatrix::inverse] Cannot inverse matrix: determinant is " + "null (given vectors do not form a well-defined base)." ); + return SquareMatrix< 1 >{ { Vector1D{ { 1. / det } } } }; + } + template <> SquareMatrix< 2 > opengeode_geometry_api SquareMatrix< 2 >::inverse() const { diff --git a/tests/geometry/test-coordinate-system.cpp b/tests/geometry/test-coordinate-system.cpp index 5756cff1f..68e0f2144 100644 --- a/tests/geometry/test-coordinate-system.cpp +++ b/tests/geometry/test-coordinate-system.cpp @@ -133,6 +133,15 @@ void optuse_system3D() geode::Point3D{ { 0.25, 4, -0.5 } } ); } +void test_system1D() +{ + geode::Logger::info( "System 1D" ); + const geode::CoordinateSystem1D coord_system{ { geode::Vector1D{ { 2 } } }, + geode::Point1D{ { 1 } } }; + check_point( + coord_system, geode::Point1D{ { 5 } }, geode::Point1D{ { 2 } } ); +} + void test() { acute_system2D(); @@ -140,6 +149,7 @@ void test() orthonormal_system3D(); acute_system3D(); optuse_system3D(); + test_system1D(); } OPENGEODE_TEST( "coordinate-system" ) \ No newline at end of file diff --git a/tests/geometry/test-square-matrix.cpp b/tests/geometry/test-square-matrix.cpp index 3b4a4f0dd..d5ae50ab1 100644 --- a/tests/geometry/test-square-matrix.cpp +++ b/tests/geometry/test-square-matrix.cpp @@ -189,10 +189,20 @@ void test_matrix_3x3() "[TEST] Wrong value for inverse matrix coeff [2,2]" ); } +void test_matrix_1x1() +{ + const geode::SquareMatrix1D matrix{ { geode::Vector1D{ { 4. } } } }; + geode::OpenGeodeGeometryException::test( + matrix.determinant() == 4., "Wrong 1x1 determinant" ); + geode::OpenGeodeGeometryException::test( + matrix.inverse().value( 0, 0 ) == 0.25, "Wrong 1x1 inverse" ); +} + void test() { test_matrix_2x2(); test_matrix_3x3(); + test_matrix_1x1(); } OPENGEODE_TEST( "vector" ) \ No newline at end of file From eb10fd2e0b0eca373940bfdcab8d5516484296e9 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:32:40 +0200 Subject: [PATCH 10/52] fix(Rotation): avoid infinite loop on huge angles in rotate rotate normalized the angle with while loops adding or removing 2*PI, which never terminate for infinite angles or angles whose ulp exceeds 2*PI (e.g. 1e17). The normalization is useless since the rotation matrix is quadratic in the quaternion, so it is removed. The axis check compared a squared length to GLOBAL_EPSILON, rejecting valid axes shorter than 1e-3; any non-null finite axis is now accepted. The homogeneous coordinate is always 1, so its check and division are dropped. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/rotation.cpp | 33 +++++++++++--------------------- tests/geometry/test-rotation.cpp | 32 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/geode/geometry/rotation.cpp b/src/geode/geometry/rotation.cpp index e56f1fe93..461bdc96e 100644 --- a/src/geode/geometry/rotation.cpp +++ b/src/geode/geometry/rotation.cpp @@ -88,38 +88,27 @@ namespace geode Point3D opengeode_geometry_api rotate( const Point3D& point, const Vector3D& axis, double angle ) { + const auto axis_length = axis.length(); OpenGeodeGeometryException::check_exception( - axis.length2() > geode::GLOBAL_EPSILON, point, + axis_length > 0 && std::isfinite( axis_length ), point, OpenGeodeBasicException::TYPE::data, - "[rotate] Given axis is almost null" ); - while( angle < 0 ) - { - angle += 2 * M_PI; - } - while( angle >= 2 * M_PI ) - { - angle -= 2 * M_PI; - } - - const auto base = axis.normalize() * std::sin( 0.5 * angle ); + "[rotate] Given axis is null or not finite" ); + // No angle normalization needed: the rotation matrix is quadratic in + // the quaternion, so q and -q (angle + 2*PI) give the same rotation. + const auto base = axis / axis_length * std::sin( 0.5 * angle ); const geode::Vector< 4 > quaternion( { base.value( 0 ), base.value( 1 ), base.value( 2 ), std::cos( 0.5 * angle ) } ); const auto matrix = rotation_matrix( quaternion ); const Vector< 4 > quat_point( { point.value( 0 ), point.value( 1 ), point.value( 2 ), 1.0 } ); - Vector< 4 > result; - for( const auto i : geode::Range{ 4 } ) + // The last matrix row is (0, 0, 0, 1): the homogeneous coordinate + // stays 1 and no division is needed. + Point3D result; + for( const auto i : geode::LRange{ 3 } ) { result.set_value( i, matrix[i].dot( quat_point ) ); } - - OpenGeodeGeometryException::check_exception( - std::fabs( result.value( 3 ) ) > GLOBAL_EPSILON, point, - OpenGeodeBasicException::TYPE::internal, - "[rotate] Error in computation" ); - const auto inverse = 1.0 / result.value( 3 ); - return Point3D{ { result.value( 0 ) * inverse, - result.value( 1 ) * inverse, result.value( 2 ) * inverse } }; + return result; } } // namespace geode diff --git a/tests/geometry/test-rotation.cpp b/tests/geometry/test-rotation.cpp index 972aa7c11..a10f00487 100644 --- a/tests/geometry/test-rotation.cpp +++ b/tests/geometry/test-rotation.cpp @@ -30,6 +30,37 @@ #include +void test_rotation_robustness() +{ + const geode::Point3D input{ { 1., 2., 3. } }; + const geode::Vector3D axis{ { 1., 1., 0. } }; + const auto huge = geode::rotate( input, axis, 1e17 ); + geode::OpenGeodeGeometryException::test( + std::fabs( geode::Vector3D{ huge }.length() + - geode::Vector3D{ input }.length() ) + <= geode::GLOBAL_EPSILON + && std::fabs( geode::Vector3D{ huge }.dot( axis ) + - geode::Vector3D{ input }.dot( axis ) ) + <= geode::GLOBAL_EPSILON, + "Wrong result for huge angle rotation" ); + const auto infinite = + geode::rotate( input, axis, std::numeric_limits< double >::infinity() ); + geode::OpenGeodeGeometryException::test( std::isnan( infinite.value( 0 ) ), + "Infinite angle rotation should give NaN" ); + + const geode::Vector3D small_axis{ { 1e-4, 0., 0. } }; + const geode::Vector3D z_axis{ { 0., 0., 3. } }; + const auto small_result = geode::rotate( z_axis, small_axis, M_PI / 2.0 ); + geode::OpenGeodeGeometryException::test( + small_result.inexact_equal( geode::Point3D{ { 0., -3., 0. } } ), + "Wrong result for rotation around a short axis" ); + + const auto result = geode::rotate( input, axis, -7 * M_PI / 3.0 ); + const auto reference = geode::rotate( input, axis, 5 * M_PI / 3.0 ); + geode::OpenGeodeGeometryException::test( result.inexact_equal( reference ), + "Wrong result for negative angle rotation" ); +} + void test() { const geode::Vector3D input( { 0., 0., 3. } ); @@ -48,6 +79,7 @@ void test() const geode::Vector3D answer3( { 0., -3., 0. } ); geode::OpenGeodeGeometryException::test( result3.inexact_equal( answer3 ), "Wrong result for configuration 2" ); + test_rotation_robustness(); } OPENGEODE_TEST( "rotation" ) From e583f38069af3c89a92624b3e0c2247c481919d4 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:37:55 +0200 Subject: [PATCH 11/52] fix(Intersection): fix line_cylinder_intersection perpendicular case When the line was perpendicular to the cylinder axis, the computed wall intersections fell through to the end-disk code, dividing by D.z == 0 and appending parameters past the 2-element array. The perpendicular case is now handled on its own, using the line-to-axis distance with an epsilon tolerance so tangent lines give a single point. Parameter insertion is bounded. Correctness bookkeeping is also fixed: the line correctness flag was overwritten instead of AND-accumulated and both correctness lists received extra points; each now holds exactly one entry per result. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/intersection.cpp | 252 +++++++++++---------------- tests/geometry/test-intersection.cpp | 62 +++++++ 2 files changed, 166 insertions(+), 148 deletions(-) diff --git a/src/geode/geometry/intersection.cpp b/src/geode/geometry/intersection.cpp index 2c163f90a..73bf0149b 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -619,6 +619,12 @@ namespace geode const InfiniteLine3D& line, const Cylinder& cylinder ) { CylinderLineResult result; + const auto add_parameter = [&result]( double parameter ) { + if( result.numIntersections < 2 ) + { + result.parameter[result.numIntersections++] = parameter; + } + }; // Create a coordinate system for the cylinder. In this system, // the cylinder segment center C is the origin and the cylinder // axis direction W is the z-axis. U and V are the other @@ -679,147 +685,105 @@ namespace geode if( D.value( 2 ) == 0 ) { // The line is perpendicular to the cylinder axis. - if( std::fabs( P.value( 2 ) ) <= halfHeight ) + if( std::fabs( P.value( 2 ) ) > halfHeight ) { - // Test intersection of line P+t*D with infinite cylinder - // x^2+y^2 = r^2. This reduces to computing the roots of a - // quadratic equation. If P = (px,py,pz) and - // D = (dx,dy,dz), then the quadratic equation is - // (dx^2+dy^2)*t^2+2*(px*dx+py*dy)*t+(px^2+py^2-r^2) = 0. - const auto a0 = P.value( 0 ) * P.value( 0 ) - + P.value( 1 ) * P.value( 1 ) - rSqr; - const auto a1 = P.value( 0 ) * D.value( 0 ) - + P.value( 1 ) * D.value( 1 ); - const auto a2 = D.value( 0 ) * D.value( 0 ) - + D.value( 1 ) * D.value( 1 ); - const auto discr = a1 * a1 - a0 * a2; - if( discr > 0 ) - { - // The line intersects the cylinder in two places. - result.intersect = true; - result.numIntersections = 2; - const auto root = std::sqrt( discr ); - result.parameter[0] = ( -a1 - root ) / a2; - result.parameter[1] = ( -a1 + root ) / a2; - } - else if( discr == 0 ) - { - // The line is tangent to the cylinder. - result.intersect = true; - result.numIntersections = 1; - result.parameter[0] = -a1 / a2; - result.parameter[1] = result.parameter[0]; - } - // else: The line does not intersect the cylinder. - } - else - { // else: The line is outside the planes of the cylinder end + // The line is outside the planes of the cylinder end // disks. return { INTERSECTION_TYPE::none }; } - } - - // At this time, the line direction is neither parallel nor - // perpendicular to the cylinder axis. The line must - // intersect both planes of the end disk, the intersection with - // the cylinder being a segment. The t-interval of the segment - // is [t0,t1]. - - // Test for intersections with the planes of the end disks. - const auto t0 = ( -halfHeight - P.value( 2 ) ) / D.value( 2 ); - auto xTmp = P.value( 0 ) + t0 * D.value( 0 ); - auto yTmp = P.value( 1 ) + t0 * D.value( 1 ); - if( xTmp * xTmp + yTmp * yTmp <= rSqr ) - { - // Plane intersection inside the bottom cylinder end disk. - result.parameter[result.numIntersections++] = t0; - } - - const auto t1 = ( halfHeight - P.value( 2 ) ) / D.value( 2 ); - xTmp = P.value( 0 ) + t1 * D.value( 0 ); - yTmp = P.value( 1 ) + t1 * D.value( 1 ); - if( xTmp * xTmp + yTmp * yTmp <= rSqr ) - { - // Plane intersection inside the top cylinder end disk. - result.parameter[result.numIntersections++] = t1; - } - - if( result.numIntersections < 2 ) - { - // Test for intersection with the cylinder wall. - const auto a0 = P.value( 0 ) * P.value( 0 ) - + P.value( 1 ) * P.value( 1 ) - rSqr; + // Test intersection of line P+t*D with infinite cylinder + // x^2+y^2 = r^2, using the distance h from the line to the + // cylinder axis: the intersections are at + // t = t_closest +- sqrt( (r-h)*(r+h) ) / |D|. const auto a1 = P.value( 0 ) * D.value( 0 ) + P.value( 1 ) * D.value( 1 ); const auto a2 = D.value( 0 ) * D.value( 0 ) + D.value( 1 ) * D.value( 1 ); - const auto discr = a1 * a1 - a0 * a2; - if( discr > 0 ) + const auto closest_parameter = -a1 / a2; + const auto x = P.value( 0 ) + closest_parameter * D.value( 0 ); + const auto y = P.value( 1 ) + closest_parameter * D.value( 1 ); + const auto distance_to_axis = std::sqrt( x * x + y * y ); + const auto radius = cylinder.radius(); + if( distance_to_axis - radius > GLOBAL_EPSILON ) { - const auto root = std::sqrt( discr ); - auto tValue = ( -a1 - root ) / a2; - if( t0 <= t1 ) - { - if( t0 <= tValue && tValue <= t1 ) - { - result.parameter[result.numIntersections++] = - tValue; - } - } - else - { - if( t1 <= tValue && tValue <= t0 ) - { - result.parameter[result.numIntersections++] = - tValue; - } - } + // The line does not intersect the cylinder. + return { INTERSECTION_TYPE::none }; + } + if( radius - distance_to_axis <= GLOBAL_EPSILON ) + { + // The line is tangent to the cylinder. + add_parameter( closest_parameter ); + } + else + { + // The line intersects the cylinder in two places. + const auto half_chord = + std::sqrt( ( radius - distance_to_axis ) + * ( radius + distance_to_axis ) / a2 ); + add_parameter( closest_parameter - half_chord ); + add_parameter( closest_parameter + half_chord ); + } + } + else + { + // At this time, the line direction is neither parallel nor + // perpendicular to the cylinder axis. The line must + // intersect both planes of the end disk, the intersection + // with the cylinder being a segment. The t-interval of the + // segment is [t0,t1]. - if( result.numIntersections < 2 ) - { - tValue = ( -a1 + root ) / a2; - if( t0 <= t1 ) - { - if( t0 <= tValue && tValue <= t1 ) - { - result.parameter[result.numIntersections++] = - tValue; - } - } - else - { - if( t1 <= tValue && tValue <= t0 ) - { - result.parameter[result.numIntersections++] = - tValue; - } - } - } - // else: Line intersects end disk and cylinder wall. + // Test for intersections with the planes of the end disks. + const auto t0 = ( -halfHeight - P.value( 2 ) ) / D.value( 2 ); + auto xTmp = P.value( 0 ) + t0 * D.value( 0 ); + auto yTmp = P.value( 1 ) + t0 * D.value( 1 ); + if( xTmp * xTmp + yTmp * yTmp <= rSqr ) + { + // Plane intersection inside the bottom cylinder end disk. + add_parameter( t0 ); } - else if( discr == 0 ) + + const auto t1 = ( halfHeight - P.value( 2 ) ) / D.value( 2 ); + xTmp = P.value( 0 ) + t1 * D.value( 0 ); + yTmp = P.value( 1 ) + t1 * D.value( 1 ); + if( xTmp * xTmp + yTmp * yTmp <= rSqr ) { - auto tValue = -a1 / a2; - if( t0 <= t1 ) - { - if( t0 <= tValue && tValue <= t1 ) + // Plane intersection inside the top cylinder end disk. + add_parameter( t1 ); + } + + if( result.numIntersections < 2 ) + { + // Test for intersection with the cylinder wall. + const auto a0 = P.value( 0 ) * P.value( 0 ) + + P.value( 1 ) * P.value( 1 ) - rSqr; + const auto a1 = P.value( 0 ) * D.value( 0 ) + + P.value( 1 ) * D.value( 1 ); + const auto a2 = D.value( 0 ) * D.value( 0 ) + + D.value( 1 ) * D.value( 1 ); + const auto discr = a1 * a1 - a0 * a2; + const auto add_wall_parameter = [&add_parameter, t0, t1]( + double parameter ) { + if( std::min( t0, t1 ) <= parameter + && parameter <= std::max( t0, t1 ) ) { - result.parameter[result.numIntersections++] = - tValue; + add_parameter( parameter ); } + }; + if( discr > 0 ) + { + const auto root = std::sqrt( discr ); + add_wall_parameter( ( -a1 - root ) / a2 ); + add_wall_parameter( ( -a1 + root ) / a2 ); } - else + else if( discr == 0 ) { - if( t1 <= tValue && tValue <= t0 ) - { - result.parameter[result.numIntersections++] = - tValue; - } + add_wall_parameter( -a1 / a2 ); } + // else: Line does not intersect cylinder wall. } - // else: Line does not intersect cylinder wall. + // else: Line intersects both top and bottom cylinder end + // disks. } - // else: Line intersects both top and bottom cylinder end disks. } if( result.numIntersections == 2 ) { @@ -850,19 +814,14 @@ namespace geode first_correctness{ true, {} }; CorrectnessInfo< absl::InlinedVector< Point3D, 2 > >::Correctness second_correctness{ true, {} }; - for( const auto r : LRange{ result.numIntersections } ) + for( const auto& point : results ) { // distance to line - first_correctness.first = - point_line_distance( results[r], line ) <= GLOBAL_EPSILON; - first_correctness.second.push_back( - point_line_projection( results[r], line ) ); first_correctness.first = first_correctness.first - && point_line_distance( results.back(), line ) - <= GLOBAL_EPSILON; + && point_line_distance( point, line ) <= GLOBAL_EPSILON; first_correctness.second.push_back( - point_line_projection( results.back(), line ) ); + point_line_projection( point, line ) ); // distance to cylinder // first check distance and projection orientation to the cylinder @@ -870,40 +829,37 @@ namespace geode bool cur_correctness{ false }; for( const auto v : LRange{ 2 } ) { - if( !cur_correctness ) + const auto distance = point_point_distance( + point, cylinder.axis().vertices()[v].get() ); + if( distance <= GLOBAL_EPSILON ) + { + cur_correctness = true; + break; + } + if( distance < cylinder.radius() ) { - const auto distance = point_point_distance( - results[r], cylinder.axis().vertices()[v].get() ); - if( distance <= GLOBAL_EPSILON ) + const geode::Vector3D point_to_vertex{ point, + cylinder.axis().vertices()[v] }; + if( cylinder.axis().direction().dot( + point_to_vertex.normalize() ) + <= GLOBAL_EPSILON ) { cur_correctness = true; - second_correctness.second.push_back( results[r] ); break; } - if( distance < cylinder.radius() ) - { - const geode::Vector3D point_to_vertex{ results[r], - cylinder.axis().vertices()[v] }; - if( cylinder.axis().direction().dot( - point_to_vertex.normalize() ) - <= GLOBAL_EPSILON ) - { - cur_correctness = true; - } - } } } // then check distance to the cylinder axis if( !cur_correctness ) { const auto distance = - point_segment_distance( results[r], cylinder.axis() ); + point_segment_distance( point, cylinder.axis() ); cur_correctness = std::fabs( distance - cylinder.radius() ) <= GLOBAL_EPSILON; } second_correctness.first = second_correctness.first && cur_correctness; - second_correctness.second.push_back( results[r] ); + second_correctness.second.push_back( point ); } return { std::move( results ), { first_correctness, second_correctness } }; diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index c02e77067..77e73b2d1 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -1099,6 +1099,67 @@ void test_degenerated_segment_intersections() } } +void test_line_cylinder_perpendicular() +{ + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; + const geode::Cylinder cylinder{ geode::Segment3D{ a, b }, 0.2 }; + const auto check_correctness = []( const auto& result ) { + return result.correctness->first.second.size() == result.result->size() + && result.correctness->second.second.size() + == result.result->size(); + }; + + for( const auto offset : { 0., 1e-7, -1e-7 } ) + { + const geode::Point3D c{ { 0.5, 0.2 + offset, -1.0 } }; + const geode::Point3D d{ { 0.5, 0.2 + offset, 1.0 } }; + const geode::InfiniteLine3D tangent{ geode::Segment3D{ c, d } }; + const auto tangent_result = + geode::line_cylinder_intersection( tangent, cylinder ); + geode::OpenGeodeGeometryException::test( + tangent_result && tangent_result.result->size() == 1 + && tangent_result.result->front().inexact_equal( + geode::Point3D{ { 0.5, 0.2, 0.0 } } ) + && check_correctness( tangent_result ), + "Wrong result for line_cylinder_intersection with perpendicular " + "tangent line" ); + } + + const geode::Point3D e{ { 0.5, 0.1, -1.0 } }; + const geode::Point3D f{ { 0.5, 0.1, 1.0 } }; + const geode::InfiniteLine3D crossing{ geode::Segment3D{ e, f } }; + const auto crossing_result = + geode::line_cylinder_intersection( crossing, cylinder ); + const auto z = std::sqrt( 0.2 * 0.2 - 0.1 * 0.1 ); + geode::OpenGeodeGeometryException::test( + crossing_result && crossing_result.result->size() == 2 + && crossing_result.result->front().inexact_equal( + geode::Point3D{ { 0.5, 0.1, -z } } ) + && crossing_result.result->back().inexact_equal( + geode::Point3D{ { 0.5, 0.1, z } } ) + && check_correctness( crossing_result ), + "Wrong result for line_cylinder_intersection with perpendicular " + "crossing line" ); + + const geode::Point3D g{ { 0.5, 0.3, -1.0 } }; + const geode::Point3D h{ { 0.5, 0.3, 1.0 } }; + geode::OpenGeodeGeometryException::test( + !geode::line_cylinder_intersection( + geode::InfiniteLine3D{ geode::Segment3D{ g, h } }, cylinder ), + "Wrong result for line_cylinder_intersection with perpendicular " + "outside line" ); + + const geode::Point3D i{ { 0.0, -1.0, 0.0 } }; + const geode::Point3D j{ { 1.0, 1.0, 0.0 } }; + const auto oblique_result = geode::line_cylinder_intersection( + geode::InfiniteLine3D{ geode::Segment3D{ i, j } }, cylinder ); + geode::OpenGeodeGeometryException::test( + oblique_result && oblique_result.result->size() == 2 + && check_correctness( oblique_result ), + "Wrong result for line_cylinder_intersection with oblique line" ); +} + void test() { test_line_sphere_intersection(); @@ -1116,6 +1177,7 @@ void test() test_line_ellipse_intersection(); test_segment_ellipse_intersection(); test_degenerated_segment_intersections(); + test_line_cylinder_perpendicular(); } OPENGEODE_TEST( "intersection" ) From bb4e8c499d0c4f49f91a8aeeea9c35e44f33fc2e Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:42:20 +0200 Subject: [PATCH 12/52] fix(Intersection): keep correctness in sync with filtered results segment_sphere, segment_cylinder and segment_ellipse intersections filtered the line intersections lying on the segment but returned the unfiltered correctness lists, so correctness points no longer matched the result points. Correctness entries are now filtered in lockstep. plane_circle_intersection overwrote its correctness flags for each point instead of AND-accumulating them, and triangle_circle_intersection ignored the circle correctness flag of the plane-circle result. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/intersection.cpp | 104 +++++++++++++-------------- tests/geometry/test-intersection.cpp | 59 +++++++++++++++ 2 files changed, 110 insertions(+), 53 deletions(-) diff --git a/src/geode/geometry/intersection.cpp b/src/geode/geometry/intersection.cpp index 73bf0149b..ddbcbf592 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -146,6 +146,43 @@ namespace return std::sqrt( axial_distance * axial_distance + radial_distance * radial_distance ); } + /* + * Keep the line intersections located on the segment, with their + * correctness information. + */ + template < typename SegmentType, typename PointType > + geode::IntersectionResult< absl::InlinedVector< PointType, 2 > > + filter_segment_intersections( + geode::IntersectionResult< absl::InlinedVector< PointType, 2 > > + line_intersections, + const SegmentType& segment ) + { + using Intersections = absl::InlinedVector< PointType, 2 >; + const auto& points = line_intersections.result.value(); + const auto& correctness = line_intersections.correctness.value(); + Intersections segment_intersections; + typename geode::CorrectnessInfo< Intersections >::Correctness + first_correctness{ correctness.first.first, {} }; + typename geode::CorrectnessInfo< Intersections >::Correctness + second_correctness{ correctness.second.first, {} }; + for( const auto i : geode::Indices{ points } ) + { + if( geode::point_segment_distance( points[i], segment ) + > geode::GLOBAL_EPSILON ) + { + continue; + } + segment_intersections.push_back( points[i] ); + first_correctness.second.push_back( correctness.first.second[i] ); + second_correctness.second.push_back( correctness.second.second[i] ); + } + if( segment_intersections.empty() ) + { + return { geode::INTERSECTION_TYPE::none }; + } + return { std::move( segment_intersections ), + { first_correctness, second_correctness } }; + } } // namespace namespace geode @@ -273,22 +310,8 @@ namespace geode InfiniteLine< dimension >{ segment }, sphere ); if( line_intersections ) { - absl::InlinedVector< Point< dimension >, 2 > segment_intersections; - segment_intersections.reserve( - line_intersections.result.value().size() ); - for( auto&& point : line_intersections.result.value() ) - { - if( point_segment_distance( point, segment ) <= GLOBAL_EPSILON ) - { - segment_intersections.emplace_back( point ); - } - } - if( segment_intersections.empty() ) - { - return { INTERSECTION_TYPE::none }; - } - return { std::move( segment_intersections ), - std::move( line_intersections.correctness.value() ) }; + return filter_segment_intersections( + std::move( line_intersections ), segment ); } return line_intersections.type; } @@ -880,22 +903,8 @@ namespace geode line_cylinder_intersection( InfiniteLine3D{ segment }, cylinder ); if( line_intersections ) { - absl::InlinedVector< Point3D, 2 > segment_intersections; - segment_intersections.reserve( - line_intersections.result.value().size() ); - for( auto&& point : line_intersections.result.value() ) - { - if( point_segment_distance( point, segment ) <= GLOBAL_EPSILON ) - { - segment_intersections.emplace_back( point ); - } - } - if( segment_intersections.empty() ) - { - return { INTERSECTION_TYPE::none }; - } - return { std::move( segment_intersections ), - std::move( line_intersections.correctness.value() ) }; + return filter_segment_intersections( + std::move( line_intersections ), segment ); } return line_intersections.type; } @@ -920,7 +929,8 @@ namespace geode CorrectnessInfo< absl::InlinedVector< Point3D, 2 > >::Correctness first_correctness{ true, {} }; CorrectnessInfo< absl::InlinedVector< Point3D, 2 > >::Correctness - second_correctness{ true, {} }; + second_correctness{ plane_intersection.correctness->second.first, + {} }; auto& intersections = plane_intersection.result.value(); for( const auto i : Indices{ intersections } ) { @@ -975,21 +985,23 @@ namespace geode } absl::InlinedVector< Point3D, 2 > result; CorrectnessInfo< absl::InlinedVector< Point3D, 2 > >::Correctness - first_correctness; + first_correctness{ true, {} }; CorrectnessInfo< absl::InlinedVector< Point3D, 2 > >::Correctness - second_correctness; + second_correctness{ true, {} }; const auto compute_correctness = [&first_correctness, &second_correctness, &plane, &circle]( const Point3D& intersection ) { auto plane_output = point_plane_distance( intersection, plane ); first_correctness.first = - std::get< 0 >( plane_output ) <= GLOBAL_EPSILON; + first_correctness.first + && std::get< 0 >( plane_output ) <= GLOBAL_EPSILON; first_correctness.second.emplace_back( std::move( std::get< 1 >( plane_output ) ) ); auto circle_output = point_circle_distance( intersection, circle ); second_correctness.first = - std::get< 0 >( circle_output ) <= GLOBAL_EPSILON; + second_correctness.first + && std::get< 0 >( circle_output ) <= GLOBAL_EPSILON; second_correctness.second.emplace_back( std::move( std::get< 1 >( circle_output ) ) ); }; @@ -1170,22 +1182,8 @@ namespace geode InfiniteLine< dimension >{ segment }, ellipse ); if( line_intersections ) { - absl::InlinedVector< Point< dimension >, 2 > segment_intersections; - segment_intersections.reserve( - line_intersections.result.value().size() ); - for( auto&& point : line_intersections.result.value() ) - { - if( point_segment_distance( point, segment ) <= GLOBAL_EPSILON ) - { - segment_intersections.emplace_back( point ); - } - } - if( segment_intersections.empty() ) - { - return { INTERSECTION_TYPE::none }; - } - return { std::move( segment_intersections ), - std::move( line_intersections.correctness.value() ) }; + return filter_segment_intersections( + std::move( line_intersections ), segment ); } return line_intersections.type; } diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index 77e73b2d1..d44d6bb99 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -1160,6 +1160,64 @@ void test_line_cylinder_perpendicular() "Wrong result for line_cylinder_intersection with oblique line" ); } +template < typename Result > +bool has_consistent_correctness( const Result& result ) +{ + return result.has_intersection() + && result.correctness->first.second.size() == result.result->size() + && result.correctness->second.second.size() == result.result->size(); +} + +void test_segment_intersections_correctness() +{ + const geode::Point3D origin{ { 0, 0, 0 } }; + const geode::Point3D outside{ { 2, 0, 0 } }; + const geode::Sphere3D sphere{ origin, 1 }; + const auto sphere_result = geode::segment_sphere_intersection( + geode::Segment3D{ origin, outside }, sphere ); + geode::OpenGeodeGeometryException::test( + sphere_result.result->size() == 1 + && has_consistent_correctness( sphere_result ) + && sphere_result.correctness->second.second.front().inexact_equal( + geode::Point3D{ { 1, 0, 0 } } ), + "Wrong correctness for segment_sphere_intersection" ); + + const geode::Point3D x_axis{ { 1, 0, 0 } }; + const geode::Cylinder cylinder{ geode::Segment3D{ origin, x_axis }, 0.2 }; + const geode::Point3D c{ { 0.5, 0.0, 0.0 } }; + const geode::Point3D d{ { 0.5, 1.0, 0.0 } }; + const auto cylinder_result = geode::segment_cylinder_intersection( + geode::Segment3D{ c, d }, cylinder ); + geode::OpenGeodeGeometryException::test( + cylinder_result.result->size() == 1 + && has_consistent_correctness( cylinder_result ), + "Wrong correctness for segment_cylinder_intersection" ); + + const geode::Point2D center{ { 0.0, 0.0 } }; + const geode::Point2D far{ { 4.0, 0.0 } }; + const geode::Frame2D frame{ { geode::Vector2D{ { 3.0, 0.0 } }, + geode::Vector2D{ { 0.0, 2.0 } } } }; + const geode::Ellipse2D ellipse{ center, frame }; + const auto ellipse_result = geode::segment_ellipse_intersection( + geode::Segment2D{ center, far }, ellipse ); + geode::OpenGeodeGeometryException::test( + ellipse_result.result->size() == 1 + && has_consistent_correctness( ellipse_result ) + && ellipse_result.correctness->second.second.front().inexact_equal( + geode::Point2D{ { 3, 0 } } ), + "Wrong correctness for segment_ellipse_intersection" ); + + const geode::Plane plane{ geode::Vector3D{ { 0, 0, 1 } }, origin }; + const geode::Plane circle_plane{ geode::Vector3D{ { 1, 0, 0 } }, origin }; + const geode::Circle circle{ circle_plane, 1 }; + const auto circle_result = + geode::plane_circle_intersection( plane, circle ); + geode::OpenGeodeGeometryException::test( + circle_result.result->size() == 2 + && has_consistent_correctness( circle_result ), + "Wrong correctness for plane_circle_intersection" ); +} + void test() { test_line_sphere_intersection(); @@ -1178,6 +1236,7 @@ void test() test_segment_ellipse_intersection(); test_degenerated_segment_intersections(); test_line_cylinder_perpendicular(); + test_segment_intersections_correctness(); } OPENGEODE_TEST( "intersection" ) From dd78ca317bd982d9e6c3e698b6f4ca7f15ff5cea Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:46:48 +0200 Subject: [PATCH 13/52] fix(Intersection): stable discriminant in line_sphere_intersection The discriminant |P-C|^2 - (D.(P-C))^2 - r^2 suffered catastrophic cancellation when the line origin was far from the sphere (no or wrong intersections for an origin at 1e8), and compared an area to GLOBAL_EPSILON, reporting tangency for lines 0.4e-4 away from a sphere of radius 1e-4. The sphere center is now projected on the line, the distance h is computed directly, the tangency test compares the length r - h to GLOBAL_EPSILON, and the roots use (r-h)*(r+h) from the projected point. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/intersection.cpp | 38 +++++++++++++++------------- tests/geometry/test-intersection.cpp | 35 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/geode/geometry/intersection.cpp b/src/geode/geometry/intersection.cpp index ddbcbf592..b239da7c1 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -219,26 +219,28 @@ namespace geode line_sphere_intersection( const InfiniteLine< dimension >& line, const Sphere< dimension >& sphere ) { - // The sphere is (X-C)^T*(X-C)-1 = 0 and the line is X = P+t*D. - // Substitute the line equation into the sphere equation to obtain a - // quadratic equation Q(t) = t^2 + 2*a1*t + a0 = 0, where a1 = - // D^T*(P-C), - // and a0 = (P-C)^T*(P-C)-1. + // The sphere is |X-C| = r and the line is X = P+t*D, with D unit. + // Let H = P+t_c*D be the projection of C on the line, with + // t_c = -D^T*(P-C), and h = |H-C| the distance from C to the line. + // The intersections are at t = t_c +- sqrt( (r-h)*(r+h) ). Computing + // h directly avoids the cancellation of |P-C|^2 - (D^T*(P-C))^2 when + // P is far from C. const Vector< dimension > diff{ sphere.origin(), line.origin() }; - const auto a0 = diff.dot( diff ) - sphere.radius() * sphere.radius(); - const auto a1 = line.direction().dot( diff ); - - // Intersection occurs when Q(t) has real roots. - const auto discr = a1 * a1 - a0; - if( discr > GLOBAL_EPSILON ) // positive + const auto closest_parameter = -line.direction().dot( diff ); + const auto closest_point = + line.origin() + line.direction() * closest_parameter; + const auto distance_to_center = + point_point_distance( closest_point, sphere.origin() ); + const auto radius_minus_distance = sphere.radius() - distance_to_center; + if( radius_minus_distance > GLOBAL_EPSILON ) // two intersections { absl::InlinedVector< Point< dimension >, 2 > results; - const auto root = std::sqrt( discr ); + const auto root = + std::sqrt( radius_minus_distance + * ( sphere.radius() + distance_to_center ) ); results.reserve( 2 ); - results.emplace_back( - line.origin() + line.direction() * ( -a1 - root ) ); - results.emplace_back( - line.origin() + line.direction() * ( -a1 + root ) ); + results.emplace_back( closest_point - line.direction() * root ); + results.emplace_back( closest_point + line.direction() * root ); typename CorrectnessInfo< absl::InlinedVector< Point< dimension >, 2 > >::Correctness first_correctness; first_correctness.first = @@ -268,11 +270,11 @@ namespace geode return { std::move( results ), { first_correctness, second_correctness } }; } - else if( discr > -GLOBAL_EPSILON ) // zero + else if( radius_minus_distance >= -GLOBAL_EPSILON ) // tangent { absl::InlinedVector< Point< dimension >, 2 > results; results.reserve( 1 ); - results.emplace_back( line.origin() - line.direction() * a1 ); + results.emplace_back( closest_point ); typename CorrectnessInfo< absl::InlinedVector< Point< dimension >, 2 > >::Correctness first_correctness; first_correctness.first = diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index d44d6bb99..fb96e4cfe 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -1218,6 +1218,40 @@ void test_segment_intersections_correctness() "Wrong correctness for plane_circle_intersection" ); } +void test_line_sphere_intersection_robustness() +{ + const geode::Point3D origin{ { 0, 0, 0 } }; + const geode::Sphere3D sphere{ origin, 1 }; + const geode::OwnerInfiniteLine3D far_line{ geode::Vector3D{ { -1, 0, 0 } }, + geode::Point3D{ { 1e8, 0.5, 0 } } }; + const auto far_result = geode::line_sphere_intersection( + geode::InfiniteLine3D{ far_line }, sphere ); + const auto x = std::sqrt( 0.75 ); + geode::OpenGeodeGeometryException::test( + far_result && far_result.result->size() == 2 + && far_result.result->front().inexact_equal( + geode::Point3D{ { x, 0.5, 0 } } ) + && far_result.result->back().inexact_equal( + geode::Point3D{ { -x, 0.5, 0 } } ), + "Wrong line_sphere_intersection with far line origin" ); + + const geode::Sphere3D small_sphere{ origin, 1e-4 }; + const geode::OwnerInfiniteLine3D close_line{ geode::Vector3D{ { 1, 0, 0 } }, + geode::Point3D{ { 0, 1.4e-4, 0 } } }; + geode::OpenGeodeGeometryException::test( + !geode::line_sphere_intersection( + geode::InfiniteLine3D{ close_line }, small_sphere ), + "Wrong line_sphere_intersection with small sphere" ); + const geode::OwnerInfiniteLine3D tangent_line{ + geode::Vector3D{ { 1, 0, 0 } }, geode::Point3D{ { 0, 1e-4, 0 } } + }; + const auto tangent_result = geode::line_sphere_intersection( + geode::InfiniteLine3D{ tangent_line }, small_sphere ); + geode::OpenGeodeGeometryException::test( + tangent_result && tangent_result.result->size() == 1, + "Wrong line_sphere_intersection with tangent line" ); +} + void test() { test_line_sphere_intersection(); @@ -1237,6 +1271,7 @@ void test() test_degenerated_segment_intersections(); test_line_cylinder_perpendicular(); test_segment_intersections_correctness(); + test_line_sphere_intersection_robustness(); } OPENGEODE_TEST( "intersection" ) From 60154c78d7271e82f0ad562b57d92d922ffef5df Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:51:21 +0200 Subject: [PATCH 14/52] fix(Circle): fix bounding_box for normals opposite to an axis The sine of the angle between the circle normal and an axis returned M_PI instead of 0 when the normal was opposite to that axis, inflating the bounding box along that axis by PI times the radius. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/basic_objects/circle.cpp | 6 +----- tests/geometry/test-basic-objects.cpp | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/geode/geometry/basic_objects/circle.cpp b/src/geode/geometry/basic_objects/circle.cpp index 6f0a27aa9..bd5f1e87f 100644 --- a/src/geode/geometry/basic_objects/circle.cpp +++ b/src/geode/geometry/basic_objects/circle.cpp @@ -60,11 +60,7 @@ namespace geode { const auto sin_angle = [this]( Vector3D direction ) { const auto dP = plane_.normal().dot( direction ); - if( dP <= -1.0 ) - { - return M_PI; - } - if( dP >= 1.0 ) + if( std::fabs( dP ) >= 1.0 ) { return 0.; } diff --git a/tests/geometry/test-basic-objects.cpp b/tests/geometry/test-basic-objects.cpp index c74ed1743..69a4d9f36 100644 --- a/tests/geometry/test-basic-objects.cpp +++ b/tests/geometry/test-basic-objects.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include @@ -311,6 +312,24 @@ void test_polygon_minimum_height() flat.is_degenerated(), "Flat polygon should be degenerated" ); } +void test_circle_bounding_box() +{ + const geode::Point3D origin{ { 0, 0, 0 } }; + for( const auto sign : { -1., 1. } ) + { + const geode::Plane plane{ geode::Vector3D{ { sign, 0, 0 } }, origin }; + const geode::Circle circle{ plane, 1 }; + const auto bbox = circle.bounding_box(); + geode::OpenGeodeGeometryException::test( + bbox.min().value( 0 ) == 0 && bbox.max().value( 0 ) == 0 + && std::fabs( bbox.min().value( 1 ) + 1 ) + <= geode::GLOBAL_EPSILON + && std::fabs( bbox.max().value( 2 ) - 1 ) + <= geode::GLOBAL_EPSILON, + "Wrong circle bounding box for normal ", sign, " x" ); + } +} + void test() { test_segment(); @@ -321,6 +340,7 @@ void test() test_sphere(); test_circle(); test_polygon_minimum_height(); + test_circle_bounding_box(); } OPENGEODE_TEST( "basic-object" ) From e56031e47072c24ae2c4b5ec4bbf0d8158f56bf8 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 22:56:49 +0200 Subject: [PATCH 15/52] fix(BoundingBox): scale-independent triangle epsilon_intersects The separating axis tests of BoundingBox3D::intersects and epsilon_intersects with a Triangle3D used unnormalized axes (triangle normal, edge x box axis cross products), so GLOBAL_EPSILON was compared to projections scaled by |axis| (up to edge length squared): large triangles 0.5e-6 away from a box were not epsilon-intersecting. The tolerance is now scaled by the axis length. The triangle normal test projects all three vertices instead of only the first one, which is wrong when the normal is noisy, and the box face tests, redundant with the triangle bounding box test, are removed. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/bounding_box.cpp | 199 ++++++++++----------------- tests/geometry/test-bounding-box.cpp | 39 ++++++ 2 files changed, 109 insertions(+), 129 deletions(-) diff --git a/src/geode/geometry/bounding_box.cpp b/src/geode/geometry/bounding_box.cpp index 30c58bbaf..54ef910ca 100644 --- a/src/geode/geometry/bounding_box.cpp +++ b/src/geode/geometry/bounding_box.cpp @@ -35,33 +35,76 @@ namespace { - geode::BoundingBox1D triangle_1d_projection( - const geode::Triangle3D& triangle, const geode::Vector3D& normal ) + /* + * Return true if the given axis separates the triangle and the box, with + * a tolerance of epsilon (a length) between their projections. + * The axis does not need to be normalized. + */ + bool is_separating_axis( const geode::Vector3D& axis, + const geode::Triangle3D& triangle, + const geode::Vector3D& box_center, + const geode::Vector3D& box_half_diagonal, + double epsilon ) { const auto& vertices = triangle.vertices(); - geode::BoundingBox1D interval; - interval.add_point( geode::Point1D{ - { normal.dot( geode::Vector3D{ vertices[0].get() } ) } } ); - interval.add_point( geode::Point1D{ - { normal.dot( geode::Vector3D{ vertices[1].get() } ) } } ); - interval.add_point( geode::Point1D{ - { normal.dot( geode::Vector3D{ vertices[2].get() } ) } } ); - return interval; - } - - geode::BoundingBox1D bbox_1d_projection( - const geode::BoundingBox3D& box, const geode::Vector3D& normal ) - { - const auto box_diagonal = box.diagonal(); - const auto origin = normal.dot( geode::Vector3D{ box.center() } ); - const auto maximum_extent = - ( std::fabs( normal.value( 0 ) * box_diagonal.value( 0 ) ) - + std::fabs( normal.value( 1 ) * box_diagonal.value( 1 ) ) - + std::fabs( normal.value( 2 ) * box_diagonal.value( 2 ) ) ) - / 2.; - return geode::BoundingBox1D{ geode::Point1D{ - { origin - maximum_extent } }, - geode::Point1D{ { origin + maximum_extent } } }; + const auto projection0 = + axis.dot( geode::Vector3D{ vertices[0].get() } ); + const auto projection1 = + axis.dot( geode::Vector3D{ vertices[1].get() } ); + const auto projection2 = + axis.dot( geode::Vector3D{ vertices[2].get() } ); + const auto triangle_min = + std::min( { projection0, projection1, projection2 } ); + const auto triangle_max = + std::max( { projection0, projection1, projection2 } ); + const auto box_origin = axis.dot( box_center ); + const auto box_extent = + std::fabs( axis.value( 0 ) * box_half_diagonal.value( 0 ) ) + + std::fabs( axis.value( 1 ) * box_half_diagonal.value( 1 ) ) + + std::fabs( axis.value( 2 ) * box_half_diagonal.value( 2 ) ); + // Projections are scaled by the axis length: so is the tolerance + const auto tolerance = epsilon == 0 ? 0. : epsilon * axis.length(); + return triangle_min - ( box_origin + box_extent ) > tolerance + || ( box_origin - box_extent ) - triangle_max > tolerance; + } + + bool triangle_box_intersects( const geode::BoundingBox3D& box, + const geode::Triangle3D& triangle, + double epsilon ) + { + // Separating axis theorem. Box face directions are already tested + // by the triangle bounding box test of the caller. + const auto& vertices = triangle.vertices(); + const geode::Vector3D box_center{ box.center() }; + const auto box_half_diagonal = box.diagonal() / 2.; + const std::array< geode::Vector3D, 3 > edges{ + geode::Vector3D{ vertices[0].get(), vertices[1].get() }, + geode::Vector3D{ vertices[0].get(), vertices[2].get() }, + geode::Vector3D{ vertices[1].get(), vertices[2].get() } + }; + + // Test direction of triangle normal. + if( is_separating_axis( edges[0].cross( edges[1] ), triangle, + box_center, box_half_diagonal, epsilon ) ) + { + return false; + } + + // Test direction of triangle-box edge cross products. + for( const auto i0 : geode::LRange{ 3 } ) + { + for( const auto i1 : geode::LRange{ 3 } ) + { + geode::Vector3D axis; + axis.set_value( i1, 1 ); + if( is_separating_axis( edges[i0].cross( axis ), triangle, + box_center, box_half_diagonal, epsilon ) ) + { + return false; + } + } + } + return true; } template < geode::index_t dimension > @@ -478,58 +521,7 @@ namespace geode { return false; } - const Vector3D box_center{ center() }; - const auto box_diagonal = diagonal(); - const std::array< Vector3D, 3 > edges{ Vector3D{ vertices[0].get(), - vertices[1].get() }, - Vector3D{ vertices[0].get(), vertices[2].get() }, - Vector3D{ vertices[1].get(), vertices[2].get() } }; - - // Test direction of triangle normal. - const auto triangle_normal = edges[0].cross( edges[1] ); - if( !bbox_1d_projection( *this, triangle_normal ) - .contains( Point1D{ { triangle_normal.dot( - Vector3D{ vertices[0].get() } ) } } ) ) - { - return false; - } - - // Test direction of box faces. - for( const auto i : LRange{ 3 } ) - { - Vector3D axis; - axis.set_value( i, 1 ); - const auto triangle_interval = - triangle_1d_projection( triangle, axis ); - const auto center_value = box_center.value( i ); - const auto extent = box_diagonal.value( i ) / 2.; - const BoundingBox1D box_interval{ Point1D{ - { center_value - extent } }, - Point1D{ { center_value + extent } } }; - if( !triangle_interval.intersects( box_interval ) ) - { - return false; - } - } - - // Test direction of triangle-box edge cross products. - for( const auto i0 : LRange{ 3 } ) - { - for( const auto i1 : LRange{ 3 } ) - { - Vector3D axis; - axis.set_value( i1, 1 ); - const auto normal = edges[i0].cross( axis ); - const auto triangle_interval = - triangle_1d_projection( triangle, normal ); - const auto box_interval = bbox_1d_projection( *this, normal ); - if( !triangle_interval.intersects( box_interval ) ) - { - return false; - } - } - } - return true; + return triangle_box_intersects( *this, triangle, 0. ); } template <> @@ -549,58 +541,7 @@ namespace geode { return false; } - const Vector3D box_center{ center() }; - const auto box_diagonal = diagonal(); - const std::array< Vector3D, 3 > edges{ Vector3D{ vertices[0].get(), - vertices[1].get() }, - Vector3D{ vertices[0].get(), vertices[2].get() }, - Vector3D{ vertices[1].get(), vertices[2].get() } }; - - // Test direction of triangle normal. - const auto triangle_normal = edges[0].cross( edges[1] ); - if( !bbox_1d_projection( *this, triangle_normal ) - .epsilon_contains( Point1D{ { triangle_normal.dot( - Vector3D{ vertices[0].get() } ) } } ) ) - { - return false; - } - - // Test direction of box faces. - for( const auto i : LRange{ 3 } ) - { - Vector3D axis; - axis.set_value( i, 1 ); - const auto triangle_interval = - triangle_1d_projection( triangle, axis ); - const auto center_value = box_center.value( i ); - const auto extent = box_diagonal.value( i ) / 2.; - const BoundingBox1D box_interval{ Point1D{ - { center_value - extent } }, - Point1D{ { center_value + extent } } }; - if( !triangle_interval.epsilon_intersects( box_interval ) ) - { - return false; - } - } - - // Test direction of triangle-box edge cross products. - for( const auto i0 : LRange{ 3 } ) - { - for( const auto i1 : LRange{ 3 } ) - { - Vector3D axis; - axis.set_value( i1, 1 ); - const auto normal = edges[i0].cross( axis ); - const auto triangle_interval = - triangle_1d_projection( triangle, normal ); - const auto box_interval = bbox_1d_projection( *this, normal ); - if( !triangle_interval.epsilon_intersects( box_interval ) ) - { - return false; - } - } - } - return true; + return triangle_box_intersects( *this, triangle, GLOBAL_EPSILON ); } template <> diff --git a/tests/geometry/test-bounding-box.cpp b/tests/geometry/test-bounding-box.cpp index 4a89ce310..9bab0fe83 100644 --- a/tests/geometry/test-bounding-box.cpp +++ b/tests/geometry/test-bounding-box.cpp @@ -354,6 +354,44 @@ void test_segment_intersections() bbox.intersects( crossing2 ), "Wrong result with crossing2" ); } +void test_large_triangle3d_epsilon_intersections() +{ + geode::BoundingBox3D box; + box.add_point( geode::Point3D{ { 0, 0, 0 } } ); + box.add_point( geode::Point3D{ { 1, 1, 1 } } ); + const auto size = 2e4; + for( const auto& [offset, expected] : + { std::make_pair( 0.5e-6, true ), std::make_pair( 1e-3, false ) } ) + { + const geode::Point3D a{ { -size, -size, 1 + offset } }; + const geode::Point3D b{ { size, -size, 1 + offset } }; + const geode::Point3D c{ { 0, size, 1 + offset } }; + geode::OpenGeodeGeometryException::test( + box.epsilon_intersects( geode::Triangle3D{ a, b, c } ) == expected, + "Wrong epsilon_intersects with large horizontal triangle at " + "offset ", + offset ); + + // Triangle on a tilted plane, beyond the (1,1,1) box corner + const auto normal = geode::Vector3D{ { 1, 1, 1 } }.normalize(); + const auto u = geode::Vector3D{ { 1, -1, 0 } }.normalize(); + const auto v = normal.cross( u ); + const auto center = geode::Point3D{ { 1, 1, 1 } } + normal * offset; + const geode::Point3D d{ center + u * size }; + const geode::Point3D e{ center + + ( u * -0.5 + v * std::sqrt( 0.75 ) ) * size }; + const geode::Point3D f{ center + + ( u * -0.5 - v * std::sqrt( 0.75 ) ) * size }; + const geode::Triangle3D tilted{ d, e, f }; + geode::OpenGeodeGeometryException::test( + box.epsilon_intersects( tilted ) == expected, + "Wrong epsilon_intersects with large tilted triangle at offset ", + offset ); + geode::OpenGeodeGeometryException::test( !box.intersects( tilted ), + "Wrong intersects with large tilted triangle at offset ", offset ); + } +} + void test() { test_bbox(); @@ -363,6 +401,7 @@ void test() test_tetra_intersections(); test_triangle2d_intersections(); test_triangle3d_intersections(); + test_large_triangle3d_epsilon_intersections(); } OPENGEODE_TEST( "bounding-box" ) From dfbb566393bb30deee89938dbe38f63bf791ee87 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 23:01:12 +0200 Subject: [PATCH 16/52] fix(BoundingBox): fix 1D ray and line intersections The 1D line test returned true only when the origin was before the box in the line direction, so rays starting inside the box were reported as not intersecting, and infinite lines (which cover the whole axis) as well. 1D lines now always intersect; rays pointing away from the box are rejected by the existing ray quick-skip test. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/bounding_box.cpp | 20 ++++++------------ tests/geometry/test-bounding-box.cpp | 31 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/geode/geometry/bounding_box.cpp b/src/geode/geometry/bounding_box.cpp index 54ef910ca..81d4d96fd 100644 --- a/src/geode/geometry/bounding_box.cpp +++ b/src/geode/geometry/bounding_box.cpp @@ -203,16 +203,12 @@ namespace } template <> - bool line_epsilon_intersects< 1 >( const geode::BoundingBox< 1 >& box, - const geode::GenericLine< geode::RefPoint< 1 >, 1 >& line ) + bool line_epsilon_intersects< 1 >( const geode::BoundingBox< 1 >& /*box*/, + const geode::GenericLine< geode::RefPoint< 1 >, 1 >& /*line*/ ) { - if( line.direction().value( 0 ) > 0 ) - { - return line.origin().value( 0 ) - < box.min().value( 0 ) - geode::GLOBAL_EPSILON; - } - return line.origin().value( 0 ) - > box.max().value( 0 ) + geode::GLOBAL_EPSILON; + // A 1D line covers the whole axis. Rays pointing away from the box + // are rejected beforehand by ray_quick_skip. + return true; } template < geode::index_t dimension > @@ -238,11 +234,7 @@ namespace bool line_intersects< 1 >( const geode::BoundingBox< 1 >& box, const geode::GenericLine< geode::RefPoint< 1 >, 1 >& line ) { - if( line.direction().value( 0 ) > 0 ) - { - return line.origin().value( 0 ) < box.min().value( 0 ); - } - return line.origin().value( 0 ) > box.max().value( 0 ); + return line_epsilon_intersects< 1 >( box, line ); } template < geode::index_t dimension, typename Condition > diff --git a/tests/geometry/test-bounding-box.cpp b/tests/geometry/test-bounding-box.cpp index 9bab0fe83..1de052243 100644 --- a/tests/geometry/test-bounding-box.cpp +++ b/tests/geometry/test-bounding-box.cpp @@ -392,6 +392,36 @@ void test_large_triangle3d_epsilon_intersections() } } +void test_1d_line_intersections() +{ + geode::BoundingBox1D bbox; + bbox.add_point( geode::Point1D{ { 0 } } ); + bbox.add_point( geode::Point1D{ { 1 } } ); + const geode::Vector1D positive{ { 1 } }; + const geode::Vector1D negative{ { -1 } }; + const auto check_ray = [&bbox]( const geode::Vector1D& direction, + double origin, bool expected ) { + const geode::OwnerRay1D ray{ direction, geode::Point1D{ { origin } } }; + geode::OpenGeodeGeometryException::test( + bbox.intersects( ray ) == expected + && bbox.epsilon_intersects( ray ) == expected, + "Wrong 1D ray intersection with origin ", origin, " and direction ", + direction.value( 0 ) ); + const geode::OwnerInfiniteLine1D line{ direction, + geode::Point1D{ { origin } } }; + geode::OpenGeodeGeometryException::test( + bbox.intersects( line ) && bbox.epsilon_intersects( line ), + "Wrong 1D line intersection with origin ", origin, + " and direction ", direction.value( 0 ) ); + }; + check_ray( positive, 0.5, true ); + check_ray( negative, 0.5, true ); + check_ray( positive, -1, true ); + check_ray( negative, -1, false ); + check_ray( positive, 2, false ); + check_ray( negative, 2, true ); +} + void test() { test_bbox(); @@ -402,6 +432,7 @@ void test() test_triangle2d_intersections(); test_triangle3d_intersections(); test_large_triangle3d_epsilon_intersections(); + test_1d_line_intersections(); } OPENGEODE_TEST( "bounding-box" ) From c49c47fb0bae6efcbec25cfd99c2cd727a9c7587 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 23:05:33 +0200 Subject: [PATCH 17/52] fix(Quality): scale-invariant degeneracy in tetrahedron qualities tetrahedron_aspect_ratio and tetrahedron_collapse_aspect_ratio compared a determinant / volume (cubic in length) to GLOBAL_EPSILON, so a regular tetrahedron with edge 0.01 was reported as degenerated. The volume ratio qualities returned NaN (0/0) when vertices were coincident. All four now use Tetrahedron::is_degenerated (minimum height below GLOBAL_EPSILON): degenerated tetrahedra get the worst value (max for aspect ratios, 0 for volume ratios). Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/quality.cpp | 12 +++++----- tests/geometry/test-quality.cpp | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/geode/geometry/quality.cpp b/src/geode/geometry/quality.cpp index 393ebd04a..73576599e 100644 --- a/src/geode/geometry/quality.cpp +++ b/src/geode/geometry/quality.cpp @@ -160,12 +160,12 @@ namespace geode const Vector3D edge01{ vertices[0], vertices[1] }; const Vector3D edge02{ vertices[0], vertices[2] }; const Vector3D edge03{ vertices[0], vertices[3] }; - const auto absolute_det = - std::fabs( edge01.dot( edge02.cross( edge03 ) ) ); - if( absolute_det < GLOBAL_EPSILON ) + if( tetra.is_degenerated() ) { return std::numeric_limits< double >::max(); } + const auto absolute_det = + std::fabs( edge01.dot( edge02.cross( edge03 ) ) ); const Vector3D edge12{ vertices[1], vertices[2] }; const Vector3D edge13{ vertices[1], vertices[3] }; const Vector3D edge23{ vertices[2], vertices[3] }; @@ -184,7 +184,7 @@ namespace geode double tetrahedron_volume_to_facet_ratio( const Tetrahedron& tetra ) { const auto signed_volume = tetrahedron_signed_volume( tetra ); - if( signed_volume < 0. ) + if( signed_volume < 0. || tetra.is_degenerated() ) { return 0.; } @@ -211,7 +211,7 @@ namespace geode double tetrahedron_volume_to_edge_ratio( const Tetrahedron& tetra ) { const auto signed_volume = tetrahedron_signed_volume( tetra ); - if( signed_volume < 0. ) + if( signed_volume < 0. || tetra.is_degenerated() ) { return 0.; } @@ -231,7 +231,7 @@ namespace geode double tetrahedron_collapse_aspect_ratio( const Tetrahedron& tetra ) { - if( tetrahedron_volume( tetra ) < GLOBAL_EPSILON ) + if( tetra.is_degenerated() ) { return std::numeric_limits< double >::max(); } diff --git a/tests/geometry/test-quality.cpp b/tests/geometry/test-quality.cpp index 61f96bc79..e6c50cf69 100644 --- a/tests/geometry/test-quality.cpp +++ b/tests/geometry/test-quality.cpp @@ -137,6 +137,47 @@ void test_other_tetrahedron() "Wrong aspect ratio 4 for random tetrahedron ", quality4 ); } +void test_scale_invariant_quality() +{ + const auto qualities = []( double scale ) { + const geode::Point3D point0{ { 0, 0, 0 } }; + const geode::Point3D point1{ { scale, 0, 0 } }; + const geode::Point3D point2{ { 0.5 * scale, std::sqrt( 3 ) / 2 * scale, + 0 } }; + const geode::Point3D point3{ { 0.5 * scale, std::sqrt( 3 ) / 6 * scale, + std::sqrt( 2. / 3. ) * scale } }; + const geode::Tetrahedron tetra{ point0, point1, point2, point3 }; + return std::array< double, 4 >{ geode::tetrahedron_aspect_ratio( + tetra ), + geode::tetrahedron_volume_to_edge_ratio( tetra ), + geode::tetrahedron_collapse_aspect_ratio( tetra ), + geode::tetrahedron_volume_to_facet_ratio( tetra ) }; + }; + const auto reference = qualities( 1 ); + const auto small = qualities( 0.01 ); + for( const auto q : geode::LRange{ 4 } ) + { + geode::OpenGeodeGeometryException::test( + std::fabs( reference[q] - 1 ) < 1e-9 + && std::fabs( small[q] - reference[q] ) < 1e-9, + "Wrong quality ", q, " for small regular tetrahedron: ", small[q], + " vs ", reference[q] ); + } + + const geode::Point3D point{ { 1, 2, 3 } }; + const geode::Tetrahedron coincident{ point, point, point, point }; + geode::OpenGeodeGeometryException::test( + geode::tetrahedron_volume_to_edge_ratio( coincident ) == 0 + && geode::tetrahedron_volume_to_facet_ratio( coincident ) == 0, + "Wrong quality for tetrahedron with coincident vertices" ); + geode::OpenGeodeGeometryException::test( + geode::tetrahedron_aspect_ratio( coincident ) + == std::numeric_limits< double >::max() + && geode::tetrahedron_collapse_aspect_ratio( coincident ) + == std::numeric_limits< double >::max(), + "Wrong aspect ratio for tetrahedron with coincident vertices" ); +} + void test() { geode::Logger::set_level( geode::Logger::LEVEL::debug ); @@ -144,6 +185,7 @@ void test() test_regular_tetrahedron(); test_sliver_tetrahedron(); test_other_tetrahedron(); + test_scale_invariant_quality(); } OPENGEODE_TEST( "quality" ) \ No newline at end of file From 769319703cb7df60347d1b9383bab2de28b9d259 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Tue, 13 Oct 2026 23:10:36 +0200 Subject: [PATCH 18/52] fix(IsPointInside): exact predicates and consistent boundary handling is_point_inside_polygon used an inexact floating-point orientation test, and boundary points were inside or outside depending on which edge they lay on (e.g. bottom edge inside, top edge outside for a unit square). The winding number now uses GEO::PCK::orient_2d, and points on the polygon boundary are always considered inside, independently of the polygon orientation. Co-Authored-By: Claude Opus 5.5 --- include/geode/geometry/is_point_inside.hpp | 3 +- src/geode/geometry/is_point_inside.cpp | 63 ++++++++++++---------- tests/geometry/test-position.cpp | 37 +++++++++++++ 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/include/geode/geometry/is_point_inside.hpp b/include/geode/geometry/is_point_inside.hpp index a928e9a38..ec9163aef 100644 --- a/include/geode/geometry/is_point_inside.hpp +++ b/include/geode/geometry/is_point_inside.hpp @@ -37,7 +37,8 @@ namespace geode { /*! * Find if point is inside a polygon. - * @param[in] point The point to rotate. + * Points on the polygon boundary are considered inside. + * @param[in] point The point to test. */ [[nodiscard]] bool opengeode_geometry_api is_point_inside_polygon( const Point2D& point, const Polygon2D& polygon ); diff --git a/src/geode/geometry/is_point_inside.cpp b/src/geode/geometry/is_point_inside.cpp index ac9db7c91..807f170a9 100644 --- a/src/geode/geometry/is_point_inside.cpp +++ b/src/geode/geometry/is_point_inside.cpp @@ -24,19 +24,24 @@ #include #include +#include #include namespace { - bool is_left( const geode::Point2D& p0, - const geode::Point2D& p1, - const geode::Point2D& p2 ) + bool is_point_on_edge( const geode::Point2D& point, + const geode::Point2D& v0, + const geode::Point2D& v1 ) { - return ( p1.value( 0 ) - p0.value( 0 ) ) - * ( p2.value( 1 ) - p0.value( 1 ) ) - - ( p2.value( 0 ) - p0.value( 0 ) ) - * ( p1.value( 1 ) - p0.value( 1 ) ) - > 0; + for( const auto d : geode::LRange{ 2 } ) + { + if( point.value( d ) < std::min( v0.value( d ), v1.value( d ) ) + || point.value( d ) > std::max( v0.value( d ), v1.value( d ) ) ) + { + return false; + } + } + return true; } } // namespace @@ -45,34 +50,36 @@ namespace geode bool is_point_inside_polygon( const Point2D& point, const Polygon2D& polygon ) { - double winding_number{ 0 }; + // Winding number using exact orientation predicates. Points on the + // polygon boundary are considered inside, whatever the polygon + // orientation. + int winding_number{ 0 }; const auto& vertices = polygon.vertices(); - for( const auto polygon_vertex : geode::Range{ polygon.nb_vertices() } ) + const auto nb_vertices = polygon.nb_vertices(); + for( const auto polygon_vertex : geode::Range{ nb_vertices } ) { - const auto& v0 = vertices[polygon_vertex]; - const auto& next_polygon_vertex = - ( polygon_vertex + 1 ) % vertices.size(); - const auto& v1 = vertices[next_polygon_vertex]; - if( v0.get().value( 1 ) <= point.value( 1 ) ) + const Point2D& v0 = vertices[polygon_vertex]; + const Point2D& v1 = vertices[polygon_vertex + 1 == nb_vertices + ? 0 + : polygon_vertex + 1]; + const auto side = GEO::PCK::orient_2d( v0, v1, point ); + if( side == GEO::SIGN::zero && is_point_on_edge( point, v0, v1 ) ) { - if( v1.get().value( 1 ) > point.value( 1 ) ) - { - if( is_left( v0, v1, point ) ) - { - winding_number++; - } - } + return true; } - else + if( v0.value( 1 ) <= point.value( 1 ) ) { - if( v1.get().value( 1 ) <= point.value( 1 ) ) + if( v1.value( 1 ) > point.value( 1 ) + && side == GEO::SIGN::positive ) { - if( !is_left( v0, v1, point ) ) - { - winding_number--; - } + winding_number++; } } + else if( v1.value( 1 ) <= point.value( 1 ) + && side == GEO::SIGN::negative ) + { + winding_number--; + } } return winding_number != 0; } diff --git a/tests/geometry/test-position.cpp b/tests/geometry/test-position.cpp index 6a7c6b90f..065e7f42b 100644 --- a/tests/geometry/test-position.cpp +++ b/tests/geometry/test-position.cpp @@ -25,10 +25,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -532,6 +534,40 @@ void test_point_side_to_triangle() "q3" ); } +void test_point_inside_polygon() +{ + const geode::Point2D a{ { 0, 0 } }; + const geode::Point2D b{ { 1, 0 } }; + const geode::Point2D c{ { 1, 1 } }; + const geode::Point2D d{ { 0, 1 } }; + const geode::Polygon2D ccw{ { a, b, c, d } }; + const geode::Polygon2D cw{ { a, d, c, b } }; + const std::array< geode::Point2D, 9 > inside_points{ + geode::Point2D{ { 0.5, 0.5 } }, geode::Point2D{ { 0.5, 0 } }, + geode::Point2D{ { 1, 0.5 } }, geode::Point2D{ { 0.5, 1 } }, + geode::Point2D{ { 0, 0.5 } }, a, b, c, d + }; + for( const auto& point : inside_points ) + { + geode::OpenGeodeGeometryException::test( + geode::is_point_inside_polygon( point, ccw ) + && geode::is_point_inside_polygon( point, cw ), + "Point ", point.string(), " should be inside the polygon" ); + } + const std::array< geode::Point2D, 6 > outside_points{ + geode::Point2D{ { 1.5, 0.5 } }, geode::Point2D{ { -0.5, 0.5 } }, + geode::Point2D{ { 0.5, 1.5 } }, geode::Point2D{ { 0.5, -0.5 } }, + geode::Point2D{ { 2, 0 } }, geode::Point2D{ { -1, 1 } } + }; + for( const auto& point : outside_points ) + { + geode::OpenGeodeGeometryException::test( + !geode::is_point_inside_polygon( point, ccw ) + && !geode::is_point_inside_polygon( point, cw ), + "Point ", point.string(), " should be outside the polygon" ); + } +} + void test() { test_point_side_to_segment(); @@ -540,6 +576,7 @@ void test() test_point_segment_position(); test_point_triangle_position(); test_point_tetrahedron_position(); + test_point_inside_polygon(); } OPENGEODE_TEST( "position" ) \ No newline at end of file From feb1afc50b78d6763c8240f59f1428a503cb626b Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 00:43:13 +0200 Subject: [PATCH 19/52] fix(Distance): compute point-line/segment distances by projection point_line_distance and point_segment_distance used Heron's formula on the three side lengths, whose cancellation gives errors above GLOBAL_EPSILON (4e-6 measured) for points far along the line. Both now measure the distance to the projection, so they are accurate and consistent with point_line_projection / point_segment_projection. point_segment_projection computes the clamped parameter directly instead of normalizing the direction, and degenerated segments use their barycenter for both distance and projection (distance used the first vertex). Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 71 ++------------------- src/geode/geometry/projection.cpp | 22 ++++--- tests/geometry/test-distance.cpp | 102 ++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 76 deletions(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index f0e26da56..5bea54f9b 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -85,33 +85,6 @@ namespace <= geode::GLOBAL_EPSILON * geode::GLOBAL_EPSILON * max_length2; } - std::optional< double > compute_point_line_distance( double segment_length, - double point_to_v0_length, - double point_to_v1_length ) - { - const auto p = - ( point_to_v0_length + point_to_v1_length + segment_length ) / 2; - if( p - point_to_v0_length <= geode::GLOBAL_EPSILON ) - { - return std::nullopt; - } - if( p - point_to_v1_length <= geode::GLOBAL_EPSILON ) - { - return std::nullopt; - } - if( p - segment_length <= geode::GLOBAL_EPSILON ) - { - return std::nullopt; - } - auto area2 = p * ( p - point_to_v0_length ) * ( p - point_to_v1_length ) - * ( p - segment_length ); - if( area2 <= 0 ) - { - area2 = 0; - } - return 2 * std::sqrt( area2 ) / segment_length; - } - void get_min_edge02( double a11, double b1, std::array< double, 2 >& p ) { p[0] = 0; @@ -860,36 +833,8 @@ namespace geode double point_segment_distance( const Point< dimension >& point, const Segment< dimension >& segment ) { - const auto length = segment.length(); - const auto length0 = - point_point_distance( segment.vertices()[0].get(), point ); - if( length <= GLOBAL_EPSILON ) - { - return length0; - } - const auto length1 = - point_point_distance( segment.vertices()[1].get(), point ); - const auto sqr_length = length * length; - const auto sqr_length0 = length0 * length0; - const auto sqr_length1 = length1 * length1; - if( length0 >= length && length0 >= length1 - && sqr_length + sqr_length1 <= sqr_length0 ) - { // obtuse by vertex 1 - return length1; - } - if( length1 >= length && length1 >= length0 - && sqr_length + sqr_length0 <= sqr_length1 ) - { // obtuse by vertex 0 - return length0; - } - // acute angles - if( const auto distance = - compute_point_line_distance( length, length0, length1 ) ) - { - return distance.value(); - } - return std::get< 0 >( - point_segment_distance_using_projection( point, segment ) ); + return point_point_distance( + point, point_segment_projection( point, segment ) ); } template < index_t dimension > @@ -973,16 +918,8 @@ namespace geode double point_line_distance( const Point< dimension >& point, const InfiniteLine< dimension >& line ) { - const auto length0 = point_point_distance( line.origin(), point ); - const auto length1 = - point_point_distance( line.origin() + line.direction(), point ); - if( const auto distance = - compute_point_line_distance( 1, length0, length1 ) ) - { - return distance.value(); - } - return std::get< 0 >( - point_line_distance_using_projection( point, line ) ); + return point_point_distance( + point, point_line_projection( point, line ) ); } double point_line_signed_distance( diff --git a/src/geode/geometry/projection.cpp b/src/geode/geometry/projection.cpp index 0d9a18311..f801e7ab8 100644 --- a/src/geode/geometry/projection.cpp +++ b/src/geode/geometry/projection.cpp @@ -35,19 +35,23 @@ namespace geode Point< dimension > point_segment_projection( const Point< dimension >& point, const Segment< dimension >& segment ) { - const auto barycenter = segment.barycenter(); - const auto length = segment.length(); - if( length <= GLOBAL_EPSILON ) + const auto& vertices = segment.vertices(); + const Vector< dimension > edge{ vertices[0], vertices[1] }; + const auto length2 = edge.length2(); + if( length2 <= GLOBAL_EPSILON * GLOBAL_EPSILON ) { - return barycenter; + return segment.barycenter(); } - const auto norm_dir = segment.direction(); - const auto d = norm_dir.dot( { barycenter, point } ); - if( std::fabs( d ) <= length / 2. ) + const auto dot = edge.dot( { vertices[0], point } ); + if( dot <= 0 ) { - return { barycenter + norm_dir * d }; + return vertices[0]; } - return d > 0 ? segment.vertices()[1] : segment.vertices()[0]; + if( dot >= length2 ) + { + return vertices[1]; + } + return vertices[0].get() + edge * ( dot / length2 ); } template < index_t dimension > diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index 8679a954a..776d366e2 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1447,6 +1447,107 @@ void test_degenerated_point_triangle_distance_2d() "Wrong point_triangle_distance 2D with point triangle: ", distance2 ); } +long double reference_distance( const geode::Point3D& point, + const geode::Point3D& origin, + const geode::Vector3D& direction, + bool clamp ) +{ + std::array< long double, 3 > diff; + long double dot{ 0 }; + long double length2{ 0 }; + for( const auto d : geode::LRange{ 3 } ) + { + diff[d] = static_cast< long double >( point.value( d ) ) + - static_cast< long double >( origin.value( d ) ); + const auto dir = static_cast< long double >( direction.value( d ) ); + dot += diff[d] * dir; + length2 += dir * dir; + } + auto t = dot / length2; + if( clamp ) + { + t = std::clamp( t, 0.0L, 1.0L ); + } + long double distance2{ 0 }; + for( const auto d : geode::LRange{ 3 } ) + { + const auto perp = + diff[d] - t * static_cast< long double >( direction.value( d ) ); + distance2 += perp * perp; + } + return std::sqrt( distance2 ); +} + +void test_point_segment_line_distance_accuracy() +{ + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -1., 1. }; + std::uniform_real_distribution< double > far_distribution{ -1e4, 1e4 }; + std::uniform_real_distribution< double > height_distribution{ 0, 1e-3 }; + const auto random_vector = [&generator, &distribution] { + return geode::Vector3D{ { distribution( generator ), + distribution( generator ), distribution( generator ) } }; + }; + for( const auto trial : geode::Range{ 1000 } ) + { + geode_unused( trial ); + const geode::Point3D v0{ random_vector() }; + const auto edge = random_vector(); + const geode::Point3D v1 = v0 + edge; + const geode::Segment3D segment{ v0, v1 }; + const geode::InfiniteLine3D line{ segment }; + const auto perpendicular = edge.cross( random_vector() ).normalize() + * height_distribution( generator ); + + const geode::Point3D far_point = + v0 + edge * far_distribution( generator ) + perpendicular; + const auto line_distance = + geode::point_line_distance( far_point, line ); + const auto line_projection = + geode::point_line_projection( far_point, line ); + const auto line_reference = reference_distance( + far_point, line.origin(), line.direction(), false ); + geode::OpenGeodeGeometryException::test( + std::fabs( geode::point_point_distance( far_point, line_projection ) + - line_distance ) + <= 1e-12, + "Inconsistent point_line_distance and point_line_projection" ); + geode::OpenGeodeGeometryException::test( + std::fabs( line_distance - line_reference ) < 1e-9, + "Inaccurate point_line_distance: ", line_distance, " vs ", + static_cast< double >( line_reference ) ); + + const geode::Point3D point = + v0 + edge * ( 2 * distribution( generator ) + 0.5 ) + perpendicular; + const auto segment_distance = + geode::point_segment_distance( point, segment ); + const auto segment_projection = + geode::point_segment_projection( point, segment ); + const auto segment_reference = + reference_distance( point, v0, edge, true ); + geode::OpenGeodeGeometryException::test( + std::fabs( geode::point_point_distance( point, segment_projection ) + - segment_distance ) + <= 1e-12, + "Inconsistent point_segment_distance and " + "point_segment_projection" ); + geode::OpenGeodeGeometryException::test( + std::fabs( segment_distance - segment_reference ) < 1e-9, + "Inaccurate point_segment_distance: ", segment_distance, " vs ", + static_cast< double >( segment_reference ) ); + } + + const geode::Point3D a{ { 1, 1, 1 } }; + const geode::Point3D b{ { 1, 1, 1 + 5e-7 } }; + const geode::Segment3D degenerated{ a, b }; + const geode::Point3D q{ { 2, 1, 1 } }; + geode::OpenGeodeGeometryException::test( + geode::point_segment_distance( q, degenerated ) + == geode::point_point_distance( + q, geode::point_segment_projection( q, degenerated ) ), + "Inconsistent distance and projection for degenerated segment" ); +} + void test() { test_point_segment_distance(); @@ -1467,6 +1568,7 @@ void test() test_random_triangle_triangle_distance(); test_nan_point_triangle_distance(); test_degenerated_point_triangle_distance_2d(); + test_point_segment_line_distance_accuracy(); } OPENGEODE_TEST( "distance" ) From a47191e809fc3f852025aad04258a1e2b875f224 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 00:46:41 +0200 Subject: [PATCH 20/52] fix(Point): combine coordinates hashes with absl::Hash std::hash XOR-ed the per-coordinate hashes, so every point with equal coordinates hashed to 0 and permuted coordinates collided ((1,2) and (2,1)). They now delegate to absl::Hash, which combines the coordinate array through AbslHashValue. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/point.cpp | 7 ++----- tests/geometry/test-point.cpp | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/geode/geometry/point.cpp b/src/geode/geometry/point.cpp index a353120cf..d0a015ab9 100644 --- a/src/geode/geometry/point.cpp +++ b/src/geode/geometry/point.cpp @@ -240,15 +240,12 @@ namespace std size_t hash< geode::Point2D >::operator()( const geode::Point2D &point ) const { - return absl::Hash< double >()( point.value( 0 ) ) - ^ absl::Hash< double >()( point.value( 1 ) ); + return absl::Hash< geode::Point2D >()( point ); } size_t hash< geode::Point3D >::operator()( const geode::Point3D &point ) const { - return absl::Hash< double >()( point.value( 0 ) ) - ^ absl::Hash< double >()( point.value( 1 ) ) - ^ absl::Hash< double >()( point.value( 2 ) ); + return absl::Hash< geode::Point3D >()( point ); } } // namespace std diff --git a/tests/geometry/test-point.cpp b/tests/geometry/test-point.cpp index 00cb53b92..0ad856810 100644 --- a/tests/geometry/test-point.cpp +++ b/tests/geometry/test-point.cpp @@ -110,11 +110,34 @@ void test_interpolation() attribute->value( 7 ).string() ); } +void test_point_hash() +{ + const std::hash< geode::Point2D > hash2d; + geode::OpenGeodeGeometryException::test( + hash2d( geode::Point2D{ { 1, 1 } } ) + != hash2d( geode::Point2D{ { 7, 7 } } ), + "Hash of (1,1) and (7,7) should differ" ); + geode::OpenGeodeGeometryException::test( + hash2d( geode::Point2D{ { 1, 2 } } ) + != hash2d( geode::Point2D{ { 2, 1 } } ), + "Hash of (1,2) and (2,1) should differ" ); + const std::hash< geode::Point3D > hash3d; + geode::OpenGeodeGeometryException::test( + hash3d( geode::Point3D{ { 1, 2, 3 } } ) + != hash3d( geode::Point3D{ { 3, 2, 1 } } ), + "Hash of (1,2,3) and (3,2,1) should differ" ); + geode::OpenGeodeGeometryException::test( + hash3d( geode::Point3D{ { 5, 5, 0 } } ) + != hash3d( geode::Point3D{ { 0, 0, 0 } } ), + "Hash of (5,5,0) and (0,0,0) should differ" ); +} + void test() { test_comparison(); test_operators(); test_interpolation(); + test_point_hash(); } OPENGEODE_TEST( "point" ) \ No newline at end of file From e2f522375e1cca917c02a40320f43f757d067495 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 01:06:54 +0200 Subject: [PATCH 21/52] fix(Predicates): statically initialize expansion constants The exact arithmetic splitter and epsilon were zero until GEO::PCK::initialize() ran (through OpenGeodeGeometryLibrary::initialize). Without it, two_product produced wrong error terms and exact orient_2d / orient_3d returned zero or wrong signs for near-degenerate inputs; this was the case in geometry unit tests, which only initialize the basic library. The constants are now statically initialized to their IEEE-754 values (2^27 + 1 and 2^-53). Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/internal/predicates.cpp | 7 ++- tests/geometry/test-position.cpp | 66 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/internal/predicates.cpp index 10ffacaf2..ebab93691 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/internal/predicates.cpp @@ -875,8 +875,11 @@ namespace namespace GEO { - double expansion_splitter_; - double expansion_epsilon_; + // Statically initialized with the IEEE-754 double values computed by + // PCK::initialize(), so that exact predicates are correct even if the + // library was not initialized. + double expansion_splitter_{ 134217729.0 }; // 2^27 + 1 + double expansion_epsilon_{ 1.1102230246251565e-16 }; // 2^-53 // ============= expansion sum and difference ========================= diff --git a/tests/geometry/test-position.cpp b/tests/geometry/test-position.cpp index 065e7f42b..4d472b17e 100644 --- a/tests/geometry/test-position.cpp +++ b/tests/geometry/test-position.cpp @@ -21,6 +21,8 @@ * */ +#include + #include #include @@ -568,6 +570,69 @@ void test_point_inside_polygon() } } +void test_exact_predicates_near_degenerate() +{ + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -1e3, 1e3 }; + for( const auto trial : geode::Range{ 1000 } ) + { + geode_unused( trial ); + auto x0 = distribution( generator ); + auto x1 = distribution( generator ); + if( x0 == x1 ) + { + continue; + } + if( x0 > x1 ) + { + std::swap( x0, x1 ); + } + const auto x2 = distribution( generator ); + // Points on the line y = x are exactly collinear + const geode::Point2D p0{ { x0, x0 } }; + const geode::Point2D p1{ { x1, x1 } }; + const geode::Segment2D segment{ p0, p1 }; + geode::OpenGeodeGeometryException::test( + geode::point_side_to_segment( + geode::Point2D{ { x2, x2 } }, segment ) + == geode::SIDE::zero, + "Wrong side for collinear point" ); + const auto side_up = geode::point_side_to_segment( + geode::Point2D{ { x2, std::nextafter( x2, 1e300 ) } }, segment ); + const auto side_down = geode::point_side_to_segment( + geode::Point2D{ { x2, std::nextafter( x2, -1e300 ) } }, segment ); + geode::OpenGeodeGeometryException::test( + side_up != geode::SIDE::zero && side_down != geode::SIDE::zero + && side_up != side_down, + "Wrong side for points one ulp away from the line" ); + + // Points on the plane x = y are exactly coplanar + const auto z0 = distribution( generator ); + const auto z1 = distribution( generator ); + const geode::Point3D q0{ { x0, x0, z0 } }; + const geode::Point3D q1{ { x1, x1, z0 } }; + const geode::Point3D q2{ { x2, x2, z1 } }; + const geode::Triangle3D triangle{ q0, q1, q2 }; + const auto x3 = distribution( generator ); + const auto z3 = distribution( generator ); + geode::OpenGeodeGeometryException::test( + geode::point_side_to_triangle( + geode::Point3D{ { x3, x3, z3 } }, triangle ) + == geode::SIDE::zero, + "Wrong side for coplanar point" ); + const auto side_above = geode::point_side_to_triangle( + geode::Point3D{ { x3, std::nextafter( x3, 1e300 ), z3 } }, + triangle ); + const auto side_below = geode::point_side_to_triangle( + geode::Point3D{ { x3, std::nextafter( x3, -1e300 ), z3 } }, + triangle ); + geode::OpenGeodeGeometryException::test( + side_above != geode::SIDE::zero && side_below != geode::SIDE::zero + && side_above != side_below, + "Wrong side for points one ulp away from the plane" ); + } +} + void test() { test_point_side_to_segment(); @@ -577,6 +642,7 @@ void test() test_point_triangle_position(); test_point_tetrahedron_position(); test_point_inside_polygon(); + test_exact_predicates_near_degenerate(); } OPENGEODE_TEST( "position" ) \ No newline at end of file From f164ce5567284cb859a24f5c69bc5b2cfa577e5a Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 01:12:44 +0200 Subject: [PATCH 22/52] fix(Predicates): disable FP contraction for exact predicates The exact arithmetic of predicates.cpp (two_product, expansion sums) relies on every operation being rounded; contracting a*b+c into fused multiply-add breaks it. Only MSVC was protected by a pragma. The file is now compiled with -ffp-contract=off on GCC and Clang (and excluded from unity builds so the option applies to it alone). Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/CMakeLists.txt | 11 ++++++++++ tests/geometry/test-position.cpp | 34 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/geode/geometry/CMakeLists.txt b/src/geode/geometry/CMakeLists.txt index b70775c6e..0e2a5fb50 100644 --- a/src/geode/geometry/CMakeLists.txt +++ b/src/geode/geometry/CMakeLists.txt @@ -110,6 +110,7 @@ add_geode_library( "internal/position_from_sides.hpp" "internal/predicates.hpp" SKIP_UNITY + "internal/predicates.cpp" "position.cpp" PUBLIC_DEPENDENCIES Bitsery::bitsery @@ -120,3 +121,13 @@ add_geode_library( earcut_hpp::earcut_hpp nanoflann::nanoflann ) + +# Exact predicates rely on exact floating-point rounding of each operation: +# forbid contraction of a*b+c into fused multiply-add instructions. +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set_source_files_properties( + "${PROJECT_SOURCE_DIR}/src/geode/geometry/internal/predicates.cpp" + PROPERTIES + COMPILE_OPTIONS "-ffp-contract=off" + ) +endif() diff --git a/tests/geometry/test-position.cpp b/tests/geometry/test-position.cpp index 4d472b17e..8e7a07dcb 100644 --- a/tests/geometry/test-position.cpp +++ b/tests/geometry/test-position.cpp @@ -633,6 +633,39 @@ void test_exact_predicates_near_degenerate() } } +void test_exact_orientation_large_coordinates() +{ + // Products of large coordinates where an FMA contraction in the exact + // arithmetic would lose the error terms. + std::mt19937 generator{ 7 }; + std::uniform_real_distribution< double > distribution{ -1e8, 1e8 }; + for( const auto trial : geode::Range{ 1000 } ) + { + geode_unused( trial ); + const auto x0 = distribution( generator ); + const auto x1 = distribution( generator ); + const auto x2 = distribution( generator ); + const geode::Point2D p0{ { x0, 3 * x0 } }; + const geode::Point2D p1{ { x1, 3 * x1 } }; + const auto y2 = 3 * x2; + const auto side_up = geode::point_side_to_segment( + geode::Point2D{ { x2, std::nextafter( y2, 1e300 ) } }, + geode::Segment2D{ p0, p1 } ); + const auto side_down = geode::point_side_to_segment( + geode::Point2D{ { x2, std::nextafter( y2, -1e300 ) } }, + geode::Segment2D{ p0, p1 } ); + geode::OpenGeodeGeometryException::test( + x0 == x1 + || ( side_up != side_down && side_up != geode::SIDE::zero + && side_down != geode::SIDE::zero ) + || ( geode::point_side_to_segment( + geode::Point2D{ { x2, y2 } }, { p0, p1 } ) + != geode::SIDE::zero ), + "Wrong side for points one ulp away from a line with large " + "coordinates" ); + } +} + void test() { test_point_side_to_segment(); @@ -643,6 +676,7 @@ void test() test_point_tetrahedron_position(); test_point_inside_polygon(); test_exact_predicates_near_degenerate(); + test_exact_orientation_large_coordinates(); } OPENGEODE_TEST( "position" ) \ No newline at end of file From 5fff17c0384e08925bd92f664ee717a36b726bbc Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 01:19:36 +0200 Subject: [PATCH 23/52] test(Geometry): add tier2-robustness benchmark Co-Authored-By: Claude Opus 5.5 --- tests/geometry/CMakeLists.txt | 7 + tests/geometry/benchmark-tier2-robustness.cpp | 460 ++++++++++++++++++ 2 files changed, 467 insertions(+) create mode 100644 tests/geometry/benchmark-tier2-robustness.cpp diff --git a/tests/geometry/CMakeLists.txt b/tests/geometry/CMakeLists.txt index 0d0da9f02..5a812ba98 100644 --- a/tests/geometry/CMakeLists.txt +++ b/tests/geometry/CMakeLists.txt @@ -173,3 +173,10 @@ add_geode_test( ${PROJECT_NAME}::geometry ) +add_geode_test( + SOURCE "benchmark-tier2-robustness.cpp" + LOCAL + DEPENDENCIES + ${PROJECT_NAME}::basic + ${PROJECT_NAME}::geometry +) diff --git a/tests/geometry/benchmark-tier2-robustness.cpp b/tests/geometry/benchmark-tier2-robustness.cpp new file mode 100644 index 000000000..ad8d8f780 --- /dev/null +++ b/tests/geometry/benchmark-tier2-robustness.cpp @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2019 - 2026 Geode-solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + constexpr geode::index_t NB_INPUTS{ 10000 }; + volatile double sink{ 0 }; + + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -10, 10 }; + std::uniform_real_distribution< double > unit_distribution{ -1, 1 }; + + double random_value() + { + return distribution( generator ); + } + + template < geode::index_t dimension > + geode::Point< dimension > random_point() + { + geode::Point< dimension > point; + for( const auto d : geode::LRange{ dimension } ) + { + point.set_value( d, random_value() ); + } + return point; + } + + template < geode::index_t dimension > + geode::Vector< dimension > random_vector( double scale ) + { + geode::Vector< dimension > vector; + for( const auto d : geode::LRange{ dimension } ) + { + vector.set_value( d, scale * unit_distribution( generator ) ); + } + return vector; + } + + template < geode::index_t dimension > + std::vector< geode::Point< dimension > > random_points( geode::index_t nb ) + { + std::vector< geode::Point< dimension > > points; + points.reserve( nb ); + for( const auto unused : geode::Range{ nb } ) + { + geode_unused( unused ); + points.emplace_back( random_point< dimension >() ); + } + return points; + } + + template < typename Function > + void bench( const std::string& name, + const std::string& input, + geode::index_t nb_calls, + const Function& function ) + { + double accumulator{ 0 }; + // warm up + for( const auto i : geode::Range{ std::min( nb_calls, NB_INPUTS ) } ) + { + accumulator += function( i % NB_INPUTS ); + } + const auto start = std::chrono::steady_clock::now(); + for( const auto i : geode::Range{ nb_calls } ) + { + accumulator += function( i % NB_INPUTS ); + } + const auto end = std::chrono::steady_clock::now(); + sink = sink + accumulator; + const auto ns = + std::chrono::duration< double, std::nano >( end - start ).count() + / nb_calls; + geode::Logger::info( "BENCH|", name, "|", input, "|", ns ); + } + + template < typename Result > + double nb_results( const Result& result ) + { + return result.has_intersection() ? result.result->size() : 0.; + } + + void bench_point_distances() + { + const auto points0 = random_points< 3 >( NB_INPUTS ); + const auto points1 = random_points< 3 >( NB_INPUTS ); + const auto queries = random_points< 3 >( NB_INPUTS ); + bench( "point_segment_distance(3D)", "random", 2000000, + [&]( geode::index_t i ) { + return geode::point_segment_distance( + queries[i], { points0[i], points1[i] } ); + } ); + bench( "point_segment_projection(3D)", "random", 2000000, + [&]( geode::index_t i ) { + return geode::point_segment_projection( + queries[i], { points0[i], points1[i] } ) + .value( 0 ); + } ); + bench( "point_line_distance(3D)", "random", 2000000, + [&]( geode::index_t i ) { + return geode::point_line_distance( queries[i], + geode::InfiniteLine3D{ + geode::Segment3D{ points0[i], points1[i] } } ); + } ); + const auto queries2d = random_points< 2 >( NB_INPUTS ); + const auto points2d = random_points< 2 >( NB_INPUTS ); + bench( "point_segment_distance(2D)", "random", 2000000, + [&]( geode::index_t i ) { + return geode::point_segment_distance( queries2d[i], + { points2d[i], points2d[( i + 1 ) % NB_INPUTS] } ); + } ); + bench( "point_triangle_distance(2D)", "random", 1000000, + [&]( geode::index_t i ) { + return std::get< 0 >( + geode::point_triangle_distance( queries2d[i], + { points2d[i], points2d[( i + 1 ) % NB_INPUTS], + points2d[( i + 2 ) % NB_INPUTS] } ) ); + } ); + const geode::Point2D a{ { 0, 0 } }; + const geode::Point2D b{ { 1, 0 } }; + const geode::Point2D c{ { 2, 0 } }; + bench( "point_triangle_distance(2D)", "flat triangle", 1000000, + [&]( geode::index_t i ) { + return std::get< 0 >( geode::point_triangle_distance( + queries2d[i], { a, b, c } ) ); + } ); + bench( "point_triangle_distance(3D)", "random", 1000000, + [&]( geode::index_t i ) { + return std::get< 0 >( geode::point_triangle_distance( + queries[i], { points0[i], points1[i], + points0[( i + 1 ) % NB_INPUTS] } ) ); + } ); + bench( "point_triangle_distance(3D)", "degenerated triangle", 1000000, + [&]( geode::index_t i ) { + return std::get< 0 >( geode::point_triangle_distance( + queries[i], { points0[i], points1[i], points1[i] } ) ); + } ); + } + + void bench_triangle_distances() + { + const auto points = random_points< 3 >( NB_INPUTS ); + const auto shifted = random_points< 3 >( NB_INPUTS ); + bench( "segment_triangle_distance", "random", 500000, + [&]( geode::index_t i ) { + return std::get< 0 >( geode::segment_triangle_distance( + { shifted[i], shifted[( i + 1 ) % NB_INPUTS] }, + { points[i], points[( i + 1 ) % NB_INPUTS], + points[( i + 2 ) % NB_INPUTS] } ) ); + } ); + bench( "triangle_triangle_distance", "random", 200000, + [&]( geode::index_t i ) { + return std::get< 0 >( geode::triangle_triangle_distance( + { shifted[i], shifted[( i + 1 ) % NB_INPUTS], + shifted[( i + 2 ) % NB_INPUTS] }, + { points[i], points[( i + 1 ) % NB_INPUTS], + points[( i + 2 ) % NB_INPUTS] } ) ); + } ); + } + + void bench_intersections() + { + const auto points0 = random_points< 3 >( NB_INPUTS ); + const auto points1 = random_points< 3 >( NB_INPUTS ); + const geode::Point3D center{ { 1, 2, 3 } }; + const geode::Sphere3D sphere{ center, 5 }; + bench( "line_sphere_intersection(3D)", "random", 1000000, + [&]( geode::index_t i ) { + return nb_results( geode::line_sphere_intersection( + geode::InfiniteLine3D{ { points0[i], points1[i] } }, + sphere ) ); + } ); + bench( "segment_sphere_intersection(3D)", "random", 1000000, + [&]( geode::index_t i ) { + return nb_results( geode::segment_sphere_intersection( + { points0[i], points1[i] }, sphere ) ); + } ); + const geode::Plane plane{ + geode::Vector3D{ { 0.2, 0.3, 1 } }.normalize(), center + }; + bench( "segment_plane_intersection", "random", 1000000, + [&]( geode::index_t i ) { + return geode::segment_plane_intersection( + { points0[i], points1[i] }, plane ) + .has_intersection(); + } ); + const geode::Point3D axis0{ { -5, 0, 0 } }; + const geode::Point3D axis1{ { 5, 1, 0 } }; + const geode::Cylinder cylinder{ { axis0, axis1 }, 4 }; + bench( "line_cylinder_intersection", "random", 500000, + [&]( geode::index_t i ) { + return nb_results( geode::line_cylinder_intersection( + geode::InfiniteLine3D{ { points0[i], points1[i] } }, + cylinder ) ); + } ); + bench( "segment_cylinder_intersection", "random", 500000, + [&]( geode::index_t i ) { + return nb_results( geode::segment_cylinder_intersection( + { points0[i], points1[i] }, cylinder ) ); + } ); + const auto points2d0 = random_points< 2 >( NB_INPUTS ); + const auto points2d1 = random_points< 2 >( NB_INPUTS ); + bench( "segment_segment_intersection(2D)", "random", 1000000, + [&]( geode::index_t i ) { + return geode::segment_segment_intersection( + { points2d0[i], points2d1[i] }, + { points2d0[( i + 1 ) % NB_INPUTS], + points2d1[( i + 1 ) % NB_INPUTS] } ) + .has_intersection(); + } ); + bench( "segment_line_intersection(2D)", "random", 1000000, + [&]( geode::index_t i ) { + return geode::segment_line_intersection( + { points2d0[i], points2d1[i] }, + geode::InfiniteLine2D{ { points2d0[( i + 1 ) % NB_INPUTS], + points2d1[( i + 1 ) % NB_INPUTS] } } ) + .has_intersection(); + } ); + const geode::Frame2D frame{ { geode::Vector2D{ { 3, 1 } }, + geode::Vector2D{ { -1, 2 } } } }; + const geode::Point2D center2d{ { 1, 1 } }; + const geode::Ellipse2D ellipse{ center2d, frame }; + bench( "segment_ellipse_intersection(2D)", "random", 500000, + [&]( geode::index_t i ) { + return nb_results( geode::segment_ellipse_intersection( + { points2d0[i], points2d1[i] }, ellipse ) ); + } ); + std::vector< geode::OwnerPlane > planes; + planes.reserve( NB_INPUTS ); + for( const auto i : geode::Range{ NB_INPUTS } ) + { + planes.emplace_back( + random_vector< 3 >( 1 ).normalize(), points0[i] ); + } + const geode::Circle circle{ plane, 6 }; + bench( "plane_circle_intersection", "random", 1000000, + [&]( geode::index_t i ) { + return nb_results( + geode::plane_circle_intersection( planes[i], circle ) ); + } ); + bench( "triangle_circle_intersection", "random", 500000, + [&]( geode::index_t i ) { + return nb_results( geode::triangle_circle_intersection( + { points0[i], points1[i], points0[( i + 1 ) % NB_INPUTS] }, + circle ) ); + } ); + } + + void bench_objects() + { + const auto points0 = random_points< 3 >( NB_INPUTS ); + const auto points1 = random_points< 3 >( NB_INPUTS ); + bench( "Triangle3D::is_degenerated", "random", 2000000, + [&]( geode::index_t i ) { + return geode::Triangle3D{ points0[i], points1[i], + points0[( i + 1 ) % NB_INPUTS] } + .is_degenerated(); + } ); + bench( "Tetrahedron::is_degenerated", "random", 2000000, + [&]( geode::index_t i ) { + return geode::Tetrahedron{ points0[i], points1[i], + points0[( i + 1 ) % NB_INPUTS], + points1[( i + 1 ) % NB_INPUTS] } + .is_degenerated(); + } ); + const auto points2d = random_points< 2 >( NB_INPUTS ); + bench( "Polygon2D::minimum_height", "random 6 vertices", 1000000, + [&]( geode::index_t i ) { + std::vector< geode::RefPoint2D > vertices; + for( const auto v : geode::LRange{ 6 } ) + { + vertices.emplace_back( points2d[( i + v ) % NB_INPUTS] ); + } + return geode::Polygon2D{ std::move( vertices ) } + .minimum_height(); + } ); + std::vector< geode::RefPoint2D > star; + std::vector< geode::Point2D > star_points; + for( const auto v : geode::Range{ 20 } ) + { + const auto angle = 2 * M_PI * v / 20.; + const auto radius = v % 2 == 0 ? 10. : 4.; + star_points.emplace_back( geode::Point2D{ + { radius * std::cos( angle ), radius * std::sin( angle ) } } ); + } + for( const auto& point : star_points ) + { + star.emplace_back( point ); + } + const geode::Polygon2D polygon{ star }; + bench( "is_point_inside_polygon", "20-vertex star", 1000000, + [&]( geode::index_t i ) { + return geode::is_point_inside_polygon( points2d[i], polygon ); + } ); + bench( "Circle::bounding_box", "random normals", 2000000, + [&]( geode::index_t i ) { + const geode::Plane plane{ + geode::Vector3D{ points1[i] }.normalize(), points0[i] + }; + return geode::Circle{ plane, 2 }.bounding_box().min().value( + 0 ); + } ); + bench( "rotate", "random", 2000000, [&]( geode::index_t i ) { + return geode::rotate( points0[i], geode::Vector3D{ points1[i] }, + points1[i].value( 0 ) ) + .value( 0 ); + } ); + const std::hash< geode::Point3D > hasher; + bench( + "std::hash", "random", 5000000, [&]( geode::index_t i ) { + return static_cast< double >( hasher( points0[i] ) & 1 ); + } ); + bench( "Tetrahedron qualities (4)", "random", 1000000, + [&]( geode::index_t i ) { + const geode::Tetrahedron tetra{ points0[i], points1[i], + points0[( i + 1 ) % NB_INPUTS], + points1[( i + 1 ) % NB_INPUTS] }; + return geode::tetrahedron_aspect_ratio( tetra ) + + geode::tetrahedron_volume_to_edge_ratio( tetra ) + + geode::tetrahedron_volume_to_facet_ratio( tetra ) + + geode::tetrahedron_collapse_aspect_ratio( tetra ); + } ); + } + + void bench_bounding_box() + { + const auto points0 = random_points< 3 >( NB_INPUTS ); + const auto points1 = random_points< 3 >( NB_INPUTS ); + geode::BoundingBox3D box; + box.add_point( geode::Point3D{ { -1, -2, -3 } } ); + box.add_point( geode::Point3D{ { 2, 1, 3 } } ); + bench( "BoundingBox3D::intersects(Triangle3D)", "random", 2000000, + [&]( geode::index_t i ) { + return box.intersects( geode::Triangle3D{ + points0[i], points1[i], points0[( i + 1 ) % NB_INPUTS] } ); + } ); + bench( "BoundingBox3D::epsilon_intersects(Triangle3D)", "random", + 2000000, [&]( geode::index_t i ) { + return box.epsilon_intersects( geode::Triangle3D{ + points0[i], points1[i], points0[( i + 1 ) % NB_INPUTS] } ); + } ); + geode::BoundingBox1D box1d; + box1d.add_point( geode::Point1D{ { -1 } } ); + box1d.add_point( geode::Point1D{ { 2 } } ); + bench( "BoundingBox1D::intersects(Ray1D)", "random", 5000000, + [&]( geode::index_t i ) { + const geode::Point1D origin{ { points0[i].value( 0 ) } }; + const geode::Vector1D direction{ { points1[i].value( 0 ) } }; + return box1d.intersects( geode::Ray1D{ direction, origin } ); + } ); + } + + void bench_nn_search() + { + const geode::NNSearch3D search{ random_points< 3 >( 100000 ) }; + const auto queries = random_points< 3 >( NB_INPUTS ); + bench( "NNSearch3D::closest_neighbor", "100k points", 1000000, + [&]( geode::index_t i ) { + return search.closest_neighbor( queries[i] ); + } ); + bench( "NNSearch3D::neighbors(k=5)", "100k points", 500000, + [&]( geode::index_t i ) { + return search.neighbors( queries[i], 5 ).front(); + } ); + } + + void bench_predicates() + { + const auto points = random_points< 3 >( NB_INPUTS ); + bench( "point_side_to_segment(2D)", "collinear +-1ulp", 2000000, + [&]( geode::index_t i ) { + const auto x0 = points[i].value( 0 ); + const auto x1 = points[i].value( 1 ); + const auto x2 = points[i].value( 2 ); + const geode::Point2D p0{ { x0, x0 } }; + const geode::Point2D p1{ { x1, x1 } }; + const geode::Point2D q{ { x2, std::nextafter( x2, 1e300 ) } }; + return static_cast< double >( + geode::point_side_to_segment( q, { p0, p1 } ) ); + } ); + bench( "point_side_to_triangle", "coplanar +-1ulp", 1000000, + [&]( geode::index_t i ) { + const auto& p = points[i]; + const auto& r = points[( i + 1 ) % NB_INPUTS]; + const geode::Point3D q0{ { p.value( 0 ), p.value( 0 ), 1 } }; + const geode::Point3D q1{ { p.value( 1 ), p.value( 1 ), 1 } }; + const geode::Point3D q2{ { p.value( 2 ), p.value( 2 ), 5 } }; + const geode::Point3D q{ { r.value( 0 ), + std::nextafter( r.value( 0 ), 1e300 ), r.value( 1 ) } }; + return static_cast< double >( + geode::point_side_to_triangle( q, { q0, q1, q2 } ) ); + } ); + } +} // namespace + +void test() +{ + geode::OpenGeodeGeometryLibrary::initialize(); + bench_point_distances(); + bench_triangle_distances(); + bench_intersections(); + bench_objects(); + bench_bounding_box(); + bench_nn_search(); + bench_predicates(); +} + +OPENGEODE_TEST( "benchmark-tier2-robustness" ) From 956ec85532ef8d2d3eb8c08cb8dc2aa1930ee0d1 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:00:09 +0200 Subject: [PATCH 24/52] refactor(Distance): reuse Triangle::is_degenerated in 2D point_triangle_distance is_triangle_flat duplicated the Triangle2D::is_degenerated formula. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index 5bea54f9b..a599c8e26 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -68,23 +68,6 @@ namespace && signed_area_3 >= 0 ); } - /* - * A triangle is flat if its minimum height (twice its area divided by its - * longest edge length) is below GLOBAL_EPSILON - */ - bool is_triangle_flat( const geode::Triangle2D& triangle ) - { - const auto& vertices = triangle.vertices(); - const geode::Vector2D edge01{ vertices[0], vertices[1] }; - const geode::Vector2D edge02{ vertices[0], vertices[2] }; - const geode::Vector2D edge12{ vertices[1], vertices[2] }; - const auto double_area = geode::dot_perpendicular( edge01, edge02 ); - const auto max_length2 = std::max( - { edge01.length2(), edge02.length2(), edge12.length2() } ); - return double_area * double_area - <= geode::GLOBAL_EPSILON * geode::GLOBAL_EPSILON * max_length2; - } - void get_min_edge02( double a11, double b1, std::array< double, 2 >& p ) { p[0] = 0; @@ -950,7 +933,7 @@ namespace geode opengeode_geometry_api point_triangle_distance( const Point2D& point, const Triangle2D& triangle ) { - if( !is_triangle_flat( triangle ) + if( !triangle.is_degenerated() && may_point_be_in_triangle( point, triangle ) ) { return { 0.0, point }; From c96cb563efc4d3dfa2af7debb3b47577294321df Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:00:09 +0200 Subject: [PATCH 25/52] fix(Intersection): report the closest cylinder point for degenerated segments point_cylinder_distance moves to the public distance API and returns the closest point on the cylinder surface, so the degenerated segment/cylinder correctness point lies on the cylinder like for the other objects. degenerated_segment_intersection parameters are renamed to match all call sites. Co-Authored-By: Claude Opus 5.5 --- include/geode/geometry/distance.hpp | 14 ++++++++ src/geode/geometry/distance.cpp | 53 ++++++++++++++++++++++++++++ src/geode/geometry/intersection.cpp | 51 +++++++------------------- tests/geometry/test-distance.cpp | 43 ++++++++++++++++++++++ tests/geometry/test-intersection.cpp | 8 +++++ 5 files changed, 131 insertions(+), 38 deletions(-) diff --git a/include/geode/geometry/distance.hpp b/include/geode/geometry/distance.hpp index 4cd37a83c..96d4c1c86 100644 --- a/include/geode/geometry/distance.hpp +++ b/include/geode/geometry/distance.hpp @@ -43,6 +43,7 @@ namespace geode class Plane; class Tetrahedron; class Circle; + class Cylinder; template < index_t dimension > using Ball = Sphere< dimension >; using Disk = Circle; @@ -261,6 +262,19 @@ namespace geode [[nodiscard]] std::tuple< double, Point3D > opengeode_geometry_api point_disk_distance( const Point3D& point, const Disk& disk ); + /*! + * Compute the smallest distance between a point and a cylinder surface, + * i.e. its wall and its two end disks + * @return a tuple containing: + * - the smallest distance. + * - the closest point on the cylinder surface. + * @details Result is always positive or null, even if the point is inside + * the cylinder. + */ + [[nodiscard]] std::tuple< double, Point3D > + opengeode_geometry_api point_cylinder_distance( + const Point3D& point, const Cylinder& cylinder ); + /*! * Compute the smallest distance between a point and an ellipse * @return a tuple containing: diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index a599c8e26..1eb712d75 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -1399,6 +1400,58 @@ namespace geode return point_circle_distance( point, disk ); } + std::tuple< double, Point3D > point_cylinder_distance( + const Point3D& point, const Cylinder& cylinder ) + { + // Cylinder frame: axial coordinate along the unit axis direction, + // radial vector orthogonal to it, both from the axis center. + const auto& axis = cylinder.axis(); + const auto axis_direction = axis.direction(); + const auto half_height = 0.5 * axis.length(); + const auto radius = cylinder.radius(); + const auto center = axis.barycenter(); + const Vector3D center_to_point{ center, point }; + const auto axial = center_to_point.dot( axis_direction ); + const auto radial_vector = center_to_point - axis_direction * axial; + const auto radial = radial_vector.length(); + const auto axial_distance = std::fabs( axial ) - half_height; + const auto radial_distance = radial - radius; + const auto disk_center = + center + axis_direction * std::copysign( half_height, axial ); + const auto to_wall = [&]( const Point3D& origin ) { + if( radial > 0 ) + { + return origin + radial_vector * ( radius / radial ); + } + // Point on the axis: any radial direction is the closest one + Vector3D other; + other.set_value( + std::fabs( axis_direction.value( 0 ) ) < 0.9 ? 0 : 1, 1. ); + return origin + axis_direction.cross( other ).normalize() * radius; + }; + if( axial_distance > 0 ) + { + if( radial_distance <= 0 ) + { + return { axial_distance, disk_center + radial_vector }; + } + return { std::sqrt( axial_distance * axial_distance + + radial_distance * radial_distance ), + to_wall( disk_center ) }; + } + if( radial_distance >= 0 ) + { + return { radial_distance, + to_wall( center + axis_direction * axial ) }; + } + if( -radial_distance <= -axial_distance ) + { + return { -radial_distance, + to_wall( center + axis_direction * axial ) }; + } + return { -axial_distance, disk_center + radial_vector }; + } + template < index_t dimension > double Bisector( const geode::index_t number_of_components, const std::array< double, dimension >& locE, diff --git a/src/geode/geometry/intersection.cpp b/src/geode/geometry/intersection.cpp index b239da7c1..72b20f720 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -97,55 +97,28 @@ namespace }; /* - * Intersection result of a degenerated segment (considered as a point) - * with an object, given the distance from the point to the object and - * the projection of the point on the object. + * Intersection result when one of the two objects is a degenerated segment + * (considered as a point), given the closest points between the two + * objects and their distance. The result is the point on the first object. */ template < typename Intersection, typename PointType > geode::IntersectionResult< Intersection > degenerated_segment_intersection( - const PointType& point, - double distance_to_object, - const PointType& projection_on_object ) + const PointType& point_on_first, + double distance, + const PointType& point_on_second ) { - if( distance_to_object > geode::GLOBAL_EPSILON ) + if( distance > geode::GLOBAL_EPSILON ) { return { geode::INTERSECTION_TYPE::none }; } typename geode::CorrectnessInfo< Intersection >::Correctness - first_correctness{ true, Intersection{ point } }; + first_correctness{ true, Intersection{ point_on_first } }; typename geode::CorrectnessInfo< Intersection >::Correctness - second_correctness{ true, Intersection{ projection_on_object } }; - return { Intersection{ point }, + second_correctness{ true, Intersection{ point_on_second } }; + return { Intersection{ point_on_first }, { first_correctness, second_correctness } }; } - double point_cylinder_distance( - const geode::Point3D& point, const geode::Cylinder& cylinder ) - { - const auto half_height = 0.5 * cylinder.axis().length(); - const geode::Vector3D center_to_point{ cylinder.axis().barycenter(), - point }; - const auto axial = - std::fabs( center_to_point.dot( cylinder.axis().direction() ) ); - const auto radial = std::sqrt( - std::max( center_to_point.length2() - axial * axial, 0. ) ); - const auto radial_distance = radial - cylinder.radius(); - const auto axial_distance = axial - half_height; - if( axial_distance <= 0 ) - { - if( radial_distance >= 0 ) - { - return radial_distance; - } - return std::min( -radial_distance, -axial_distance ); - } - if( radial_distance <= 0 ) - { - return axial_distance; - } - return std::sqrt( axial_distance * axial_distance - + radial_distance * radial_distance ); - } /* * Keep the line intersections located on the segment, with their * correctness information. @@ -897,9 +870,11 @@ namespace geode if( segment.is_degenerated() ) { const auto point = segment.barycenter(); + const auto [distance, projection] = + point_cylinder_distance( point, cylinder ); return degenerated_segment_intersection< absl::InlinedVector< Point3D, 2 > >( - point, point_cylinder_distance( point, cylinder ), point ); + point, distance, projection ); } auto line_intersections = line_cylinder_intersection( InfiniteLine3D{ segment }, cylinder ); diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index 776d366e2..838055e1c 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -1548,6 +1549,47 @@ void test_point_segment_line_distance_accuracy() "Inconsistent distance and projection for degenerated segment" ); } +void test_point_cylinder_distance() +{ + // Cylinder along x, from x=0 to x=4, radius 1 + const geode::Point3D axis_start{ { 0, 0, 0 } }; + const geode::Point3D axis_end{ { 4, 0, 0 } }; + const geode::Cylinder cylinder{ geode::Segment3D{ axis_start, axis_end }, + 1 }; + const auto check = [&cylinder]( const geode::Point3D& point, + double expected_distance, + const geode::Point3D& expected_closest, + std::string_view label ) { + const auto [distance, closest] = + geode::point_cylinder_distance( point, cylinder ); + geode::OpenGeodeGeometryException::test( + std::fabs( distance - expected_distance ) <= geode::GLOBAL_EPSILON + && closest.inexact_equal( expected_closest ), + "Wrong point_cylinder_distance: ", label ); + }; + check( geode::Point3D{ { 2, 3, 0 } }, 2, geode::Point3D{ { 2, 1, 0 } }, + "outside the wall" ); + check( geode::Point3D{ { 6, 0, 0.5 } }, 2, geode::Point3D{ { 4, 0, 0.5 } }, + "outside an end disk" ); + check( geode::Point3D{ { -3, 0, 5 } }, 5, geode::Point3D{ { 0, 0, 1 } }, + "outside the rim" ); + check( geode::Point3D{ { 2, 0, 0.75 } }, 0.25, + geode::Point3D{ { 2, 0, 1 } }, "inside, close to the wall" ); + check( geode::Point3D{ { 3.75, 0.5, 0 } }, 0.25, + geode::Point3D{ { 4, 0.5, 0 } }, "inside, close to an end disk" ); + check( geode::Point3D{ { 2, 1, 0 } }, 0, geode::Point3D{ { 2, 1, 0 } }, + "on the wall" ); + const auto [axis_distance, axis_closest] = geode::point_cylinder_distance( + geode::Point3D{ { 2, 0, 0 } }, cylinder ); + geode::OpenGeodeGeometryException::test( + std::fabs( axis_distance - 1 ) <= geode::GLOBAL_EPSILON + && std::fabs( axis_closest.value( 0 ) - 2 ) <= geode::GLOBAL_EPSILON + && std::get< 0 >( + geode::point_cylinder_distance( axis_closest, cylinder ) ) + <= geode::GLOBAL_EPSILON, + "Wrong point_cylinder_distance: on the axis" ); +} + void test() { test_point_segment_distance(); @@ -1563,6 +1605,7 @@ void test() test_segment_triangle_distance(); test_triangle_triangle_distance(); test_point_ellipse_distance(); + test_point_cylinder_distance(); test_degenerated_segment_triangle_distance(); test_degenerated_triangle_triangle_distance(); test_random_triangle_triangle_distance(); diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index fb96e4cfe..ec8b975b1 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -1042,6 +1042,14 @@ void test_degenerated_segment_intersections() geode::OpenGeodeGeometryException::test( cylinder_result && cylinder_result.result->size() == 1, "Wrong degenerated segment_cylinder_intersection" ); + const auto& cylinder_projection = + cylinder_result.correctness->second.second.front(); + geode::OpenGeodeGeometryException::test( + std::get< 0 >( geode::point_cylinder_distance( + cylinder_projection, cylinder ) ) + <= 1e-15, + "Wrong degenerated segment_cylinder_intersection correctness: " + "point should be on the cylinder" ); const geode::Point3D in_cylinder{ { 0.5, 0, 0 } }; const geode::Point3D in_cylinder2{ { 0.5, length, 0 } }; geode::OpenGeodeGeometryException::test( From f87d6847c29759b189d365b7e445bed9d89185cf Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:00:10 +0200 Subject: [PATCH 26/52] fix(Intersection): length tolerances in line_cylinder_intersection All branches now compare lengths against GLOBAL_EPSILON (wall, end disks, parallel case), and parameters closer than GLOBAL_EPSILON are merged: a line through an end disk rim reported the rim point twice and lost its second intersection. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/intersection.cpp | 154 +++++++++++++-------------- tests/geometry/test-intersection.cpp | 38 +++++++ 2 files changed, 113 insertions(+), 79 deletions(-) diff --git a/src/geode/geometry/intersection.cpp b/src/geode/geometry/intersection.cpp index 72b20f720..0151895d5 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -617,7 +617,17 @@ namespace geode const InfiniteLine3D& line, const Cylinder& cylinder ) { CylinderLineResult result; + // The line direction is unit: parameters are lengths along the line. + // Parameters closer than GLOBAL_EPSILON give the same point. const auto add_parameter = [&result]( double parameter ) { + for( const auto i : LRange{ result.numIntersections } ) + { + if( std::fabs( parameter - result.parameter[i] ) + <= GLOBAL_EPSILON ) + { + return; + } + } if( result.numIntersections < 2 ) { result.parameter[result.numIntersections++] = parameter; @@ -635,7 +645,7 @@ namespace geode const auto& U = basis[1]; const auto& V = basis[2]; const auto halfHeight = 0.5 * cylinder.axis().length(); - const auto rSqr = cylinder.radius() * cylinder.radius(); + const auto radius = cylinder.radius(); const auto cylinder_barycenter = cylinder.axis().barycenter(); @@ -643,6 +653,42 @@ namespace geode const Vector3D diff{ cylinder_barycenter, line.origin() }; Vector3D P{ { U.dot( diff ), V.dot( diff ), W.dot( diff ) } }; + const auto is_inside_radius = [radius]( double x, double y ) { + return std::sqrt( x * x + y * y ) - radius <= GLOBAL_EPSILON; + }; + // Intersections of line P+t*D with the infinite cylinder + // x^2+y^2 = r^2, using the distance h from the line to the cylinder + // axis: the intersections are at + // t = t_closest +- sqrt( (r-h)*(r+h) ) / |D_xy|. + const auto wall_parameters = [&P, &is_inside_radius, radius]( + const Vector3D& D ) { + absl::InlinedVector< double, 2 > parameters; + const auto a1 = + P.value( 0 ) * D.value( 0 ) + P.value( 1 ) * D.value( 1 ); + const auto a2 = + D.value( 0 ) * D.value( 0 ) + D.value( 1 ) * D.value( 1 ); + const auto closest_parameter = -a1 / a2; + const auto x = P.value( 0 ) + closest_parameter * D.value( 0 ); + const auto y = P.value( 1 ) + closest_parameter * D.value( 1 ); + if( !is_inside_radius( x, y ) ) + { + return parameters; + } + const auto distance_to_axis = std::sqrt( x * x + y * y ); + if( radius - distance_to_axis <= GLOBAL_EPSILON ) + { + // The line is tangent to the cylinder. + parameters.push_back( closest_parameter ); + return parameters; + } + const auto half_chord = + std::sqrt( ( radius - distance_to_axis ) + * ( radius + distance_to_axis ) / a2 ); + parameters.push_back( closest_parameter - half_chord ); + parameters.push_back( closest_parameter + half_chord ); + return parameters; + }; + // Get the z-value, in cylinder coordinates, of the incoming // line's unit-length direction. const auto dz = W.dot( line.direction() ); @@ -650,28 +696,20 @@ namespace geode { // The line is parallel to the cylinder axis. Determine // whether the line intersects the cylinder end disks. - const auto radialSqrDist = rSqr - P.value( 0 ) * P.value( 0 ) - - P.value( 1 ) * P.value( 1 ); - if( radialSqrDist >= 0 ) + if( !is_inside_radius( P.value( 0 ), P.value( 1 ) ) ) { - // The line intersects the cylinder end disks. - result.intersect = true; - result.numIntersections = 2; - if( dz > 0 ) - { - result.parameter[0] = -P.value( 2 ) - halfHeight; - result.parameter[1] = -P.value( 2 ) + halfHeight; - } - else - { - result.parameter[0] = P.value( 2 ) - halfHeight; - result.parameter[1] = P.value( 2 ) + halfHeight; - } + // The line is outside the cylinder, no intersection. + return { INTERSECTION_TYPE::none }; + } + if( dz > 0 ) + { + add_parameter( -P.value( 2 ) - halfHeight ); + add_parameter( -P.value( 2 ) + halfHeight ); } else { - // else: The line is outside the cylinder, no intersection. - return { INTERSECTION_TYPE::none }; + add_parameter( P.value( 2 ) - halfHeight ); + add_parameter( P.value( 2 ) + halfHeight ); } } else @@ -683,43 +721,15 @@ namespace geode if( D.value( 2 ) == 0 ) { // The line is perpendicular to the cylinder axis. - if( std::fabs( P.value( 2 ) ) > halfHeight ) + if( std::fabs( P.value( 2 ) ) - halfHeight > GLOBAL_EPSILON ) { // The line is outside the planes of the cylinder end // disks. return { INTERSECTION_TYPE::none }; } - // Test intersection of line P+t*D with infinite cylinder - // x^2+y^2 = r^2, using the distance h from the line to the - // cylinder axis: the intersections are at - // t = t_closest +- sqrt( (r-h)*(r+h) ) / |D|. - const auto a1 = - P.value( 0 ) * D.value( 0 ) + P.value( 1 ) * D.value( 1 ); - const auto a2 = - D.value( 0 ) * D.value( 0 ) + D.value( 1 ) * D.value( 1 ); - const auto closest_parameter = -a1 / a2; - const auto x = P.value( 0 ) + closest_parameter * D.value( 0 ); - const auto y = P.value( 1 ) + closest_parameter * D.value( 1 ); - const auto distance_to_axis = std::sqrt( x * x + y * y ); - const auto radius = cylinder.radius(); - if( distance_to_axis - radius > GLOBAL_EPSILON ) - { - // The line does not intersect the cylinder. - return { INTERSECTION_TYPE::none }; - } - if( radius - distance_to_axis <= GLOBAL_EPSILON ) - { - // The line is tangent to the cylinder. - add_parameter( closest_parameter ); - } - else + for( const auto parameter : wall_parameters( D ) ) { - // The line intersects the cylinder in two places. - const auto half_chord = - std::sqrt( ( radius - distance_to_axis ) - * ( radius + distance_to_axis ) / a2 ); - add_parameter( closest_parameter - half_chord ); - add_parameter( closest_parameter + half_chord ); + add_parameter( parameter ); } } else @@ -732,18 +742,16 @@ namespace geode // Test for intersections with the planes of the end disks. const auto t0 = ( -halfHeight - P.value( 2 ) ) / D.value( 2 ); - auto xTmp = P.value( 0 ) + t0 * D.value( 0 ); - auto yTmp = P.value( 1 ) + t0 * D.value( 1 ); - if( xTmp * xTmp + yTmp * yTmp <= rSqr ) + if( is_inside_radius( P.value( 0 ) + t0 * D.value( 0 ), + P.value( 1 ) + t0 * D.value( 1 ) ) ) { // Plane intersection inside the bottom cylinder end disk. add_parameter( t0 ); } const auto t1 = ( halfHeight - P.value( 2 ) ) / D.value( 2 ); - xTmp = P.value( 0 ) + t1 * D.value( 0 ); - yTmp = P.value( 1 ) + t1 * D.value( 1 ); - if( xTmp * xTmp + yTmp * yTmp <= rSqr ) + if( is_inside_radius( P.value( 0 ) + t1 * D.value( 0 ), + P.value( 1 ) + t1 * D.value( 1 ) ) ) { // Plane intersection inside the top cylinder end disk. add_parameter( t1 ); @@ -751,33 +759,21 @@ namespace geode if( result.numIntersections < 2 ) { - // Test for intersection with the cylinder wall. - const auto a0 = P.value( 0 ) * P.value( 0 ) - + P.value( 1 ) * P.value( 1 ) - rSqr; - const auto a1 = P.value( 0 ) * D.value( 0 ) - + P.value( 1 ) * D.value( 1 ); - const auto a2 = D.value( 0 ) * D.value( 0 ) - + D.value( 1 ) * D.value( 1 ); - const auto discr = a1 * a1 - a0 * a2; - const auto add_wall_parameter = [&add_parameter, t0, t1]( - double parameter ) { - if( std::min( t0, t1 ) <= parameter - && parameter <= std::max( t0, t1 ) ) + // Test for intersection with the cylinder wall, between + // the end disk planes. A parameter shift of + // GLOBAL_EPSILON / |dz| moves the point by GLOBAL_EPSILON + // along the axis. + const auto tolerance = GLOBAL_EPSILON / std::fabs( dz ); + const auto min_parameter = std::min( t0, t1 ) - tolerance; + const auto max_parameter = std::max( t0, t1 ) + tolerance; + for( const auto parameter : wall_parameters( D ) ) + { + if( min_parameter <= parameter + && parameter <= max_parameter ) { add_parameter( parameter ); } - }; - if( discr > 0 ) - { - const auto root = std::sqrt( discr ); - add_wall_parameter( ( -a1 - root ) / a2 ); - add_wall_parameter( ( -a1 + root ) / a2 ); - } - else if( discr == 0 ) - { - add_wall_parameter( -a1 / a2 ); } - // else: Line does not intersect cylinder wall. } // else: Line intersects both top and bottom cylinder end // disks. diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index ec8b975b1..279588a4d 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -1168,6 +1168,43 @@ void test_line_cylinder_perpendicular() "Wrong result for line_cylinder_intersection with oblique line" ); } +void test_line_cylinder_tolerance() +{ + // Oblique line tangent to the wall, within GLOBAL_EPSILON + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; + const geode::Cylinder cylinder{ geode::Segment3D{ a, b }, 0.2 }; + for( const auto offset : { 0., 1e-7, -1e-7 } ) + { + const geode::Point3D c{ { 0.0, 0.2 + offset, -0.5 } }; + const geode::Point3D d{ { 1.0, 0.2 + offset, 0.5 } }; + const auto result = geode::line_cylinder_intersection( + geode::InfiniteLine3D{ geode::Segment3D{ c, d } }, cylinder ); + geode::OpenGeodeGeometryException::test( + result && result.result->size() == 1 + && result.result->front().inexact_equal( + geode::Point3D{ { 0.5, 0.2, 0.0 } } ), + "Wrong result for line_cylinder_intersection with oblique " + "tangent line (offset ", + offset, ")" ); + } + + // Line entering through the rim of an end disk and leaving through the + // wall: the rim point must be reported once + const geode::Point3D bottom{ { 0.0, 0.0, -1.0 } }; + const geode::Point3D top{ { 0.0, 0.0, 1.0 } }; + const geode::Cylinder z_cylinder{ geode::Segment3D{ bottom, top }, 1.0 }; + const geode::Point3D rim{ { 1.0, 0.0, 1.0 } }; + const geode::Point3D wall{ { -1.0, 0.0, 0.0 } }; + const auto rim_result = geode::line_cylinder_intersection( + geode::InfiniteLine3D{ geode::Segment3D{ rim, wall } }, z_cylinder ); + geode::OpenGeodeGeometryException::test( + rim_result && rim_result.result->size() == 2 + && rim_result.result->at( 0 ).inexact_equal( rim ) + && rim_result.result->at( 1 ).inexact_equal( wall ), + "Wrong result for line_cylinder_intersection through the rim" ); +} + template < typename Result > bool has_consistent_correctness( const Result& result ) { @@ -1278,6 +1315,7 @@ void test() test_segment_ellipse_intersection(); test_degenerated_segment_intersections(); test_line_cylinder_perpendicular(); + test_line_cylinder_tolerance(); test_segment_intersections_correctness(); test_line_sphere_intersection_robustness(); } From 157faa92a0a07dc1b29d746128e2135cd6c18c3e Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:00:10 +0200 Subject: [PATCH 27/52] fix(Rotation): throw on non-finite angle in rotate Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/rotation.cpp | 3 +++ tests/geometry/test-rotation.cpp | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/geode/geometry/rotation.cpp b/src/geode/geometry/rotation.cpp index 461bdc96e..72ffa8703 100644 --- a/src/geode/geometry/rotation.cpp +++ b/src/geode/geometry/rotation.cpp @@ -93,6 +93,9 @@ namespace geode axis_length > 0 && std::isfinite( axis_length ), point, OpenGeodeBasicException::TYPE::data, "[rotate] Given axis is null or not finite" ); + OpenGeodeGeometryException::check_exception( std::isfinite( angle ), + point, OpenGeodeBasicException::TYPE::data, + "[rotate] Given angle is not finite" ); // No angle normalization needed: the rotation matrix is quadratic in // the quaternion, so q and -q (angle + 2*PI) give the same rotation. const auto base = axis / axis_length * std::sin( 0.5 * angle ); diff --git a/tests/geometry/test-rotation.cpp b/tests/geometry/test-rotation.cpp index a10f00487..91fbff55c 100644 --- a/tests/geometry/test-rotation.cpp +++ b/tests/geometry/test-rotation.cpp @@ -43,10 +43,16 @@ void test_rotation_robustness() - geode::Vector3D{ input }.dot( axis ) ) <= geode::GLOBAL_EPSILON, "Wrong result for huge angle rotation" ); - const auto infinite = - geode::rotate( input, axis, std::numeric_limits< double >::infinity() ); - geode::OpenGeodeGeometryException::test( std::isnan( infinite.value( 0 ) ), - "Infinite angle rotation should give NaN" ); + try + { + [[maybe_unused]] const auto infinite = geode::rotate( + input, axis, std::numeric_limits< double >::infinity() ); + geode::OpenGeodeGeometryException::test( + false, "Infinite angle rotation should throw" ); + } + catch( const geode::OpenGeodeGeometryException& ) + { + } const geode::Vector3D small_axis{ { 1e-4, 0., 0. } }; const geode::Vector3D z_axis{ { 0., 0., 3. } }; From dcad44cc89e207efc0649bd81c5ca385a6922ee2 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:00:10 +0200 Subject: [PATCH 28/52] perf(IsPointInside): run exact orientation only on edges spanning the point row 20-vertex star: 157 ns -> 74 ns per call. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/is_point_inside.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/geode/geometry/is_point_inside.cpp b/src/geode/geometry/is_point_inside.cpp index 807f170a9..d1b96e662 100644 --- a/src/geode/geometry/is_point_inside.cpp +++ b/src/geode/geometry/is_point_inside.cpp @@ -62,21 +62,27 @@ namespace geode const Point2D& v1 = vertices[polygon_vertex + 1 == nb_vertices ? 0 : polygon_vertex + 1]; + const auto y = point.value( 1 ); + const auto y0 = v0.value( 1 ); + const auto y1 = v1.value( 1 ); + if( ( y0 < y && y1 < y ) || ( y0 > y && y1 > y ) ) + { + // The edge neither crosses the point row nor contains it + continue; + } const auto side = GEO::PCK::orient_2d( v0, v1, point ); if( side == GEO::SIGN::zero && is_point_on_edge( point, v0, v1 ) ) { return true; } - if( v0.value( 1 ) <= point.value( 1 ) ) + if( y0 <= y ) { - if( v1.value( 1 ) > point.value( 1 ) - && side == GEO::SIGN::positive ) + if( y1 > y && side == GEO::SIGN::positive ) { winding_number++; } } - else if( v1.value( 1 ) <= point.value( 1 ) - && side == GEO::SIGN::negative ) + else if( y1 <= y && side == GEO::SIGN::negative ) { winding_number--; } From 221eac0dcd986c46f5fee366c0e374d748103e7b Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:00:10 +0200 Subject: [PATCH 29/52] test(Geometry): remove tier2-robustness benchmark Co-Authored-By: Claude Opus 5.5 --- tests/geometry/CMakeLists.txt | 8 - tests/geometry/benchmark-tier2-robustness.cpp | 460 ------------------ 2 files changed, 468 deletions(-) delete mode 100644 tests/geometry/benchmark-tier2-robustness.cpp diff --git a/tests/geometry/CMakeLists.txt b/tests/geometry/CMakeLists.txt index 5a812ba98..b15bdc4ab 100644 --- a/tests/geometry/CMakeLists.txt +++ b/tests/geometry/CMakeLists.txt @@ -172,11 +172,3 @@ add_geode_test( ${PROJECT_NAME}::basic ${PROJECT_NAME}::geometry ) - -add_geode_test( - SOURCE "benchmark-tier2-robustness.cpp" - LOCAL - DEPENDENCIES - ${PROJECT_NAME}::basic - ${PROJECT_NAME}::geometry -) diff --git a/tests/geometry/benchmark-tier2-robustness.cpp b/tests/geometry/benchmark-tier2-robustness.cpp deleted file mode 100644 index ad8d8f780..000000000 --- a/tests/geometry/benchmark-tier2-robustness.cpp +++ /dev/null @@ -1,460 +0,0 @@ -/* - * Copyright (c) 2019 - 2026 Geode-solutions - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - */ - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace -{ - constexpr geode::index_t NB_INPUTS{ 10000 }; - volatile double sink{ 0 }; - - std::mt19937 generator{ 42 }; - std::uniform_real_distribution< double > distribution{ -10, 10 }; - std::uniform_real_distribution< double > unit_distribution{ -1, 1 }; - - double random_value() - { - return distribution( generator ); - } - - template < geode::index_t dimension > - geode::Point< dimension > random_point() - { - geode::Point< dimension > point; - for( const auto d : geode::LRange{ dimension } ) - { - point.set_value( d, random_value() ); - } - return point; - } - - template < geode::index_t dimension > - geode::Vector< dimension > random_vector( double scale ) - { - geode::Vector< dimension > vector; - for( const auto d : geode::LRange{ dimension } ) - { - vector.set_value( d, scale * unit_distribution( generator ) ); - } - return vector; - } - - template < geode::index_t dimension > - std::vector< geode::Point< dimension > > random_points( geode::index_t nb ) - { - std::vector< geode::Point< dimension > > points; - points.reserve( nb ); - for( const auto unused : geode::Range{ nb } ) - { - geode_unused( unused ); - points.emplace_back( random_point< dimension >() ); - } - return points; - } - - template < typename Function > - void bench( const std::string& name, - const std::string& input, - geode::index_t nb_calls, - const Function& function ) - { - double accumulator{ 0 }; - // warm up - for( const auto i : geode::Range{ std::min( nb_calls, NB_INPUTS ) } ) - { - accumulator += function( i % NB_INPUTS ); - } - const auto start = std::chrono::steady_clock::now(); - for( const auto i : geode::Range{ nb_calls } ) - { - accumulator += function( i % NB_INPUTS ); - } - const auto end = std::chrono::steady_clock::now(); - sink = sink + accumulator; - const auto ns = - std::chrono::duration< double, std::nano >( end - start ).count() - / nb_calls; - geode::Logger::info( "BENCH|", name, "|", input, "|", ns ); - } - - template < typename Result > - double nb_results( const Result& result ) - { - return result.has_intersection() ? result.result->size() : 0.; - } - - void bench_point_distances() - { - const auto points0 = random_points< 3 >( NB_INPUTS ); - const auto points1 = random_points< 3 >( NB_INPUTS ); - const auto queries = random_points< 3 >( NB_INPUTS ); - bench( "point_segment_distance(3D)", "random", 2000000, - [&]( geode::index_t i ) { - return geode::point_segment_distance( - queries[i], { points0[i], points1[i] } ); - } ); - bench( "point_segment_projection(3D)", "random", 2000000, - [&]( geode::index_t i ) { - return geode::point_segment_projection( - queries[i], { points0[i], points1[i] } ) - .value( 0 ); - } ); - bench( "point_line_distance(3D)", "random", 2000000, - [&]( geode::index_t i ) { - return geode::point_line_distance( queries[i], - geode::InfiniteLine3D{ - geode::Segment3D{ points0[i], points1[i] } } ); - } ); - const auto queries2d = random_points< 2 >( NB_INPUTS ); - const auto points2d = random_points< 2 >( NB_INPUTS ); - bench( "point_segment_distance(2D)", "random", 2000000, - [&]( geode::index_t i ) { - return geode::point_segment_distance( queries2d[i], - { points2d[i], points2d[( i + 1 ) % NB_INPUTS] } ); - } ); - bench( "point_triangle_distance(2D)", "random", 1000000, - [&]( geode::index_t i ) { - return std::get< 0 >( - geode::point_triangle_distance( queries2d[i], - { points2d[i], points2d[( i + 1 ) % NB_INPUTS], - points2d[( i + 2 ) % NB_INPUTS] } ) ); - } ); - const geode::Point2D a{ { 0, 0 } }; - const geode::Point2D b{ { 1, 0 } }; - const geode::Point2D c{ { 2, 0 } }; - bench( "point_triangle_distance(2D)", "flat triangle", 1000000, - [&]( geode::index_t i ) { - return std::get< 0 >( geode::point_triangle_distance( - queries2d[i], { a, b, c } ) ); - } ); - bench( "point_triangle_distance(3D)", "random", 1000000, - [&]( geode::index_t i ) { - return std::get< 0 >( geode::point_triangle_distance( - queries[i], { points0[i], points1[i], - points0[( i + 1 ) % NB_INPUTS] } ) ); - } ); - bench( "point_triangle_distance(3D)", "degenerated triangle", 1000000, - [&]( geode::index_t i ) { - return std::get< 0 >( geode::point_triangle_distance( - queries[i], { points0[i], points1[i], points1[i] } ) ); - } ); - } - - void bench_triangle_distances() - { - const auto points = random_points< 3 >( NB_INPUTS ); - const auto shifted = random_points< 3 >( NB_INPUTS ); - bench( "segment_triangle_distance", "random", 500000, - [&]( geode::index_t i ) { - return std::get< 0 >( geode::segment_triangle_distance( - { shifted[i], shifted[( i + 1 ) % NB_INPUTS] }, - { points[i], points[( i + 1 ) % NB_INPUTS], - points[( i + 2 ) % NB_INPUTS] } ) ); - } ); - bench( "triangle_triangle_distance", "random", 200000, - [&]( geode::index_t i ) { - return std::get< 0 >( geode::triangle_triangle_distance( - { shifted[i], shifted[( i + 1 ) % NB_INPUTS], - shifted[( i + 2 ) % NB_INPUTS] }, - { points[i], points[( i + 1 ) % NB_INPUTS], - points[( i + 2 ) % NB_INPUTS] } ) ); - } ); - } - - void bench_intersections() - { - const auto points0 = random_points< 3 >( NB_INPUTS ); - const auto points1 = random_points< 3 >( NB_INPUTS ); - const geode::Point3D center{ { 1, 2, 3 } }; - const geode::Sphere3D sphere{ center, 5 }; - bench( "line_sphere_intersection(3D)", "random", 1000000, - [&]( geode::index_t i ) { - return nb_results( geode::line_sphere_intersection( - geode::InfiniteLine3D{ { points0[i], points1[i] } }, - sphere ) ); - } ); - bench( "segment_sphere_intersection(3D)", "random", 1000000, - [&]( geode::index_t i ) { - return nb_results( geode::segment_sphere_intersection( - { points0[i], points1[i] }, sphere ) ); - } ); - const geode::Plane plane{ - geode::Vector3D{ { 0.2, 0.3, 1 } }.normalize(), center - }; - bench( "segment_plane_intersection", "random", 1000000, - [&]( geode::index_t i ) { - return geode::segment_plane_intersection( - { points0[i], points1[i] }, plane ) - .has_intersection(); - } ); - const geode::Point3D axis0{ { -5, 0, 0 } }; - const geode::Point3D axis1{ { 5, 1, 0 } }; - const geode::Cylinder cylinder{ { axis0, axis1 }, 4 }; - bench( "line_cylinder_intersection", "random", 500000, - [&]( geode::index_t i ) { - return nb_results( geode::line_cylinder_intersection( - geode::InfiniteLine3D{ { points0[i], points1[i] } }, - cylinder ) ); - } ); - bench( "segment_cylinder_intersection", "random", 500000, - [&]( geode::index_t i ) { - return nb_results( geode::segment_cylinder_intersection( - { points0[i], points1[i] }, cylinder ) ); - } ); - const auto points2d0 = random_points< 2 >( NB_INPUTS ); - const auto points2d1 = random_points< 2 >( NB_INPUTS ); - bench( "segment_segment_intersection(2D)", "random", 1000000, - [&]( geode::index_t i ) { - return geode::segment_segment_intersection( - { points2d0[i], points2d1[i] }, - { points2d0[( i + 1 ) % NB_INPUTS], - points2d1[( i + 1 ) % NB_INPUTS] } ) - .has_intersection(); - } ); - bench( "segment_line_intersection(2D)", "random", 1000000, - [&]( geode::index_t i ) { - return geode::segment_line_intersection( - { points2d0[i], points2d1[i] }, - geode::InfiniteLine2D{ { points2d0[( i + 1 ) % NB_INPUTS], - points2d1[( i + 1 ) % NB_INPUTS] } } ) - .has_intersection(); - } ); - const geode::Frame2D frame{ { geode::Vector2D{ { 3, 1 } }, - geode::Vector2D{ { -1, 2 } } } }; - const geode::Point2D center2d{ { 1, 1 } }; - const geode::Ellipse2D ellipse{ center2d, frame }; - bench( "segment_ellipse_intersection(2D)", "random", 500000, - [&]( geode::index_t i ) { - return nb_results( geode::segment_ellipse_intersection( - { points2d0[i], points2d1[i] }, ellipse ) ); - } ); - std::vector< geode::OwnerPlane > planes; - planes.reserve( NB_INPUTS ); - for( const auto i : geode::Range{ NB_INPUTS } ) - { - planes.emplace_back( - random_vector< 3 >( 1 ).normalize(), points0[i] ); - } - const geode::Circle circle{ plane, 6 }; - bench( "plane_circle_intersection", "random", 1000000, - [&]( geode::index_t i ) { - return nb_results( - geode::plane_circle_intersection( planes[i], circle ) ); - } ); - bench( "triangle_circle_intersection", "random", 500000, - [&]( geode::index_t i ) { - return nb_results( geode::triangle_circle_intersection( - { points0[i], points1[i], points0[( i + 1 ) % NB_INPUTS] }, - circle ) ); - } ); - } - - void bench_objects() - { - const auto points0 = random_points< 3 >( NB_INPUTS ); - const auto points1 = random_points< 3 >( NB_INPUTS ); - bench( "Triangle3D::is_degenerated", "random", 2000000, - [&]( geode::index_t i ) { - return geode::Triangle3D{ points0[i], points1[i], - points0[( i + 1 ) % NB_INPUTS] } - .is_degenerated(); - } ); - bench( "Tetrahedron::is_degenerated", "random", 2000000, - [&]( geode::index_t i ) { - return geode::Tetrahedron{ points0[i], points1[i], - points0[( i + 1 ) % NB_INPUTS], - points1[( i + 1 ) % NB_INPUTS] } - .is_degenerated(); - } ); - const auto points2d = random_points< 2 >( NB_INPUTS ); - bench( "Polygon2D::minimum_height", "random 6 vertices", 1000000, - [&]( geode::index_t i ) { - std::vector< geode::RefPoint2D > vertices; - for( const auto v : geode::LRange{ 6 } ) - { - vertices.emplace_back( points2d[( i + v ) % NB_INPUTS] ); - } - return geode::Polygon2D{ std::move( vertices ) } - .minimum_height(); - } ); - std::vector< geode::RefPoint2D > star; - std::vector< geode::Point2D > star_points; - for( const auto v : geode::Range{ 20 } ) - { - const auto angle = 2 * M_PI * v / 20.; - const auto radius = v % 2 == 0 ? 10. : 4.; - star_points.emplace_back( geode::Point2D{ - { radius * std::cos( angle ), radius * std::sin( angle ) } } ); - } - for( const auto& point : star_points ) - { - star.emplace_back( point ); - } - const geode::Polygon2D polygon{ star }; - bench( "is_point_inside_polygon", "20-vertex star", 1000000, - [&]( geode::index_t i ) { - return geode::is_point_inside_polygon( points2d[i], polygon ); - } ); - bench( "Circle::bounding_box", "random normals", 2000000, - [&]( geode::index_t i ) { - const geode::Plane plane{ - geode::Vector3D{ points1[i] }.normalize(), points0[i] - }; - return geode::Circle{ plane, 2 }.bounding_box().min().value( - 0 ); - } ); - bench( "rotate", "random", 2000000, [&]( geode::index_t i ) { - return geode::rotate( points0[i], geode::Vector3D{ points1[i] }, - points1[i].value( 0 ) ) - .value( 0 ); - } ); - const std::hash< geode::Point3D > hasher; - bench( - "std::hash", "random", 5000000, [&]( geode::index_t i ) { - return static_cast< double >( hasher( points0[i] ) & 1 ); - } ); - bench( "Tetrahedron qualities (4)", "random", 1000000, - [&]( geode::index_t i ) { - const geode::Tetrahedron tetra{ points0[i], points1[i], - points0[( i + 1 ) % NB_INPUTS], - points1[( i + 1 ) % NB_INPUTS] }; - return geode::tetrahedron_aspect_ratio( tetra ) - + geode::tetrahedron_volume_to_edge_ratio( tetra ) - + geode::tetrahedron_volume_to_facet_ratio( tetra ) - + geode::tetrahedron_collapse_aspect_ratio( tetra ); - } ); - } - - void bench_bounding_box() - { - const auto points0 = random_points< 3 >( NB_INPUTS ); - const auto points1 = random_points< 3 >( NB_INPUTS ); - geode::BoundingBox3D box; - box.add_point( geode::Point3D{ { -1, -2, -3 } } ); - box.add_point( geode::Point3D{ { 2, 1, 3 } } ); - bench( "BoundingBox3D::intersects(Triangle3D)", "random", 2000000, - [&]( geode::index_t i ) { - return box.intersects( geode::Triangle3D{ - points0[i], points1[i], points0[( i + 1 ) % NB_INPUTS] } ); - } ); - bench( "BoundingBox3D::epsilon_intersects(Triangle3D)", "random", - 2000000, [&]( geode::index_t i ) { - return box.epsilon_intersects( geode::Triangle3D{ - points0[i], points1[i], points0[( i + 1 ) % NB_INPUTS] } ); - } ); - geode::BoundingBox1D box1d; - box1d.add_point( geode::Point1D{ { -1 } } ); - box1d.add_point( geode::Point1D{ { 2 } } ); - bench( "BoundingBox1D::intersects(Ray1D)", "random", 5000000, - [&]( geode::index_t i ) { - const geode::Point1D origin{ { points0[i].value( 0 ) } }; - const geode::Vector1D direction{ { points1[i].value( 0 ) } }; - return box1d.intersects( geode::Ray1D{ direction, origin } ); - } ); - } - - void bench_nn_search() - { - const geode::NNSearch3D search{ random_points< 3 >( 100000 ) }; - const auto queries = random_points< 3 >( NB_INPUTS ); - bench( "NNSearch3D::closest_neighbor", "100k points", 1000000, - [&]( geode::index_t i ) { - return search.closest_neighbor( queries[i] ); - } ); - bench( "NNSearch3D::neighbors(k=5)", "100k points", 500000, - [&]( geode::index_t i ) { - return search.neighbors( queries[i], 5 ).front(); - } ); - } - - void bench_predicates() - { - const auto points = random_points< 3 >( NB_INPUTS ); - bench( "point_side_to_segment(2D)", "collinear +-1ulp", 2000000, - [&]( geode::index_t i ) { - const auto x0 = points[i].value( 0 ); - const auto x1 = points[i].value( 1 ); - const auto x2 = points[i].value( 2 ); - const geode::Point2D p0{ { x0, x0 } }; - const geode::Point2D p1{ { x1, x1 } }; - const geode::Point2D q{ { x2, std::nextafter( x2, 1e300 ) } }; - return static_cast< double >( - geode::point_side_to_segment( q, { p0, p1 } ) ); - } ); - bench( "point_side_to_triangle", "coplanar +-1ulp", 1000000, - [&]( geode::index_t i ) { - const auto& p = points[i]; - const auto& r = points[( i + 1 ) % NB_INPUTS]; - const geode::Point3D q0{ { p.value( 0 ), p.value( 0 ), 1 } }; - const geode::Point3D q1{ { p.value( 1 ), p.value( 1 ), 1 } }; - const geode::Point3D q2{ { p.value( 2 ), p.value( 2 ), 5 } }; - const geode::Point3D q{ { r.value( 0 ), - std::nextafter( r.value( 0 ), 1e300 ), r.value( 1 ) } }; - return static_cast< double >( - geode::point_side_to_triangle( q, { q0, q1, q2 } ) ); - } ); - } -} // namespace - -void test() -{ - geode::OpenGeodeGeometryLibrary::initialize(); - bench_point_distances(); - bench_triangle_distances(); - bench_intersections(); - bench_objects(); - bench_bounding_box(); - bench_nn_search(); - bench_predicates(); -} - -OPENGEODE_TEST( "benchmark-tier2-robustness" ) From 5fa318eab6189458102893187750e40798394fd3 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 10:20:11 +0200 Subject: [PATCH 30/52] fix(Tetrahedron): include opposite edges in is_degenerated A tetrahedron is degenerated if its vertices lie in a slab of thickness GLOBAL_EPSILON. The thinnest slab may be bounded by planes containing two opposite edges instead of a facet: slivers of thickness below GLOBAL_EPSILON have heights up to twice as large and were not reported. Document the strip/slab definitions of Triangle and Tetrahedron. Co-Authored-By: Claude Opus 5.5 --- .../geometry/basic_objects/tetrahedron.hpp | 4 ++++ .../geode/geometry/basic_objects/triangle.hpp | 5 +++++ .../geometry/basic_objects/tetrahedron.cpp | 22 +++++++++++-------- tests/geometry/test-triangle.cpp | 16 ++++++++++++++ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/include/geode/geometry/basic_objects/tetrahedron.hpp b/include/geode/geometry/basic_objects/tetrahedron.hpp index a98c77cfa..47e59b8e7 100644 --- a/include/geode/geometry/basic_objects/tetrahedron.hpp +++ b/include/geode/geometry/basic_objects/tetrahedron.hpp @@ -66,6 +66,10 @@ namespace geode void set_point( local_index_t vertex, PointType point ); [[nodiscard]] const std::array< PointType, 4 >& vertices() const; [[nodiscard]] BoundingBox3D bounding_box() const; + /*! + * A tetrahedron is degenerated if all its vertices lie in a slab + * (thick plane) of thickness GLOBAL_EPSILON. + */ [[nodiscard]] bool is_degenerated() const; [[nodiscard]] std::string string() const; diff --git a/include/geode/geometry/basic_objects/triangle.hpp b/include/geode/geometry/basic_objects/triangle.hpp index 07986ba14..b92e60ca5 100644 --- a/include/geode/geometry/basic_objects/triangle.hpp +++ b/include/geode/geometry/basic_objects/triangle.hpp @@ -98,6 +98,11 @@ namespace geode [[nodiscard]] local_index_t longest_edge_index() const; [[nodiscard]] local_index_t smallest_edge_index() const; [[nodiscard]] double minimum_height() const; + /*! + * A triangle is degenerated if all its vertices lie in a strip + * (thick line) of thickness GLOBAL_EPSILON, i.e. if its minimum + * height is below GLOBAL_EPSILON. + */ [[nodiscard]] bool is_degenerated() const; [[nodiscard]] std::string string() const; diff --git a/src/geode/geometry/basic_objects/tetrahedron.cpp b/src/geode/geometry/basic_objects/tetrahedron.cpp index dcd4a0b60..f34e5c9b0 100644 --- a/src/geode/geometry/basic_objects/tetrahedron.cpp +++ b/src/geode/geometry/basic_objects/tetrahedron.cpp @@ -86,9 +86,12 @@ namespace geode template < typename PointType > bool GenericTetrahedron< PointType >::is_degenerated() const { - // The tetrahedron is degenerated if its minimum height, i.e. three - // times its volume divided by its largest facet area, is below - // GLOBAL_EPSILON. + // The tetrahedron is degenerated if all its vertices lie in a slab of + // thickness GLOBAL_EPSILON. The thinnest slab is either bounded by a + // facet plane (width = 6 * volume / facet double area) or by two + // parallel planes containing opposite edges (width = 6 * volume / + // |edge cross product|): the largest of these denominators gives the + // minimum width. const Point3D& point0 = vertices_[0]; const Point3D& point1 = vertices_[1]; const Point3D& point2 = vertices_[2]; @@ -98,14 +101,15 @@ namespace geode const Vector3D edge03{ point0, point3 }; const Vector3D edge12{ point1, point2 }; const Vector3D edge13{ point1, point3 }; + const Vector3D edge23{ point2, point3 }; const auto six_volume = edge01.dot( edge02.cross( edge03 ) ); - const auto max_double_area2 = - std::max( { edge01.cross( edge02 ).length2(), - edge01.cross( edge03 ).length2(), - edge02.cross( edge03 ).length2(), - edge12.cross( edge13 ).length2() } ); + const auto max_cross2 = std::max( { edge01.cross( edge02 ).length2(), + edge01.cross( edge03 ).length2(), edge02.cross( edge03 ).length2(), + edge12.cross( edge13 ).length2(), edge01.cross( edge23 ).length2(), + edge02.cross( edge13 ).length2(), + edge03.cross( edge12 ).length2() } ); return six_volume * six_volume - <= GLOBAL_EPSILON * GLOBAL_EPSILON * max_double_area2; + <= GLOBAL_EPSILON * GLOBAL_EPSILON * max_cross2; } template < typename PointType > diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index ee377f42b..faa14495f 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -64,6 +64,22 @@ void test_degenerated_consistency() geode::OpenGeodeGeometryException::test( !tetra3.is_degenerated(), "Valid tetrahedron should not be degenerated" ); + // Sliver: opposite edges 01 and 23 lie in planes z=0 and z=width, so + // its thickness is width while all its heights are about 2 * width. + for( const auto width : { 0.5, 0.8, 1., 1.2 } ) + { + const auto delta = width * geode::GLOBAL_EPSILON; + const geode::Point3D s0{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D s1{ { 1.0, 1.0, 0.0 } }; + const geode::Point3D s2{ { 1.0, 0.0, delta } }; + const geode::Point3D s3{ { 0.0, 1.0, delta } }; + const geode::Tetrahedron sliver{ s0, s1, s2, s3 }; + geode::OpenGeodeGeometryException::test( + sliver.is_degenerated() == ( width <= 1. ), + "Wrong degeneracy for sliver of thickness ", width, + " * GLOBAL_EPSILON" ); + } + std::mt19937 generator{ 42 }; std::uniform_real_distribution< double > distribution{ -1., 1. }; std::uniform_real_distribution< double > exponent{ -9., -4. }; From eaeaaedccc099b99cbceab8261c37662bbcd5008 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 11:40:57 +0200 Subject: [PATCH 31/52] perf(Geometry): add zero-filter and missing filters to exact predicates Exactly degenerate inputs (duplicated points, points sharing coordinates) always fell back to expansion arithmetic since the floating-point filters cannot certify a zero sign. A zero-filter (Bartels et al. 2022) now decides them before the exact computation. aligned_3d had no filter and dot_2d had none: both always used exact arithmetic. aligned_3d now concludes as soon as one cross product component is certified non-zero; dot_2d gets the orient_2d error bound. Speedups: aligned_3d ~350 -> ~10 ns, dot_2d ~110 -> ~10 ns, orient_3d on exactly coplanar points ~380 -> ~25 ns. Generic inputs unchanged, results identical. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/internal/predicates.cpp | 405 ++++++++++++--------- tests/geometry/CMakeLists.txt | 6 + tests/geometry/test-predicates.cpp | 275 ++++++++++++++ 3 files changed, 504 insertions(+), 182 deletions(-) create mode 100644 tests/geometry/test-predicates.cpp diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/internal/predicates.cpp index 10ffacaf2..9af5c46aa 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/internal/predicates.cpp @@ -23,6 +23,9 @@ #include +#include +#include + /* * Copyright (c) 2012-2014, Bruno Levy * All rights reserved. @@ -1406,176 +1409,189 @@ namespace GEO return int_tmp_result; } - /******* extracted from predicates/aligned3d.h *******/ + /******* dot_2d filter (not generated by FPG) *******/ - inline int aligned_3d_filter( const geode::Point3D& p0, - const geode::Point3D& p1, - const geode::Point3D& p2 ) + // Same error bound as orient_2d_filter: two products of differences of + // the input coordinates, summed instead of subtracted. + inline int dot_2d_filter( const geode::Point2D& p0, + const geode::Point2D& p1, + const geode::Point2D& p2 ) { - double a11; - a11 = ( p1.value( 0 ) - p0.value( 0 ) ); - double a12; - a12 = ( p1.value( 1 ) - p0.value( 1 ) ); - double a13; - a13 = ( p1.value( 2 ) - p0.value( 2 ) ); - double a21; - a21 = ( p2.value( 0 ) - p0.value( 0 ) ); - double a22; - a22 = ( p2.value( 1 ) - p0.value( 1 ) ); - double a23; - a23 = ( p2.value( 2 ) - p0.value( 2 ) ); - double delta1; - delta1 = ( ( a12 * a23 ) - ( a22 * a13 ) ); - double delta2; - delta2 = ( ( a13 * a21 ) - ( a23 * a11 ) ); - double delta3; - delta3 = ( ( a11 * a22 ) - ( a21 * a12 ) ); - int int_tmp_result; - double eps; - int int_tmp_result_FFWKCAA; - int int_tmp_result_k60Ocge; - double max1 = fabs( a12 ); - if( ( max1 < fabs( a22 ) ) ) - { - max1 = fabs( a22 ); - } - double max2 = fabs( a13 ); - if( ( max2 < fabs( a23 ) ) ) - { - max2 = fabs( a23 ); - } - double lower_bound_1; - double upper_bound_1; - lower_bound_1 = max1; - upper_bound_1 = max1; - if( ( max2 < lower_bound_1 ) ) - { - lower_bound_1 = max2; - } - else - { - if( ( max2 > upper_bound_1 ) ) - { - upper_bound_1 = max2; - } - } - if( ( lower_bound_1 < 5.00368081960964635413e-147 ) ) + const double a11 = p1.value( 0 ) - p0.value( 0 ); + const double a12 = p1.value( 1 ) - p0.value( 1 ); + const double a21 = p2.value( 0 ) - p0.value( 0 ); + const double a22 = p2.value( 1 ) - p0.value( 1 ); + const double Delta = ( a11 * a21 ) + ( a12 * a22 ); + const double max1 = std::max( std::fabs( a11 ), std::fabs( a12 ) ); + const double max2 = std::max( std::fabs( a21 ), std::fabs( a22 ) ); + const double lower_bound = std::min( max1, max2 ); + const double upper_bound = std::max( max1, max2 ); + if( lower_bound < 5.00368081960964635413e-147 + || upper_bound > 1.67597599124282407923e+153 ) { return FPG_UNCERTAIN_VALUE; } - else - { - if( ( upper_bound_1 > 1.67597599124282407923e+153 ) ) - { - return FPG_UNCERTAIN_VALUE; - } - eps = ( 8.88720573725927976811e-16 * ( max1 * max2 ) ); - if( ( delta1 > eps ) ) - { - int_tmp_result = 1; - } - else - { - if( ( delta1 < -eps ) ) - { - int_tmp_result = -1; - } - else - { - return FPG_UNCERTAIN_VALUE; - } - } - } - double max3 = fabs( a11 ); - if( ( max3 < fabs( a21 ) ) ) - { - max3 = fabs( a21 ); - } - lower_bound_1 = max3; - upper_bound_1 = max3; - if( ( max2 < lower_bound_1 ) ) - { - lower_bound_1 = max2; - } - else - { - if( ( max2 > upper_bound_1 ) ) - { - upper_bound_1 = max2; - } - } - if( ( lower_bound_1 < 5.00368081960964635413e-147 ) ) - { - return FPG_UNCERTAIN_VALUE; - } - else - { - if( ( upper_bound_1 > 1.67597599124282407923e+153 ) ) - { - return FPG_UNCERTAIN_VALUE; - } - eps = ( 8.88720573725927976811e-16 * ( max2 * max3 ) ); - if( ( delta2 > eps ) ) - { - int_tmp_result_FFWKCAA = 1; - } - else - { - if( ( delta2 < -eps ) ) - { - int_tmp_result_FFWKCAA = -1; - } - else - { - return FPG_UNCERTAIN_VALUE; - } - } - } - lower_bound_1 = max1; - upper_bound_1 = max1; - if( ( max3 < lower_bound_1 ) ) - { - lower_bound_1 = max3; - } - else + const double eps = 8.88720573725927976811e-16 * ( max1 * max2 ); + if( Delta > eps ) { - if( ( max3 > upper_bound_1 ) ) - { - upper_bound_1 = max3; - } + return 1; } - if( ( lower_bound_1 < 5.00368081960964635413e-147 ) ) + if( Delta < -eps ) { - return FPG_UNCERTAIN_VALUE; + return -1; } - else + return FPG_UNCERTAIN_VALUE; + } + + /******* aligned_3d filter (not generated by FPG) *******/ + + // Certifies that a 2x2 determinant of coordinate differences is not zero, + // max1 and max2 bounding the absolute values of the factors of each of + // its two products (same error bound as orient_2d_filter). + inline bool is_det_2d_certainly_non_zero( + double delta, double max1, double max2 ) + { + const double lower_bound = std::min( max1, max2 ); + const double upper_bound = std::max( max1, max2 ); + if( lower_bound < 5.00368081960964635413e-147 + || upper_bound > 1.67597599124282407923e+153 ) { - if( ( upper_bound_1 > 1.67597599124282407923e+153 ) ) - { - return FPG_UNCERTAIN_VALUE; - } - eps = ( 8.88720573725927976811e-16 * ( max3 * max1 ) ); - if( ( delta3 > eps ) ) - { - int_tmp_result_k60Ocge = 1; - } - else - { - if( ( delta3 < -eps ) ) - { - int_tmp_result_k60Ocge = -1; - } - else - { - return FPG_UNCERTAIN_VALUE; - } - } + return false; } - return ( - ( ( ( int_tmp_result == 0 ) && ( int_tmp_result_FFWKCAA == 0 ) ) - && ( int_tmp_result_k60Ocge == 0 ) ) - ? 0 - : 1 ); + return std::fabs( delta ) + > 8.88720573725927976811e-16 * ( max1 * max2 ); + } + + // Returns 1 if the points are certainly not aligned, FPG_UNCERTAIN_VALUE + // otherwise. The points are aligned if and only if the three components + // of (p1-p0)x(p2-p0) are zero: one component certified non-zero is + // enough to conclude. + inline int aligned_3d_filter( const geode::Point3D& p0, + const geode::Point3D& p1, + const geode::Point3D& p2 ) + { + const double a11 = p1.value( 0 ) - p0.value( 0 ); + const double a12 = p1.value( 1 ) - p0.value( 1 ); + const double a13 = p1.value( 2 ) - p0.value( 2 ); + const double a21 = p2.value( 0 ) - p0.value( 0 ); + const double a22 = p2.value( 1 ) - p0.value( 1 ); + const double a23 = p2.value( 2 ) - p0.value( 2 ); + const double max_x = std::max( std::fabs( a11 ), std::fabs( a21 ) ); + const double max_y = std::max( std::fabs( a12 ), std::fabs( a22 ) ); + const double max_z = std::max( std::fabs( a13 ), std::fabs( a23 ) ); + if( is_det_2d_certainly_non_zero( + ( a12 * a23 ) - ( a22 * a13 ), max_y, max_z ) + || is_det_2d_certainly_non_zero( + ( a13 * a21 ) - ( a23 * a11 ), max_z, max_x ) + || is_det_2d_certainly_non_zero( + ( a11 * a22 ) - ( a21 * a12 ), max_x, max_y ) ) + { + return 1; + } + return FPG_UNCERTAIN_VALUE; + } + + /******* zero-filters *******/ + + // Zero-filters (T. Bartels, V. Fisikopoulos, M. Weiser, "Fast + // Floating-Point Filters for Robust Predicates", 2022, Definition 19). + // They certify that a polynomial is exactly zero when each of its + // products has an exactly zero factor. The difference of two floating + // point numbers is zero only if they are equal, so degenerate inputs + // sharing coordinates (duplicated points, grid aligned points, points + // on an axis aligned plane or line) are decided without exact + // arithmetic. The floating-point filters cannot decide such cases. + + inline bool is_product_zero( double a, double b ) + { + return a == 0. || b == 0.; + } + + inline bool is_det_2d_zero( double a11, double a12, double a21, double a22 ) + { + return is_product_zero( a11, a22 ) && is_product_zero( a12, a21 ); + } + + inline bool is_det_3d_zero( double a11, + double a12, + double a13, + double a21, + double a22, + double a23, + double a31, + double a32, + double a33 ) + { + return ( a11 == 0. || is_det_2d_zero( a22, a23, a32, a33 ) ) + && ( a21 == 0. || is_det_2d_zero( a12, a13, a32, a33 ) ) + && ( a31 == 0. || is_det_2d_zero( a12, a13, a22, a23 ) ); + } + + inline bool is_orient_2d_zero( const geode::Point2D& p0, + const geode::Point2D& p1, + const geode::Point2D& p2 ) + { + return is_det_2d_zero( p1.value( 0 ) - p0.value( 0 ), + p1.value( 1 ) - p0.value( 1 ), p2.value( 0 ) - p0.value( 0 ), + p2.value( 1 ) - p0.value( 1 ) ); + } + + inline bool is_orient_3d_zero( const geode::Point3D& p0, + const geode::Point3D& p1, + const geode::Point3D& p2, + const geode::Point3D& p3 ) + { + return is_det_3d_zero( p1.value( 0 ) - p0.value( 0 ), + p1.value( 1 ) - p0.value( 1 ), p1.value( 2 ) - p0.value( 2 ), + p2.value( 0 ) - p0.value( 0 ), p2.value( 1 ) - p0.value( 1 ), + p2.value( 2 ) - p0.value( 2 ), p3.value( 0 ) - p0.value( 0 ), + p3.value( 1 ) - p0.value( 1 ), p3.value( 2 ) - p0.value( 2 ) ); + } + + inline bool is_det_3d_zero( const geode::Vector3D& p0, + const geode::Vector3D& p1, + const geode::Vector3D& p2 ) + { + return is_det_3d_zero( p0.value( 0 ), p0.value( 1 ), p0.value( 2 ), + p1.value( 0 ), p1.value( 1 ), p1.value( 2 ), p2.value( 0 ), + p2.value( 1 ), p2.value( 2 ) ); + } + + inline bool is_aligned_3d_zero( const geode::Point3D& p0, + const geode::Point3D& p1, + const geode::Point3D& p2 ) + { + const double a11 = p1.value( 0 ) - p0.value( 0 ); + const double a12 = p1.value( 1 ) - p0.value( 1 ); + const double a13 = p1.value( 2 ) - p0.value( 2 ); + const double a21 = p2.value( 0 ) - p0.value( 0 ); + const double a22 = p2.value( 1 ) - p0.value( 1 ); + const double a23 = p2.value( 2 ) - p0.value( 2 ); + return is_det_2d_zero( a12, a13, a22, a23 ) + && is_det_2d_zero( a13, a11, a23, a21 ) + && is_det_2d_zero( a11, a12, a21, a22 ); + } + + inline bool is_dot_3d_zero( const geode::Point3D& p0, + const geode::Point3D& p1, + const geode::Point3D& p2 ) + { + return is_product_zero( p1.value( 0 ) - p0.value( 0 ), + p2.value( 0 ) - p0.value( 0 ) ) + && is_product_zero( p1.value( 1 ) - p0.value( 1 ), + p2.value( 1 ) - p0.value( 1 ) ) + && is_product_zero( p1.value( 2 ) - p0.value( 2 ), + p2.value( 2 ) - p0.value( 2 ) ); + } + + inline bool is_dot_2d_zero( const geode::Point2D& p0, + const geode::Point2D& p1, + const geode::Point2D& p2 ) + { + return is_product_zero( p1.value( 0 ) - p0.value( 0 ), + p2.value( 0 ) - p0.value( 0 ) ) + && is_product_zero( p1.value( 1 ) - p0.value( 1 ), + p2.value( 1 ) - p0.value( 1 ) ); } } // namespace GEO @@ -1734,12 +1750,16 @@ namespace GEO const geode::Point2D& p1, const geode::Point2D& p2 ) { - SIGN result = SIGN( orient_2d_filter( p0, p1, p2 ) ); - if( result == 0 ) + const auto result = SIGN( orient_2d_filter( p0, p1, p2 ) ); + if( result != zero ) { - result = orient_2d_exact( p0, p1, p2 ); + return result; } - return result; + if( is_orient_2d_zero( p0, p1, p2 ) ) + { + return zero; + } + return orient_2d_exact( p0, p1, p2 ); } SIGN orient_3d( const geode::Point3D& p0, @@ -1747,38 +1767,46 @@ namespace GEO const geode::Point3D& p2, const geode::Point3D& p3 ) { - SIGN result = SIGN( orient_3d_filter( p0, p1, p2, p3 ) ); - if( result == 0 ) + const auto result = SIGN( orient_3d_filter( p0, p1, p2, p3 ) ); + if( result != zero ) + { + return result; + } + if( is_orient_3d_zero( p0, p1, p2, p3 ) ) { - result = orient_3d_exact( p0, p1, p2, p3 ); + return zero; } - return result; + return orient_3d_exact( p0, p1, p2, p3 ); } SIGN det_3d( const geode::Vector3D& p0, const geode::Vector3D& p1, const geode::Vector3D& p2 ) { - SIGN result = SIGN( det_3d_filter( p0, p1, p2 ) ); - if( result == 0 ) + const auto result = SIGN( det_3d_filter( p0, p1, p2 ) ); + if( result != zero ) + { + return result; + } + if( is_det_3d_zero( p0, p1, p2 ) ) { - result = det_3d_exact( p0, p1, p2 ); + return zero; } - return result; + return det_3d_exact( p0, p1, p2 ); } bool aligned_3d( const geode::Point3D& p0, const geode::Point3D& p1, const geode::Point3D& p2 ) { - /* - SIGN result = SIGN( - aligned_3d_filter(p0,p1,p2) - ); - if(result != 0) { - return false; + if( aligned_3d_filter( p0, p1, p2 ) != FPG_UNCERTAIN_VALUE ) + { + return false; + } + if( is_aligned_3d_zero( p0, p1, p2 ) ) + { + return true; } - */ return aligned_3d_exact( p0, p1, p2 ); } @@ -1786,18 +1814,31 @@ namespace GEO const geode::Point3D& p1, const geode::Point3D& p2 ) { - SIGN result = SIGN( dot_3d_filter( p0, p1, p2 ) ); - if( result == 0 ) + const auto result = SIGN( dot_3d_filter( p0, p1, p2 ) ); + if( result != zero ) { - result = dot_3d_exact( p0, p1, p2 ); + return result; } - return result; + if( is_dot_3d_zero( p0, p1, p2 ) ) + { + return zero; + } + return dot_3d_exact( p0, p1, p2 ); } SIGN dot_2d( const geode::Point2D& p0, const geode::Point2D& p1, const geode::Point2D& p2 ) { + const auto result = SIGN( dot_2d_filter( p0, p1, p2 ) ); + if( result != zero ) + { + return result; + } + if( is_dot_2d_zero( p0, p1, p2 ) ) + { + return zero; + } return dot_2d_exact( p0, p1, p2 ); } diff --git a/tests/geometry/CMakeLists.txt b/tests/geometry/CMakeLists.txt index 0d0da9f02..4567d0bdf 100644 --- a/tests/geometry/CMakeLists.txt +++ b/tests/geometry/CMakeLists.txt @@ -130,6 +130,12 @@ add_geode_test( ${PROJECT_NAME}::basic ${PROJECT_NAME}::geometry ) +add_geode_test( + SOURCE "test-predicates.cpp" + DEPENDENCIES + ${PROJECT_NAME}::basic + ${PROJECT_NAME}::geometry +) add_geode_test( SOURCE "test-projection.cpp" DEPENDENCIES diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp new file mode 100644 index 000000000..56b0a29e6 --- /dev/null +++ b/tests/geometry/test-predicates.cpp @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2019 - 2026 Geode-solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include + +#include +#include + +#include +#include +#include + +#include + +namespace +{ + // Smallest representable increment above value + double next( double value ) + { + return std::nextafter( value, 2 * value + 1 ); + } + + void test_orient_2d() + { + const geode::Point2D a{ { 0, 0 } }; + const geode::Point2D b{ { 1, 0 } }; + const geode::Point2D c{ { 0, 1 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( a, b, c ) == GEO::positive, + "[Test] Wrong orient_2d on generic points" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( a, c, b ) == GEO::negative, + "[Test] Wrong orient_2d on generic points (inverted)" ); + + // Decided by the zero-filter + const geode::Point2D x1{ { 5, 0 } }; + const geode::Point2D x2{ { -3, 0 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( a, x1, x2 ) == GEO::zero, + "[Test] Wrong orient_2d on points with same y" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( a, a, c ) == GEO::zero, + "[Test] Wrong orient_2d on duplicated points" ); + + // Decided by the exact arithmetic + const geode::Point2D d1{ { 0.1, 0.3 } }; + const geode::Point2D d2{ { 0.2, 0.6 } }; + const geode::Point2D d3{ { 0.4, 1.2 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( d1, d2, d3 ) + == GEO::PCK::orient_2d( d2, d3, d1 ), + "[Test] Wrong orient_2d consistency on nearly collinear points" ); + const geode::Point2D e1{ { 1, 1 } }; + const geode::Point2D e2{ { 2, 2 } }; + const geode::Point2D e3{ { 3, next( 3 ) } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( e1, e2, geode::Point2D{ { 3, 3 } } ) + == GEO::zero, + "[Test] Wrong orient_2d on collinear points" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( e1, e2, e3 ) == GEO::positive, + "[Test] Wrong orient_2d on nearly collinear points" ); + } + + void test_orient_3d() + { + const geode::Point3D a{ { 0, 0, 0 } }; + const geode::Point3D b{ { 1, 0, 0 } }; + const geode::Point3D c{ { 0, 1, 0 } }; + const geode::Point3D d{ { 0, 0, 1 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( a, b, c, d ) != GEO::zero, + "[Test] Wrong orient_3d on generic points" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( a, b, c, d ) + == -GEO::PCK::orient_3d( a, c, b, d ), + "[Test] Wrong orient_3d on generic points (inverted)" ); + + // Decided by the zero-filter + const geode::Point3D z1{ { 12, 7, 0 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( a, b, c, z1 ) == GEO::zero, + "[Test] Wrong orient_3d on points with same z" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( a, b, b, d ) == GEO::zero, + "[Test] Wrong orient_3d on duplicated points" ); + const geode::Point3D y1{ { 3, 0, 5 } }; + const geode::Point3D y2{ { -2, 0, 1 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( a, b, y1, y2 ) == GEO::zero, + "[Test] Wrong orient_3d on points with same y" ); + + // Decided by the exact arithmetic + const geode::Point3D p0{ { 1, 1, 1 } }; + const geode::Point3D p1{ { 2, 3, 4 } }; + const geode::Point3D p2{ { 4, 1, 2 } }; + const geode::Point3D coplanar{ { 5, 3, 5 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( p0, p1, p2, coplanar ) == GEO::zero, + "[Test] Wrong orient_3d on coplanar points" ); + const geode::Point3D above{ { 5, 3, next( 5 ) } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( p0, p1, p2, above ) + == -GEO::PCK::orient_3d( + p0, p1, p2, geode::Point3D{ { 5, 3, 5 - 1e-15 } } ), + "[Test] Wrong orient_3d on nearly coplanar points" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( p0, p1, p2, above ) != GEO::zero, + "[Test] Wrong orient_3d on nearly coplanar points" ); + } + + void test_det_3d() + { + const geode::Vector3D x{ { 1, 0, 0 } }; + const geode::Vector3D y{ { 0, 1, 0 } }; + const geode::Vector3D z{ { 0, 0, 1 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( x, y, z ) == GEO::positive, + "[Test] Wrong det_3d on generic vectors" ); + + // Decided by the zero-filter + const geode::Vector3D xy{ { 3, 5, 0 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( x, y, xy ) == GEO::zero, + "[Test] Wrong det_3d on vectors in plane z=0" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( x, geode::Vector3D{ { 0, 0, 0 } }, z ) + == GEO::zero, + "[Test] Wrong det_3d with null vector" ); + + // Decided by the exact arithmetic + const geode::Vector3D u{ { 1, 2, 3 } }; + const geode::Vector3D v{ { 4, 5, 6 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( u, v, geode::Vector3D{ { 7, 8, 9 } } ) + == GEO::zero, + "[Test] Wrong det_3d on coplanar vectors" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( u, v, geode::Vector3D{ { 7, 8, next( 9 ) } } ) + == GEO::negative, + "[Test] Wrong det_3d on nearly coplanar vectors" ); + } + + void test_aligned_3d() + { + const geode::Point3D a{ { 0, 0, 0 } }; + const geode::Point3D b{ { 1, 0, 0 } }; + const geode::Point3D c{ { 0, 1, 0 } }; + geode::OpenGeodeGeometryException::test( + !GEO::PCK::aligned_3d( a, b, c ), + "[Test] Wrong aligned_3d on generic points" ); + geode::OpenGeodeGeometryException::test( + !GEO::PCK::aligned_3d( a, geode::Point3D{ { 1, 1, 0 } }, + geode::Point3D{ { 2, 2, 1 } } ), + "[Test] Wrong aligned_3d on points aligned in projection" ); + + // Decided by the zero-filter + geode::OpenGeodeGeometryException::test( + GEO::PCK::aligned_3d( a, b, geode::Point3D{ { -7, 0, 0 } } ), + "[Test] Wrong aligned_3d on points along x axis" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::aligned_3d( a, a, c ), + "[Test] Wrong aligned_3d on duplicated points" ); + + // Decided by the exact arithmetic + const geode::Point3D p0{ { 1, 2, 3 } }; + const geode::Point3D p1{ { 2, 4, 6 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::aligned_3d( p0, p1, geode::Point3D{ { 4, 8, 12 } } ), + "[Test] Wrong aligned_3d on aligned points" ); + geode::OpenGeodeGeometryException::test( + !GEO::PCK::aligned_3d( + p0, p1, geode::Point3D{ { 4, 8, next( 12 ) } } ), + "[Test] Wrong aligned_3d on nearly aligned points" ); + // Too small for the floating-point filter, aligned in projection + geode::OpenGeodeGeometryException::test( + !GEO::PCK::aligned_3d( a, b, geode::Point3D{ { 2, 0, 1e-300 } } ), + "[Test] Wrong aligned_3d on nearly aligned tiny points" ); + } + + void test_dot_3d() + { + const geode::Point3D a{ { 0, 0, 0 } }; + const geode::Point3D b{ { 1, 2, 3 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( a, b, geode::Point3D{ { 1, 1, 1 } } ) + == GEO::positive, + "[Test] Wrong dot_3d on acute angle" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( a, b, geode::Point3D{ { -1, -1, -1 } } ) + == GEO::negative, + "[Test] Wrong dot_3d on obtuse angle" ); + + // Decided by the zero-filter + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( a, geode::Point3D{ { 1, 0, 0 } }, + geode::Point3D{ { 0, 5, 2 } } ) + == GEO::zero, + "[Test] Wrong dot_3d on orthogonal axes" ); + + // Decided by the exact arithmetic + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( a, b, geode::Point3D{ { 1, 1, -1 } } ) + == GEO::zero, + "[Test] Wrong dot_3d on orthogonal vectors" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( a, b, geode::Point3D{ { next( 1 ), 1, -1 } } ) + == GEO::positive, + "[Test] Wrong dot_3d on nearly orthogonal vectors" ); + } + + void test_dot_2d() + { + const geode::Point2D a{ { 0, 0 } }; + const geode::Point2D b{ { 1, 2 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( a, b, geode::Point2D{ { 1, 1 } } ) + == GEO::positive, + "[Test] Wrong dot_2d on acute angle" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( a, b, geode::Point2D{ { -1, -1 } } ) + == GEO::negative, + "[Test] Wrong dot_2d on obtuse angle" ); + + // Decided by the zero-filter + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( + a, geode::Point2D{ { 3, 0 } }, geode::Point2D{ { 0, -4 } } ) + == GEO::zero, + "[Test] Wrong dot_2d on orthogonal axes" ); + + // Decided by the exact arithmetic + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( a, b, geode::Point2D{ { -2, 1 } } ) == GEO::zero, + "[Test] Wrong dot_2d on orthogonal vectors" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( a, b, geode::Point2D{ { -2, next( 1 ) } } ) + == GEO::positive, + "[Test] Wrong dot_2d on nearly orthogonal vectors" ); + } +} // namespace + +void test() +{ + test_orient_2d(); + test_orient_3d(); + test_det_3d(); + test_aligned_3d(); + test_dot_3d(); + test_dot_2d(); + + geode::Logger::info( "TEST SUCCESS" ); +} + +OPENGEODE_TEST( "predicates" ) From 667f3ed9826dde452b7446c845a937ce933b6a39 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 12:02:06 +0200 Subject: [PATCH 32/52] fix(Geometry): exact predicates no longer depend on library initialization The splitter and epsilon used by the expansion arithmetic were only set by GEO::PCK::initialize(), called by OpenGeodeGeometryLibrary::initialize(). Code using the geometry library without initializing it (e.g. geometry tests, which only initialize the basic library) silently got wrong exact signs: two_product() splits with a zero splitter. They are now constant initialized with their IEEE 754 double values. The new consistency test (orient_2d/orient_3d under point permutations on nearly degenerate inputs) failed before this fix. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/internal/predicates.cpp | 7 ++- tests/geometry/test-predicates.cpp | 72 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/internal/predicates.cpp index 9af5c46aa..c801eb847 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/internal/predicates.cpp @@ -878,8 +878,11 @@ namespace namespace GEO { - double expansion_splitter_; - double expansion_epsilon_; + // Values for IEEE 754 doubles, as computed by PCK::initialize(): set at + // compile time for the predicates to be exact even if the library has + // not been initialized. + double expansion_splitter_ = 134217729.0; // 2^27 + 1 + double expansion_epsilon_ = 1.1102230246251565404e-16; // 2^-53 // ============= expansion sum and difference ========================= diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index 56b0a29e6..be9887f82 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -22,6 +22,7 @@ */ #include +#include #include #include @@ -258,6 +259,75 @@ namespace == GEO::positive, "[Test] Wrong dot_2d on nearly orthogonal vectors" ); } + + // Exactly degenerate, without shared coordinates: decided once the + // coordinate differences are known to be exact. + void test_exact_degenerate() + { + const geode::Point3D a{ { 1, 2, 5 } }; + const geode::Point3D b{ { 4, 1, 6 } }; + const geode::Point3D c{ { 2, 7, 16 } }; + const geode::Point3D d{ { 9, 3, 15 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( a, b, c, d ) == GEO::zero, + "[Test] Wrong orient_3d on points of plane z = x + 2y" ); + const geode::Point2D e{ { 1, 3 } }; + const geode::Point2D f{ { 4, 12 } }; + const geode::Point2D g{ { -7, -21 } }; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( e, f, g ) == GEO::zero, + "[Test] Wrong orient_2d on points of line y = 3x" ); + } + + // Nearly degenerate configurations, decided by different evaluation + // stages: the sign must not depend on the order of the points. + void test_nearly_degenerate_consistency() + { + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ 0, 1000 }; + const auto random = [&] { + return distribution( generator ); + }; + for( const auto offset : { 0., 5e5 } ) + { + for( const auto trial : geode::Range{ 10000 } ) + { + geode_unused( trial ); + std::array< geode::Point3D, 4 > p3; + for( auto& point : p3 ) + { + const auto x = offset + random(); + const auto y = offset + random(); + point = geode::Point3D{ { x, y, 0.3 * x + 0.7 * y } }; + } + const auto sign = + GEO::PCK::orient_3d( p3[0], p3[1], p3[2], p3[3] ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( p3[1], p3[0], p3[3], p3[2] ) == sign + && GEO::PCK::orient_3d( p3[2], p3[3], p3[0], p3[1] ) + == sign + && GEO::PCK::orient_3d( p3[1], p3[0], p3[2], p3[3] ) + == -sign + && GEO::PCK::orient_3d( p3[3], p3[1], p3[2], p3[0] ) + == -sign, + "[Test] Inconsistent orient_3d on nearly coplanar points" ); + + std::array< geode::Point2D, 3 > p2; + for( auto& point : p2 ) + { + const auto t = random(); + point = geode::Point2D{ { offset + t, offset + 0.3 * t } }; + } + const auto sign2 = GEO::PCK::orient_2d( p2[0], p2[1], p2[2] ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( p2[1], p2[2], p2[0] ) == sign2 + && GEO::PCK::orient_2d( p2[2], p2[0], p2[1] ) == sign2 + && GEO::PCK::orient_2d( p2[1], p2[0], p2[2] ) == -sign2, + "[Test] Inconsistent orient_2d on nearly collinear " + "points" ); + } + } + } } // namespace void test() @@ -268,6 +338,8 @@ void test() test_aligned_3d(); test_dot_3d(); test_dot_2d(); + test_exact_degenerate(); + test_nearly_degenerate_consistency(); geode::Logger::info( "TEST SUCCESS" ); } From 6bc6f7088ca3d4580ffcab7b2449d75e308a73a0 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 12:07:38 +0200 Subject: [PATCH 33/52] perf(Geometry): add Shewchuk adaptive stages to orient_2d and orient_3d When the floating-point filter is uncertain, stages B and C of Shewchuk's adaptive predicates (public domain predicates.c) are now evaluated before the full exact computation: stage B computes exactly the determinant of the rounded coordinate differences with short fixed size expansions, stage C concludes if these differences were exact or adds the first order corrections of their roundoff errors. Nearly degenerate and exactly degenerate (without shared coordinates) configurations are 2.5x to 4x faster, e.g. orient_3d on nearly coplanar points 660 -> 270 ns, orient_2d on nearly collinear points 128 -> 41 ns. Configurations still undecided after stage C pay the stages overhead (~10% slower) as stage D is not ported. Generic inputs unchanged; the signs of all benchmark calls are identical. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/internal/predicates.cpp | 384 +++++++++++++++++++++ tests/geometry/test-predicates.cpp | 20 ++ 2 files changed, 404 insertions(+) diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/internal/predicates.cpp index c801eb847..ac6560a14 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/internal/predicates.cpp @@ -1596,6 +1596,380 @@ namespace GEO && is_product_zero( p1.value( 1 ) - p0.value( 1 ), p2.value( 1 ) - p0.value( 1 ) ); } + + /******* adaptive stages *******/ + + // Stages B and C of the adaptive predicates of J. R. Shewchuk, "Adaptive + // Precision Floating-Point Arithmetic and Fast Robust Geometric + // Predicates", 1997 (predicates.c, public domain). They are evaluated + // when the floating-point filter fails: stage B computes exactly the + // determinant of the rounded coordinate differences, with short + // expansions. Stage C checks whether these differences were exact, or + // adds the first order corrections of their roundoff errors. If still + // uncertain, the full exact evaluation is required. + namespace adaptive + { + constexpr double EPSILON = 1.1102230246251565404e-16; // 2^-53 + constexpr double RESULT_ERRBOUND = ( 3.0 + 8.0 * EPSILON ) * EPSILON; + constexpr double CCW_ERRBOUND_B = ( 2.0 + 12.0 * EPSILON ) * EPSILON; + constexpr double CCW_ERRBOUND_C = + ( 9.0 + 64.0 * EPSILON ) * EPSILON * EPSILON; + constexpr double O3D_ERRBOUND_B = ( 3.0 + 28.0 * EPSILON ) * EPSILON; + constexpr double O3D_ERRBOUND_C = + ( 26.0 + 288.0 * EPSILON ) * EPSILON * EPSILON; + + inline void fast_two_sum( double a, double b, double& x, double& y ) + { + x = a + b; + const double bvirt = x - a; + y = b - bvirt; + } + + inline double two_diff_tail( double a, double b ) + { + double x; + double y; + two_diff( a, b, x, y ); + return y; + } + + // Exact a1 + a0 - b, as x2 + x1 + x0 + inline void two_one_diff( + double a1, double a0, double b, double& x2, double& x1, double& x0 ) + { + double i; + two_diff( a0, b, i, x0 ); + two_sum( a1, i, x2, x1 ); + } + + // Exact a*b - c*d as a 4 components expansion + inline void two_two_product_diff( + double a, double b, double c, double d, double x[4] ) + { + double ab1; + double ab0; + two_product( a, b, ab1, ab0 ); + double cd1; + double cd0; + two_product( c, d, cd1, cd0 ); + double j; + double r0; + two_one_diff( ab1, ab0, cd0, j, r0, x[0] ); + two_one_diff( j, r0, cd1, x[3], x[2], x[1] ); + } + + // h cannot be e or f. + inline int fast_expansion_sum_zeroelim( + int elen, const double* e, int flen, const double* f, double* h ) + { + int eindex = 0; + int findex = 0; + int hindex = 0; + double enow = e[0]; + double fnow = f[0]; + const auto next_e = [&] { + eindex++; + enow = eindex < elen ? e[eindex] : 0.; + }; + const auto next_f = [&] { + findex++; + fnow = findex < flen ? f[findex] : 0.; + }; + double Q; + double Qnew; + double hh; + if( ( fnow > enow ) == ( fnow > -enow ) ) + { + Q = enow; + next_e(); + } + else + { + Q = fnow; + next_f(); + } + if( eindex < elen && findex < flen ) + { + if( ( fnow > enow ) == ( fnow > -enow ) ) + { + fast_two_sum( enow, Q, Qnew, hh ); + next_e(); + } + else + { + fast_two_sum( fnow, Q, Qnew, hh ); + next_f(); + } + Q = Qnew; + if( hh != 0. ) + { + h[hindex++] = hh; + } + while( eindex < elen && findex < flen ) + { + if( ( fnow > enow ) == ( fnow > -enow ) ) + { + two_sum( Q, enow, Qnew, hh ); + next_e(); + } + else + { + two_sum( Q, fnow, Qnew, hh ); + next_f(); + } + Q = Qnew; + if( hh != 0. ) + { + h[hindex++] = hh; + } + } + } + while( eindex < elen ) + { + two_sum( Q, enow, Qnew, hh ); + next_e(); + Q = Qnew; + if( hh != 0. ) + { + h[hindex++] = hh; + } + } + while( findex < flen ) + { + two_sum( Q, fnow, Qnew, hh ); + next_f(); + Q = Qnew; + if( hh != 0. ) + { + h[hindex++] = hh; + } + } + if( Q != 0. || hindex == 0 ) + { + h[hindex++] = Q; + } + return hindex; + } + + // e and h cannot be the same. + inline int scale_expansion_zeroelim( + int elen, const double* e, double b, double* h ) + { + int hindex = 0; + double Q; + double hh; + two_product( e[0], b, Q, hh ); + if( hh != 0. ) + { + h[hindex++] = hh; + } + for( int eindex = 1; eindex < elen; eindex++ ) + { + double product1; + double product0; + two_product( e[eindex], b, product1, product0 ); + double sum; + two_sum( Q, product0, sum, hh ); + if( hh != 0. ) + { + h[hindex++] = hh; + } + fast_two_sum( product1, sum, Q, hh ); + if( hh != 0. ) + { + h[hindex++] = hh; + } + } + if( Q != 0. || hindex == 0 ) + { + h[hindex++] = Q; + } + return hindex; + } + + inline double estimate( int elen, const double* e ) + { + double Q = e[0]; + for( int eindex = 1; eindex < elen; eindex++ ) + { + Q += e[eindex]; + } + return Q; + } + + // Components are non-overlapping and sorted by increasing magnitude: + // the sign is the one of the largest non-zero component. + inline SIGN expansion_sign( int elen, const double* e ) + { + for( int eindex = elen - 1; eindex >= 0; eindex-- ) + { + if( e[eindex] > 0. ) + { + return positive; + } + if( e[eindex] < 0. ) + { + return negative; + } + } + return zero; + } + + inline SIGN sign_of( double value ) + { + return value > 0. ? positive : negative; + } + + // Returns true if the sign of (p1-p0)x(p2-p0) has been determined + inline bool orient_2d( const geode::Point2D& p0, + const geode::Point2D& p1, + const geode::Point2D& p2, + SIGN& result ) + { + const double acx = p1.value( 0 ) - p0.value( 0 ); + const double bcx = p2.value( 0 ) - p0.value( 0 ); + const double acy = p1.value( 1 ) - p0.value( 1 ); + const double bcy = p2.value( 1 ) - p0.value( 1 ); + const double detsum = + std::fabs( acx * bcy ) + std::fabs( acy * bcx ); + + double B[4]; + two_two_product_diff( acx, bcy, acy, bcx, B ); + double det = estimate( 4, B ); + double errbound = CCW_ERRBOUND_B * detsum; + if( det >= errbound || -det >= errbound ) + { + result = sign_of( det ); + return true; + } + + const double acxtail = + two_diff_tail( p1.value( 0 ), p0.value( 0 ) ); + const double bcxtail = + two_diff_tail( p2.value( 0 ), p0.value( 0 ) ); + const double acytail = + two_diff_tail( p1.value( 1 ), p0.value( 1 ) ); + const double bcytail = + two_diff_tail( p2.value( 1 ), p0.value( 1 ) ); + if( acxtail == 0. && acytail == 0. && bcxtail == 0. + && bcytail == 0. ) + { + result = expansion_sign( 4, B ); + return true; + } + + errbound = + CCW_ERRBOUND_C * detsum + RESULT_ERRBOUND * std::fabs( det ); + det += ( acx * bcytail + bcy * acxtail ) + - ( acy * bcxtail + bcx * acytail ); + if( det >= errbound || -det >= errbound ) + { + result = sign_of( det ); + return true; + } + return false; + } + + // Returns true if the sign of det(p1-p0, p2-p0, p3-p0) has been + // determined + inline bool orient_3d( const geode::Point3D& p0, + const geode::Point3D& p1, + const geode::Point3D& p2, + const geode::Point3D& p3, + SIGN& result ) + { + const double adx = p1.value( 0 ) - p0.value( 0 ); + const double bdx = p2.value( 0 ) - p0.value( 0 ); + const double cdx = p3.value( 0 ) - p0.value( 0 ); + const double ady = p1.value( 1 ) - p0.value( 1 ); + const double bdy = p2.value( 1 ) - p0.value( 1 ); + const double cdy = p3.value( 1 ) - p0.value( 1 ); + const double adz = p1.value( 2 ) - p0.value( 2 ); + const double bdz = p2.value( 2 ) - p0.value( 2 ); + const double cdz = p3.value( 2 ) - p0.value( 2 ); + const double permanent = + ( std::fabs( bdx * cdy ) + std::fabs( cdx * bdy ) ) + * std::fabs( adz ) + + ( std::fabs( cdx * ady ) + std::fabs( adx * cdy ) ) + * std::fabs( bdz ) + + ( std::fabs( adx * bdy ) + std::fabs( bdx * ady ) ) + * std::fabs( cdz ); + + double bc[4]; + two_two_product_diff( bdx, cdy, cdx, bdy, bc ); + double adet[8]; + const auto alen = scale_expansion_zeroelim( 4, bc, adz, adet ); + double ca[4]; + two_two_product_diff( cdx, ady, adx, cdy, ca ); + double bdet[8]; + const auto blen = scale_expansion_zeroelim( 4, ca, bdz, bdet ); + double ab[4]; + two_two_product_diff( adx, bdy, bdx, ady, ab ); + double cdet[8]; + const auto clen = scale_expansion_zeroelim( 4, ab, cdz, cdet ); + double abdet[16]; + const auto ablen = + fast_expansion_sum_zeroelim( alen, adet, blen, bdet, abdet ); + double fin[24]; + const auto finlength = + fast_expansion_sum_zeroelim( ablen, abdet, clen, cdet, fin ); + + double det = estimate( finlength, fin ); + double errbound = O3D_ERRBOUND_B * permanent; + if( det >= errbound || -det >= errbound ) + { + result = sign_of( det ); + return true; + } + + const double adxtail = + two_diff_tail( p1.value( 0 ), p0.value( 0 ) ); + const double bdxtail = + two_diff_tail( p2.value( 0 ), p0.value( 0 ) ); + const double cdxtail = + two_diff_tail( p3.value( 0 ), p0.value( 0 ) ); + const double adytail = + two_diff_tail( p1.value( 1 ), p0.value( 1 ) ); + const double bdytail = + two_diff_tail( p2.value( 1 ), p0.value( 1 ) ); + const double cdytail = + two_diff_tail( p3.value( 1 ), p0.value( 1 ) ); + const double adztail = + two_diff_tail( p1.value( 2 ), p0.value( 2 ) ); + const double bdztail = + two_diff_tail( p2.value( 2 ), p0.value( 2 ) ); + const double cdztail = + two_diff_tail( p3.value( 2 ), p0.value( 2 ) ); + if( adxtail == 0. && bdxtail == 0. && cdxtail == 0. && adytail == 0. + && bdytail == 0. && cdytail == 0. && adztail == 0. + && bdztail == 0. && cdztail == 0. ) + { + result = expansion_sign( finlength, fin ); + return true; + } + + errbound = + O3D_ERRBOUND_C * permanent + RESULT_ERRBOUND * std::fabs( det ); + det += ( adz + * ( ( bdx * cdytail + cdy * bdxtail ) + - ( bdy * cdxtail + cdx * bdytail ) ) + + adztail * ( bdx * cdy - bdy * cdx ) ) + + ( bdz + * ( ( cdx * adytail + ady * cdxtail ) + - ( cdy * adxtail + adx * cdytail ) ) + + bdztail * ( cdx * ady - cdy * adx ) ) + + ( cdz + * ( ( adx * bdytail + bdy * adxtail ) + - ( ady * bdxtail + bdx * adytail ) ) + + cdztail * ( adx * bdy - ady * bdx ) ); + if( det >= errbound || -det >= errbound ) + { + result = sign_of( det ); + return true; + } + return false; + } + } // namespace adaptive } // namespace GEO /******* extracted from predicates.cpp *******/ @@ -1762,6 +2136,11 @@ namespace GEO { return zero; } + SIGN adaptive_result; + if( adaptive::orient_2d( p0, p1, p2, adaptive_result ) ) + { + return adaptive_result; + } return orient_2d_exact( p0, p1, p2 ); } @@ -1779,6 +2158,11 @@ namespace GEO { return zero; } + SIGN adaptive_result; + if( adaptive::orient_3d( p0, p1, p2, p3, adaptive_result ) ) + { + return adaptive_result; + } return orient_3d_exact( p0, p1, p2, p3 ); } diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index be9887f82..3cb3afdd0 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -277,6 +277,26 @@ namespace geode::OpenGeodeGeometryException::test( GEO::PCK::orient_2d( e, f, g ) == GEO::zero, "[Test] Wrong orient_2d on points of line y = 3x" ); + + // Same configurations, but the coordinate differences with the tiny + // first point are not representable: rounded differences give a non + // zero determinant. + const auto tiny = std::ldexp( 1., -60 ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( geode::Point3D{ { 3 * tiny, tiny, 5 * tiny } }, + geode::Point3D{ { 1000.5, 3.25, 1007 } }, + geode::Point3D{ { -20.75, 100.5, 180.25 } }, + geode::Point3D{ { 7, 9, 25 } } ) + == GEO::zero, + "[Test] Wrong orient_3d on points of plane z = x + 2y with " + "inexact differences" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( geode::Point2D{ { tiny, 3 * tiny } }, + geode::Point2D{ { 1000.5, 3001.5 } }, + geode::Point2D{ { -20.25, -60.75 } } ) + == GEO::zero, + "[Test] Wrong orient_2d on points of line y = 3x with inexact " + "differences" ); } // Nearly degenerate configurations, decided by different evaluation From f79047f29214def52b5a174b43117d4cfebaa9ee Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 12:24:39 +0200 Subject: [PATCH 34/52] perf(Geometry): add Shewchuk stage D to orient_2d and orient_3d When stages B and C cannot decide, stage D now adds exactly all the remaining roundoff error terms to the stage B expansion, instead of restarting a full exact evaluation. orient_2d_exact and orient_3d_exact are no longer used and removed. This removes the slowdown of configurations undecided after stage C: orient_3d on nearly collinear points 617 -> 473 ns (574 ns before the adaptive stages). Other timings unchanged. Signs checked against Shewchuk's original predicates.c on 20M hard cases (mixed magnitudes, exact and nearly degenerate configurations): no mismatch. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/internal/predicates.cpp | 275 +++++++++++++++------ tests/geometry/test-predicates.cpp | 16 ++ 2 files changed, 211 insertions(+), 80 deletions(-) diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/internal/predicates.cpp index ac6560a14..9dafdaefd 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/internal/predicates.cpp @@ -1599,14 +1599,15 @@ namespace GEO /******* adaptive stages *******/ - // Stages B and C of the adaptive predicates of J. R. Shewchuk, "Adaptive + // Stages B, C and D of the adaptive predicates of J. R. Shewchuk, "Adaptive // Precision Floating-Point Arithmetic and Fast Robust Geometric // Predicates", 1997 (predicates.c, public domain). They are evaluated // when the floating-point filter fails: stage B computes exactly the // determinant of the rounded coordinate differences, with short // expansions. Stage C checks whether these differences were exact, or // adds the first order corrections of their roundoff errors. If still - // uncertain, the full exact evaluation is required. + // uncertain, stage D adds exactly all the remaining terms to the stage B + // result. namespace adaptive { constexpr double EPSILON = 1.1102230246251565404e-16; // 2^-53 @@ -1820,11 +1821,48 @@ namespace GEO return value > 0. ? positive : negative; } - // Returns true if the sign of (p1-p0)x(p2-p0) has been determined - inline bool orient_2d( const geode::Point2D& p0, + // Exact (a1 + a0) * b as a 4 components expansion + inline void two_one_product( + double a1, double a0, double b, double x[4] ) + { + double i; + two_product( a0, b, i, x[0] ); + double j; + double r0; + two_product( a1, b, j, r0 ); + double k; + two_sum( i, r0, k, x[1] ); + fast_two_sum( j, k, x[3], x[2] ); + } + + // Exact xtail * y - ytail * x, skipping the zero tails. Returns the + // expansion length. + inline int tail_cross( + double xtail, double ytail, double x, double y, double h[4] ) + { + if( xtail == 0. ) + { + if( ytail == 0. ) + { + h[0] = 0.; + return 1; + } + two_product( -ytail, x, h[1], h[0] ); + return 2; + } + if( ytail == 0. ) + { + two_product( xtail, y, h[1], h[0] ); + return 2; + } + two_two_product_diff( xtail, y, ytail, x, h ); + return 4; + } + + // Sign of (p1-p0)x(p2-p0) + inline SIGN orient_2d( const geode::Point2D& p0, const geode::Point2D& p1, - const geode::Point2D& p2, - SIGN& result ) + const geode::Point2D& p2 ) { const double acx = p1.value( 0 ) - p0.value( 0 ); const double bcx = p2.value( 0 ) - p0.value( 0 ); @@ -1833,16 +1871,17 @@ namespace GEO const double detsum = std::fabs( acx * bcy ) + std::fabs( acy * bcx ); + // Stage B double B[4]; two_two_product_diff( acx, bcy, acy, bcx, B ); double det = estimate( 4, B ); double errbound = CCW_ERRBOUND_B * detsum; if( det >= errbound || -det >= errbound ) { - result = sign_of( det ); - return true; + return sign_of( det ); } + // Stage C const double acxtail = two_diff_tail( p1.value( 0 ), p0.value( 0 ) ); const double bcxtail = @@ -1854,29 +1893,38 @@ namespace GEO if( acxtail == 0. && acytail == 0. && bcxtail == 0. && bcytail == 0. ) { - result = expansion_sign( 4, B ); - return true; + return expansion_sign( 4, B ); } - errbound = CCW_ERRBOUND_C * detsum + RESULT_ERRBOUND * std::fabs( det ); det += ( acx * bcytail + bcy * acxtail ) - ( acy * bcxtail + bcx * acytail ); if( det >= errbound || -det >= errbound ) { - result = sign_of( det ); - return true; + return sign_of( det ); } - return false; + + // Stage D: exact sum of all the terms + double u[4]; + two_two_product_diff( acxtail, bcy, acytail, bcx, u ); + double C1[8]; + const auto C1length = fast_expansion_sum_zeroelim( 4, B, 4, u, C1 ); + two_two_product_diff( acx, bcytail, acy, bcxtail, u ); + double C2[12]; + const auto C2length = + fast_expansion_sum_zeroelim( C1length, C1, 4, u, C2 ); + two_two_product_diff( acxtail, bcytail, acytail, bcxtail, u ); + double D[16]; + const auto Dlength = + fast_expansion_sum_zeroelim( C2length, C2, 4, u, D ); + return expansion_sign( Dlength, D ); } - // Returns true if the sign of det(p1-p0, p2-p0, p3-p0) has been - // determined - inline bool orient_3d( const geode::Point3D& p0, + // Sign of det(p1-p0, p2-p0, p3-p0) + inline SIGN orient_3d( const geode::Point3D& p0, const geode::Point3D& p1, const geode::Point3D& p2, - const geode::Point3D& p3, - SIGN& result ) + const geode::Point3D& p3 ) { const double adx = p1.value( 0 ) - p0.value( 0 ); const double bdx = p2.value( 0 ) - p0.value( 0 ); @@ -1895,6 +1943,7 @@ namespace GEO + ( std::fabs( adx * bdy ) + std::fabs( bdx * ady ) ) * std::fabs( cdz ); + // Stage B double bc[4]; two_two_product_diff( bdx, cdy, cdx, bdy, bc ); double adet[8]; @@ -1910,18 +1959,19 @@ namespace GEO double abdet[16]; const auto ablen = fast_expansion_sum_zeroelim( alen, adet, blen, bdet, abdet ); - double fin[24]; - const auto finlength = - fast_expansion_sum_zeroelim( ablen, abdet, clen, cdet, fin ); + double fin1[192]; + double fin2[192]; + auto finlength = + fast_expansion_sum_zeroelim( ablen, abdet, clen, cdet, fin1 ); - double det = estimate( finlength, fin ); + double det = estimate( finlength, fin1 ); double errbound = O3D_ERRBOUND_B * permanent; if( det >= errbound || -det >= errbound ) { - result = sign_of( det ); - return true; + return sign_of( det ); } + // Stage C const double adxtail = two_diff_tail( p1.value( 0 ), p0.value( 0 ) ); const double bdxtail = @@ -1944,8 +1994,7 @@ namespace GEO && bdytail == 0. && cdytail == 0. && adztail == 0. && bdztail == 0. && cdztail == 0. ) { - result = expansion_sign( finlength, fin ); - return true; + return expansion_sign( finlength, fin1 ); } errbound = @@ -1964,10 +2013,124 @@ namespace GEO + cdztail * ( adx * bdy - ady * bdx ) ); if( det >= errbound || -det >= errbound ) { - result = sign_of( det ); - return true; + return sign_of( det ); } - return false; + + // Stage D: stage B result plus all the terms involving tails + double* finnow = fin1; + double* finother = fin2; + const auto add = [&]( int length, const double* expansion ) { + finlength = fast_expansion_sum_zeroelim( + finlength, finnow, length, expansion, finother ); + std::swap( finnow, finother ); + }; + + double at_b[4]; + const auto at_blen = tail_cross( adxtail, adytail, bdx, bdy, at_b ); + double at_c[4]; + const auto at_clen = + tail_cross( -adxtail, -adytail, cdx, cdy, at_c ); + double bt_c[4]; + const auto bt_clen = tail_cross( bdxtail, bdytail, cdx, cdy, bt_c ); + double bt_a[4]; + const auto bt_alen = + tail_cross( -bdxtail, -bdytail, adx, ady, bt_a ); + double ct_a[4]; + const auto ct_alen = tail_cross( cdxtail, cdytail, adx, ady, ct_a ); + double ct_b[4]; + const auto ct_blen = + tail_cross( -cdxtail, -cdytail, bdx, bdy, ct_b ); + + double w[16]; + double bct[8]; + const auto bctlen = fast_expansion_sum_zeroelim( + bt_clen, bt_c, ct_blen, ct_b, bct ); + add( scale_expansion_zeroelim( bctlen, bct, adz, w ), w ); + double cat[8]; + const auto catlen = fast_expansion_sum_zeroelim( + ct_alen, ct_a, at_clen, at_c, cat ); + add( scale_expansion_zeroelim( catlen, cat, bdz, w ), w ); + double abt[8]; + const auto abtlen = fast_expansion_sum_zeroelim( + at_blen, at_b, bt_alen, bt_a, abt ); + add( scale_expansion_zeroelim( abtlen, abt, cdz, w ), w ); + + double v[12]; + if( adztail != 0. ) + { + add( scale_expansion_zeroelim( 4, bc, adztail, v ), v ); + } + if( bdztail != 0. ) + { + add( scale_expansion_zeroelim( 4, ca, bdztail, v ), v ); + } + if( cdztail != 0. ) + { + add( scale_expansion_zeroelim( 4, ab, cdztail, v ), v ); + } + + // xtail * ytail * (z + ztail) + const auto add_tails_product = [&]( double xtail, double ytail, + double z, double ztail ) { + double product1; + double product0; + two_product( xtail, ytail, product1, product0 ); + double u[4]; + two_one_product( product1, product0, z, u ); + add( 4, u ); + if( ztail != 0. ) + { + two_one_product( product1, product0, ztail, u ); + add( 4, u ); + } + }; + if( adxtail != 0. ) + { + if( bdytail != 0. ) + { + add_tails_product( adxtail, bdytail, cdz, cdztail ); + } + if( cdytail != 0. ) + { + add_tails_product( -adxtail, cdytail, bdz, bdztail ); + } + } + if( bdxtail != 0. ) + { + if( cdytail != 0. ) + { + add_tails_product( bdxtail, cdytail, adz, adztail ); + } + if( adytail != 0. ) + { + add_tails_product( -bdxtail, adytail, cdz, cdztail ); + } + } + if( cdxtail != 0. ) + { + if( adytail != 0. ) + { + add_tails_product( cdxtail, adytail, bdz, bdztail ); + } + if( bdytail != 0. ) + { + add_tails_product( -cdxtail, bdytail, adz, adztail ); + } + } + + if( adztail != 0. ) + { + add( scale_expansion_zeroelim( bctlen, bct, adztail, w ), w ); + } + if( bdztail != 0. ) + { + add( scale_expansion_zeroelim( catlen, cat, bdztail, w ), w ); + } + if( cdztail != 0. ) + { + add( scale_expansion_zeroelim( abtlen, abt, cdztail, w ), w ); + } + return expansion_sign( finlength, finnow ); } } // namespace adaptive } // namespace GEO @@ -1997,46 +2160,8 @@ namespace GEO { // ============ orient2d ============================================== - SIGN orient_2d_exact( const geode::Point2D& p0, - const geode::Point2D& p1, - const geode::Point2D& p2 ) - { - const expansion& a11 = expansion_diff( p1.value( 0 ), p0.value( 0 ) ); - const expansion& a12 = expansion_diff( p1.value( 1 ), p0.value( 1 ) ); - - const expansion& a21 = expansion_diff( p2.value( 0 ), p0.value( 0 ) ); - const expansion& a22 = expansion_diff( p2.value( 1 ), p0.value( 1 ) ); - - const expansion& Delta = expansion_det2x2( a11, a12, a21, a22 ); - - return Delta.sign(); - } - // ============ orient3d ============================================== - SIGN orient_3d_exact( const geode::Point3D& p0, - const geode::Point3D& p1, - const geode::Point3D& p2, - const geode::Point3D& p3 ) - { - const expansion& a11 = expansion_diff( p1.value( 0 ), p0.value( 0 ) ); - const expansion& a12 = expansion_diff( p1.value( 1 ), p0.value( 1 ) ); - const expansion& a13 = expansion_diff( p1.value( 2 ), p0.value( 2 ) ); - - const expansion& a21 = expansion_diff( p2.value( 0 ), p0.value( 0 ) ); - const expansion& a22 = expansion_diff( p2.value( 1 ), p0.value( 1 ) ); - const expansion& a23 = expansion_diff( p2.value( 2 ), p0.value( 2 ) ); - - const expansion& a31 = expansion_diff( p3.value( 0 ), p0.value( 0 ) ); - const expansion& a32 = expansion_diff( p3.value( 1 ), p0.value( 1 ) ); - const expansion& a33 = expansion_diff( p3.value( 2 ), p0.value( 2 ) ); - - const expansion& Delta = - expansion_det3x3( a11, a12, a13, a21, a22, a23, a31, a32, a33 ); - - return Delta.sign(); - } - // ================================ det and dot ======================= SIGN det_3d_exact( const geode::Vector3D& p0, @@ -2136,12 +2261,7 @@ namespace GEO { return zero; } - SIGN adaptive_result; - if( adaptive::orient_2d( p0, p1, p2, adaptive_result ) ) - { - return adaptive_result; - } - return orient_2d_exact( p0, p1, p2 ); + return adaptive::orient_2d( p0, p1, p2 ); } SIGN orient_3d( const geode::Point3D& p0, @@ -2158,12 +2278,7 @@ namespace GEO { return zero; } - SIGN adaptive_result; - if( adaptive::orient_3d( p0, p1, p2, p3, adaptive_result ) ) - { - return adaptive_result; - } - return orient_3d_exact( p0, p1, p2, p3 ); + return adaptive::orient_3d( p0, p1, p2, p3 ); } SIGN det_3d( const geode::Vector3D& p0, diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index 3cb3afdd0..007474cab 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -297,6 +297,22 @@ namespace == GEO::zero, "[Test] Wrong orient_2d on points of line y = 3x with inexact " "differences" ); + + // Nearly collinear points, exactly coplanar: requires all the + // roundoff error terms of the coordinate differences, including + // the products of three of them. + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( + geode::Point3D{ { 0x1.47d6541ade7cdp+6, 0x1.896798203e2f6p+4, + 0x1.caf8dc259de1ep+5 } }, + geode::Point3D{ { -0x1.73ff1093c559p+9, -0x1.be654717b99ep+7, + -0x1.0465becdd6f18p+9 } }, + geode::Point3D{ { 0x1.22acf2099e883p+9, 0x1.5ccf88d857d6ap+7, + 0x1.96f21fa711251p+8 } }, + geode::Point3D{ { 0x1.0dfd21cd4d26ap+3, 0x1.43fc8ef65c94cp+1, + 0x1.79fbfc1f6c02fp+2 } } ) + == GEO::zero, + "[Test] Wrong orient_3d on nearly collinear coplanar points" ); } // Nearly degenerate configurations, decided by different evaluation From f3667b025463b9c50fdbba6ad4a0d2ac46cf4ada Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 13:01:44 +0200 Subject: [PATCH 35/52] test(Geometry): exhaustive non-regression tests for exact predicates test-predicates now checks every predicate against references that do not depend on the implementation: - integer configurations (random, exactly degenerate, one unit away from degenerate, shared coordinates, duplicated points), at usual scales and at scales below the floating-point filters underflow guard, compared with exact 64 bits integer determinants; - a table of configurations decided by each evaluation stage (filter, zero-filter, adaptive stages B, C and D, exact evaluation), including configurations where plain floating-point evaluation gives a wrong sign, with signs checked with exact rational arithmetic. Co-Authored-By: Claude Opus 5.5 --- tests/geometry/test-predicates.cpp | 1774 ++++++++++++++++++++++++++++ 1 file changed, 1774 insertions(+) diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index 007474cab..a3c711adc 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -21,8 +21,12 @@ * */ +#include #include +#include +#include #include +#include #include #include @@ -148,6 +152,21 @@ namespace GEO::PCK::det_3d( x, geode::Vector3D{ { 0, 0, 0 } }, z ) == GEO::zero, "[Test] Wrong det_3d with null vector" ); + // Too small for the floating-point filter, many zero coordinates but + // non zero determinant: the zero-filter must not conclude. + const auto tiny = std::ldexp( 1., -330 ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( geode::Vector3D{ { 0, tiny, 0 } }, + geode::Vector3D{ { 0, 0, tiny } }, + geode::Vector3D{ { tiny, 0, 0 } } ) + == GEO::positive, + "[Test] Wrong det_3d on tiny permutation vectors" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( geode::Vector3D{ { tiny, 0, 0 } }, + geode::Vector3D{ { 0, 0, tiny } }, + geode::Vector3D{ { 0, tiny, 0 } } ) + == GEO::negative, + "[Test] Wrong det_3d on tiny permutation vectors (odd)" ); // Decided by the exact arithmetic const geode::Vector3D u{ { 1, 2, 3 } }; @@ -364,6 +383,1759 @@ namespace } } } + + // Exact reference on integer coordinates. The predicates are evaluated + // on scale * (offset + n) with n integers of absolute value lower than + // 2^15: the coordinate differences are exact, and the reference 3x3 + // determinants of differences of n are lower than 2^54. + class IntegerConfigurations + { + public: + std::int64_t random_integer( std::int64_t min, std::int64_t max ) + { + return std::uniform_int_distribution< std::int64_t >{ min, max }( + generator_ ); + } + + // Random coordinates, or degenerate configurations: + // all points on the plane (or line) passing through the first one + // and spanned by two (or one) random directions, possibly moved by + // one unit, sharing coordinates or duplicated. + std::vector< std::array< std::int64_t, 3 > > points( + geode::index_t nb_points, geode::index_t nb_directions ) + { + std::vector< std::array< std::int64_t, 3 > > result( nb_points ); + const auto type = random_integer( 0, 4 ); + for( auto& point : result ) + { + for( auto& value : point ) + { + value = random_integer( -10000, 10000 ); + } + } + if( type == 0 ) + { + return result; + } + std::array< std::array< std::int64_t, 3 >, 2 > directions; + for( auto& direction : directions ) + { + for( auto& value : direction ) + { + value = random_integer( -60, 60 ); + } + } + for( const auto p : geode::Range{ 1, nb_points } ) + { + for( const auto d : geode::LRange{ 3 } ) + { + result[p][d] = result[0][d]; + for( const auto i : geode::Range{ nb_directions } ) + { + result[p][d] += + random_integer( -60, 60 ) * directions[i][d]; + } + } + } + if( type == 2 ) + { + result.back()[random_integer( 0, 2 )] += + random_integer( 0, 1 ) == 0 ? -1 : 1; + } + else if( type == 3 ) + { + const auto d = random_integer( 0, 2 ); + for( auto& point : result ) + { + point[d] = result[0][d]; + } + } + else if( type == 4 ) + { + result.back() = result[random_integer( 0, nb_points - 2 )]; + } + return result; + } + + // Frame 0 has usual scales. Frames 1 and 2 have scales so small that + // the floating-point filters of the degree 3 and degree 2 predicates + // are uncertain (underflow guard), but large enough to avoid + // underflow in the exact evaluations. + void new_frame( geode::index_t frame ) + { + static constexpr std::array< std::array< int, 2 >, 3 > EXPONENTS{ + { { -40, 40 }, { -335, -326 }, { -505, -490 } } + }; + scale_ = std::ldexp( + 1., static_cast< int >( random_integer( + EXPONENTS[frame][0], EXPONENTS[frame][1] ) ) ); + for( auto& value : offset_ ) + { + value = random_integer( 0, 1 ) == 0 + ? 0 + : random_integer( -( 1 << 30 ), 1 << 30 ); + } + } + + double coordinate( std::int64_t value, geode::local_index_t d ) const + { + return scale_ * static_cast< double >( offset_[d] + value ); + } + + geode::Point3D point3d( + const std::array< std::int64_t, 3 >& point ) const + { + return geode::Point3D{ { coordinate( point[0], 0 ), + coordinate( point[1], 1 ), coordinate( point[2], 2 ) } }; + } + + geode::Point2D point2d( + const std::array< std::int64_t, 3 >& point ) const + { + return geode::Point2D{ { coordinate( point[0], 0 ), + coordinate( point[1], 1 ) } }; + } + + geode::Vector3D vector3d( + const std::array< std::int64_t, 3 >& vector ) const + { + return geode::Vector3D{ { scale_ + * static_cast< double >( vector[0] ), + scale_ * static_cast< double >( vector[1] ), + scale_ * static_cast< double >( vector[2] ) } }; + } + + private: + std::mt19937_64 generator_{ 42 }; + double scale_{ 1 }; + std::array< std::int64_t, 3 > offset_{}; + }; + + int sign( std::int64_t value ) + { + return ( value > 0 ) - ( value < 0 ); + } + + std::array< std::int64_t, 3 > difference( + const std::array< std::int64_t, 3 >& a, + const std::array< std::int64_t, 3 >& b ) + { + return { a[0] - b[0], a[1] - b[1], a[2] - b[2] }; + } + + std::int64_t determinant( const std::array< std::int64_t, 3 >& a, + const std::array< std::int64_t, 3 >& b, + const std::array< std::int64_t, 3 >& c ) + { + return a[0] * ( b[1] * c[2] - b[2] * c[1] ) + - a[1] * ( b[0] * c[2] - b[2] * c[0] ) + + a[2] * ( b[0] * c[1] - b[1] * c[0] ); + } + + void test_integer_configurations() + { + IntegerConfigurations configurations; + for( const auto trial : geode::Range{ 20000 } ) + { + const auto frame = trial % 3; + configurations.new_frame( frame ); + + const auto p4 = configurations.points( 4, 2 ); + if( frame != 2 ) + { + const auto v1 = difference( p4[1], p4[0] ); + const auto v2 = difference( p4[2], p4[0] ); + const auto v3 = difference( p4[3], p4[0] ); + const auto orient_3d = sign( determinant( v1, v2, v3 ) ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( configurations.point3d( p4[0] ), + configurations.point3d( p4[1] ), + configurations.point3d( p4[2] ), + configurations.point3d( p4[3] ) ) + == orient_3d, + "[Test] Wrong orient_3d on integer configuration" ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( configurations.vector3d( v1 ), + configurations.vector3d( v2 ), + configurations.vector3d( v3 ) ) + == orient_3d, + "[Test] Wrong det_3d on integer configuration" ); + } + + const auto p3 = configurations.points( 3, 1 ); + const auto u = difference( p3[1], p3[0] ); + const auto v = difference( p3[2], p3[0] ); + const auto orient_2d = sign( u[0] * v[1] - u[1] * v[0] ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( configurations.point2d( p3[0] ), + configurations.point2d( p3[1] ), + configurations.point2d( p3[2] ) ) + == orient_2d, + "[Test] Wrong orient_2d on integer configuration" ); + const auto aligned = u[1] * v[2] - u[2] * v[1] == 0 + && u[2] * v[0] - u[0] * v[2] == 0 + && orient_2d == 0; + geode::OpenGeodeGeometryException::test( + GEO::PCK::aligned_3d( configurations.point3d( p3[0] ), + configurations.point3d( p3[1] ), + configurations.point3d( p3[2] ) ) + == aligned, + "[Test] Wrong aligned_3d on integer configuration" ); + + // Second point orthogonal to the first one around the origin + auto orthogonal = p3; + const auto w = difference( orthogonal[2], orthogonal[0] ); + const std::array< std::int64_t, 3 > normal{ u[1] * w[2] + - u[2] * w[1], + u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0] }; + if( configurations.random_integer( 0, 1 ) == 0 + && std::abs( normal[0] ) < 10000 + && std::abs( normal[1] ) < 10000 + && std::abs( normal[2] ) < 10000 ) + { + for( const auto d : geode::LRange{ 3 } ) + { + orthogonal[2][d] = orthogonal[0][d] + normal[d]; + } + } + const auto a = difference( orthogonal[1], orthogonal[0] ); + const auto b = difference( orthogonal[2], orthogonal[0] ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( configurations.point3d( orthogonal[0] ), + configurations.point3d( orthogonal[1] ), + configurations.point3d( orthogonal[2] ) ) + == sign( a[0] * b[0] + a[1] * b[1] + a[2] * b[2] ), + "[Test] Wrong dot_3d on integer configuration" ); + auto orthogonal_2d = p3; + if( configurations.random_integer( 0, 1 ) == 0 ) + { + orthogonal_2d[2][0] = orthogonal_2d[0][0] - u[1]; + orthogonal_2d[2][1] = orthogonal_2d[0][1] + u[0]; + } + const auto c = difference( orthogonal_2d[1], orthogonal_2d[0] ); + const auto e = difference( orthogonal_2d[2], orthogonal_2d[0] ); + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( configurations.point2d( orthogonal_2d[0] ), + configurations.point2d( orthogonal_2d[1] ), + configurations.point2d( orthogonal_2d[2] ) ) + == sign( c[0] * e[0] + c[1] * e[1] ), + "[Test] Wrong dot_2d on integer configuration" ); + } + } + + struct RegressionCase + { + const char* stage; + int expected; + std::vector< double > values; + }; + + // Configurations decided by each evaluation stage of the predicates: + // floating-point filter, zero-filter, adaptive stages B, C and D for the + // orientations, exact evaluation for the others. Generated from random + // coordinates of mixed magnitudes (without subnormal numbers, as + // expansion arithmetic is not exact on underflow), nearly or exactly + // degenerate, including configurations where a plain floating-point + // evaluation gives a wrong sign. Expected signs computed with exact + // rational arithmetic. + const std::vector< RegressionCase > ORIENT_2D_CASES{ + { "filter", -1, + { 0x1.58p-29, 0x1.4afdc553fdedfp-28, -0x1.28p+5, + 0x1.4afdc553fdedfp-28, -0x1.6cp+6, 0x1.2decf20fde86p-8 } }, + { "filter", -1, + { 0x1.6f5c50890241p-31, -0x1.8000000000001p+6, 0x1.15f0feccb4fep-40, + -0x1.8p+6, 0x1.18p-35, -0x1.7ffffffffffffp+6 } }, + { "filter", -1, + { 0x1.8f9008ec5f9c1p-8, 0x1.76dc6863deebdp+11, 0x1.fbp-23, + -0x1.b071dc6739e8ap+5, 0x1.8f9008ec5f9c1p-8, + 0x1.6ec3de33860ffp+22 } }, + { "filter", 1, + { 0x1.e81af5dfee914p+18, 0x1.f75c698eee05dp+23, + -0x1.871ab4cf3e2b3p+12, -0x1.932c8a75b81ccp+17, + 0x1.f75c698eee05dp+23, 0x1.37ffffffffffp+6 } }, + { "filter", 1, + { -0x1.067ffffe6b8p+11, -0x1.067ffffe6b8p+11, -0x1.4p+6, + -0x1.a3fffffcd7p+10, 0x1.e7a9ba24781bdp+18, + 0x1.40076227eeeb8p+23 } }, + { "filter", 1, + { -0x1.9dp-16, 0x1.e8a9622516bf1p+18, 0x1.e8a9622516bf1p+18, + 0x1.d177895166469p+37, 0x1.e8ea1adfe7b4p+18, + 0x1.d2f584be6e0abp+37 } }, + { "zero-filter", 0, + { -0x1.1p+2, -0x1.2p+5, 0x1.e84858a783c8ap+18, -0x1.2p+5, + 0x1.e7f869e36cd89p+18, -0x1.2p+5 } }, + { "zero-filter", 0, + { 0x1.e873e5e06bea9p+18, 0x1.600000000001ep+6, + 0x1.e8269855e5f38p+18, 0x1.600000000001ep+6, + 0x1.e83ed7c21a0fdp+18, 0x1.600000000001ep+6 } }, + { "zero-filter", 0, + { 0x1.e92bc3e4bf7e8p+18, 0x1.9d58cb98b31c2p-8, + 0x1.e86f3ce2e6fd9p+18, 0x1.9d58cb98b31c2p-8, -0x1.e8p-23, + 0x1.9d58cb98b31c2p-8 } }, + { "stage B", -1, + { 0x1.e8a3087ab5437p+18, -0x1.44cf555e4a497p-15, 0x1.2dp-56, + -0x1.44cfb0a13d4ebp-15, -0x1.3cp-24, -0x1.44cfb0a13d4ebp-15 } }, + { "stage B", -1, + { -0x1.93e0393288503p+14, 0x1.c000000024492p+5, + -0x1.4055d881f63eep+9, 0x1.c000000000e66p+5, + 0x1.e7ea91809638ap+18, 0x1.bffffffd429edp+5 } }, + { "stage B", -1, + { 0x1.e7e8c99e8d15cp+18, 0x1.d171f803fa15cp+37, + 0x1.e75a1728bb73ep+18, 0x1.d0e9d78bf4822p+37, 0x1p+6, + 0x1.f010d5bd8523fp+24 } }, + { "stage B", 1, + { -0x1.2000000000001p+5, -0x1.2000000000001p+5, + -0x1.3d9c8945b5a58p-16, -0x1.2p+5, -0x1.6cp+6, -0x1.2p+5 } }, + { "stage B", 1, + { 0x1.e7ef323939776p+18, 0x1.d18c32ae0e7e4p+37, + 0x1.e78fa7b892f4ep+18, 0x1.d1310a5900be2p+37, + -0x1.0d7168262599cp+3, -0x1.0114afd61c6dcp+22 } }, + { "stage B", 1, + { 0x1.44p+6, 0x1.c00000000004ep+4, 0x1.0f8ee117e7b68p-4, 0x1.cp+4, + 0x1.e7c084ee78a88p+18, 0x1.c000000074d15p+4 } }, + { "stage C exact differences", -1, + { 0x1.cp+3, -0x1.094p+10, 0x1.8p+6, -0x1.c2bp+12, 0x1.54p+6, + -0x1.8f20000000002p+12 } }, + { "stage C exact differences", -1, + { 0x1.74p+6, -0x1.d5b3177ea7faap+7, -0x1.ep+5, 0x1.a57bcd17e83eap+7, + 0x1.88p+6, -0x1.f2d2bdeb50aa8p+7 } }, + { "stage C exact differences", -1, + { 0x1.38p+6, -0x1.1abffff434p+12, 0x1.e7d3d45a629b6p+18, + -0x1.ba17f871e89ffp+24, 0x1.e8e5c742fd948p+18, + -0x1.bb103c94b511dp+24 } }, + { "stage C exact differences", 0, + { -0x1.ap+5, -0x1.fe2p+11, -0x1.cp+2, -0x1.1d8p+9, -0x1.308p-5, + -0x1.be638p+4 } }, + { "stage C exact differences", 0, + { -0x1.6p+5, 0x1.5p+4, 0x1.e85bee3a1b6abp+18, 0x1.e86c2e3a1b6abp+18, + 0x1.078ea0b379fe9p+12, 0x1.0b9ea0b379fe9p+12 } }, + { "stage C exact differences", 0, + { 0x1.6p+6, 0x1.ee214184c01aep+18, 0x1.5p+4, 0x1.e9390184c01aep+18, + -0x1.7p-29, 0x1.e7af4184bf434p+18 } }, + { "stage C exact differences", 1, + { 0x1.e8dfecdb9bf94p+18, -0x1.67049f71468b1p+25, 0x1.ap+5, + -0x1.373p+12, -0x1.e8p+5, 0x1.60bp+12 } }, + { "stage C exact differences", 1, + { 0x1.78p+6, 0x1.751000224p-33, 0x1.e775245d490efp+18, + 0x1.e3a63a1490a0dp-21, -0x1.36a3e5965774cp+13, + -0x1.34369dcae645dp-26 } }, + { "stage C exact differences", 1, + { -0x1.9f8p-6, 0x1.8de8p+3, 0x1.e7eed47c90e36p+18, + -0x1.6df31a5d6caa8p+25, -0x1.8cp+6, 0x1.295p+13 } }, + { "stage C", -1, + { -0x1.0fffffffffffep+5, -0x1.039f11da6df74p+24, + 0x1.18001b6803aadp-59, 0x1.0b41f74dcd001p-40, 0x1.1a8p-15, + 0x1.0da4bd5daf634p+4 } }, + { "stage C", -1, + { 0x1.b8p-10, 0x1.eeffffc1ep-7, 0x1.4d0d7f1983b18p-36, + 0x1.f8bcbbf2d09ecp-35, 0x1.e74ffd686e29ep+18, + 0x1.121cfe8abdf79p+22 } }, + { "stage C", -1, + { -0x1.0df3058767eecp-60, -0x1.d800b99713cd1p-39, + 0x1.e856cc9387412p+18, 0x1.4fbbaca56cfccp+23, + -0x1.05533215806ffp-27, -0x1.67543cdd9099fp-23 } }, + { "stage C", 1, + { 0x1.e90b3ed0aa80cp+18, 0x1.d2b2fb8f9154p+37, -0x1.898p-6, + 0x1.dd01f89e9d87ep+18, 0x1.112ff8607691fp-11, + 0x1.e8ff4e925331fp+18 } }, + { "stage C", 1, + { 0x1.f9p-15, -0x1.a476ed4778f4ep-8, 0x1.e8d9563aff343p+18, + 0x1.abbe2b6f796fdp+23, 0x1.04dfc099d9a37p-27, + -0x1.0975ae1c2b6d3p-7 } }, + { "stage C", 1, + { 0x1.8fffffffffffdp+5, -0x1.e780000031c99p+11, + 0x1.e8732683a2b8p+18, -0x1.29a62b783728ep+25, + 0x1.e799515b18716p+18, -0x1.29216d9382e5dp+25 } }, + { "stage D", -1, + { 0x1.e90fbb4bd6729p+18, 0x1.d32b55b467cedp+37, + 0x1.6100000000003p-46, 0x1.5132cc67a5862p-27, -0x1.3dp-35, + -0x1.2ecf5721933d7p-16 } }, + { "stage D", -1, + { 0x1.e8ac7657ca17ap+18, 0x1.66dea6e878696p+25, 0x1.28p-32, + 0x1.b2bffffec388ap-26, -0x1.9f91f883aee09p-48, + -0x1.312fd0bc66d87p-41 } }, + { "stage D", -1, + { -0x1.6p+3, -0x1.60000000000cdp+4, -0x1.b2p-47, + -0x1.a6bf457c3fa0bp-41, -0x1.75p-53, -0x1.995de57c3fa0bp-41 } }, + { "stage D", -1, + { 0x1.e9351ecdf356dp+18, -0x1.31c133408ccc2p+21, -0x1.3p-29, + 0x1.5a5d0b7428083p-14, 0x1.41p-47, 0x1.5a512b7105883p-14 } }, + { "stage D", -1, + { 0x1.e8abdd5aaf0ebp+18, 0x1.ca211f85041dcp+23, -0x1.ec8p-57, + -0x1.ec00736ep-34, 0x1.cp-59, -0x1.ebffe5cp-34 } }, + { "stage D", -1, + { 0x1.e75aadca55d56p+18, 0x1.d0f054a4ad9d6p+37, -0x1.1p-42, + -0x1.038a8a9526b2ap-23, -0x1.3cp-51, -0x1.47df42b4ced74p-32 } }, + { "stage D", -1, + { 0x1.e915b062af275p+18, -0x1.9caa4cd343c93p+24, -0x1.2ep-39, + 0x1.fda0000099c97p-34, -0x1.8d8p-38, 0x1.4f64000026726p-32 } }, + { "stage D", -1, + { 0x1.e8121bc87d3eap+18, 0x1.399fa2da547acp-3, 0x1.a3p-37, + 0x1.0d0f03fe82819p-58, 0x1.3aa885cf379bcp-41, + 0x1.8e9313c79ea96p-63 } }, + { "stage D", 0, + { 0x1.e7e8de9c0512bp+18, 0x1.e7e8e55a0512bp+23, + -0x1.a6e5a6abd27b4p-2, -0x1.3b05a6abd27b4p+3, 0x1.8p+3, + 0x1.835fp+8 } }, + { "stage D", 0, + { 0x1.2169217a56b68p-3, 0x1.2169bf7a56b68p+3, -0x1.1f8p-46, + 0x1.3bffffb82p-14, -0x1.ep+5, -0x1.dfffff62p+11 } }, + { "stage D", 0, + { -0x1p+6, -0x1.9dp+10, 0x1.18p+6, 0x1.a88p+10, 0x1.08p-39, + -0x1.9ffffffffe638p+5 } }, + { "stage D", 0, + { -0x1.5a0a585bedd18p-3, -0x1.2ec9129070175p+3, -0x1.84p+6, + -0x1.53800002ap+12, 0x1.5ep-20, 0x1.27cp-14 } }, + { "stage D", 0, + { -0x1.59011db6d266p-58, -0x1.43f48bdb653fap-52, 0x1.7ap-40, + 0x1.625fffdf2p-34, 0x1.998p-55, 0x1.7fd79p-49 } }, + { "stage D", 0, + { 0x1.4p+4, -0x1.4efffb46p+1, 0x1.9p+4, -0x1.a2bffb46p+1, + -0x1.7cp-58, 0x1.2e800000018ddp-21 } }, + { "stage D", 0, + { 0x1.1cp+6, 0x1.6dcp+12, -0x1.7cp-39, -0x1.4800000007b34p+5, + 0x1.9ap-14, -0x1.47ef624p+5 } }, + { "stage D", 0, + { -0x1.ab8p-38, -0x1.7e8000440cap-2, -0x1.e58p-44, + -0x1.7e80000135208p-2, 0x1.28p+6, 0x1.78ef40cp+15 } }, + { "stage D", 1, + { 0x1.8cp+6, -0x1.1357ffffffffep+13, -0x1.a5p-53, 0x1.e44974p-39, + 0x1.a5182c3b99136p-57, 0x1.e1db66a627d1fp-39 } }, + { "stage D", 1, + { 0x1p+4, 0x1.e869ad1f407fcp+22, 0x1.16d745934d092p-65, + 0x1.0616bbbeec4d7p-46, 0x1.adp-58, 0x1.9934bb8eaf8aep-39 } }, + { "stage D", 1, + { 0x1.e8a2105940ccdp+18, 0x1.d23e5b05b0b21p+37, 0x1.c8p-59, + 0x1.b89e627fecaedp-40, -0x1.77746c98a1846p-52, + -0x1.663547f93c8f8p-33 } }, + { "stage D", 1, + { 0x1.e851ea244b43fp+18, 0x1.d207956b6720ap+37, -0x1.12p-57, + -0x1.0c8020afc4517p-19, 0x1.ac9ecf3808759p-51, + -0x1.0c73378f253d6p-19 } }, + { "stage D", 1, + { 0x1.e8ea82bf9bdbep+18, 0x1.d162e5488285ap+37, 0x0p+0, + -0x1.3cf830f2042ebp-50, 0x1.3aef4ca2a58cdp-37, + 0x1.2bc731ab6759bp-18 } }, + { "stage D", 1, + { 0x1.28p+5, -0x1.848p+10, 0x1.a4p-51, -0x1.13b158p-45, + -0x1.16547fa6d7994p-57, 0x1.64a2e78afaf92p-52 } }, + { "stage D", 1, + { 0x1.e8bc10c8f7f41p+18, 0x1.50014af9ea77dp+22, -0x1.32p-33, + -0x1.2080003498p-3, 0x1.dep-31, -0x1.207ffeb76p-3 } }, + { "stage D", 1, + { 0x1.e889adf679db8p+18, 0x1.d12aeba8eef8cp+37, 0x1.2p-45, + 0x1.1239140fa6393p-26, -0x1.6ea0e8fce5c32p-48, + -0x1.5d171530f2e5fp-29 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.0fffffffffffep+5, -0x1.039f11da6df74p+24, + 0x1.18001b6803aadp-59, 0x1.0b41f74dcd001p-40, 0x1.1a8p-15, + 0x1.0da4bd5daf634p+4 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.81bc6db459accp+1, 0x1.5d92c36b72a97p+6, 0x1.c4p-52, + 0x1.64edfa142a51cp-34, 0x1.e7f31ffa9d5e4p+18, + 0x1.ba3454fb1e9d7p+23 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.e7c462f0b2101p+18, 0x1.20b57b6809466p+18, 0x1.ba8p-43, + -0x1.2032249dd8d6ep+3, -0x1.bfffffffffffep+2, + -0x1.a4c90992508b6p+3 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.ep+5, -0x1.c8f8d38476824p+24, 0x1.6bddf7c5210eep+16, + 0x1.5a695a001d6c6p+35, 0x1.e7d82bcc112f6p+18, + 0x1.d071002b3eb5ep+37 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.e7a5ecce6b37p+18, 0x1.c7bdd18fe9b3p+17, -0x1.7730bf4397d9dp+6, + -0x1.5ea44ebfeda09p+5, 0x1.1p+5, 0x1.fc67fffff826ep+3 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.594bb3a1f650dp-54, -0x1.a4d442ed6432ap-49, + -0x1.83fffffffffffp-46, 0x1.d8dffffffffffp-41, 0x1.2p-28, + -0x1.5fp-23 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.7006525b4d167p-62, 0x1.4p+3, 0x1.e76bf4b96b441p+18, + -0x1.b9b9c1c80935bp+23, -0x1.8ae9f9dfe6312p+8, + 0x1.66340a72e89c8p+13 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.a9p-9, 0x1.7d4211ffffffdp+6, -0x1.73ffffffffffep+6, + -0x1.16efffffffffep+13, 0x1.e8844d0d1876dp+18, + 0x1.723471e3ec8a1p+25 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.59011db6d266p-58, -0x1.43f48bdb653fap-52, 0x1.7ap-40, + 0x1.625fffdf2p-34, 0x1.998p-55, 0x1.7fd79p-49 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.ab8p-38, -0x1.7e8000440cap-2, -0x1.e58p-44, + -0x1.7e80000135208p-2, 0x1.28p+6, 0x1.78ef40cp+15 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e8p-52, 0x1.8600028638p-44, 0x1.d8p-2, 0x1.38840030cp-17, + -0x1.c8p+5, -0x1.2debffff9e8p-10 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.56p-9, 0x1.213b03d8p-36, -0x1.75p-11, -0x1.3b7270ap-38, + -0x1.1e8p-49, 0x1.ebff86da6p-59 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.7cp+6, -0x1.b8630bddd5b62p-15, -0x1.4cp+6, + -0x1.80c253226f438p-15, -0x1p+5, -0x1.28ae840bf7b8bp-16 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.68p-43, -0x1.effffffffef2p+4, 0x1.e781f21e6b44p+18, + -0x1.6da18516d073p+25, -0x1.3p+5, 0x1.c42p+11 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.6p+5, 0x1.a0a000002e7p-4, 0x1.c8p-42, 0x1.73a1bb8p-39, + 0x1.78p+6, 0x1.bd0800001738p-3 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.46p-38, 0x1.0f00003393p-19, 0x0p+0, 0x1.0fp-19, -0x1.3cp+6, + -0x1.8fef788p-2 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.e90b3ed0aa80cp+18, 0x1.d2b2fb8f9154p+37, -0x1.898p-6, + 0x1.dd01f89e9d87ep+18, 0x1.112ff8607691fp-11, + 0x1.e8ff4e925331fp+18 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.e8fb70c388c39p+18, 0x1.d278dc3e90604p+37, 0x1.56p-42, + -0x1.399e3570b4cb8p+4, 0x1.1p+4, 0x1.037a5e5a10acbp+23 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.c529f1fdc56eap-14, -0x1.1007699fa556bp+6, 0x1.5p+6, + -0x1.64p+12, 0x1.08p+6, -0x1.18ap+12 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.7fffffffffffdp+3, -0x1.9b00000164ffdp-19, -0x1.4cp+6, + 0x1.6357ffffd3601p-16, 0x1.e88p-6, -0x1.056c8165p-27 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.e8dfecdb9bf94p+18, -0x1.67049f71468b1p+25, 0x1.ap+5, + -0x1.373p+12, -0x1.e8p+5, 0x1.60bp+12 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.2cp+6, -0x1.0680000000015p+12, 0x1.e93f6317ad20dp+18, + 0x1.ac1776b4b77cbp+24, 0x1.c57890fd1c852p-8, + 0x1.8cc97edd257ap-2 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.fp+5, -0x1.d19b02046df18p+24, 0x1.e783629e783ebp+18, + 0x1.d12146f667f7bp+37, 0x1.e882d259e2e78p+18, + 0x1.d214fc3be0bc4p+37 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.1220848c94c93p-34, -0x1.40f751ed19209p+6, + 0x1.e795ec4eef81bp+18, 0x1.29915a04634f4p+11, + 0x1.e84a5e5857791p+18, 0x1.2a032ff55265dp+11 } }, + }; + + const std::vector< RegressionCase > ORIENT_3D_CASES{ + { "filter", -1, + { -0x1.48p+6, 0x1.12p-23, -0x1.57fff900dp+5, -0x1.33p-46, + 0x1.9aa7fc4417cc6p-39, -0x1.57ffffffffffdp+5, 0x1.598p-45, + 0x1.dcp-54, -0x1.5800000000001p+5, 0x1.d786101dc84d1p-33, + -0x1.6p+5, -0x1.57ffffffffff6p+5 } }, + { "filter", -1, + { 0x1.3cp+6, 0x1.18p+5, 0x1.e82e68dea2621p+18, + 0x1.e82e68dea2621p+18, 0x1.a4p-59, 0x1.e8d93b0a926a8p+18, + 0x1.e82e68dea2621p+18, 0x1.8000000000003p+6, + 0x1.e60293444b5c2p-53, 0x1.e77c92f7d6a7ap+18, -0x1.fcp-3, + -0x1.4fp-11 } }, + { "filter", -1, + { 0x1.e757f6e097aeep+18, -0x1.23d8407774a78p-18, -0x1.6c8p-46, + -0x1.5p-56, -0x1.9225d7e23e18dp+2, -0x1.187fffffffffep-14, + 0x1.c8p+5, 0x1.4e90c92716c87p-54, 0x1.79b3dc470306cp-41, + -0x1.5p-56, -0x1.23d8407774a78p-18, -0x1.6c8p-46 } }, + { "filter", 1, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.18p+6, + 0x1.e7be2e932563p+18, 0x1.928p+7, 0x1.56166bf8c7cfap-41, + -0x1.17ffffffffffep+5, 0x1.e93fdd899b1a3p+18, + 0x1.6200000000001p-17, -0x1.9d1459ef983p-61, + 0x1.fa9603b88efp-51, 0x1.e8cf1652e42f2p+18 } }, + { "filter", 1, + { -0x1.6p+5, 0x1.e8b7220826db4p+18, -0x1.991b997468855p+14, + 0x1.868bf64a9c8cfp-9, -0x1.6p+5, 0x1.139ca39e9c9efp+1, + 0x1.e8b7220826db4p+18, 0x1.6fffffffffffep-10, + -0x1.8d76f7d1f2fb7p+22, 0x1.c8p-27, 0x1.138p+1, + -0x1.d752f5e571934p-4 } }, + { "filter", 1, + { -0x1.0900000000001p-13, 0x1.99ef178098934p-38, 0x1.5p+6, + -0x1.6000000000001p-31, -0x1.3ap-47, 0x1.df8p+7, + -0x1.82dd7fbaf8692p+7, 0x1.7cp+6, 0x1.99ef178098934p-38, + 0x1.99ef178098934p-38, -0x1.dddf32716857ap-11, -0x1.8cp-42 } }, + { "zero-filter", 0, + { 0x1.90e9fdc1c0a6dp-19, 0x1.ap-53, 0x1.8ap-6, + -0x1.27ffffffffffep-40, -0x1.9ba9c20e79c39p-21, + 0x1.c64562e3eb6d8p+15, -0x1.7feeb5e84bac5p-18, + 0x1.e7b8c459a5245p+18, 0x1.fbdfeb653bf35p-28, + 0x1.90e9fdc1c0a6dp-19, 0x1.ap-53, 0x1.8ap-6 } }, + { "zero-filter", 0, + { 0x1.59174a8fcacb8p-64, -0x1.b400000000001p-51, + 0x1.e7b24a096077ep+18, -0x1.b400000000001p-51, + 0x1.40dd970823a4ap+8, 0x1.09ef9a95b7b02p-7, + -0x1.48ffffffffffep-19, 0x1.e8f531cbd6aefp+18, + 0x1.e78176049d6bp+18, 0x1.59174a8fcacb8p-64, + -0x1.b400000000001p-51, 0x1.e7b24a096077ep+18 } }, + { "zero-filter", 0, + { 0x1.72c76e4675043p-45, 0x1.bcef512159383p-47, + 0x1.038b99fe1eb62p-45, -0x1p+4, -0x1.3333333333333p+2, + -0x1.6666666666666p+3, 0x1.72c76e4675043p-45, + 0x1.24f9726194b5cp+17, 0x1.55cdb01c82d3dp+18, + 0x1.72c76e4675043p-45, 0x1.bcef512159383p-47, + 0x1.038b99fe1eb62p-45 } }, + { "stage B", -1, + { -0x1.659fdac2e6cd6p-47, 0x1.f1ec2241e0388p+0, + 0x1.f1ec2241e0388p+0, -0x1.77p-17, -0x1.2dp-7, + 0x1.f1ec2241e1923p+0, 0x1.3fp-21, 0x1.b100000000003p-60, + 0x1.f1ec2241e0262p+0, -0x1.659fdac2e6cd6p-47, + 0x1.b100000000003p-60, 0x1.f1ec2241e0388p+0 } }, + { "stage B", -1, + { -0x1.da00000000001p-42, -0x1.2p-5, 0x1.e77ab05ec953bp+18, + 0x1.e8532a6e4d8c7p+18, 0x1.b8p-35, 0x1.d17a2f9d2358ep+37, + -0x1.4fa349f7f42a7p-5, -0x1.28724c15c472fp-48, + 0x1.d37bc0c1d265ap+18, 0x1.dbe96d952d05bp-48, + 0x1.e810a4e30f379p+18, 0x1.e77ab05eca393p+18 } }, + { "stage B", -1, + { -0x1.f798f2e719d8fp-16, -0x1.2e2891bddc4efp-17, + -0x1.6084aa082bb17p-16, 0x1.f800000000001p-24, + 0x1.2e66666666666p-25, -0x1.6084aa082bb17p-16, + 0x1.e76bd845e27ap+18, 0x1.2473e829ee493p+17, + 0x1.5531e430eb555p+18, -0x1.f798f2e719d8fp-16, + 0x1.2e66666666666p-25, -0x1.6084aa082bb17p-16 } }, + { "stage B", 1, + { 0x1.e7cc178e1e3d6p+18, 0x1.e859b4d621535p+18, + 0x1.d2743f827c8f3p+37, -0x1.5p+6, 0x1.1fffffffffffep+5, + 0x1.13163c0177eep+24, 0x1.e909b83abf596p+18, + 0x1.e82af72e6068fp+18, 0x1.d2479a5cbf498p+37, -0x1.6p+6, + 0x1.e8020242814a5p+18, 0x1.d2207b8d97443p+37 } }, + { "stage B", 1, + { 0x1.ap+4, 0x1.bp-13, 0x1.bp-13, 0x1.e8afc68b9d45bp+18, + -0x1.3bp-41, -0x1.3ce1d23e87fd9p+25, -0x1.ap+5, + -0x1.91112922ca084p-34, 0x1.12cfffd037b6ep+12, + 0x1.f2704dacaab7p-68, -0x1.99p-50, 0x1.43fffffff3d17p+6 } }, + { "stage B", 1, + { 0x1.848p-19, 0x1.e7c715b4b4e9ep+18, 0x1.d15b880782fb1p+37, + 0x1.88p-16, -0x1.ecp-17, 0x1.e9316e03920e2p+18, + 0x1.e8e6c99158543p+18, 0x1.848p-19, 0x1.d2926f0291b66p+37, + 0x1.41p-58, 0x1.e7c0a362108ffp+18, 0x1.d15561908da22p+37 } }, + { "stage C exact differences", -1, + { 0x1.e893b1b90c98fp+18, 0x1.25256aa23ac22p+17, + 0x1.5600fc67ef37dp+18, 0x1.e91866f3c736dp+18, + 0x1.25750a9244541p+17, 0x1.565de1aaa50c9p+18, + 0x1.e8f9cdbbd4cf6p+18, 0x1.2562aea3e616p+17, + 0x1.56487669e1c45p+18, 0x1.e927e8ffd11dep+18, + 0x1.257e58997d787p+17, 0x1.5668bcb31261bp+18 } }, + { "stage C exact differences", -1, + { 0x1.18p+5, 0x1.5p+3, 0x1.88p+4, -0x1.b8p+5, -0x1.07ffffffffffdp+4, + -0x1.34p+5, 0x1.ep+4, 0x1.2p+3, 0x1.4ffffffffffffp+4, + 0x1.e767bcf7fefe5p+18, 0x1.2471716198ffp+17, + 0x1.552f0447327edp+18 } }, + { "stage C exact differences", -1, + { 0x1.b3p-12, 0x1.05p-13, 0x1.308p-12, 0x1.be80000000003p+5, + 0x1.0be6666666666p+4, 0x1.388ccccccccccp+5, + 0x1.e76938b9dc92cp+18, 0x1.2472553c5124dp+17, + 0x1.55300e1bb4002p+18, -0x1.cp+2, -0x1.0cccccccccccdp+1, + -0x1.3999999999999p+2 } }, + { "stage C exact differences", 0, + { 0x1.e89089a245f3bp+18, 0x1.252385c7c3923p+17, + 0x1.55fec6be642a9p+18, 0x1.e8efa2ebe4c3bp+18, + 0x1.255c94f3efa8ap+17, 0x1.56415871ecef6p+18, + 0x1.e8977fc208375p+18, 0x1.55fec6be642a9p+18, + 0x1.5603a63b05c05p+18, 0x1.e8977fc208375p+18, + 0x1.55fec6be642a9p+18, 0x1.5603a63b05c05p+18 } }, + { "stage C exact differences", 0, + { 0x1.d88p+5, 0x1.1b8p+4, 0x1.4acp+5, 0x1.95p-6, 0x1.e6p-8, + 0x1.1b8p-6, 0x1.68p+6, 0x1.affffffffffffp+4, + 0x1.f7fffffffffffp+5, 0x1.ep-26, 0x1.2p-27, 0x1.5p-26 } }, + { "stage C exact differences", 0, + { -0x1.a58p-56, 0x1.e5p-6, 0x1.e76320f4367e3p+18, -0x1.52p-29, + -0x1.67afa74c0454ep-4, 0x1.e763a8379d8b9p+18, + -0x1.d1555baee08ap-40, 0x1.86p-42, 0x1.e763430e367e3p+18, + -0x1.d1555baee08ap-40, 0x1.86p-42, 0x1.e763430e367e3p+18 } }, + { "stage C exact differences", 1, + { 0x1.e8d0caad2dd07p+18, 0x1.254a1334b516ap+17, + 0x1.562bc112d3451p+18, 0x0p+0, 0x0p+0, 0x0p+0, + 0x1.e83d734dc485ap+18, 0x1.24f1ab950f836p+17, + 0x1.55c49d833cc3fp+18, 0x1.e820057fe2c0fp+18, + 0x1.24e0034cbb409p+17, 0x1.55b003d98520ap+18 } }, + { "stage C exact differences", 1, + { 0x0p+0, 0x0p+0, 0x0p+0, 0x1.e7f0e73c2c4bfp+18, + 0x1.24c3bdf0e760cp+17, 0x1.558f0843b89bbp+18, + 0x1.b2d60215ea7a7p-8, 0x1.04e6ce0d26497p-9, + 0x1.30629b0f5755ep-8, 0x1.e7f0e73c2c4bfp+18, + 0x1.24c3bdf0e760cp+17, 0x0p+0 } }, + { "stage C exact differences", 1, + { 0x0p+0, 0x0p+0, 0x0p+0, 0x1.e7a5f778d7539p+18, + 0x1.2496c7aee7988p+17, 0x1.555a93a163872p+18, 0x1.5ap-46, + 0x1.9f33333333333p-48, 0x0p+0, 0x1.28p-13, + 0x1.6333333333336p-15, 0x1.9e66666666666p-14 } }, + { "stage C", -1, + { 0x1.a8p+5, -0x1.59183079c12abp-40, 0x1.9c3e299232779p+24, + -0x1.b000000000001p+4, 0x1.e87035a3820ddp+18, + 0x1.d24511968a1ecp+37, 0x1.58p+6, -0x1.88p+6, + -0x1.512676b182d02p+22, 0x1.e77378074c3f9p+18, + -0x1.2a703adf58ee7p-47, 0x1.d12852200dccfp+37 } }, + { "stage C", -1, + { -0x1.1266666666666p-1, -0x1.d666666666669p-3, + -0x1.1266666666666p-1, 0x1.b100000000003p-50, + 0x1.03ccccccccccfp-51, 0x1.2f19999999999p-50, + 0x1.e7889b004c62ap+18, 0x1.248529ccfaa17p+17, + 0x1.55460619cf11ep+18, -0x1.497b9f7314552p-17, + -0x1.8b6125bd4b993p-19, -0x1.cd46ac0782dd9p-18 } }, + { "stage C", -1, + { 0x1.e893c5db9020cp+18, 0x1.6p+5, 0x1.8cd292e44f0b4p+30, + 0x1.548p-30, 0x1.e7ff7651fca2bp+18, 0x1.d07ecd617ef93p+37, + 0x1.7800000000003p+6, -0x1.2905de1a4c58ep-37, + 0x1.2d5bc32e0e195p+18, -0x1.86p-27, -0x1.379b9b8f901cap+15, + -0x1.2899b588e0c8ap+34 } }, + { "stage C", 1, + { 0x1.518p-41, 0x1.95p-43, 0x1.d87ffffffffffp-42, -0x1.38p+5, + -0x1.7666666666666p+3, -0x1.b4cccccccccccp+4, + 0x1.e81d5af01ca6ap+18, 0x1.24de69c34464p+17, + 0x1.55ae260e7a74bp+18, 0x1.d87ffffffffffp-42, + 0x1.24ba706644b5p+17, 0x1.55842dcca57ddp+18 } }, + { "stage C", 1, + { -0x1.07119fb168bc4p-69, -0x1.3baebfa1b0e1ep-71, + -0x1.704bdf91f9078p-70, -0x1.bp+5, -0x1.0333333333331p+4, + -0x1.2e66666666666p+5, -0x1.07119fb168bc4p-69, + -0x1.2886837051d8ep-6, -0x1.59f244030a27bp-5, -0x1.1p+5, + -0x1.4666666666666p+3, -0x1.7ccccccccccccp+4 } }, + { "stage C", 1, + { 0x1.e8ab49655d035p+18, 0x1.e8bc79d913b37p+18, + 0x1.03a7404d6702ep+23, 0x1.0cbd102950a76p-1, -0x1.a5p-54, + 0x1.ab013abcc5b2ep+5, 0x1.07fffffffffffp+6, -0x1.24p-37, + -0x1.1200000000c8cp+8, 0x1.4p+3, 0x1.f2eef4045d15ap-46, + 0x1.80000000002bp+2 } }, + { "stage D", -1, + { -0x1.dep-45, -0x1.1eccccccccccdp-46, -0x1.4e99999999998p-45, + 0x1.e771cc5a15fep+18, 0x1.24777a9c73986p+17, + 0x1.55360f0bdc31cp+18, -0x1.08p+5, -0x1.3cccccccccccdp+3, + -0x1.7199999999999p+4, 0x1.2bp+3, 0x1.66ccccccccccdp+1, + 0x1.a299999999999p+2 } }, + { "stage D", -1, + { -0x1.16p+7, -0x1.4d99999999999p+5, -0x1.8533333333333p+6, + 0x1.b78p+7, 0x1.07b3333333333p+6, 0x1.33a6666666664p+7, + 0x1.b900000000003p-50, 0x1.0899999999999p-51, + 0x1.34b3333333333p-50, -0x1.47p-44, -0x1.8866666666664p-46, + -0x1.c9cccccccccccp-45 } }, + { "stage D", -1, + { 0x1.614b08257241dp-32, 0x1.a7f3a36022b56p-34, + 0x1.ee9c3e9ad328ep-33, 0x1.e76dde7ee8253p+18, + 0x1.24751f18f1afep+17, 0x1.55334ef26f4d3p+18, -0x1.9e8p-23, + -0x1.f166666666664p-25, -0x1.2226666666666p-23, + -0x1.8108a0ee527f3p-54, -0x1.ce0a5ab796326p-56, + -0x1.0d860a406cf29p-54 } }, + { "stage D", -1, + { 0x1.e896e69f1f5dcp+18, 0x1.2527572c46051p+17, + 0x1.56033b08fc5b3p+18, -0x1.a8p+5, -0x1.fccccccccccccp+3, + -0x1.28cccccccccccp+5, -0x1.e6p-34, -0x1.2399999999999p-35, + -0x1.5433333333333p-34, -0x1.78p+6, -0x1.c333333333333p+4, + -0x1.0733333333334p+6 } }, + { "stage D", -1, + { 0x1.1a74d6f7a25dap-7, 0x1.52f29b8f8fa39p-9, 0x1.8b7060277ce97p-8, + -0x1.be8e2c6ff0ec9p-44, -0x1.0beee7765d5acp-45, + -0x1.3896b8b4c23f3p-44, 0x1.621116bb5775ap+4, + 0x1.a8e14e7a68f3bp+2, 0x1.efb186397a71ap+3, 0x1.4p+4, 0x1.8p+2, + 0x1.bffffffffffffp+3 } }, + { "stage D", -1, + { -0x1.ef3d8abb290cep-5, -0x1.2924ecd6b23bp-6, + -0x1.5aab144fcfef8p-5, 0x1.e7fa40fd5035dp+18, + 0x1.24c95a319686bp+17, 0x1.559593e484f27p+18, 0x1.0cp+6, + 0x1.4199999999999p+4, 0x1.7733333333333p+5, 0x1.b8p+5, + 0x1.08p+4, 0x1.34p+5 } }, + { "stage D", -1, + { 0x1.08p-12, 0x1.3cccccccccccdp-14, 0x1.7199999999999p-13, + 0x1.1800000000001p+5, 0x1.5000000000003p+3, + 0x1.87fffffffffffp+4, -0x1.5400000000002p-44, + -0x1.9800000000003p-46, -0x1.dbfffffffffffp-45, + -0x1.1a0edcf81a414p-50, -0x1.52783c5cec4e5p-52, + -0x1.8ae19bc1be5b5p-51 } }, + { "stage D", -1, + { -0x1.bdfd4b3771ee5p+5, -0x1.0b97f9ee11289p+4, + -0x1.38314e40695ap+5, -0x1.6cp+6, -0x1.b4ccccccccccdp+4, + -0x1.fd99999999999p+5, -0x1p+6, -0x1.3333333333336p+4, + -0x1.6666666666663p+5, 0x1.8265139f1f4b5p-64, + 0x1.cfac7df2258d9p-66, 0x1.0e79f42295e7cp-64 } }, + { "stage D", 0, + { -0x1.eb8p-3, -0x1.26e6666666668p-4, -0x1.26e6666666668p-4, + -0x1.9bce637a2b8a8p-45, -0x1.ee2addc5cdd96p-47, + -0x1.2043ac08b8142p-45, 0x1.e80beb0bfedaep+18, + 0x1.24d3f36d98e9bp+17, 0x1.55a1f15532661p+18, + -0x1.9bce637a2b8a8p-45, -0x1.ee2addc5cdd96p-47, + -0x1.2043ac08b8142p-45 } }, + { "stage D", 0, + { 0x1.e8cd41a5f2bdep+18, 0x1.e7b211a0c56bfp+18, + 0x1.d2d1e28419266p+37, -0x1.542abf239265fp-54, -0x1.0cp+6, + 0x1.e8975cc12ec57p+18, 0x1.e8634fe2f170dp+18, 0x1.3p-5, + 0x1.d26cb47ff9a36p+37, 0x1.e8634fe2f170dp+18, 0x1.3p-5, + 0x1.d26cb47ff9a36p+37 } }, + { "stage D", 0, + { -0x1.e5p-17, 0x1.f2p-6, -0x1.1820000000002p+0, + 0x1.e926b3a714d3dp+18, 0x1.e7c1c0504d47fp+18, + -0x1.125cfc2d2b787p+24, 0x1.e810094f7f481p+18, -0x1.44p+6, + 0x1.6c8p+11, 0x1.e926b3a714d3dp+18, 0x1.e7c1c0504d47fp+18, + -0x1.125cfc2d2b787p+24 } }, + { "stage D", 0, + { -0x1p+6, -0x1.3333333333333p+4, -0x1.6666666666668p+5, 0x1p-52, + 0x1.333333333333p-54, 0x1.6666666666666p-53, + -0x1.0734791a71418p+2, -0x1.3bd89152ee4ebp+0, + -0x1.707ca98b6b5bcp+1, 0x1p-52, 0x1.333333333333p-54, + 0x1.6666666666666p-53 } }, + { "stage D", 0, + { 0x1.e76c88759431ep+18, 0x1.e7cd57e27de35p+18, + 0x1.d09223852a123p+37, 0x1.978f8e130ebb5p-54, + -0x1.27ffffffffffep+2, -0x1.19e7179bccd03p+21, + 0x1.e76c88759431ep+18, 0x1.0a148850c0f7ap-20, + -0x1.372c08ce4c9b7p+5, 0x1.978f8e130ebb5p-54, + -0x1.27ffffffffffep+2, -0x1.19e7179bccd03p+21 } }, + { "stage D", 0, + { -0x1.70da3ece5d43dp-21, -0x1.ba9f7e913cb7fp-23, + -0x1.02325f2a0e16p-21, 0x1.001fb8edc2c7dp-40, + -0x1.70da3ece5d43dp-21, 0x1.6692cfb343e48p-41, + 0x1.773082522d292p-48, 0x1.c23a35fc36315p-50, + 0x1.06a1f4d31f9ccp-48, 0x1.773082522d292p-48, + 0x1.c23a35fc36315p-50, 0x1.06a1f4d31f9ccp-48 } }, + { "stage D", 0, + { 0x1.e82b5ea000612p+18, 0x1.847681a86d5ap-14, -0x1.58p+5, + -0x1.1f169da82ce98p-50, 0x1.3p+5, 0x1.64p+6, + 0x1.8840fa6f8ddb1p-31, 0x1.e74e58bfd38efp+18, + 0x1.e82b5ea000612p+18, 0x1.8840fa6f8ddb1p-31, + 0x1.e74e58bfd38efp+18, 0x1.e82b5ea000612p+18 } }, + { "stage D", 0, + { 0x1.1eccccccccccfp-54, 0x1.1eccccccccccfp-54, + 0x1.4e99999999999p-53, -0x1.0a6b2c3018678p-38, + -0x1.3fb3cea01d492p-40, -0x1.74fc7110222abp-39, -0x1.6cp-22, + -0x1.b4ccccccccccdp-24, -0x1.fd99999999999p-23, + -0x1.0a6b2c3018678p-38, -0x1.3fb3cea01d492p-40, + -0x1.74fc7110222abp-39 } }, + { "stage D", 1, + { -0x1.9p+5, -0x1.dffffffffffffp+3, -0x1.18p+5, + -0x1.67fffffffffffp+6, -0x1.bp+4, -0x1.f7fffffffffffp+5, + -0x1.c1182267e4d41p-32, -0x1.0d74e171894cp-33, + -0x1.3a5db1af202ep-32, -0x1.8800000000001p+6, + -0x1.d666666666666p+4, -0x1.1266666666666p+6 } }, + { "stage D", 1, + { -0x1.5dp-51, -0x1.a2ccccccccccbp-53, -0x1.e899999999999p-52, + 0x1.3p+6, 0x1.6cccccccccccdp+4, 0x1.a999999999999p+5, + -0x1.e8p-52, -0x1.24ccccccccccdp-53, -0x1.5599999999999p-52, + -0x1.4dp-9, -0x1.8f9999999999bp-11, -0x1.d233333333333p-10 } }, + { "stage D", 1, + { 0x1.e7f6ae2f84e42p+18, 0x1.24c7354fb6227p+17, + 0x1.55931387a9d2ep+18, 0x1.3bffffffffffdp+6, + 0x1.7b33333333333p+4, 0x1.ba66666666666p+5, + -0x1.3c00000000001p-43, -0x1.7b33333333333p-45, + -0x1.ba66666666666p-44, -0x1.87ffffffffffdp+5, + -0x1.d666666666666p+3, -0x1.1266666666666p+5 } }, + { "stage D", 1, + { -0x1.b28p-26, -0x1.04b3333333333p-27, -0x1.3026666666665p-26, + 0x1.4e981b7123e1p-22, 0x1.918354215e413p-24, + 0x1.d46e8cd198a13p-23, 0x1.e85bbd8a4a731p+18, + 0x1.2503d81fc645p+17, 0x1.55d9d17a67508p+18, + 0x1.f800000000002p+5, 0x1.2e66666666666p+4, + 0x1.60cccccccccccp+5 } }, + { "stage D", 1, + { 0x1.dc00000000003p+6, 0x1.1d99999999999p+5, 0x1.4d33333333333p+6, + 0x1.baac160da5626p-39, 0x1.099a73a1c9a18p-40, + 0x1.35dedc3cc091bp-39, 0x1.e8ca92a3ed3d8p+18, + 0x1.254657fbf4be8p+17, 0x1.562766a5f2de4p+18, 0x1.73p-19, + 0x1.bd33333333333p-21, 0x1.03b3333333333p-19 } }, + { "stage D", 1, + { -0x1.1cp+6, -0x1.54ccccccccccdp+4, -0x1.8d99999999999p+5, + 0x1.e7c415edd558fp+18, 0x1.24a8d9f5199bcp+17, + 0x1.556fa8f3488afp+18, 0x1p+3, 0x1.3333333333333p+1, + 0x1.6666666666664p+2, -0x1.1838004e4d246p-68, + -0x1.50433391295edp-70, -0x1.884e66d405995p-69 } }, + { "stage D", 1, + { 0x1.2c792b7ba008ap-7, 0x1.689167612670cp-9, 0x1.a4a9a346acd8bp-8, + -0x1.72b945ede4ce8p-18, -0x1.bcde53ea45c47p-20, + -0x1.0381b0f3535d5p-18, -0x1.68p+5, -0x1.affffffffffffp+3, + -0x1.f7fffffffffffp+4, 0x1.7580000000002p-11, + 0x1.c033333333335p-13, 0x1.0573333333332p-11 } }, + { "stage D", 1, + { 0x1.e759871910d7dp+18, 0x1.2468eaa8a3b4bp+17, + 0x1.552511c4befd7p+18, 0x1.2p+3, 0x1.5999999999999p+1, + 0x1.9333333333334p+2, 0x1.950531a1b3f92p+12, + 0x1.e6063b8ed7f81p+10, 0x1.1b83a2bdfdfb5p+12, 0x0p+0, 0x0p+0, + 0x0p+0 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.1266666666666p-1, -0x1.d666666666669p-3, + -0x1.1266666666666p-1, 0x1.b100000000003p-50, + 0x1.03ccccccccccfp-51, 0x1.2f19999999999p-50, + 0x1.e7889b004c62ap+18, 0x1.248529ccfaa17p+17, + 0x1.55460619cf11ep+18, -0x1.497b9f7314552p-17, + -0x1.8b6125bd4b993p-19, -0x1.cd46ac0782dd9p-18 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.4ap-49, 0x1.64p+6, -0x1.0b00000000278p+13, + 0x1.e8a778b6e0defp+18, 0x1.6e8p-39, 0x1.d24a8e3c62f12p+37, + 0x1.e7eb56ac213c9p+18, -0x1.17ffffffffffdp+5, + 0x1.d19708b5b240dp+37, 0x1.ddffffffffffep-61, + 0x1.b417d4c9b35ccp-41, -0x1.4549b2325ca5p-34 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.29e0d5bbdd285p-41, 0x1.e7c39a67e949ep+18, + 0x1.d080b05e9c91ep+37, -0x1.713eb0f1b4e1dp-23, 0x1.32p+5, + 0x1.2b0a208e54d6p+24, 0x1.e77e8424e7b66p+18, + 0x1.e82105025d602p+18, 0x1.d0d9a671a7bd3p+37, + 0x1.6d399883fb49dp+14, 0x1.e2b0d297b39f8p-11, + 0x1.e8f9c3d208987p+18 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.16p+7, -0x1.4d99999999999p+5, -0x1.8533333333333p+6, + 0x1.b78p+7, 0x1.07b3333333333p+6, 0x1.33a6666666664p+7, + 0x1.b900000000003p-50, 0x1.0899999999999p-51, + 0x1.34b3333333333p-50, -0x1.47p-44, -0x1.8866666666664p-46, + -0x1.c9cccccccccccp-45 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.e7f2af0a01eefp+18, 0x1.e7f7557f732cep+18, + 0x1.faef0aa9702e7p+17, -0x1.58p+5, 0x1.a6004a886cb4p+2, + -0x1.656284e8336e5p+4, 0x1.e881dec2ebe92p+18, + -0x1.da56cca59a6a3p-45, 0x1.fb83cca31809dp+17, -0x1.4cp+6, + 0x1.e8f726cbc65e2p+18, -0x1.58eafd4b43739p+5 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.e5424739d220fp-1, -0x1.048p-2, -0x1.f0ee6102e50f1p+16, + 0x1.e7b817a290fffp+18, 0x1.b28p-38, -0x1.d43238b04de29p+22, + 0x1.2780000000003p-8, 0x1.b98p-9, 0x1.90deb2e2c7a3fp+10, + -0x1.1p+6, 0x1.6c8p-24, 0x1.e23d6dedc8698p+9 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.82p-24, -0x1.fp+4, 0x1.7d47c7e26b369p+4, + 0x1.e8a3c0e4fe921p+18, 0x1.e7c97d00aef44p+18, + 0x1.d268888727e8fp+37, 0x1.e7978a55bfb07p+18, -0x1.3p+6, + 0x1.d16885dd869fp+37, -0x1.4ep-61, 0x1.e7af934633c69p+18, + 0x1.7dfffffffffbp+4 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.e896e69f1f5dcp+18, 0x1.2527572c46051p+17, + 0x1.56033b08fc5b3p+18, -0x1.a8p+5, -0x1.fccccccccccccp+3, + -0x1.28cccccccccccp+5, -0x1.e6p-34, -0x1.2399999999999p-35, + -0x1.5433333333333p-34, -0x1.78p+6, -0x1.c333333333333p+4, + -0x1.0733333333334p+6 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.b8p+5, -0x1.08p+4, -0x1.34p+5, 0x1.18p-26, 0x1.5p-28, + 0x1.88p-27, -0x1.15p-33, -0x1.4c66666666666p-35, + -0x1.83cccccccccccp-34, -0x1.d07faab9000a6p-41, + -0x1.16b30008999fdp-42, -0x1.45262ab4b33a7p-41 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.7d3a7a2b0c7ap+2, -0x1.c9795f66dbc59p+0, + -0x1.0adc225155889p+2, 0x1.6p+6, 0x1.a666666666666p+4, + 0x1.eccccccccccccp+5, -0x1.b2p-7, -0x1.0466666666666p-8, + -0x1.2fcccccccccccp-7, -0x1.54d6555d238dep-57, + -0x1.9901333c9110ap-59, -0x1.dd2c111bfe936p-58 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e84683f9ffbb4p+18, 0x1.24f71bfc663d2p+17, + 0x1.55caf5fbcc9cap+18, 0x1.e7945b255ab92p+18, + 0x1.248c36b0033bep+17, 0x1.554e3fcd591b3p+18, -0x1.38p+6, + -0x1.7666666666666p+4, -0x1.b4cccccccccccp+5, 0x1.7dp+7, + 0x1.c933333333333p+5, 0x1.0ab3333333333p+7 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.d88p+5, 0x1.1b8p+4, 0x1.4acp+5, 0x1.95p-6, 0x1.e6p-8, + 0x1.1b8p-6, 0x1.68p+6, 0x1.affffffffffffp+4, + 0x1.f7fffffffffffp+5, 0x1.ep-26, 0x1.2p-27, 0x1.5p-26 } }, + { "exact, wrong floating-point sign", 0, + { -0x1p-62, -0x1.3333333333333p-64, -0x1.6666666666666p-63, + 0x1.e914e6629c41ap+18, 0x1.2572f0a190f43p+17, + 0x1.565b6e11d3c79p+18, 0x1.78p+6, 0x1.c333333333333p+4, + 0x1.0733333333333p+6, 0x1.84p-60, 0x1.d199999999999p-62, + 0x1.0f99999999999p-60 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.46p-11, 0x1.8733333333333p-13, 0x1.c866666666666p-12, + 0x1.3cf1c3f8c8b83p-41, 0x1.7c555190f0ddp-43, + 0x1.bbb8df291901dp-42, 0x1.78p+6, 0x1.c333333333333p+4, + 0x1.0733333333333p+6, 0x1.09p-36, 0x1.3ep-38, 0x1.73p-37 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.8cp-58, -0x1.db33333333333p-60, -0x1.1533333333333p-58, + -0x1.c2065c2ff3ff6p-2, -0x1.0e03d0e992661p-3, + -0x1.3b0473bb2acc7p-2, 0x1.7p+5, 0x1.b999999999999p+3, + 0x1.0199999999999p+5, 0x1.5cfd6208a7c8cp-37, + 0x1.a2c9a8d72fbdbp-39, 0x1.e895efa5b7b2ap-38 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e8b08e7ff8f4ap+18, 0x1.2536bbe6622c6p+17, + 0x1.5615308cc7de7p+18, 0x1.88p+6, 0x1.d666666666666p+4, + 0x1.1266666666666p+6, 0x1.e824fe44d097ep+18, + 0x1.24e2fef616c18p+17, 0x1.55b37ec9c5371p+18, 0x1.1p+5, + 0x1.4666666666666p+3, 0x1.7ccccccccccccp+4 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.5dp-51, -0x1.a2ccccccccccbp-53, -0x1.e899999999999p-52, + 0x1.3p+6, 0x1.6cccccccccccdp+4, 0x1.a999999999999p+5, + -0x1.e8p-52, -0x1.24ccccccccccdp-53, -0x1.5599999999999p-52, + -0x1.4dp-9, -0x1.8f9999999999bp-11, -0x1.d233333333333p-10 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.518p-41, 0x1.95p-43, 0x1.d87ffffffffffp-42, -0x1.38p+5, + -0x1.7666666666666p+3, -0x1.b4cccccccccccp+4, + 0x1.e81d5af01ca6ap+18, 0x1.24de69c34464p+17, + 0x1.55ae260e7a74bp+18, 0x1.d87ffffffffffp-42, + 0x1.24ba706644b5p+17, 0x1.55842dcca57ddp+18 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.b28p-26, -0x1.04b3333333333p-27, -0x1.3026666666665p-26, + 0x1.4e981b7123e1p-22, 0x1.918354215e413p-24, + 0x1.d46e8cd198a13p-23, 0x1.e85bbd8a4a731p+18, + 0x1.2503d81fc645p+17, 0x1.55d9d17a67508p+18, + 0x1.f800000000002p+5, 0x1.2e66666666666p+4, + 0x1.60cccccccccccp+5 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.9p+6, 0x1.ep+4, 0x1.18p+6, -0x1.5dffffffffffep-57, + -0x1.a4p-59, 0x1.ep+4, 0x1.c88p-54, 0x1.11e6666666666p-55, + 0x1.3f8ccccccccc9p-54, -0x1p+0, -0x1.3333333333334p-2, + -0x1.6666666666666p-1 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.8p+3, 0x1.e8950465c0aa7p+18, 0x1.d2a77ce8b36dbp+37, + 0x1.0100000000002p+4, 0x1.e81a66537d853p+18, 0x1.d2325fb0a7p+37, + 0x1.380f8cd10533p-67, -0x1.93ef94d17ffc9p-39, + 0x1.3b672e4c3447dp-2, 0x1.b48p-5, -0x1.ea00000000002p-49, + 0x1.5b1678ea299cfp+2 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.0dc2c9c7bd2dfp-60, -0x1.43b68bbc7c9d8p-62, + -0x1.79aa4db13c0d1p-61, -0x1.0967b20b52cb5p-28, + -0x1.3e7c6f40c9c0cp-30, -0x1.73912c7640b63p-29, 0x1.ap+3, + 0x1.f333333333333p+1, -0x1.0dc2c9c7bd2dfp-60, + -0x1.6278f622c571bp+15, -0x1.a95df429b9bbap+13, + -0x1.f042f230ae058p+14 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.2c792b7ba008ap-7, 0x1.689167612670cp-9, 0x1.a4a9a346acd8bp-8, + -0x1.72b945ede4ce8p-18, -0x1.bcde53ea45c47p-20, + -0x1.0381b0f3535d5p-18, -0x1.68p+5, -0x1.affffffffffffp+3, + -0x1.f7fffffffffffp+4, 0x1.7580000000002p-11, + 0x1.c033333333335p-13, 0x1.0573333333332p-11 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.ffffffffffffdp+0, 0x1.3333333333333p-1, 0x1.6666666666666p+0, + 0x1.7c6ea44fd360dp-21, 0x1.c884c52c9740fp-23, + 0x1.0a4d7304ad908p-21, -0x1.07c72542a016fp+9, + -0x1.3c88931cc01b9p+7, -0x1.714a00f6e02p+8, -0x1.2bp-22, + -0x1.66ccccccccccdp-24, -0x1.a299999999999p-23 } }, + }; + + const std::vector< RegressionCase > DET_3D_CASES{ + { "filter", -1, + { 0x1.868bf64a9c8cfp-9, -0x1.6p+5, 0x1.139ca39e9c9efp+1, + 0x1.e8b7220826db4p+18, 0x1.6fffffffffffep-10, + -0x1.8d76f7d1f2fb7p+22, 0x1.c8p-27, 0x1.138p+1, + -0x1.d752f5e571934p-4 } }, + { "filter", -1, + { 0x1.1p+6, 0x1.e74fac642745p+18, 0x1.e902bbb60aaedp+18, + 0x1.5ca0358103241p-14, 0x1.1368c1c3eb0acp-41, + -0x1.7a8481e0e431p-34, 0x1.6p+5, -0x1.4b7e742389ec9p-64, + -0x1.95176f0aa877fp-43 } }, + { "filter", -1, + { 0x1.e8532a6e4d8c7p+18, 0x1.b8p-35, 0x1.d17a2f9d2358ep+37, + -0x1.4fa349f7f42a7p-5, -0x1.28724c15c472fp-48, + 0x1.d37bc0c1d265ap+18, 0x1.dbe96d952d05bp-48, + 0x1.e810a4e30f379p+18, 0x1.e77ab05eca393p+18 } }, + { "filter", 1, + { -0x1.b000000000001p+4, 0x1.e87035a3820ddp+18, + 0x1.d24511968a1ecp+37, 0x1.58p+6, -0x1.88p+6, + -0x1.512676b182d02p+22, 0x1.e77378074c3f9p+18, + -0x1.2a703adf58ee7p-47, 0x1.d12852200dccfp+37 } }, + { "filter", 1, + { 0x1.e7be2e932563p+18, 0x1.928p+7, 0x1.56166bf8c7cfap-41, + -0x1.17ffffffffffep+5, 0x1.e93fdd899b1a3p+18, + 0x1.6200000000001p-17, -0x1.9d1459ef983p-61, + 0x1.fa9603b88efp-51, 0x1.e8cf1652e42f2p+18 } }, + { "filter", 1, + { -0x1.6000000000001p-31, -0x1.3ap-47, 0x1.df8p+7, + -0x1.82dd7fbaf8692p+7, 0x1.7cp+6, 0x1.99ef178098934p-38, + 0x1.99ef178098934p-38, -0x1.dddf32716857ap-11, -0x1.8cp-42 } }, + { "zero-filter", 0, + { 0x1.2p+3, 0x1.5999999999999p+1, 0x1.9333333333334p+2, + 0x1.950531a1b3f92p+12, 0x1.e6063b8ed7f81p+10, + 0x1.1b83a2bdfdfb5p+12, 0x0p+0, 0x0p+0, 0x0p+0 } }, + { "zero-filter", 0, + { 0x1.e7c0d9abc41a9p+18, 0x1.24a6e900a8dccp+17, + 0x1.556d652b6fac3p+18, 0x0p+0, 0x0p+0, 0x0p+0, 0x1.6p+6, + -0x1.3de6b040960b6p+10, 0x1.eccccccccccccp+5 } }, + { "zero-filter", 0, + { 0x0p+0, 0x0p+0, 0x0p+0, 0x1.e83d734dc485ap+18, + 0x1.24f1ab950f836p+17, 0x1.55c49d833cc3fp+18, + 0x1.e820057fe2c0fp+18, 0x1.24e0034cbb409p+17, + 0x1.55b003d98520ap+18 } }, + { "exact", -1, + { -0x1.67fffffffffffp+6, -0x1.bp+4, -0x1.f7fffffffffffp+5, + -0x1.c1182267e4d41p-32, -0x1.0d74e171894cp-33, + -0x1.3a5db1af202ep-32, -0x1.8800000000001p+6, + -0x1.d666666666666p+4, -0x1.1266666666666p+6 } }, + { "exact", -1, + { 0x1.b100000000003p-50, 0x1.03ccccccccccfp-51, + 0x1.2f19999999999p-50, 0x1.e7889b004c62ap+18, + 0x1.248529ccfaa17p+17, 0x1.55460619cf11ep+18, + -0x1.497b9f7314552p-17, -0x1.8b6125bd4b993p-19, + -0x1.cd46ac0782dd9p-18 } }, + { "exact", -1, + { 0x1.e81e90526b1f3p+18, 0x1.24df2364a6ac4p+17, + 0x1.55aefea017c8fp+18, 0x1.4642545def031p-22, + 0x1.8782cba3eb9d4p-24, 0x1.c8c342e9e8377p-23, + 0x1.4642545def031p-22, 0x1.24df2364a6ac4p+17, + 0x1.0ab3333333333p-46 } }, + { "exact", 0, + { -0x1.9bce637a2b8a8p-45, -0x1.ee2addc5cdd96p-47, + -0x1.2043ac08b8142p-45, 0x1.e80beb0bfedaep+18, + 0x1.24d3f36d98e9bp+17, 0x1.55a1f15532661p+18, + -0x1.9bce637a2b8a8p-45, -0x1.ee2addc5cdd96p-47, + -0x1.2043ac08b8142p-45 } }, + { "exact", 0, + { 0x1.e80602af846a8p+18, 0x1.24d06802e90cbp+17, + 0x1.559dceae0fe42p+18, 0x1.d644e6b5a8de8p-44, + 0x1.1a295739cbb8bp-45, 0x1.49303b18c3022p-44, -0x1.ep-51, + -0x1.2p-52, -0x1.5p-51 } }, + { "exact", 0, + { 0x1.e92a021750d96p+18, 0x1.257f9adaca1cp+17, + 0x1.566a34a9ebcb5p+18, -0x1.7p+5, -0x1.b999999999999p+3, + -0x1.0199999999999p+5, 0x1.3c5392b8cb23dp-38, + 0x1.7b977cddc0916p-40, 0x1.badb6702b5fefp-39 } }, + { "exact", 1, + { -0x1.5p+6, 0x1.1fffffffffffep+5, 0x1.13163c0177eep+24, + 0x1.e909b83abf596p+18, 0x1.e82af72e6068fp+18, + 0x1.d2479a5cbf498p+37, -0x1.6p+6, 0x1.e8020242814a5p+18, + 0x1.d2207b8d97443p+37 } }, + { "exact", 1, + { 0x1.3p+6, 0x1.6cccccccccccdp+4, 0x1.a999999999999p+5, -0x1.e8p-52, + -0x1.24ccccccccccdp-53, -0x1.5599999999999p-52, -0x1.4dp-9, + -0x1.8f9999999999bp-11, -0x1.d233333333333p-10 } }, + { "exact", 1, + { 0x1.548p-30, 0x1.e7ff7651fca2bp+18, 0x1.d07ecd617ef93p+37, + 0x1.7800000000003p+6, -0x1.2905de1a4c58ep-37, + 0x1.2d5bc32e0e195p+18, -0x1.86p-27, -0x1.379b9b8f901cap+15, + -0x1.2899b588e0c8ap+34 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.b100000000003p-50, 0x1.03ccccccccccfp-51, + 0x1.2f19999999999p-50, 0x1.e7889b004c62ap+18, + 0x1.248529ccfaa17p+17, 0x1.55460619cf11ep+18, + -0x1.497b9f7314552p-17, -0x1.8b6125bd4b993p-19, + -0x1.cd46ac0782dd9p-18 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.a8p-53, -0x1.fccccccccccccp-55, -0x1.28cccccccccccp-53, + 0x1.4bfffffffffffp+6, 0x1.8e66666666666p+4, + 0x1.d0cccccccccccp+5, 0x1.3d8p-33, 0x1.7dp-35, 0x1.bc8p-34 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.72b945ede4ce8p-18, -0x1.bcde53ea45c47p-20, + -0x1.0381b0f3535d5p-18, -0x1.68p+5, -0x1.affffffffffffp+3, + -0x1.f7fffffffffffp+4, 0x1.7580000000002p-11, + 0x1.c033333333335p-13, 0x1.0573333333332p-11 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.a8p-56, -0x1.fcccccccccccap-58, -0x1.28cccccccccccp-56, + 0x1.1cp+6, 0x1.54ccccccccccdp+4, 0x1.8d99999999999p+5, + 0x1.e79a1df73e606p+18, 0x1.248fab9458a04p+17, + 0x1.5552482d12105p+18 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.1800000000001p+5, 0x1.5000000000003p+3, 0x1.87fffffffffffp+4, + -0x1.5400000000002p-44, -0x1.9800000000003p-46, + -0x1.dbfffffffffffp-45, -0x1.1a0edcf81a414p-50, + -0x1.52783c5cec4e5p-52, -0x1.8ae19bc1be5b5p-51 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.e8d4935e52ccp+18, 0x1.254c586bcb473p+17, + 0x1.562e67286d286p+18, 0x1.44989c08ecap+13, + 0x1.8583ee711bf33p+11, 0x1.c66f40d94b466p+12, + -0x1.a880000000002p-27, 0x1.9819999999999p-26, + -0x1.2926666666666p-27 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.d68p-13, 0x1.1a4cccccccccdp-14, 0x1.495999999999bp-13, + -0x1.16p-50, -0x1.4d99999999999p-52, -0x1.8533333333333p-51, + 0x1.5b37d992ac686p-67, 0x1.a0a96b7ccee3ap-69, + 0x1.e61afd66f15eep-68 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.64p+6, -0x1.ab33333333333p+4, -0x1.f266666666666p+5, + 0x1.e867b179f2aedp+18, 0x1.250b0415f802ap+17, + 0x1.55e22f6ef6ad9p+18, 0x1.368p-44, 0x1.7499999999999p-46, + 0x1.b2b3333333334p-45 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e92a021750d96p+18, 0x1.257f9adaca1cp+17, + 0x1.566a34a9ebcb5p+18, -0x1.7p+5, -0x1.b999999999999p+3, + -0x1.0199999999999p+5, 0x1.3c5392b8cb23dp-38, + 0x1.7b977cddc0916p-40, 0x1.badb6702b5fefp-39 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.9p+5, -0x1.ep+3, -0x1.18p+5, 0x1.e7cc9c8e5aec2p+18, + 0x1.24adf7889cf41p+17, 0x1.5575a0ca0c721p+18, -0x1.dp+5, + -0x1.1666666666666p+4, -0x1.44cccccccccccp+5 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.6p+6, -0x1.a666666666666p+4, 0x1.2333333333333p-4, 0x1.4p+6, + 0x1.8p+4, 0x1.cp+5, 0x1.b8p+5, 0x1.08p+4, 0x1.34p+5 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.95p+1, -0x1.e6p-1, -0x1.1b8p+1, -0x1.1cb1c32b3814p-61, + -0x1.55a21d671018p-63, -0x1.8e9277a2e81cp-62, + 0x1.e8c38262eb7adp+18, 0x1.25421b0826e35p+17, + 0x1.562274ded808fp+18 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.77b86c3e91155p+16, 0x1.c2dd4eb17ae66p+14, + 0x1.07011892325bdp+16, 0x1.832f903e2e4fap-25, + 0x1.d09f79e43792cp-27, 0x1.0f07b1c5206adp-25, + 0x1.e7958a660e749p+18, 0x1.248ceca3a245fp+17, + 0x1.554f14143d519p+18 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e847a332f86d6p+18, 0x1.24f7c851c841ap+17, + 0x1.55cbbf0a144c9p+18, 0x1.e8197c672c2cep+18, + 0x1.24dc17711a815p+17, 0x1.55ab70ae9eec3p+18, + 0x1.d4182cf254a1cp-48, 0x1.18db4e2aff944p-49, + 0x1.47aa85dcd4d7ap-48 } }, + { "exact, wrong floating-point sign", 0, + { 0x1p+4, 0x1.3333333333333p+2, 0x1.6666666666663p+3, + 0x1.e913cf2996126p+18, 0x1.25724918f3a4ap+17, + 0x1.565aaa9d1c401p+18, 0x1.9p-61, 0x1.ep-63, 0x1.18p-61 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.36p-58, 0x1.74p-60, 0x1.b2p-59, 0x1.ca4f470a2eb6p-2, + 0x1.12fc5dd2e8d39p-3, 0x1.40d11820ba4c2p-2, 0x1.ccp-53, + 0x1.14p-54, 0x1.42p-53 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.3p+6, 0x1.6cccccccccccdp+4, 0x1.a999999999999p+5, -0x1.e8p-52, + -0x1.24ccccccccccdp-53, -0x1.5599999999999p-52, -0x1.4dp-9, + -0x1.8f9999999999bp-11, -0x1.d233333333333p-10 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.38p+5, -0x1.7666666666666p+3, -0x1.b4cccccccccccp+4, + 0x1.e81d5af01ca6ap+18, 0x1.24de69c34464p+17, + 0x1.55ae260e7a74bp+18, 0x1.d87ffffffffffp-42, + 0x1.24ba706644b5p+17, 0x1.55842dcca57ddp+18 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.a8p+5, -0x1.fccccccccccccp+3, -0x1.28cccccccccccp+5, + -0x1.e6p-34, -0x1.2399999999999p-35, -0x1.5433333333333p-34, + -0x1.78p+6, -0x1.c333333333333p+4, -0x1.0733333333334p+6 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.0967b20b52cb5p-28, -0x1.3e7c6f40c9c0cp-30, + -0x1.73912c7640b63p-29, 0x1.ap+3, 0x1.f333333333333p+1, + -0x1.0dc2c9c7bd2dfp-60, -0x1.6278f622c571bp+15, + -0x1.a95df429b9bbap+13, -0x1.f042f230ae058p+14 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.7c6ea44fd360dp-21, 0x1.c884c52c9740fp-23, + 0x1.0a4d7304ad908p-21, -0x1.07c72542a016fp+9, + -0x1.3c88931cc01b9p+7, -0x1.714a00f6e02p+8, -0x1.2bp-22, + -0x1.66ccccccccccdp-24, -0x1.a299999999999p-23 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.05p-45, -0x1.3933333333332p-47, -0x1.6d66666666666p-46, + -0x1.98f6690d325a7p-40, -0x1.eac14adca2d32p-42, + -0x1.1e46165609a5dp-40, -0x1.353158dfd5dfcp-47, + -0x1.7308043fcd72ep-49, -0x1.b0deaf9fc505ep-48 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.ap+5, 0x1.f333333333333p+3, 0x1.2333333333331p+5, + 0x1.e9396289baa4fp+18, 0x1.2588d4b909964p+17, + 0x1.5674f82d35d9fp+18, 0x1.cp+3, 0x1.0cccccccccccdp+2, + 0x1.3999999999999p+3 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.e8cb9e408719ap+18, 0x1.2546f88d1ddc3p+17, + 0x1.562821f9f82b8p+18, 0x1.3cdd0a69c9b49p-42, + 0x1.7c3c72e558724p-44, 0x1.bb9bdb60e72fdp-43, -0x1p-61, + -0x1.3333333333333p-63, -0x1.6666666666666p-62 } }, + }; + + const std::vector< RegressionCase > ALIGNED_3D_CASES{ + { "filter", 0, + { 0x1.a8p+5, -0x1.59183079c12abp-40, 0x1.9c3e299232779p+24, + -0x1.b000000000001p+4, 0x1.e87035a3820ddp+18, + 0x1.d24511968a1ecp+37, 0x1.58p+6, -0x1.88p+6, + -0x1.512676b182d02p+22 } }, + { "filter", 0, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.18p+6, + 0x1.e7be2e932563p+18, 0x1.928p+7, 0x1.56166bf8c7cfap-41, + -0x1.17ffffffffffep+5, 0x1.e93fdd899b1a3p+18, + 0x1.6200000000001p-17 } }, + { "filter", 0, + { -0x1.6p+5, 0x1.e8b7220826db4p+18, -0x1.991b997468855p+14, + 0x1.868bf64a9c8cfp-9, -0x1.6p+5, 0x1.139ca39e9c9efp+1, + 0x1.e8b7220826db4p+18, 0x1.6fffffffffffep-10, + -0x1.8d76f7d1f2fb7p+22 } }, + { "zero-filter", 1, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.18p+6, + -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.18p+6, + -0x1.7222a2c932b4ep-35, 0x1.d9bfffffffffep+10, + -0x1.1800000000003p+6 } }, + { "zero-filter", 1, + { -0x1.0900000000001p-13, 0x1.99ef178098934p-38, 0x1.5p+6, + -0x1.0900000000001p-13, 0x1.99ef178098934p-38, 0x1.5p+6, + -0x1.0900000000001p-13, 0x1.9e247570c34bep-38, + 0x1.5000000005b28p+6 } }, + { "zero-filter", 1, + { 0x1.e893c5db9020cp+18, 0x1.6p+5, 0x1.8cd292e44f0b4p+30, + 0x1.e893c4979020cp+18, 0x1.e897717ee749cp+18, + 0x1.8cd292e44f0b4p+30, 0x1.e893c5db9020cp+18, 0x1.6p+5, + 0x1.8cd292e44f0b4p+30 } }, + { "exact", 0, + { 0x1.a8p+5, -0x1.59183079c12abp-40, 0x1.9c3e299232779p+24, + -0x1.e932439f9b65cp+19, 0x1.a7fffffffffaap+6, + 0x1.9c3e259232779p+24, 0x1.d2687f0d0135ep+37, + -0x1.943a3cdcd4838p+24, 0x1.ab7f27099feb9p+24 } }, + { "exact", 0, + { -0x1.6p+5, 0x1.e8b7220826db4p+18, -0x1.991b997468855p+14, + 0x1.6e1b8727c9f84p+20, 0x1.e8b4220826db4p+18, + -0x1.9957997468855p+14, 0x1.d21bfbf9f405fp+37, + -0x1.6eb2269928741p+20, -0x1.34be2ca3e82bcp+21 } }, + { "exact", 0, + { -0x1.9p+5, -0x1.dffffffffffffp+3, -0x1.18p+5, + -0x1.67fffffffffffp+6, -0x1.bp+4, -0x1.f7fffffffffffp+5, + -0x1.c1182267e4d41p-32, -0x1.0d74e171894cp-33, + -0x1.3a5db1af202ep-32 } }, + { "exact", 1, + { 0x1.e8420023b3205p+18, 0x1.24f4667bd1e03p+17, + 0x1.55c7cce5ca303p+18, 0x1.e8538023b3205p+18, + 0x1.24f4d4fbd1e03p+17, 0x1.55c7cce5ca303p+18, + 0x1.e6b84023b3205p+18, 0x1.24eab03bd1e03p+17, + 0x1.55c7cce5ca303p+18 } }, + { "exact", 1, + { 0x1.e751397690a99p+18, 0x1.2463ef472398fp+17, + 0x1.551f41d2fedd2p+18, 0x1.e7513976c7d99p+18, + 0x1.2463ef472398fp+17, 0x1.552ac1d2fedd2p+18, + 0x1.e751397302119p+18, 0x1.2463ef472398fp+17, + 0x1.546181d2fedd2p+18 } }, + { "exact", 1, + { 0x1.e899e77c6ee34p+18, 0x1.2529247ddc222p+17, + 0x1.5605553d80d25p+18, 0x1.e899e77c6f03ap+18, + 0x1.24e7247ddc222p+17, 0x1.55f7853d80d25p+18, + 0x1.e899e77c6cbcep+18, 0x1.298b247ddc222p+17, + 0x1.56f0253d80d25p+18 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.da00000000001p-42, -0x1.2p-5, 0x1.e77ab05ec953bp+18, + 0x1.ad381c83a9b4p-7, 0x1.beep+3, -0x1.ec0bd7aaf1401p+9, + 0x1.4f53d6470a682p+0, 0x1.5dfdcp+10, -0x1.79c919098e7dp+25 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.dep-45, -0x1.1eccccccccccdp-46, -0x1.4e99999999998p-45, + 0x1.e771cc5a15fep+18, 0x1.24777a9c73986p+17, + 0x1.55360f0bdc31cp+18, -0x1.08p+5, -0x1.3cccccccccccdp+3, + -0x1.7199999999999p+4 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.3fc42b4d9a261p-68, 0x1.e7dadd360a0ap+18, + 0x1.e8a6c0473dbe2p+18, -0x1.38bfff601dea6p-43, + 0x1.e7e79d360a0ap+18, 0x1.e8a6c0473dbe2p+18, + -0x1.4285fffb00ef5p-38, 0x1.e97f9d360a0ap+18, + 0x1.e8a6c0473dbe2p+18 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e896e69f1f5dcp+18, 0x1.2527572c46051p+17, + 0x1.56033b08fc5b3p+18, -0x1.a8p+5, -0x1.fccccccccccccp+3, + -0x1.28cccccccccccp+5, -0x1.e6p-34, -0x1.2399999999999p-35, + -0x1.5433333333333p-34 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.64af0ce37a57cp+9, 0x1.4p+6, -0x1.5315df845a20ap+28, + -0x1.6e0799c8298ap+20, 0x1.3fffc4cp+6, -0x1.5315df845a20ap+28, + 0x1.3818ccec51d2ap+16, 0x1.4000032fecp+6, + -0x1.5315df845a20ap+28 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.19298d76cc428p+13, 0x1.51651028284fdp+11, + 0x1.89a092d9845d1p+12, -0x1.05p-45, -0x1.3933333333332p-47, + -0x1.6d66666666666p-46, -0x1.98f6690d325a7p-40, + -0x1.eac14adca2d32p-42, -0x1.1e46165609a5dp-40 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.f226719f9689fp+8, -0x1.2ae3ddc6271f9p+7, + -0x1.5cb482bc82fa2p+8, 0x1.e92a021750d96p+18, + 0x1.257f9adaca1cp+17, 0x1.566a34a9ebcb5p+18, -0x1.7p+5, + -0x1.b999999999999p+3, -0x1.0199999999999p+5 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.237fffffffffdp-25, 0x1.237fffffffffdp-25, + 0x1.9819999999999p-26, 0x1.237fdfefffffdp-25, + -0x1.e76a549cdcc25p+19, -0x1.e84ff91e2ceep+19, + 0x1.238200ffffffdp-25, 0x1.e76a549cdcd5ap+23, + 0x1.e84ff91e2cfb9p+23 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.4p+3, -0x1.8p+1, -0x1.cp+2, 0x1.d0426ca3c1222p-32, + 0x1.168e412f0d7aep-33, 0x1.44fb4c0c3a64bp-32, + 0x1.7a6a5352872f4p+0, 0x1.c61930c96f058p-2, + 0x1.08e407202b6dep+0 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.e777dd8d5babp+18, 0x1.247b1e8803cdp+17, 0x1.553a4e4959c48p+18, + 0x1.a9p-25, 0x1.fep-27, 0x1.298p-25, -0x1.b58p-27, -0x1.068p-28, + -0x1.324p-27 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.2a792afadb578p-51, 0x1.662b00603a69p-53, + 0x1.a1dcd5c5997a8p-52, -0x1.9p+3, -0x1.ep+1, -0x1.18p+3, + 0x1.7cp-5, 0x1.c8p-7, 0x1.0ap-5 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.e79c124264023p+18, 0x1.2490d7c16f348p+17, + 0x1.5553a661ac67ep+18, 0x1.e79c124264023p+18, + 0x1.2490d7c08b0e7p+17, 0x1.c40cdd9f56952p+20, + 0x1.e79c124264023p+18, 0x1.2490d7c12327dp+17, + 0x1.9f2475e01ddb6p+19 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.7ep-3, 0x1.078p-43, 0x1.6bd0af8bbabd8p+16, + 0x1.7e00000000b5ep-3, -0x1.a8012e71b6594p-3, + 0x1.6cf9af8bbabd8p+16, 0x1.7e000000071acp-3, + -0x1.0900bd07128cp+1, 0x1.776aaf8bbabd8p+16 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.9p+6, -0x1.ep+4, -0x1.18p+6, -0x1.9p+5, -0x1.ep+3, + -0x1.18p+5, 0x1.6e2c82cc348a6p-44, 0x1.b7689cf50bd94p-46, + 0x1.00525b8ef1941p-44 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.84p-7, 0x1.e7865ebbcefb1p+18, 0x1.e7ec56ae751efp+18, + 0x1.e37p-1, -0x1.e8b48df943279p+18, 0x1.e7f9aeae751efp+18, + 0x1.57fd8p+4, -0x1.4f969dbcb9244p+24, 0x1.e91892ae751efp+18 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.10b9908c3edb8p-62, -0x1.474513db7ea1p-64, + -0x1.7dd0972abe668p-63, -0x1.fa216dcf1316ep-59, + -0x1.2fada8490b742p-60, -0x1.624a99aa8d5cdp-59, -0x1.4p+6, + -0x1.8p+4, -0x1.cp+5 } }, + }; + + const std::vector< RegressionCase > DOT_2D_CASES{ + { "filter", -1, + { -0x1.b28p-26, -0x1.04b3333333333p-27, -0x1.b2800000ca5ddp-26, + -0x1.04b3334873335p-27, -0x1.b27ffff560003p-26, + -0x1.04b33334c7ee7p-27 } }, + { "filter", -1, + { 0x1.e865a729a8d94p+18, 0x1.e883f706d5e17p+18, + 0x1.e87be729a8d97p+18, 0x1.e883f7a5d5e17p+18, + 0x1.e865a68aa8d93p+18, 0x1.e89a3706d5e17p+18 } }, + { "filter", -1, + { -0x1.98p+5, -0x1.2p+4, -0x1.97feefp+5, -0x1.2000000000003p+4, + -0x1.97fffffffffffp+5, -0x1.1ffddep+4 } }, + { "filter", 1, + { 0x1.e8ab49655d035p+18, 0x1.e8bc79d913b37p+18, + 0x1.e8b309655d035p+18, 0x1.e8a379d913b37p+18, + 0x1.e8c449655d037p+18, 0x1.e8c439d913b37p+18 } }, + { "filter", 1, + { 0x1.3fc42b4d9a261p-68, 0x1.e7dadd360a0ap+18, 0x1.3800000000003p+5, + 0x1.e7dadd360a0ap+18, 0x1.9ceb4ee6751fep-34, + 0x1.e7e49d360a0ap+18 } }, + { "filter", 1, + { 0x1.e8296e4e4cdddp+18, 0x1.e8ca0f62ded75p+18, + 0x1.e8296e4e4cd7bp+18, 0x1.e8d54f62ded75p+18, + 0x1.e81e2e4e4cdddp+18, 0x1.e8ca0f62ded14p+18 } }, + { "zero-filter", 0, + { -0x1.1266666666666p-1, -0x1.d666666666669p-3, + -0x1.17d6666666666p-1, -0x1.d666666666669p-3, + -0x1.1266666666666p-1, -0x1.ec26666666669p-3 } }, + { "zero-filter", 0, + { 0x1.e82f4ccdfe8c2p+18, 0x1.e82f4ccdfe8c2p+18, + 0x1.e82f4ccdfe8c2p+18, 0x1.e82f4ccdfe96bp+18, + 0x1.e82f4ccdfe819p+18, 0x1.e82f4ccdfe8c2p+18 } }, + { "zero-filter", 0, + { 0x1.9p+6, 0x1.ep+4, 0x1.e8b0dd41f25ecp+18, 0x1.ep+4, 0x1.9p+6, + 0x1.e89f5d41f25ebp+18 } }, + { "exact", -1, + { 0x1.a8p+5, -0x1.59183079c12abp-40, 0x1.a800000002422p+5, + 0x1.2ffffffffffaap+6, -0x1.7p+4, 0x1.1bab4641aa1c9p-34 } }, + { "exact", -1, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.0cp+6, + -0x1.ff6fffffffffdp+5, 0x1.bb8p+2, -0x1.efffffffffffep+6 } }, + { "exact", -1, + { -0x1.6p+5, 0x1.e8b7220826db4p+18, 0x1.e74e1abdb21a7p+18, + 0x1.e8c9e20826db3p+18, -0x1.dcp+6, 0x1.e8081e62ec7aep+19 } }, + { "exact", 0, + { 0x1.253f30d6aabfdp+17, 0x1.253f30d6aabfdp+17, + 0x1.253f30d6a91c5p+17, 0x1.253f3ba1fc1b1p+17, + 0x1.253f260b59649p+17, 0x1.253f30d6a91c5p+17 } }, + { "exact", 0, + { 0x1.e7f6ae2f84e42p+18, 0x1.24c7354fb6227p+17, + 0x1.e7f6ae33cce42p+18, 0x1.24c7354f3b227p+17, + 0x1.e7f6ae2fc2642p+18, 0x1.24c7355846227p+17 } }, + { "exact", 0, + { -0x1.58p-28, 0x1.1b8p-23, 0x1.e85e2466c8b8dp+18, 0x1.c580011b8p+1, + -0x1.c580000acp+1, 0x1.e85e2466c94bfp+18 } }, + { "exact", 1, + { -0x1.9p+5, -0x1.dffffffffffffp+3, -0x1.5cp+6, -0x1.ccp+6, + 0x1.9p+5, -0x1.ap+5 } }, + { "exact", 1, + { -0x1.0900000000001p-13, 0x1.99ef178098934p-38, -0x1.3c00212p+6, + -0x1.8fffffffff998p+4, 0x1.8fff7b7fffffep+4, + -0x1.3bffffffffe66p+6 } }, + { "exact", 1, + { -0x1.659fdac2e6cd6p-47, 0x1.f1ec2241e0388p+0, + 0x1.368ffbdf911eap-45, 0x1.e77eefdbe1dap+18, + -0x1.e77e7360d9497p+18, 0x1.f1ec2241e045p+0 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.0cp+6, + -0x1.ff6fffffffffdp+5, 0x1.bb8p+2, -0x1.efffffffffffep+6 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.8990fdb9327fp-14, 0x1.6f2adcf12ab38p-35, 0x1.580031321fb72p+5, + -0x1.1fffffffff487p+6, 0x1.200018990fdb9p+6, + 0x1.58000000016f2p+5 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.29e0d5bbdd285p-41, 0x1.e7c39a67e949ep+18, + 0x1.e77056f8407e6p+18, 0x1.e7c39a685449ep+18, + -0x1.ac000094f06aep-16, 0x1.e799f8b014e42p+19 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.2b42e5f8852e8p+12, 0x1.0800000000003p+6, + -0x1.2872e5f8852e8p+12, 0x1.e83df65cd15dp+18, + -0x1.ecda81f4b371cp+18, 0x1.bc00000000003p+6 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.37ffffffffffep-10, -0x1.7666666666665p-12, -0x1.880138p+6, + 0x1.e8c5eaeb5fba3p+18, -0x1.e8c5eb04b953dp+18, + -0x1.88005d999999ap+6 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.90e9fdc1c0a6dp-19, 0x1.ap-53, 0x1.90ea0579c0a6dp-19, 0x1.8p+6, + -0x1.7fffff378b012p+6, 0x1.ee1ap-41 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.3cp-6, 0x1.bfc87ed643f8fp-53, 0x1.e8bea5ff7e81bp+18, + -0x1.52p+2, 0x1.533cp+2, 0x1.e8bea4c37e81bp+18 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.e87e1e821d5bfp+18, 0x1.e8c3a2a21855ep+18, + 0x1.e88f1e821d5bfp+18, 0x1.e88a9d757e0eep+19, 0x1.6431c9ccap+7, + 0x1.e8d4a2a21855ep+18 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.9p+5, -0x1.dffffffffffffp+3, -0x1.5cp+6, -0x1.ccp+6, + 0x1.9p+5, -0x1.ap+5 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.dep-45, -0x1.1eccccccccccdp-46, 0x1.e7fa85c890f16p+18, + -0x1.1ed42eccccccdp-46, -0x1.ddfc4fp-45, + 0x1.e7fa85c890f16p+18 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.e5424739d220fp-1, -0x1.048p-2, 0x1.e9364a34c9007p+18, + 0x1.6afb8p+6, -0x1.6fca848e73a44p+6, 0x1.e936769511e7bp+18 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.dc00000000003p+6, 0x1.1d99999999999p+5, 0x1.e805cf67d1a7ap+18, + 0x1.ed99999999999p+5, 0x1.7400000000003p+6, + 0x1.e7f0fc349e747p+18 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.1cp+6, -0x1.54ccccccccccdp+4, -0x1.1bff5fp+6, + -0x1.8933333333333p+6, 0x1.8p+2, -0x1.54ca48ccccccdp+4 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.1f3309ffffeb7p+5, 0x1.598p-58, -0x1.21eb09ffffeb7p+5, + -0x1.4p+5, 0x1.0667b00000a48p+2, -0x1.5cp-2 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.51f64c27546d3p-60, -0x1.b18p-55, -0x1.e8fdf392ca9ccp-3, + -0x1.8p+6, 0x1.8p+6, -0x1.e8fdf392ca9cep-3 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.cp-35, 0x1.82p-38, 0x1.e7cf52cbc3b16p+18, 0x1.8a00000608p-8, + -0x1.8a000038p-8, 0x1.e7cf52cbc3b17p+18 } }, + }; + + const std::vector< RegressionCase > DOT_3D_CASES{ + { "filter", -1, + { 0x1.e7cc178e1e3d6p+18, 0x1.e859b4d621535p+18, + 0x1.d2743f827c8f3p+37, 0x1.e8269b370deb8p+19, + 0x1.e876abf54b1fp+19, 0x1.d2747c8d304e7p+37, + 0x1.e7cc178e34e46p+18, 0x1.d2133725c13dp+37, + 0x1.30a05f94d8p+26 } }, + { "filter", -1, + { -0x1.48p+6, 0x1.12p-23, -0x1.57fff900dp+5, 0x1.ap+3, + 0x1.e8f040dc8c294p+18, 0x1.e75eb8dbbdb1p+18, + -0x1.8a34a00c23ac9p+27, 0x1.12p-23, -0x1.5829fda662fa9p+5 } }, + { "filter", -1, + { 0x1.e81e113ffe3ffp+18, -0x1.951b24fc9e053p-61, + -0x1.1a2cfebefe0eep+24, 0x1.e80c113ffe3ffp+18, + 0x1.e93676a7f1feep+18, -0x1.1a2cfebefde76p+24, + 0x1.6ee013790b425p+23, -0x1.1a2cfebefe0eep+24, + -0x1.9f43d7f61f2d1p+25 } }, + { "filter", 1, + { 0x1.a8p+5, -0x1.59183079c12abp-40, 0x1.9c3e299232779p+24, + 0x1.a800000002422p+5, 0x1.2ffffffffffaap+6, + 0x1.9c42599f3b2b2p+24, -0x1.5b5f39d239fa3p+16, + -0x1.a79318f9a682p-28, 0x1.9c3e29923277bp+24 } }, + { "filter", 1, + { 0x1.e893c5db9020cp+18, 0x1.6p+5, 0x1.8cd292e44f0b4p+30, + 0x1.e89385db90209p+18, 0x1.e84fa98e6e9b9p+18, + 0x1.8cd291844f0b4p+30, -0x1.c9bde6131ea5bp+23, + 0x1.8cd292e44f0b4p+30, 0x1.92e6edc0c50bdp+30 } }, + { "filter", 1, + { -0x1.29e0d5bbdd285p-41, 0x1.e7c39a67e949ep+18, + 0x1.d080b05e9c91ep+37, 0x1.e77056f8407e5p+18, + 0x1.e7c39a685449ep+18, 0x1.d080b05e9c921p+37, + 0x1.e723a3c602196p-14, -0x1.b0d99bbc637bp+20, + 0x1.d089f8a2448c2p+37 } }, + { "zero-filter", 0, + { 0x1.e79730b3f9a71p+18, 0x1.e928518961dap+18, 0x1.b6602e2b60a6p+25, + 0x1.e79730b3f9a71p+18, 0x1.e928518961dap+18, + 0x1.ba2f76ff22921p+25, -0x1.f01368fdcc978p+20, + 0x1.e9285188f72e1p+18, 0x1.b6602e2b60a6p+25 } }, + { "zero-filter", 0, + { -0x1.cp+5, -0x1.bp+5, -0x1.042574824591cp+15, -0x1.cp+5, + 0x1.8e7292099b84ap+12, -0x1.042574824591cp+15, + -0x1.c01ccccfd670dp+5, -0x1.bp+5, 0x1.19f7f11afd135p+19 } }, + { "zero-filter", 0, + { 0x1.e9159e4067582p+18, 0x1.e9159e4067582p+18, + 0x1.565beec6aebd9p+18, 0x1.e9159e4067582p+18, + 0x1.e89d7a097af1ap+19, 0x1.565beec6aebd9p+18, + -0x1.f762bf5f6b9c5p+24, 0x1.e9159e4067582p+18, + -0x1.2de24fc59f765p+32 } }, + { "exact", -1, + { -0x1.9p+5, -0x1.dffffffffffffp+3, -0x1.18p+5, -0x1.5cp+6, + -0x1.cbffffffffffdp+6, -0x1.18p+5, -0x1.9p+5, + -0x1.dfffffffff8c1p+3, 0x1.e06fe177a24e6p+24 } }, + { "exact", -1, + { -0x1.659fdac2e6cd6p-47, 0x1.f1ec2241e0388p+0, + 0x1.f1ec2241e0388p+0, 0x1.368ffbdf911eap-45, + 0x1.e77eefdbe1d9fp+18, 0x1.f1ec259aa56a2p+0, + -0x1.faaef580eb99dp-35, 0x1.f1ec54756e20ep+0, + -0x1.c90684631b2bep+22 } }, + { "exact", -1, + { -0x1.5dp-51, -0x1.a2ccccccccccbp-53, -0x1.e899999999999p-52, + -0x1.1p+9, 0x1.e78046bc350dcp+18, -0x1.1c80000000003p-1, + 0x1.d0d1fe1a98df1p+37, 0x1.0358526bf19e9p+28, + -0x1.0d041fc80bc0cp+28 } }, + { "exact", 0, + { 0x1.e78ec14fa2f87p+18, 0x1.2488da62fb61ep+17, + 0x1.554a541e25478p+18, 0x1.e78ec14fa2f87p+18, + 0x1.2488e16afb61ep+17, 0x1.5558d41e25478p+18, + 0x1.013462ec596f9p+19, 0x1.227eda62fb61ep+17, + 0x1.554a936625478p+18 } }, + { "exact", 0, + { -0x1.cp+5, -0x1.48p+5, 0x1.ffffffffdbcep+2, -0x1.bfffc6bp+5, + -0x1.47f44p+5, 0x1.ffffffffdbcep+2, -0x1.c2e44p+5, + -0x1.47f1e55p+5, 0x1.ffba2fd6fbce1p+2 } }, + { "exact", 0, + { 0x1.14p+6, -0x1.d4p-29, -0x1.34p-37, 0x1.14p+6, 0x1.44bp-23, + 0x1.a57ffffffff66p+8, 0x1.1400033899fp+6, -0x1.c323000eap+0, + 0x1.5e88p-31 } }, + { "exact", 1, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.18p+6, + -0x1.0cp+6, -0x1.ff6ffffffffffp+5, 0x1.e7fcfa74ea5dbp+18, + 0x1.e80e4304ea5dbp+21, 0x1.960f73dd7b633p+7, + 0x1.d200af686p+8 } }, + { "exact", 1, + { -0x1.6p+5, 0x1.e8b7220826db4p+18, -0x1.991b997468855p+14, + 0x1.e74e1abdb21a7p+18, 0x1.e8c9e20826db4p+18, + -0x1.991bb6ec68855p+14, 0x1.e1424ep+8, -0x1.6d571324f6f78p+21, + 0x1.10890371550d3p+22 } }, + { "exact", 1, + { -0x1.1266666666666p-1, -0x1.d666666666669p-3, + -0x1.1266666666666p-1, -0x1.17d6666666666p-1, + -0x1.d666666666669p-3, 0x1.e769e81ed2783p+18, + -0x1.d0e5d1ddf348bp+37, -0x1.d734650b69dd3p-3, + -0x1.4bf5157c3fea7p+12 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.659fdac2e6cd6p-47, 0x1.f1ec2241e0388p+0, + 0x1.f1ec2241e0388p+0, 0x1.368ffbdf911eap-45, + 0x1.e77eefdbe1d9fp+18, 0x1.f1ec259aa56a2p+0, + -0x1.faaef580eb99dp-35, 0x1.f1ec54756e20ep+0, + -0x1.c90684631b2bep+22 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.2b42e5f8852e8p+12, 0x1.0800000000003p+6, + 0x1.0800000000003p+6, -0x1.2872e5f8852e8p+12, + 0x1.e83df65cd15dp+18, 0x1.e8c4eaf2ebcc8p+18, + 0x1.d10e1a04806eap+37, -0x1.56f297434afa5p+24, + 0x1.0800000007baap+6 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.9a5f26c60cb8fp+8, -0x1.0cp+0, -0x1.bbcc06f7356c6p+4, + -0x1.4e5f26c60cb8fp+8, -0x1.f0cp+4, -0x1.55e6037b9ab63p+5, + 0x1.83506c9ade856p+9, -0x1.0bd7174062753p+0, + 0x1.75842eda9b65fp+12 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.1a74d6f7a25dap-7, 0x1.52f29b8f8fa39p-9, 0x1.8b7060277ce97p-8, + 0x1.1a74d706d25dap-7, -0x1.23fd5a1ac8e1p+6, + 0x1.e88d55330741ep+18, 0x1.1a74dfcf00a82p-7, + 0x1.d12e2967c06b1p+37, 0x1.1607f3cc81ff7p+25 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.c333333333336p-22, 0x1.c333333333336p-22, + 0x1.0733333333332p-20, 0x1.53ee15db06c93p+8, + 0x1.cc6b333333336p-22, -0x1.73ffffbe33333p+6, + -0x1.b9bffffe3cccdp+10, -0x1.62cf55d463786p+25, + -0x1.93aacb8095446p+12 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.9p+5, -0x1.ep+3, -0x1.18p+5, -0x1.fp+6, + -0x1.dffffffffe38fp+3, 0x1.e8bc626a1d3edp+18, + -0x1.d2c5c898f7f04p+37, -0x1.0b4c9c11bf92ap+25, + -0x1.1aadb66211e62p+25 } }, + { "exact, wrong floating-point sign", -1, + { -0x1.4p-51, -0x1.8p-53, -0x1.cp-52, -0x1.70027ffffffffp-36, + 0x1.e7f731b33f8fap+18, -0x1.7918dfb95dde7p-36, + 0x1.62899a1c3c2e6p+25, -0x1.67ad3952130a3p-17, + -0x1.d1842b539c9bap+37 } }, + { "exact, wrong floating-point sign", -1, + { 0x1.1c00000000001p+6, 0x1.54ccccccccccdp+4, 0x1.8d99999999999p+5, + 0x1.1bfffffffea3bp+6, 0x1.e7d8817d72fb7p+18, + 0x1.e8d84b0395501p+18, 0x1.891bd8747e5f6p+25, + 0x1.4cb618cd96c8cp+20, -0x1.4c08630ed6b55p+20 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.ep+4, -0x1.74p+6, -0x1.0cp-28, -0x1.38p+5, -0x1.8ap+6, + -0x1.0ec2p-28, -0x1.3cp+8, 0x1.76fffffff761cp+8, + 0x1.12ffffffef4p+8 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.1dp-30, 0x1.56p-32, 0x1.8fp-31, -0x1.bffffffee3p+2, + -0x1.7fffffffd54p+3, 0x1.8effc67p-31, 0x1.1d96714454e3cp-8, + -0x1.4d2f7c472a8f9p-9, 0x1.81000000031ep+8 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.d9p+0, -0x1p+4, -0x1.6d62fe3a07a2ep+11, -0x1.ad9p+4, + 0x1.cp+5, -0x1.62e2fe3a07a2ep+11, -0x1.5f1d90000a5b4p+12, + -0x1.eb84cddp+10, -0x1.6d60ef2a0dcdp+11 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.f4p-16, -0x1.2cp-17, -0x1.5ep-16, -0x1.40001f4p+4, + -0x1.900004bp+5, -0x1.5dffd6ep-16, -0x1.3880003e89b81p+11, + 0x1.f3ffffb5p+9, -0x1.2e80000afp+13 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e7795ffaec358p+18, 0x1.247c06635a868p+17, + 0x1.553b5cc93ef24p+18, 0x1.e77961346c358p+18, + 0x1.3dacaf7b30e48p+19, 0x1.553b5cc93ef24p+18, + -0x1.1339835b42104p+22, 0x1.247c1ee15a868p+17, + 0x1.0fb09ac34a482p+17 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e8ebe9ad83f6fp+18, 0x1.7p+4, 0x1.7p+4, 0x1.e8d769ad83f6fp+18, + 0x1.e75906ccd0f3fp+18, 0x1.cp+4, -0x1.3091147a41327p+25, + -0x1.9efp+12, 0x1.e6ded6ccd0f3fp+20 } }, + { "exact, wrong floating-point sign", 0, + { 0x1.e77666d238cb4p+18, 0x1.e77666d238cb4p+18, -0x1.18p+7, + 0x1.e761e6d238cb4p+18, 0x1.e78ae6d238cb4p+18, + 0x1.e905af138482p+18, 0x1.5030d3d944b6ap+22, + -0x1.41097c05d88ep+24, 0x1.06ep+12 } }, + { "exact, wrong floating-point sign", 0, + { -0x1.6cp+6, 0x1.e805224d11858p+18, -0x1.58011f26c95d3p+25, + -0x1.9p+6, 0x1.e805221d11858p+18, -0x1.58011f26c4561p+25, + -0x1.849b47299ae46p+10, 0x1.30e4c7840543ap+22, + -0x1.58011f26c95d3p+25 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.856edb339ab87p-54, -0x1.c7ffffffffffdp+5, -0x1.18p+6, + -0x1.0cp+6, -0x1.ff6ffffffffffp+5, 0x1.e7fcfa74ea5dbp+18, + 0x1.e80e4304ea5dbp+21, 0x1.960f73dd7b633p+7, + 0x1.d200af686p+8 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.518p-41, 0x1.95p-43, 0x1.d87ffffffffffp-42, + 0x1.3400000000015p+7, -0x1.57ffffffffff3p+6, + 0x1.c000000000076p+4, 0x1.41ffe941f7dd8p+11, + 0x1.ab42da55be6c3p+23, 0x1.47f7a95f38d58p+25 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.a7f0079a5f538p-68, -0x1.a8p+5, 0x1.aa1f61697967fp-21, + -0x1.c08p-2, 0x1.48p+5, 0x1.e76537a36d015p+18, + -0x1.d116cca7c26eep+37, -0x1.a7ffffffffbf2p+5, + -0x1.abf949b01e5bep+17 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.808p-52, -0x1.51p-59, 0x1.56p-12, -0x1.96a0b89232093p-4, + 0x1.e7639d282d197p+18, 0x1.5618340be28a6p-12, + 0x1.d0dcdb6effdb4p+37, 0x1.83d59df0482a5p+15, + -0x1.d0e17cb5580b3p+37 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.07119fb168bc4p-69, -0x1.3baebfa1b0e1ep-71, + -0x1.704bdf91f9078p-70, 0x1.e752ebad36336p+18, -0x1.58p+5, + -0x1.18p+5, -0x1.5ep+11, 0x1.2520000000002p+11, + -0x1.309973cc41e02p+25 } }, + { "exact, wrong floating-point sign", 1, + { -0x1.56p-52, 0x1.e7a6dda937f5ap+18, -0x1.2fffb9a35c646p+5, + -0x1.bc738e95275f6p+15, 0x1.e7a6dda937f5ap+18, + -0x1.300479a35c646p+5, -0x1.d47800000055bp-10, + -0x1.a76adbf4e21f6p+19, 0x1.562809ae03bf4p+15 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.2c792b7ba008ap-7, 0x1.689167612670cp-9, 0x1.a4a9a346acd8bp-8, + 0x1.e84b1a5da13a7p+18, -0x1.4ef4bb74c4f6bp+4, + -0x1.b4ec86642a94cp-3, 0x1.ad4a2224dac56p+16, + -0x1.ab1afbabf6ffp+16, 0x1.d1c49265f2b1cp+37 } }, + { "exact, wrong floating-point sign", 1, + { 0x1.7ap-61, 0x1.c599999999999p-63, 0x1.0899999999999p-61, + 0x1.ce1c5c71a34e5p-31, -0x1.aea6666666666p-59, -0x1.cp+2, + 0x1.aa8f48d0c579ep+21, -0x1.f8f60f973ac25p+11, + 0x1.b7fec40b4e957p-12 } }, + }; + + void test_regression_cases() + { + for( const auto& test_case : ORIENT_2D_CASES ) + { + const auto& v = test_case.values; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( geode::Point2D{ { v[0], v[1] } }, + geode::Point2D{ { v[2], v[3] } }, + geode::Point2D{ { v[4], v[5] } } ) + == test_case.expected, + "[Test] Wrong orient_2d on case decided by ", test_case.stage ); + } + for( const auto& test_case : ORIENT_3D_CASES ) + { + const auto& v = test_case.values; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( geode::Point3D{ { v[0], v[1], v[2] } }, + geode::Point3D{ { v[3], v[4], v[5] } }, + geode::Point3D{ { v[6], v[7], v[8] } }, + geode::Point3D{ { v[9], v[10], v[11] } } ) + == test_case.expected, + "[Test] Wrong orient_3d on case decided by ", test_case.stage ); + } + for( const auto& test_case : DET_3D_CASES ) + { + const auto& v = test_case.values; + geode::OpenGeodeGeometryException::test( + GEO::PCK::det_3d( geode::Vector3D{ { v[0], v[1], v[2] } }, + geode::Vector3D{ { v[3], v[4], v[5] } }, + geode::Vector3D{ { v[6], v[7], v[8] } } ) + == test_case.expected, + "[Test] Wrong det_3d on case decided by ", test_case.stage ); + } + for( const auto& test_case : ALIGNED_3D_CASES ) + { + const auto& v = test_case.values; + geode::OpenGeodeGeometryException::test( + GEO::PCK::aligned_3d( geode::Point3D{ { v[0], v[1], v[2] } }, + geode::Point3D{ { v[3], v[4], v[5] } }, + geode::Point3D{ { v[6], v[7], v[8] } } ) + == ( test_case.expected == 1 ), + "[Test] Wrong aligned_3d on case decided by ", + test_case.stage ); + } + for( const auto& test_case : DOT_2D_CASES ) + { + const auto& v = test_case.values; + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_2d( geode::Point2D{ { v[0], v[1] } }, + geode::Point2D{ { v[2], v[3] } }, + geode::Point2D{ { v[4], v[5] } } ) + == test_case.expected, + "[Test] Wrong dot_2d on case decided by ", test_case.stage ); + } + for( const auto& test_case : DOT_3D_CASES ) + { + const auto& v = test_case.values; + geode::OpenGeodeGeometryException::test( + GEO::PCK::dot_3d( geode::Point3D{ { v[0], v[1], v[2] } }, + geode::Point3D{ { v[3], v[4], v[5] } }, + geode::Point3D{ { v[6], v[7], v[8] } } ) + == test_case.expected, + "[Test] Wrong dot_3d on case decided by ", test_case.stage ); + } + } } // namespace void test() @@ -376,6 +2148,8 @@ void test() test_dot_2d(); test_exact_degenerate(); test_nearly_degenerate_consistency(); + test_integer_configurations(); + test_regression_cases(); geode::Logger::info( "TEST SUCCESS" ); } From 9e4d1adf8a4f2eb642c12093b433b121fc3bc4f4 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 13:22:35 +0200 Subject: [PATCH 36/52] fix(Geometry): adaptive predicates return zero on a zero estimate sign_of() returned negative for a zero estimate, reachable when all the products underflow. Also share the double epsilon constant, return a bool from the aligned_3d filter and document the array expansion helpers. Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/internal/predicates.cpp | 39 ++++++++++++---------- tests/geometry/test-predicates.cpp | 23 +++++++++++++ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/internal/predicates.cpp index 9dafdaefd..4f3681cda 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/internal/predicates.cpp @@ -881,8 +881,9 @@ namespace GEO // Values for IEEE 754 doubles, as computed by PCK::initialize(): set at // compile time for the predicates to be exact even if the library has // not been initialized. + constexpr double DOUBLE_EPSILON = 1.1102230246251565404e-16; // 2^-53 double expansion_splitter_ = 134217729.0; // 2^27 + 1 - double expansion_epsilon_ = 1.1102230246251565404e-16; // 2^-53 + double expansion_epsilon_ = DOUBLE_EPSILON; // ============= expansion sum and difference ========================= @@ -1465,11 +1466,11 @@ namespace GEO > 8.88720573725927976811e-16 * ( max1 * max2 ); } - // Returns 1 if the points are certainly not aligned, FPG_UNCERTAIN_VALUE - // otherwise. The points are aligned if and only if the three components + // Returns true if the points are certainly not aligned, false if + // uncertain. The points are aligned if and only if the three components // of (p1-p0)x(p2-p0) are zero: one component certified non-zero is // enough to conclude. - inline int aligned_3d_filter( const geode::Point3D& p0, + inline bool is_certainly_not_aligned_3d( const geode::Point3D& p0, const geode::Point3D& p1, const geode::Point3D& p2 ) { @@ -1482,16 +1483,12 @@ namespace GEO const double max_x = std::max( std::fabs( a11 ), std::fabs( a21 ) ); const double max_y = std::max( std::fabs( a12 ), std::fabs( a22 ) ); const double max_z = std::max( std::fabs( a13 ), std::fabs( a23 ) ); - if( is_det_2d_certainly_non_zero( - ( a12 * a23 ) - ( a22 * a13 ), max_y, max_z ) - || is_det_2d_certainly_non_zero( - ( a13 * a21 ) - ( a23 * a11 ), max_z, max_x ) - || is_det_2d_certainly_non_zero( - ( a11 * a22 ) - ( a21 * a12 ), max_x, max_y ) ) - { - return 1; - } - return FPG_UNCERTAIN_VALUE; + return is_det_2d_certainly_non_zero( + ( a12 * a23 ) - ( a22 * a13 ), max_y, max_z ) + || is_det_2d_certainly_non_zero( + ( a13 * a21 ) - ( a23 * a11 ), max_z, max_x ) + || is_det_2d_certainly_non_zero( + ( a11 * a22 ) - ( a21 * a12 ), max_x, max_y ); } /******* zero-filters *******/ @@ -1610,7 +1607,7 @@ namespace GEO // result. namespace adaptive { - constexpr double EPSILON = 1.1102230246251565404e-16; // 2^-53 + constexpr double EPSILON = DOUBLE_EPSILON; constexpr double RESULT_ERRBOUND = ( 3.0 + 8.0 * EPSILON ) * EPSILON; constexpr double CCW_ERRBOUND_B = ( 2.0 + 12.0 * EPSILON ) * EPSILON; constexpr double CCW_ERRBOUND_C = @@ -1659,6 +1656,10 @@ namespace GEO two_one_diff( j, r0, cd1, x[3], x[2], x[1] ); } + // Array versions of the GEO::expansion functions above: the adaptive + // stages work on fixed size stack arrays instead of allocated + // expansions, which is where their speed comes from. + // h cannot be e or f. inline int fast_expansion_sum_zeroelim( int elen, const double* e, int flen, const double* f, double* h ) @@ -1818,7 +1819,11 @@ namespace GEO inline SIGN sign_of( double value ) { - return value > 0. ? positive : negative; + if( value > 0. ) + { + return positive; + } + return value < 0. ? negative : zero; } // Exact (a1 + a0) * b as a 4 components expansion @@ -2301,7 +2306,7 @@ namespace GEO const geode::Point3D& p1, const geode::Point3D& p2 ) { - if( aligned_3d_filter( p0, p1, p2 ) != FPG_UNCERTAIN_VALUE ) + if( is_certainly_not_aligned_3d( p0, p1, p2 ) ) { return false; } diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index a3c711adc..1bad0cf52 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -334,6 +334,28 @@ namespace "[Test] Wrong orient_3d on nearly collinear coplanar points" ); } + // Positive determinants whose products all underflow to zero, while no + // coordinate difference is zero: out of the exact range of the adaptive + // stages, the result may be zero but must never be the opposite sign. + void test_underflow() + { + const auto tiny = 1e-170; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_2d( geode::Point2D{ { 0, 0 } }, + geode::Point2D{ { tiny, tiny } }, + geode::Point2D{ { tiny, 3 * tiny } } ) + != GEO::negative, + "[Test] Wrong orient_2d on underflowing products" ); + const auto small = 1e-110; + geode::OpenGeodeGeometryException::test( + GEO::PCK::orient_3d( geode::Point3D{ { 0, 0, 0 } }, + geode::Point3D{ { small, 0, 0 } }, + geode::Point3D{ { 0, small, 0 } }, + geode::Point3D{ { 0, 0, small } } ) + != GEO::negative, + "[Test] Wrong orient_3d on underflowing products" ); + } + // Nearly degenerate configurations, decided by different evaluation // stages: the sign must not depend on the order of the points. void test_nearly_degenerate_consistency() @@ -2147,6 +2169,7 @@ void test() test_dot_3d(); test_dot_2d(); test_exact_degenerate(); + test_underflow(); test_nearly_degenerate_consistency(); test_integer_configurations(); test_regression_cases(); From 085e6a2c97637b74c5b63ddaae9f47fab1e8d91f Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 13:39:56 +0200 Subject: [PATCH 37/52] fix(Geometry): export exact predicates for Windows test linking test-predicates calls GEO::PCK functions directly: they need to be exported from the geometry DLL on Windows. Co-Authored-By: Claude Opus 5.5 --- include/geode/geometry/internal/predicates.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/geode/geometry/internal/predicates.hpp b/include/geode/geometry/internal/predicates.hpp index 617821515..a4043ed7e 100644 --- a/include/geode/geometry/internal/predicates.hpp +++ b/include/geode/geometry/internal/predicates.hpp @@ -561,32 +561,32 @@ namespace GEO { namespace PCK { - SIGN orient_2d( const geode::Point2D& p0, + opengeode_geometry_api SIGN orient_2d( const geode::Point2D& p0, const geode::Point2D& p1, const geode::Point2D& p2 ); - SIGN orient_3d( const geode::Point3D& p0, + opengeode_geometry_api SIGN orient_3d( const geode::Point3D& p0, const geode::Point3D& p1, const geode::Point3D& p2, const geode::Point3D& p3 ); - SIGN det_3d( const geode::Vector3D& p0, + opengeode_geometry_api SIGN det_3d( const geode::Vector3D& p0, const geode::Vector3D& p1, const geode::Vector3D& p2 ); - bool aligned_3d( const geode::Point3D& p0, + opengeode_geometry_api bool aligned_3d( const geode::Point3D& p0, const geode::Point3D& p1, const geode::Point3D& p2 ); - SIGN dot_3d( const geode::Point3D& p0, + opengeode_geometry_api SIGN dot_3d( const geode::Point3D& p0, const geode::Point3D& p1, const geode::Point3D& p2 ); - SIGN dot_2d( const geode::Point2D& p0, + opengeode_geometry_api SIGN dot_2d( const geode::Point2D& p0, const geode::Point2D& p1, const geode::Point2D& p2 ); - void initialize(); + opengeode_geometry_api void initialize(); } // namespace PCK } // namespace GEO From fd73a339f087c2e1771699ab6e489740bd717456 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 13:41:53 +0200 Subject: [PATCH 38/52] refactor(Geometry): move exact predicates out of internal The GEO::PCK predicates are now exported: predicates.hpp becomes a public header and predicates.cpp leaves the internal folder. Co-Authored-By: Claude Opus 5.5 --- include/geode/geometry/internal/intersection_from_sides.hpp | 2 +- include/geode/geometry/internal/position_from_sides.hpp | 2 +- include/geode/geometry/{internal => }/predicates.hpp | 0 src/geode/geometry/CMakeLists.txt | 4 ++-- src/geode/geometry/common.cpp | 2 +- src/geode/geometry/intersection_detection.cpp | 2 +- src/geode/geometry/position.cpp | 2 +- src/geode/geometry/{internal => }/predicates.cpp | 2 +- src/geode/geometry/radial_sort.cpp | 2 +- src/geode/geometry/sign.cpp | 2 +- tests/geometry/test-predicates.cpp | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) rename include/geode/geometry/{internal => }/predicates.hpp (100%) rename src/geode/geometry/{internal => }/predicates.cpp (99%) diff --git a/include/geode/geometry/internal/intersection_from_sides.hpp b/include/geode/geometry/internal/intersection_from_sides.hpp index 068d19c03..fb73ba05f 100644 --- a/include/geode/geometry/internal/intersection_from_sides.hpp +++ b/include/geode/geometry/internal/intersection_from_sides.hpp @@ -23,7 +23,7 @@ #pragma once -#include +#include namespace geode { diff --git a/include/geode/geometry/internal/position_from_sides.hpp b/include/geode/geometry/internal/position_from_sides.hpp index 44ff770e0..cf9c30089 100644 --- a/include/geode/geometry/internal/position_from_sides.hpp +++ b/include/geode/geometry/internal/position_from_sides.hpp @@ -23,7 +23,7 @@ #pragma once -#include +#include namespace geode { diff --git a/include/geode/geometry/internal/predicates.hpp b/include/geode/geometry/predicates.hpp similarity index 100% rename from include/geode/geometry/internal/predicates.hpp rename to include/geode/geometry/predicates.hpp diff --git a/src/geode/geometry/CMakeLists.txt b/src/geode/geometry/CMakeLists.txt index b70775c6e..3b853798d 100644 --- a/src/geode/geometry/CMakeLists.txt +++ b/src/geode/geometry/CMakeLists.txt @@ -54,7 +54,7 @@ add_geode_library( "perpendicular.cpp" "points_sort.cpp" "position.cpp" - "internal/predicates.cpp" + "predicates.cpp" "point.cpp" "projection.cpp" "quality.cpp" @@ -95,6 +95,7 @@ add_geode_library( "point.hpp" "points_sort.hpp" "position.hpp" + "predicates.hpp" "projection.hpp" "quality.hpp" "radial_sort.hpp" @@ -108,7 +109,6 @@ add_geode_library( INTERNAL_HEADERS "internal/intersection_from_sides.hpp" "internal/position_from_sides.hpp" - "internal/predicates.hpp" SKIP_UNITY "position.cpp" PUBLIC_DEPENDENCIES diff --git a/src/geode/geometry/common.cpp b/src/geode/geometry/common.cpp index 140ba2310..5413de5e7 100644 --- a/src/geode/geometry/common.cpp +++ b/src/geode/geometry/common.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include namespace geode { diff --git a/src/geode/geometry/intersection_detection.cpp b/src/geode/geometry/intersection_detection.cpp index 4dd59b513..e1003d870 100644 --- a/src/geode/geometry/intersection_detection.cpp +++ b/src/geode/geometry/intersection_detection.cpp @@ -29,10 +29,10 @@ #include #include #include -#include #include #include #include +#include #include namespace diff --git a/src/geode/geometry/position.cpp b/src/geode/geometry/position.cpp index c5b995600..9044d9629 100644 --- a/src/geode/geometry/position.cpp +++ b/src/geode/geometry/position.cpp @@ -33,9 +33,9 @@ #include #include #include -#include #include #include +#include namespace { diff --git a/src/geode/geometry/internal/predicates.cpp b/src/geode/geometry/predicates.cpp similarity index 99% rename from src/geode/geometry/internal/predicates.cpp rename to src/geode/geometry/predicates.cpp index 4f3681cda..89ab6e1db 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/predicates.cpp @@ -21,7 +21,7 @@ * */ -#include +#include #include #include diff --git a/src/geode/geometry/radial_sort.cpp b/src/geode/geometry/radial_sort.cpp index 1fce1a600..7cf578c64 100644 --- a/src/geode/geometry/radial_sort.cpp +++ b/src/geode/geometry/radial_sort.cpp @@ -29,8 +29,8 @@ #include #include #include -#include #include +#include #include namespace diff --git a/src/geode/geometry/sign.cpp b/src/geode/geometry/sign.cpp index 75e66e249..0f1a8009e 100644 --- a/src/geode/geometry/sign.cpp +++ b/src/geode/geometry/sign.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include namespace { diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index 1bad0cf52..6073c472f 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -31,8 +31,8 @@ #include #include -#include #include +#include #include #include From 8eb88c47cef6539672bee28170b471fd9afc6399 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 13:45:05 +0200 Subject: [PATCH 39/52] refactor(Geometry): move exact predicates to detail The exported GEO::PCK predicates embed Geogram definitions: they are advanced API, not public API. Co-Authored-By: Claude Opus 5.5 --- include/geode/geometry/{ => detail}/predicates.hpp | 0 include/geode/geometry/internal/intersection_from_sides.hpp | 2 +- include/geode/geometry/internal/position_from_sides.hpp | 2 +- src/geode/geometry/CMakeLists.txt | 4 ++-- src/geode/geometry/common.cpp | 2 +- src/geode/geometry/{ => detail}/predicates.cpp | 2 +- src/geode/geometry/intersection_detection.cpp | 2 +- src/geode/geometry/position.cpp | 2 +- src/geode/geometry/radial_sort.cpp | 2 +- src/geode/geometry/sign.cpp | 2 +- tests/geometry/test-predicates.cpp | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) rename include/geode/geometry/{ => detail}/predicates.hpp (100%) rename src/geode/geometry/{ => detail}/predicates.cpp (99%) diff --git a/include/geode/geometry/predicates.hpp b/include/geode/geometry/detail/predicates.hpp similarity index 100% rename from include/geode/geometry/predicates.hpp rename to include/geode/geometry/detail/predicates.hpp diff --git a/include/geode/geometry/internal/intersection_from_sides.hpp b/include/geode/geometry/internal/intersection_from_sides.hpp index fb73ba05f..680fc0969 100644 --- a/include/geode/geometry/internal/intersection_from_sides.hpp +++ b/include/geode/geometry/internal/intersection_from_sides.hpp @@ -23,7 +23,7 @@ #pragma once -#include +#include namespace geode { diff --git a/include/geode/geometry/internal/position_from_sides.hpp b/include/geode/geometry/internal/position_from_sides.hpp index cf9c30089..e84a69d75 100644 --- a/include/geode/geometry/internal/position_from_sides.hpp +++ b/include/geode/geometry/internal/position_from_sides.hpp @@ -23,7 +23,7 @@ #pragma once -#include +#include namespace geode { diff --git a/src/geode/geometry/CMakeLists.txt b/src/geode/geometry/CMakeLists.txt index 3b853798d..0b7ae6f5a 100644 --- a/src/geode/geometry/CMakeLists.txt +++ b/src/geode/geometry/CMakeLists.txt @@ -54,7 +54,7 @@ add_geode_library( "perpendicular.cpp" "points_sort.cpp" "position.cpp" - "predicates.cpp" + "detail/predicates.cpp" "point.cpp" "projection.cpp" "quality.cpp" @@ -95,7 +95,6 @@ add_geode_library( "point.hpp" "points_sort.hpp" "position.hpp" - "predicates.hpp" "projection.hpp" "quality.hpp" "radial_sort.hpp" @@ -106,6 +105,7 @@ add_geode_library( ADVANCED_HEADERS "detail/aabb_impl.hpp" "detail/bitsery_archive.hpp" + "detail/predicates.hpp" INTERNAL_HEADERS "internal/intersection_from_sides.hpp" "internal/position_from_sides.hpp" diff --git a/src/geode/geometry/common.cpp b/src/geode/geometry/common.cpp index 5413de5e7..4913c8252 100644 --- a/src/geode/geometry/common.cpp +++ b/src/geode/geometry/common.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include namespace geode { diff --git a/src/geode/geometry/predicates.cpp b/src/geode/geometry/detail/predicates.cpp similarity index 99% rename from src/geode/geometry/predicates.cpp rename to src/geode/geometry/detail/predicates.cpp index 89ab6e1db..3d3ea7519 100644 --- a/src/geode/geometry/predicates.cpp +++ b/src/geode/geometry/detail/predicates.cpp @@ -21,7 +21,7 @@ * */ -#include +#include #include #include diff --git a/src/geode/geometry/intersection_detection.cpp b/src/geode/geometry/intersection_detection.cpp index e1003d870..68777a2d9 100644 --- a/src/geode/geometry/intersection_detection.cpp +++ b/src/geode/geometry/intersection_detection.cpp @@ -28,11 +28,11 @@ #include #include #include +#include #include #include #include #include -#include #include namespace diff --git a/src/geode/geometry/position.cpp b/src/geode/geometry/position.cpp index 9044d9629..37848770b 100644 --- a/src/geode/geometry/position.cpp +++ b/src/geode/geometry/position.cpp @@ -31,11 +31,11 @@ #include #include #include +#include #include #include #include #include -#include namespace { diff --git a/src/geode/geometry/radial_sort.cpp b/src/geode/geometry/radial_sort.cpp index 7cf578c64..c8c99b62f 100644 --- a/src/geode/geometry/radial_sort.cpp +++ b/src/geode/geometry/radial_sort.cpp @@ -28,9 +28,9 @@ #include #include #include +#include #include #include -#include #include namespace diff --git a/src/geode/geometry/sign.cpp b/src/geode/geometry/sign.cpp index 0f1a8009e..3ac958f18 100644 --- a/src/geode/geometry/sign.cpp +++ b/src/geode/geometry/sign.cpp @@ -26,8 +26,8 @@ #include #include #include +#include #include -#include namespace { diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp index 6073c472f..3f2e4c3ea 100644 --- a/tests/geometry/test-predicates.cpp +++ b/tests/geometry/test-predicates.cpp @@ -31,8 +31,8 @@ #include #include +#include #include -#include #include #include From 047c437e45700f28cc9bca583314990ad1d55edd Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:02:52 +0200 Subject: [PATCH 40/52] refactor(Distance): iterate edge vertex pairs with geode::Range Co-Authored-By: Claude Opus 5.5 --- src/geode/geometry/distance.cpp | 3 ++- src/geode/geometry/is_point_inside.cpp | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index 1eb712d75..bdfadecbe 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -416,7 +416,8 @@ namespace for( const auto i : geode::Indices{ non_colocated_points } ) { const auto vertex0 = non_colocated_points[i]; - for( auto j = i + 1; j < non_colocated_points.size(); j++ ) + for( const auto j : + geode::Range{ i + 1, non_colocated_points.size() } ) { const auto vertex1 = non_colocated_points[j]; if( geode::point_point_distance( base_vertices[vertex0].get(), diff --git a/src/geode/geometry/is_point_inside.cpp b/src/geode/geometry/is_point_inside.cpp index d1b96e662..3d3bd924e 100644 --- a/src/geode/geometry/is_point_inside.cpp +++ b/src/geode/geometry/is_point_inside.cpp @@ -29,14 +29,26 @@ namespace { + // Returns true if the point lies exactly on the edge, given the exact + // orientation of the point relative to the edge line. bool is_point_on_edge( const geode::Point2D& point, - const geode::Point2D& v0, - const geode::Point2D& v1 ) + const geode::Point2D& edge_vertex0, + const geode::Point2D& edge_vertex1, + GEO::SIGN point_side ) { + if( point_side != GEO::SIGN::zero ) + { + return false; + } + // The point is on the edge line: it is on the edge if and only if it + // is inside the edge bounding box for( const auto d : geode::LRange{ 2 } ) { - if( point.value( d ) < std::min( v0.value( d ), v1.value( d ) ) - || point.value( d ) > std::max( v0.value( d ), v1.value( d ) ) ) + const auto value = point.value( d ); + const auto value0 = edge_vertex0.value( d ); + const auto value1 = edge_vertex1.value( d ); + if( value < std::min( value0, value1 ) + || value > std::max( value0, value1 ) ) { return false; } @@ -71,7 +83,7 @@ namespace geode continue; } const auto side = GEO::PCK::orient_2d( v0, v1, point ); - if( side == GEO::SIGN::zero && is_point_on_edge( point, v0, v1 ) ) + if( is_point_on_edge( point, v0, v1, side ) ) { return true; } From 6586706ef3d73f996f3211ea34a92c6dd1900826 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:02:52 +0200 Subject: [PATCH 41/52] refactor(IsPointInside): check collinearity inside is_point_on_edge is_point_on_edge only checked the edge bounding box and relied on its caller to have checked that the point is on the edge line. It now takes the exact orientation and checks both, so the function does what its name says. Tests cover points inside the bounding box of a diagonal edge but off it, and name the one-ulp moves in the exact predicate tests. Co-Authored-By: Claude Opus 5.5 --- tests/geometry/test-position.cpp | 50 +++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/tests/geometry/test-position.cpp b/tests/geometry/test-position.cpp index 8e7a07dcb..373e2ccbd 100644 --- a/tests/geometry/test-position.cpp +++ b/tests/geometry/test-position.cpp @@ -21,6 +21,8 @@ * */ +#include +#include #include #include @@ -536,6 +538,18 @@ void test_point_side_to_triangle() "q3" ); } +// Smallest double strictly greater than value, i.e. value + 1 ulp +double next_double_above( double value ) +{ + return std::nextafter( value, std::numeric_limits< double >::infinity() ); +} + +// Largest double strictly lower than value, i.e. value - 1 ulp +double next_double_below( double value ) +{ + return std::nextafter( value, -std::numeric_limits< double >::infinity() ); +} + void test_point_inside_polygon() { const geode::Point2D a{ { 0, 0 } }; @@ -568,6 +582,28 @@ void test_point_inside_polygon() && !geode::is_point_inside_polygon( point, cw ), "Point ", point.string(), " should be outside the polygon" ); } + + // Points inside the bounding box of the diagonal edge but not on it + const geode::Polygon2D triangle{ { a, b, c } }; + const std::array< geode::Point2D, 2 > off_diagonal_points{ + geode::Point2D{ { 0.2, 0.8 } }, + geode::Point2D{ { 0.5, next_double_above( 0.5 ) } } + }; + for( const auto& point : off_diagonal_points ) + { + geode::OpenGeodeGeometryException::test( + !geode::is_point_inside_polygon( point, triangle ), "Point ", + point.string(), " should be outside the triangle" ); + } + const std::array< geode::Point2D, 2 > on_diagonal_points{ + geode::Point2D{ { 0.2, 0.2 } }, geode::Point2D{ { 0.5, 0.5 } } + }; + for( const auto& point : on_diagonal_points ) + { + geode::OpenGeodeGeometryException::test( + geode::is_point_inside_polygon( point, triangle ), "Point ", + point.string(), " should be on the triangle boundary" ); + } } void test_exact_predicates_near_degenerate() @@ -598,9 +634,9 @@ void test_exact_predicates_near_degenerate() == geode::SIDE::zero, "Wrong side for collinear point" ); const auto side_up = geode::point_side_to_segment( - geode::Point2D{ { x2, std::nextafter( x2, 1e300 ) } }, segment ); + geode::Point2D{ { x2, next_double_above( x2 ) } }, segment ); const auto side_down = geode::point_side_to_segment( - geode::Point2D{ { x2, std::nextafter( x2, -1e300 ) } }, segment ); + geode::Point2D{ { x2, next_double_below( x2 ) } }, segment ); geode::OpenGeodeGeometryException::test( side_up != geode::SIDE::zero && side_down != geode::SIDE::zero && side_up != side_down, @@ -621,11 +657,9 @@ void test_exact_predicates_near_degenerate() == geode::SIDE::zero, "Wrong side for coplanar point" ); const auto side_above = geode::point_side_to_triangle( - geode::Point3D{ { x3, std::nextafter( x3, 1e300 ), z3 } }, - triangle ); + geode::Point3D{ { x3, next_double_above( x3 ), z3 } }, triangle ); const auto side_below = geode::point_side_to_triangle( - geode::Point3D{ { x3, std::nextafter( x3, -1e300 ), z3 } }, - triangle ); + geode::Point3D{ { x3, next_double_below( x3 ), z3 } }, triangle ); geode::OpenGeodeGeometryException::test( side_above != geode::SIDE::zero && side_below != geode::SIDE::zero && side_above != side_below, @@ -649,10 +683,10 @@ void test_exact_orientation_large_coordinates() const geode::Point2D p1{ { x1, 3 * x1 } }; const auto y2 = 3 * x2; const auto side_up = geode::point_side_to_segment( - geode::Point2D{ { x2, std::nextafter( y2, 1e300 ) } }, + geode::Point2D{ { x2, next_double_above( y2 ) } }, geode::Segment2D{ p0, p1 } ); const auto side_down = geode::point_side_to_segment( - geode::Point2D{ { x2, std::nextafter( y2, -1e300 ) } }, + geode::Point2D{ { x2, next_double_below( y2 ) } }, geode::Segment2D{ p0, p1 } ); geode::OpenGeodeGeometryException::test( x0 == x1 From 712b5a9c0309c35c4813c244ed309b07f602a999 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:02:52 +0200 Subject: [PATCH 42/52] test(Geometry): check is_degenerated against vertex order Triangles (cap, obtuse, needle) and tetrahedra (cap, wedge, sliver) of thickness 0.5, 0.9, 1.1 and 2 GLOBAL_EPSILON, in a rotated frame offset by up to 1e6, give the expected result for every vertex permutation. Co-Authored-By: Claude Opus 5.5 --- tests/geometry/test-triangle.cpp | 99 +++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index faa14495f..0b137f375 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -21,6 +21,8 @@ * */ +#include +#include #include #include @@ -39,7 +41,7 @@ void test_degenerated_consistency() { const geode::Point3D p0{ { 0.0, 0.0, 0.0 } }; const geode::Point3D p1{ { 1.0, 0.0, 0.0 } }; - const geode::Point3D p2{ { 3.0, 2.5e-6, 0.0 } }; + const geode::Point3D p2{ { 3.0, 2.5 * geode::GLOBAL_EPSILON, 0.0 } }; const geode::Point3D p3{ { 0.0, 0.0, 1.0 } }; const geode::Triangle3D triangle{ p0, p1, p2 }; geode::OpenGeodeGeometryException::test( triangle.is_degenerated(), @@ -55,7 +57,7 @@ void test_degenerated_consistency() const geode::Triangle3D triangle2{ p0, p1, q2 }; geode::OpenGeodeGeometryException::test( !triangle2.is_degenerated(), "Flat but valid triangle should not be degenerated" ); - const geode::Point3D q3{ { 1.0, 1.0, 5e-7 } }; + const geode::Point3D q3{ { 1.0, 1.0, 0.5 * geode::GLOBAL_EPSILON } }; const geode::Point3D q4{ { 0.0, 1.0, 0.0 } }; const geode::Tetrahedron flat_tetra{ p0, p1, q3, q4 }; geode::OpenGeodeGeometryException::test( @@ -106,6 +108,97 @@ void test_degenerated_consistency() } } +// Maps local coordinates into a frame which is neither aligned with the axes +// nor at the origin +geode::Point3D to_global( const std::array< double, 3 >& local, double offset ) +{ + const geode::Vector3D u{ { 2. / 3., 1. / 3., 2. / 3. } }; + const geode::Vector3D v{ { 1. / 3., 2. / 3., -2. / 3. } }; + const geode::Vector3D w{ { 2. / 3., -2. / 3., -1. / 3. } }; + const geode::Point3D origin{ { offset, -offset, offset } }; + return origin + u * local[0] + v * local[1] + w * local[2]; +} + +void test_degenerated_triangle_vertex_order() +{ + // Each triangle has a minimum height of factor * GLOBAL_EPSILON + for( const auto factor : { 0.5, 0.9, 1.1, 2. } ) + { + const auto height = factor * geode::GLOBAL_EPSILON; + const std::array< std::array< std::array< double, 3 >, 3 >, 3 > + configurations{ { + // Cap: the apex projects inside the longest edge + { { { 0, 0, 0 }, { 1, 0, 0 }, { 0.5, height, 0 } } }, + // Obtuse: the apex projects outside the opposite edge + { { { 0, 0, 0 }, { 1, 0, 0 }, { 1.5, 1.5 * height, 0 } } }, + // Needle: one edge is shorter than GLOBAL_EPSILON + { { { 0, 0, 0 }, { 1, 0, 0 }, { 1, height, 0 } } }, + } }; + for( const auto offset : { 0., 1e3, 1e6 } ) + { + for( const auto& configuration : configurations ) + { + std::array< geode::local_index_t, 3 > order{ 0, 1, 2 }; + do + { + const geode::Triangle3D triangle{ + to_global( configuration[order[0]], offset ), + to_global( configuration[order[1]], offset ), + to_global( configuration[order[2]], offset ) + }; + geode::OpenGeodeGeometryException::test( + triangle.is_degenerated() == ( factor < 1 ), + "Wrong degeneracy for triangle ", triangle.string(), + " of minimum height ", factor, " * GLOBAL_EPSILON" ); + } while( std::next_permutation( order.begin(), order.end() ) ); + } + } + } +} + +void test_degenerated_tetrahedron_vertex_order() +{ + // Each tetrahedron lies in a slab of minimum thickness + // factor * GLOBAL_EPSILON + for( const auto factor : { 0.5, 0.9, 1.1, 2. } ) + { + const auto width = factor * geode::GLOBAL_EPSILON; + const std::array< std::array< std::array< double, 3 >, 4 >, 3 > + configurations{ { + // Cap: the apex projects inside the opposite facet + { { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, + { 0.3, 0.3, width } } }, + // Wedge: the apex projects outside the opposite facet, the + // thinnest slab is between edges 03 and 12 + { { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, + { 1, 1, 2 * width } } }, + // Sliver: thinnest between two opposite edges + { { { 0, 0, 0 }, { 1, 1, 0 }, { 1, 0, width }, + { 0, 1, width } } }, + } }; + for( const auto offset : { 0., 1e3, 1e6 } ) + { + for( const auto& configuration : configurations ) + { + std::array< geode::local_index_t, 4 > order{ 0, 1, 2, 3 }; + do + { + const geode::Tetrahedron tetra{ + to_global( configuration[order[0]], offset ), + to_global( configuration[order[1]], offset ), + to_global( configuration[order[2]], offset ), + to_global( configuration[order[3]], offset ) + }; + geode::OpenGeodeGeometryException::test( + tetra.is_degenerated() == ( factor < 1 ), + "Wrong degeneracy for tetrahedron ", tetra.string(), + " of thickness ", factor, " * GLOBAL_EPSILON" ); + } while( std::next_permutation( order.begin(), order.end() ) ); + } + } + } +} + void test() { const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; @@ -135,6 +228,8 @@ void test() geode::OpenGeodeGeometryException::test( !pivot2, "Pivot of triangle2 should not be calculated" ); test_degenerated_consistency(); + test_degenerated_triangle_vertex_order(); + test_degenerated_tetrahedron_vertex_order(); } OPENGEODE_TEST( "distance" ) \ No newline at end of file From fe632c89b4b16a07597ee1e50abf85e5cbe1c3ce Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:02:52 +0200 Subject: [PATCH 43/52] test(Geometry): express tolerances with GLOBAL_EPSILON Co-Authored-By: Claude Opus 5.5 --- tests/geometry/test-bounding-box.cpp | 3 ++- tests/geometry/test-distance.cpp | 18 ++++++++++-------- tests/geometry/test-quality.cpp | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/geometry/test-bounding-box.cpp b/tests/geometry/test-bounding-box.cpp index 1de052243..c5938fdda 100644 --- a/tests/geometry/test-bounding-box.cpp +++ b/tests/geometry/test-bounding-box.cpp @@ -361,7 +361,8 @@ void test_large_triangle3d_epsilon_intersections() box.add_point( geode::Point3D{ { 1, 1, 1 } } ); const auto size = 2e4; for( const auto& [offset, expected] : - { std::make_pair( 0.5e-6, true ), std::make_pair( 1e-3, false ) } ) + { std::make_pair( 0.5 * geode::GLOBAL_EPSILON, true ), + std::make_pair( 1e-3, false ) } ) { const geode::Point3D a{ { -size, -size, 1 + offset } }; const geode::Point3D b{ { size, -size, 1 + offset } }; diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index 838055e1c..49234ca9a 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1310,7 +1310,8 @@ void test_degenerated_segment_triangle_distance() const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; const geode::Point3D c{ { 0.0, 1.0, 0.0 } }; const geode::Triangle3D triangle{ a, b, c }; - for( const auto length : { 4e-7, 1e-7 } ) + for( const auto length : + { 0.4 * geode::GLOBAL_EPSILON, 0.1 * geode::GLOBAL_EPSILON } ) { const geode::Point3D p0{ { 0.2, 0.2, 1.0 } }; const geode::Point3D p1{ { 0.2, 0.2 + length, 1.0 } }; @@ -1391,13 +1392,13 @@ void test_random_triangle_triangle_distance() const auto [distance, point0, point1] = geode::triangle_triangle_distance( triangle0, triangle1 ); geode::OpenGeodeGeometryException::test( - std::fabs( distance - reference ) <= 1e-12, + std::fabs( distance - reference ) <= geode::GLOBAL_EPSILON, "Wrong triangle_triangle_distance on random triangles: ", distance, " vs ", reference ); geode::OpenGeodeGeometryException::test( std::fabs( geode::point_point_distance( point0, point1 ) - distance ) - <= 1e-12, + <= geode::GLOBAL_EPSILON, "Wrong triangle_triangle_distance closest points" ); } } @@ -1511,10 +1512,10 @@ void test_point_segment_line_distance_accuracy() geode::OpenGeodeGeometryException::test( std::fabs( geode::point_point_distance( far_point, line_projection ) - line_distance ) - <= 1e-12, + <= geode::GLOBAL_EPSILON, "Inconsistent point_line_distance and point_line_projection" ); geode::OpenGeodeGeometryException::test( - std::fabs( line_distance - line_reference ) < 1e-9, + std::fabs( line_distance - line_reference ) < geode::GLOBAL_EPSILON, "Inaccurate point_line_distance: ", line_distance, " vs ", static_cast< double >( line_reference ) ); @@ -1529,17 +1530,18 @@ void test_point_segment_line_distance_accuracy() geode::OpenGeodeGeometryException::test( std::fabs( geode::point_point_distance( point, segment_projection ) - segment_distance ) - <= 1e-12, + <= geode::GLOBAL_EPSILON, "Inconsistent point_segment_distance and " "point_segment_projection" ); geode::OpenGeodeGeometryException::test( - std::fabs( segment_distance - segment_reference ) < 1e-9, + std::fabs( segment_distance - segment_reference ) + < geode::GLOBAL_EPSILON, "Inaccurate point_segment_distance: ", segment_distance, " vs ", static_cast< double >( segment_reference ) ); } const geode::Point3D a{ { 1, 1, 1 } }; - const geode::Point3D b{ { 1, 1, 1 + 5e-7 } }; + const geode::Point3D b{ { 1, 1, 1 + 0.5 * geode::GLOBAL_EPSILON } }; const geode::Segment3D degenerated{ a, b }; const geode::Point3D q{ { 2, 1, 1 } }; geode::OpenGeodeGeometryException::test( diff --git a/tests/geometry/test-quality.cpp b/tests/geometry/test-quality.cpp index e6c50cf69..6fe3a85fe 100644 --- a/tests/geometry/test-quality.cpp +++ b/tests/geometry/test-quality.cpp @@ -158,8 +158,8 @@ void test_scale_invariant_quality() for( const auto q : geode::LRange{ 4 } ) { geode::OpenGeodeGeometryException::test( - std::fabs( reference[q] - 1 ) < 1e-9 - && std::fabs( small[q] - reference[q] ) < 1e-9, + std::fabs( reference[q] - 1 ) < geode::GLOBAL_EPSILON + && std::fabs( small[q] - reference[q] ) < geode::GLOBAL_EPSILON, "Wrong quality ", q, " for small regular tetrahedron: ", small[q], " vs ", reference[q] ); } From 9720b70cfa7846ece873780af99c0e3e8eed4058 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:02:52 +0200 Subject: [PATCH 44/52] test(Intersection): split degenerated segment tests by object One function per intersected object, with lengths and offsets expressed with GLOBAL_EPSILON. The degenerated segment is closer than GLOBAL_EPSILON to the cylinder, so its correctness point is compared exactly to the closest point on the cylinder. Co-Authored-By: Claude Opus 5.5 --- tests/geometry/test-intersection.cpp | 252 +++++++++++++++------------ 1 file changed, 144 insertions(+), 108 deletions(-) diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index 279588a4d..bbe381311 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -995,115 +995,149 @@ void test_segment_ellipse_intersection() test_segment_ellipse_intersection_3D(); } +void test_degenerated_segment_sphere_intersection( double length ) +{ + const geode::Point3D origin{ { 0, 0, 0 } }; + const geode::Sphere3D sphere{ origin, 1 }; + const geode::Point3D on_sphere{ { 1, 0, 0 } }; + const geode::Point3D on_sphere2{ { 1 + length, 0, 0 } }; + const auto sphere_result = geode::segment_sphere_intersection( + geode::Segment3D{ on_sphere, on_sphere2 }, sphere ); + geode::OpenGeodeGeometryException::test( + sphere_result && sphere_result.result->size() == 1, + "Wrong degenerated segment_sphere_intersection" ); + const geode::Point3D far_sphere{ { 5, 0, 0 } }; + const geode::Point3D far_sphere2{ { 5 + length, 0, 0 } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_sphere_intersection( + geode::Segment3D{ far_sphere, far_sphere2 }, sphere ), + "Wrong degenerated segment_sphere_intersection (far)" ); +} + +void test_degenerated_segment_plane_intersection( double length ) +{ + const geode::Point3D origin{ { 0, 0, 0 } }; + const geode::Plane plane{ geode::Vector3D{ { 0, 0, 1 } }, origin }; + const geode::Point3D on_plane{ { 0.3, 0.2, 0 } }; + const geode::Point3D on_plane2{ { 0.3, 0.2, length } }; + const auto plane_result = geode::segment_plane_intersection( + geode::Segment3D{ on_plane, on_plane2 }, plane ); + geode::OpenGeodeGeometryException::test( + plane_result + && plane_result.result->inexact_equal( + geode::Point3D{ { 0.3, 0.2, 0 } } ), + "Wrong degenerated segment_plane_intersection" ); + const geode::Point3D off_plane{ { 0.3, 0.2, 1 } }; + const geode::Point3D off_plane2{ { 0.3, 0.2, 1 + length } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_plane_intersection( + geode::Segment3D{ off_plane, off_plane2 }, plane ), + "Wrong degenerated segment_plane_intersection (far)" ); +} + +void test_degenerated_segment_cylinder_intersection( double length ) +{ + const geode::Point3D origin{ { 0, 0, 0 } }; + const geode::Point3D x_axis{ { 1, 0, 0 } }; + const geode::Cylinder cylinder{ geode::Segment3D{ origin, x_axis }, 0.2 }; + const geode::Point3D on_cylinder{ { 0.5, 0.2, 0 } }; + const geode::Point3D on_cylinder2{ { 0.5, 0.2 + length, 0 } }; + const auto cylinder_result = geode::segment_cylinder_intersection( + geode::Segment3D{ on_cylinder, on_cylinder2 }, cylinder ); + geode::OpenGeodeGeometryException::test( + cylinder_result && cylinder_result.result->size() == 1, + "Wrong degenerated segment_cylinder_intersection" ); + // The segment itself is closer than GLOBAL_EPSILON to the cylinder: only + // an exact comparison checks that the reported point is on the cylinder + const auto closest_point = std::get< 1 >( geode::point_cylinder_distance( + geode::Segment3D{ on_cylinder, on_cylinder2 }.barycenter(), + cylinder ) ); + geode::OpenGeodeGeometryException::test( + cylinder_result.correctness->second.second.front() == closest_point, + "Wrong degenerated segment_cylinder_intersection correctness: " + "point should be the closest point on the cylinder" ); + const geode::Point3D in_cylinder{ { 0.5, 0, 0 } }; + const geode::Point3D in_cylinder2{ { 0.5, length, 0 } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_cylinder_intersection( + geode::Segment3D{ in_cylinder, in_cylinder2 }, cylinder ), + "Wrong degenerated segment_cylinder_intersection (inside)" ); +} + +void test_degenerated_segment_ellipse_intersection( double length ) +{ + const geode::Frame2D frame{ { geode::Vector2D{ { 3.0, 0.0 } }, + geode::Vector2D{ { 0.0, 2.0 } } } }; + const geode::Point2D origin2d{ { 0, 0 } }; + const geode::Ellipse2D ellipse{ origin2d, frame }; + const geode::Point2D on_ellipse{ { 3, 0 } }; + const geode::Point2D on_ellipse2{ { 3, length } }; + const auto ellipse_result = geode::segment_ellipse_intersection( + geode::Segment2D{ on_ellipse, on_ellipse2 }, ellipse ); + geode::OpenGeodeGeometryException::test( + ellipse_result && ellipse_result.result->size() == 1, + "Wrong degenerated segment_ellipse_intersection" ); + const geode::Point2D in_ellipse{ { 1, 0 } }; + const geode::Point2D in_ellipse2{ { 1, length } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_ellipse_intersection( + geode::Segment2D{ in_ellipse, in_ellipse2 }, ellipse ), + "Wrong degenerated segment_ellipse_intersection (inside)" ); +} + +void test_degenerated_segment_segment_intersection( double length ) +{ + const geode::Point2D origin2d{ { 0, 0 } }; + const geode::Point2D x_axis2d{ { 1, 0 } }; + const geode::Segment2D segment{ origin2d, x_axis2d }; + const geode::Point2D on_segment{ { 0.5, 0 } }; + const geode::Point2D on_segment2{ { 0.5, length } }; + const geode::Segment2D small_on{ on_segment, on_segment2 }; + const geode::Point2D off_segment{ { 0.5, 1 } }; + const geode::Point2D off_segment2{ { 0.5, 1 + length } }; + const geode::Segment2D small_off{ off_segment, off_segment2 }; + const auto segment_result0 = + geode::segment_segment_intersection( small_on, segment ); + const auto segment_result1 = + geode::segment_segment_intersection( segment, small_on ); + geode::OpenGeodeGeometryException::test( + segment_result0 && segment_result1 + && segment_result0.result->inexact_equal( + segment_result1.result.value() ), + "Wrong degenerated segment_segment_intersection" ); + geode::OpenGeodeGeometryException::test( + !geode::segment_segment_intersection( small_off, segment ) + && !geode::segment_segment_intersection( segment, small_off ), + "Wrong degenerated segment_segment_intersection (far)" ); +} + +void test_degenerated_segment_line_intersection( double length ) +{ + const geode::InfiniteLine2D line{ geode::Vector2D{ { 1, 0 } }, + geode::Point2D{ { 0, 0 } } }; + const geode::Segment2D small_on{ geode::Point2D{ { 0.5, 0 } }, + geode::Point2D{ { 0.5, length } } }; + geode::OpenGeodeGeometryException::test( + geode::segment_line_intersection( small_on, line ), + "Wrong degenerated segment_line_intersection" ); + const geode::Segment2D small_off{ geode::Point2D{ { 0.5, 1 } }, + geode::Point2D{ { 0.5, 1 + length } } }; + geode::OpenGeodeGeometryException::test( + !geode::segment_line_intersection( small_off, line ), + "Wrong degenerated segment_line_intersection (far)" ); +} + void test_degenerated_segment_intersections() { - for( const auto length : { 4e-7, 1e-7 } ) + for( const auto length : + { 0.4 * geode::GLOBAL_EPSILON, 0.1 * geode::GLOBAL_EPSILON } ) { - const geode::Point3D origin{ { 0, 0, 0 } }; - const geode::Point3D x_axis{ { 1, 0, 0 } }; - const geode::Sphere3D sphere{ origin, 1 }; - const geode::Point3D on_sphere{ { 1, 0, 0 } }; - const geode::Point3D on_sphere2{ { 1 + length, 0, 0 } }; - const auto sphere_result = geode::segment_sphere_intersection( - geode::Segment3D{ on_sphere, on_sphere2 }, sphere ); - geode::OpenGeodeGeometryException::test( - sphere_result && sphere_result.result->size() == 1, - "Wrong degenerated segment_sphere_intersection" ); - const geode::Point3D far_sphere{ { 5, 0, 0 } }; - const geode::Point3D far_sphere2{ { 5 + length, 0, 0 } }; - geode::OpenGeodeGeometryException::test( - !geode::segment_sphere_intersection( - geode::Segment3D{ far_sphere, far_sphere2 }, sphere ), - "Wrong degenerated segment_sphere_intersection (far)" ); - - const geode::Plane plane{ geode::Vector3D{ { 0, 0, 1 } }, origin }; - const geode::Point3D on_plane{ { 0.3, 0.2, 0 } }; - const geode::Point3D on_plane2{ { 0.3, 0.2, length } }; - const auto plane_result = geode::segment_plane_intersection( - geode::Segment3D{ on_plane, on_plane2 }, plane ); - geode::OpenGeodeGeometryException::test( - plane_result - && plane_result.result->inexact_equal( - geode::Point3D{ { 0.3, 0.2, 0 } } ), - "Wrong degenerated segment_plane_intersection" ); - const geode::Point3D off_plane{ { 0.3, 0.2, 1 } }; - const geode::Point3D off_plane2{ { 0.3, 0.2, 1 + length } }; - geode::OpenGeodeGeometryException::test( - !geode::segment_plane_intersection( - geode::Segment3D{ off_plane, off_plane2 }, plane ), - "Wrong degenerated segment_plane_intersection (far)" ); - - const geode::Cylinder cylinder{ geode::Segment3D{ origin, x_axis }, - 0.2 }; - const geode::Point3D on_cylinder{ { 0.5, 0.2, 0 } }; - const geode::Point3D on_cylinder2{ { 0.5, 0.2 + length, 0 } }; - const auto cylinder_result = geode::segment_cylinder_intersection( - geode::Segment3D{ on_cylinder, on_cylinder2 }, cylinder ); - geode::OpenGeodeGeometryException::test( - cylinder_result && cylinder_result.result->size() == 1, - "Wrong degenerated segment_cylinder_intersection" ); - const auto& cylinder_projection = - cylinder_result.correctness->second.second.front(); - geode::OpenGeodeGeometryException::test( - std::get< 0 >( geode::point_cylinder_distance( - cylinder_projection, cylinder ) ) - <= 1e-15, - "Wrong degenerated segment_cylinder_intersection correctness: " - "point should be on the cylinder" ); - const geode::Point3D in_cylinder{ { 0.5, 0, 0 } }; - const geode::Point3D in_cylinder2{ { 0.5, length, 0 } }; - geode::OpenGeodeGeometryException::test( - !geode::segment_cylinder_intersection( - geode::Segment3D{ in_cylinder, in_cylinder2 }, cylinder ), - "Wrong degenerated segment_cylinder_intersection (inside)" ); - - const geode::Frame2D frame{ { geode::Vector2D{ { 3.0, 0.0 } }, - geode::Vector2D{ { 0.0, 2.0 } } } }; - const geode::Point2D origin2d{ { 0, 0 } }; - const geode::Point2D x_axis2d{ { 1, 0 } }; - const geode::Ellipse2D ellipse{ origin2d, frame }; - const geode::Point2D on_ellipse{ { 3, 0 } }; - const geode::Point2D on_ellipse2{ { 3, length } }; - const auto ellipse_result = geode::segment_ellipse_intersection( - geode::Segment2D{ on_ellipse, on_ellipse2 }, ellipse ); - geode::OpenGeodeGeometryException::test( - ellipse_result && ellipse_result.result->size() == 1, - "Wrong degenerated segment_ellipse_intersection" ); - const geode::Point2D in_ellipse{ { 1, 0 } }; - const geode::Point2D in_ellipse2{ { 1, length } }; - geode::OpenGeodeGeometryException::test( - !geode::segment_ellipse_intersection( - geode::Segment2D{ in_ellipse, in_ellipse2 }, ellipse ), - "Wrong degenerated segment_ellipse_intersection (inside)" ); - - const geode::Segment2D segment{ origin2d, x_axis2d }; - const geode::Point2D on_segment{ { 0.5, 0 } }; - const geode::Point2D on_segment2{ { 0.5, length } }; - const geode::Segment2D small_on{ on_segment, on_segment2 }; - const auto segment_result0 = - geode::segment_segment_intersection( small_on, segment ); - const auto segment_result1 = - geode::segment_segment_intersection( segment, small_on ); - geode::OpenGeodeGeometryException::test( - segment_result0 && segment_result1 - && segment_result0.result->inexact_equal( - segment_result1.result.value() ), - "Wrong degenerated segment_segment_intersection" ); - const geode::Point2D off_segment{ { 0.5, 1 } }; - const geode::Point2D off_segment2{ { 0.5, 1 + length } }; - const geode::Segment2D small_off{ off_segment, off_segment2 }; - geode::OpenGeodeGeometryException::test( - !geode::segment_segment_intersection( small_off, segment ) - && !geode::segment_segment_intersection( segment, small_off ), - "Wrong degenerated segment_segment_intersection (far)" ); - - const geode::InfiniteLine2D line{ segment }; - geode::OpenGeodeGeometryException::test( - geode::segment_line_intersection( small_on, line ), - "Wrong degenerated segment_line_intersection" ); - geode::OpenGeodeGeometryException::test( - !geode::segment_line_intersection( small_off, line ), - "Wrong degenerated segment_line_intersection (far)" ); + test_degenerated_segment_sphere_intersection( length ); + test_degenerated_segment_plane_intersection( length ); + test_degenerated_segment_cylinder_intersection( length ); + test_degenerated_segment_ellipse_intersection( length ); + test_degenerated_segment_segment_intersection( length ); + test_degenerated_segment_line_intersection( length ); } } @@ -1118,7 +1152,8 @@ void test_line_cylinder_perpendicular() == result.result->size(); }; - for( const auto offset : { 0., 1e-7, -1e-7 } ) + for( const auto offset : + { 0., 0.1 * geode::GLOBAL_EPSILON, -0.1 * geode::GLOBAL_EPSILON } ) { const geode::Point3D c{ { 0.5, 0.2 + offset, -1.0 } }; const geode::Point3D d{ { 0.5, 0.2 + offset, 1.0 } }; @@ -1174,7 +1209,8 @@ void test_line_cylinder_tolerance() const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; const geode::Point3D b{ { 1.0, 0.0, 0.0 } }; const geode::Cylinder cylinder{ geode::Segment3D{ a, b }, 0.2 }; - for( const auto offset : { 0., 1e-7, -1e-7 } ) + for( const auto offset : + { 0., 0.1 * geode::GLOBAL_EPSILON, -0.1 * geode::GLOBAL_EPSILON } ) { const geode::Point3D c{ { 0.0, 0.2 + offset, -0.5 } }; const geode::Point3D d{ { 1.0, 0.2 + offset, 0.5 } }; From d361d077142d00a898235347e2cc1fd1bc8d4df5 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 20:47:29 +0200 Subject: [PATCH 45/52] Replace internal predicates include with detail predicates --- src/geode/geometry/is_point_inside.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/geode/geometry/is_point_inside.cpp b/src/geode/geometry/is_point_inside.cpp index 3d3bd924e..7c88ede5e 100644 --- a/src/geode/geometry/is_point_inside.cpp +++ b/src/geode/geometry/is_point_inside.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include namespace @@ -101,4 +101,4 @@ namespace geode } return winding_number != 0; } -} // namespace geode \ No newline at end of file +} // namespace geode From b23aac1d25c461671d9a7d2c50bb5a17d13eafd0 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 21:02:12 +0200 Subject: [PATCH 46/52] Refactor double initialization in predicates.cpp Replaced static double initialization with constexpr for DOUBLE_EPSILON and updated expansion_splitter and expansion_epsilon to use these constants. --- src/geode/geometry/detail/predicates.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/geode/geometry/detail/predicates.cpp b/src/geode/geometry/detail/predicates.cpp index 104c3db0b..76be019ad 100644 --- a/src/geode/geometry/detail/predicates.cpp +++ b/src/geode/geometry/detail/predicates.cpp @@ -881,8 +881,9 @@ namespace GEO // Statically initialized with the IEEE-754 double values computed by // PCK::initialize(), so that exact predicates are correct even if the // library was not initialized. - double expansion_splitter_{ 134217729.0 }; // 2^27 + 1 - double expansion_epsilon_{ 1.1102230246251565e-16 }; // 2^-53 + constexpr double DOUBLE_EPSILON = 1.1102230246251565404e-16; // 2^-53 + double expansion_splitter_ = 134217729.0; // 2^27 + 1 + double expansion_epsilon_ = DOUBLE_EPSILON; // ============= expansion sum and difference ========================= From 082b75e356916061de4008353d0e8bb2d9f45a9a Mon Sep 17 00:00:00 2001 From: BotellaA <3213882+BotellaA@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:44:15 +0000 Subject: [PATCH 47/52] Apply prepare changes --- tests/geometry/test-distance.cpp | 134 ++++++++++++++++--------------- tests/geometry/test-triangle.cpp | 107 ++++++++++++------------ 2 files changed, 122 insertions(+), 119 deletions(-) diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index b6077a4c7..f2222f8cf 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1857,78 +1857,80 @@ void test_point_cylinder_distance() geode::point_cylinder_distance( axis_closest, cylinder ) ) <= geode::GLOBAL_EPSILON, "Wrong point_cylinder_distance: on the axis" ); -void test_point_skewed_tetrahedron_distance() -{ - const geode::Point3D a{ { 0, 0, 0 } }; - const geode::Point3D b{ { 4, 0.2, 0.1 } }; - const geode::Point3D c{ { 3.5, 1, 0.3 } }; - const geode::Point3D d{ { 0.5, 0.3, 0.8 } }; - const geode::Tetrahedron tetra{ a, b, c, d }; - const auto& vertices = tetra.vertices(); - std::mt19937 generator{ 42 }; - std::uniform_real_distribution< double > distribution{ -2, 6 }; - for( const auto unused : geode::Range{ 1000 } ) + void test_point_skewed_tetrahedron_distance() { - geode_unused( unused ); - const geode::Point3D query{ { distribution( generator ), - distribution( generator ), distribution( generator ) } }; - const auto [distance, closest_point] = - geode::point_tetrahedron_distance( query, tetra ); - if( distance == 0 ) + const geode::Point3D a{ { 0, 0, 0 } }; + const geode::Point3D b{ { 4, 0.2, 0.1 } }; + const geode::Point3D c{ { 3.5, 1, 0.3 } }; + const geode::Point3D d{ { 0.5, 0.3, 0.8 } }; + const geode::Tetrahedron tetra{ a, b, c, d }; + const auto& vertices = tetra.vertices(); + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -2, 6 }; + for( const auto unused : geode::Range{ 1000 } ) { + geode_unused( unused ); + const geode::Point3D query{ { distribution( generator ), + distribution( generator ), distribution( generator ) } }; + const auto [distance, closest_point] = + geode::point_tetrahedron_distance( query, tetra ); + if( distance == 0 ) + { + geode::OpenGeodeGeometryException::test( + geode::point_tetrahedron_position( query, tetra ) + != geode::POSITION::outside, + "Wrong null result for point_tetrahedron_distance with " + "query ", + query.string() ); + continue; + } + auto brute_force = std::numeric_limits< double >::max(); + for( const auto& facet : + geode::Tetrahedron::tetrahedron_facet_vertex ) + { + brute_force = std::min( brute_force, + std::get< 0 >( geode::point_triangle_distance( query, + geode::Triangle3D{ vertices[facet[0]], + vertices[facet[1]], vertices[facet[2]] } ) ) ); + } + geode::OpenGeodeGeometryException::test( + std::fabs( distance - brute_force ) < geode::GLOBAL_EPSILON, + "Wrong result for point_tetrahedron_distance with query ", + query.string(), ": ", distance, " | expected: ", brute_force ); geode::OpenGeodeGeometryException::test( - geode::point_tetrahedron_position( query, tetra ) - != geode::POSITION::outside, - "Wrong null result for point_tetrahedron_distance with " + std::fabs( geode::point_point_distance( query, closest_point ) + - distance ) + < geode::GLOBAL_EPSILON, + "Wrong closest point for point_tetrahedron_distance with " "query ", query.string() ); - continue; } - auto brute_force = std::numeric_limits< double >::max(); - for( const auto& facet : geode::Tetrahedron::tetrahedron_facet_vertex ) - { - brute_force = std::min( brute_force, - std::get< 0 >( geode::point_triangle_distance( - query, geode::Triangle3D{ vertices[facet[0]], - vertices[facet[1]], vertices[facet[2]] } ) ) ); - } - geode::OpenGeodeGeometryException::test( - std::fabs( distance - brute_force ) < geode::GLOBAL_EPSILON, - "Wrong result for point_tetrahedron_distance with query ", - query.string(), ": ", distance, " | expected: ", brute_force ); - geode::OpenGeodeGeometryException::test( - std::fabs( - geode::point_point_distance( query, closest_point ) - distance ) - < geode::GLOBAL_EPSILON, - "Wrong closest point for point_tetrahedron_distance with query ", - query.string() ); } -} -void test() -{ - test_point_segment_distance(); - test_segment_segment_distance(); - test_segment_line_distance(); - test_point_line_distance(); - test_point_triangle_distance(); - test_point_tetrahedron_distance(); - test_point_skewed_tetrahedron_distance(); - test_point_plane_distance(); - test_point_sphere_distance(); - test_point_circle_distance(); - test_point_circle_signed_distance_far_from_origin(); - test_line_triangle_distance(); - test_segment_triangle_distance(); - test_triangle_triangle_distance(); - test_point_ellipse_distance(); - test_point_cylinder_distance(); - test_degenerated_segment_triangle_distance(); - test_degenerated_triangle_triangle_distance(); - test_random_triangle_triangle_distance(); - test_nan_point_triangle_distance(); - test_degenerated_point_triangle_distance_2d(); - test_point_segment_line_distance_accuracy(); -} + void test() + { + test_point_segment_distance(); + test_segment_segment_distance(); + test_segment_line_distance(); + test_point_line_distance(); + test_point_triangle_distance(); + test_point_tetrahedron_distance(); + test_point_skewed_tetrahedron_distance(); + test_point_plane_distance(); + test_point_sphere_distance(); + test_point_circle_distance(); + test_point_circle_signed_distance_far_from_origin(); + test_line_triangle_distance(); + test_segment_triangle_distance(); + test_triangle_triangle_distance(); + test_point_ellipse_distance(); + test_point_cylinder_distance(); + test_degenerated_segment_triangle_distance(); + test_degenerated_triangle_triangle_distance(); + test_random_triangle_triangle_distance(); + test_nan_point_triangle_distance(); + test_degenerated_point_triangle_distance_2d(); + test_point_segment_line_distance_accuracy(); + } -OPENGEODE_TEST( "distance" ) + OPENGEODE_TEST( "distance" ) diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index 9c8e162da..3eab2a894 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -197,61 +197,62 @@ void test_degenerated_tetrahedron_vertex_order() } } } -void test_set_point() -{ - geode::OwnerTriangle3D triangle{ geode::Point3D{ { 0, 0, 0 } }, - geode::Point3D{ { 1, 0, 0 } }, geode::Point3D{ { 0, 1, 0 } } }; - const auto normal = triangle.normal(); - geode::OpenGeodeGeometryException::test( - normal && normal->inexact_equal( geode::Vector3D{ { 0, 0, 1 } } ), - "Wrong normal before set_point" ); - auto copy = triangle; - triangle.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); - const auto new_normal = triangle.normal(); - geode::OpenGeodeGeometryException::test( - new_normal - && new_normal->inexact_equal( geode::Vector3D{ { 0, -1, 0 } } ), - "Wrong normal after set_point" ); - copy.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); - const auto copy_normal = copy.normal(); - geode::OpenGeodeGeometryException::test( - copy_normal - && copy_normal->inexact_equal( geode::Vector3D{ { 0, -1, 0 } } ), - "Wrong normal after set_point on copy" ); -} + void test_set_point() + { + geode::OwnerTriangle3D triangle{ geode::Point3D{ { 0, 0, 0 } }, + geode::Point3D{ { 1, 0, 0 } }, geode::Point3D{ { 0, 1, 0 } } }; + const auto normal = triangle.normal(); + geode::OpenGeodeGeometryException::test( + normal && normal->inexact_equal( geode::Vector3D{ { 0, 0, 1 } } ), + "Wrong normal before set_point" ); + auto copy = triangle; + triangle.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); + const auto new_normal = triangle.normal(); + geode::OpenGeodeGeometryException::test( + new_normal + && new_normal->inexact_equal( geode::Vector3D{ { 0, -1, 0 } } ), + "Wrong normal after set_point" ); + copy.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); + const auto copy_normal = copy.normal(); + geode::OpenGeodeGeometryException::test( + copy_normal + && copy_normal->inexact_equal( + geode::Vector3D{ { 0, -1, 0 } } ), + "Wrong normal after set_point on copy" ); + } -void test() -{ - test_set_point(); - const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; - const geode::Point3D b_ref{ { 0.5, 0.1, 0.0 } }; - const geode::Point3D b{ { 0.6, 0.3 * geode::GLOBAL_EPSILON, 0.0 } }; - const geode::Point3D c{ { 1.0, 0.0, 0.0 } }; + void test() + { + test_set_point(); + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b_ref{ { 0.5, 0.1, 0.0 } }; + const geode::Point3D b{ { 0.6, 0.3 * geode::GLOBAL_EPSILON, 0.0 } }; + const geode::Point3D c{ { 1.0, 0.0, 0.0 } }; - const geode::Vector3D answer{ { 0, 0, -1 } }; + const geode::Vector3D answer{ { 0, 0, -1 } }; - const geode::Triangle3D triangle_ref{ a, b_ref, c }; - const auto normal_ref = triangle_ref.normal(); - geode::OpenGeodeGeometryException::test( normal_ref.has_value(), - "Normal of reference triangle should be calculated" ); - geode::OpenGeodeGeometryException::test( - normal_ref.value() == answer, "Wrong normal of reference triangle" ); - const auto pivot_ref = triangle_ref.pivot(); - geode::OpenGeodeGeometryException::test( pivot_ref.has_value(), - "Pivot of reference triangle should be calculated" ); - geode::OpenGeodeGeometryException::test( - pivot_ref.value() == 0, "Wrong pivot of reference triangle" ); + const geode::Triangle3D triangle_ref{ a, b_ref, c }; + const auto normal_ref = triangle_ref.normal(); + geode::OpenGeodeGeometryException::test( normal_ref.has_value(), + "Normal of reference triangle should be calculated" ); + geode::OpenGeodeGeometryException::test( normal_ref.value() == answer, + "Wrong normal of reference triangle" ); + const auto pivot_ref = triangle_ref.pivot(); + geode::OpenGeodeGeometryException::test( pivot_ref.has_value(), + "Pivot of reference triangle should be calculated" ); + geode::OpenGeodeGeometryException::test( + pivot_ref.value() == 0, "Wrong pivot of reference triangle" ); - const geode::Triangle3D triangle2{ a, b, c }; - const auto normal2 = triangle2.normal(); - geode::OpenGeodeGeometryException::test( - !normal2, "Normal of triangle2 should not be calculated" ); - const auto pivot2 = triangle2.pivot(); - geode::OpenGeodeGeometryException::test( - !pivot2, "Pivot of triangle2 should not be calculated" ); - test_degenerated_consistency(); - test_degenerated_triangle_vertex_order(); - test_degenerated_tetrahedron_vertex_order(); -} + const geode::Triangle3D triangle2{ a, b, c }; + const auto normal2 = triangle2.normal(); + geode::OpenGeodeGeometryException::test( + !normal2, "Normal of triangle2 should not be calculated" ); + const auto pivot2 = triangle2.pivot(); + geode::OpenGeodeGeometryException::test( + !pivot2, "Pivot of triangle2 should not be calculated" ); + test_degenerated_consistency(); + test_degenerated_triangle_vertex_order(); + test_degenerated_tetrahedron_vertex_order(); + } -OPENGEODE_TEST( "distance" ) \ No newline at end of file + OPENGEODE_TEST( "distance" ) \ No newline at end of file From 2c946102cb14fb16b2e626f8c7692a4bec4279ac Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 22:02:31 +0200 Subject: [PATCH 48/52] Add test_set_point function to test-triangle.cpp --- tests/geometry/test-triangle.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index 3eab2a894..dac7de881 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -197,6 +197,8 @@ void test_degenerated_tetrahedron_vertex_order() } } } +} + void test_set_point() { geode::OwnerTriangle3D triangle{ geode::Point3D{ { 0, 0, 0 } }, @@ -255,4 +257,4 @@ void test_degenerated_tetrahedron_vertex_order() test_degenerated_tetrahedron_vertex_order(); } - OPENGEODE_TEST( "distance" ) \ No newline at end of file + OPENGEODE_TEST( "distance" ) From 3782a94c776efaddd6c150cc8b47a0eeb0792771 Mon Sep 17 00:00:00 2001 From: BotellaA <3213882+BotellaA@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:04:05 +0000 Subject: [PATCH 49/52] Apply prepare changes --- tests/geometry/test-triangle.cpp | 109 +++++++++++++++---------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index dac7de881..65455ccb8 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -197,64 +197,63 @@ void test_degenerated_tetrahedron_vertex_order() } } } -} +} - void test_set_point() - { - geode::OwnerTriangle3D triangle{ geode::Point3D{ { 0, 0, 0 } }, - geode::Point3D{ { 1, 0, 0 } }, geode::Point3D{ { 0, 1, 0 } } }; - const auto normal = triangle.normal(); - geode::OpenGeodeGeometryException::test( - normal && normal->inexact_equal( geode::Vector3D{ { 0, 0, 1 } } ), - "Wrong normal before set_point" ); - auto copy = triangle; - triangle.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); - const auto new_normal = triangle.normal(); - geode::OpenGeodeGeometryException::test( - new_normal - && new_normal->inexact_equal( geode::Vector3D{ { 0, -1, 0 } } ), - "Wrong normal after set_point" ); - copy.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); - const auto copy_normal = copy.normal(); - geode::OpenGeodeGeometryException::test( - copy_normal - && copy_normal->inexact_equal( - geode::Vector3D{ { 0, -1, 0 } } ), - "Wrong normal after set_point on copy" ); - } +void test_set_point() +{ + geode::OwnerTriangle3D triangle{ geode::Point3D{ { 0, 0, 0 } }, + geode::Point3D{ { 1, 0, 0 } }, geode::Point3D{ { 0, 1, 0 } } }; + const auto normal = triangle.normal(); + geode::OpenGeodeGeometryException::test( + normal && normal->inexact_equal( geode::Vector3D{ { 0, 0, 1 } } ), + "Wrong normal before set_point" ); + auto copy = triangle; + triangle.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); + const auto new_normal = triangle.normal(); + geode::OpenGeodeGeometryException::test( + new_normal + && new_normal->inexact_equal( geode::Vector3D{ { 0, -1, 0 } } ), + "Wrong normal after set_point" ); + copy.set_point( 2, geode::Point3D{ { 0, 0, 1 } } ); + const auto copy_normal = copy.normal(); + geode::OpenGeodeGeometryException::test( + copy_normal + && copy_normal->inexact_equal( geode::Vector3D{ { 0, -1, 0 } } ), + "Wrong normal after set_point on copy" ); +} - void test() - { - test_set_point(); - const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; - const geode::Point3D b_ref{ { 0.5, 0.1, 0.0 } }; - const geode::Point3D b{ { 0.6, 0.3 * geode::GLOBAL_EPSILON, 0.0 } }; - const geode::Point3D c{ { 1.0, 0.0, 0.0 } }; +void test() +{ + test_set_point(); + const geode::Point3D a{ { 0.0, 0.0, 0.0 } }; + const geode::Point3D b_ref{ { 0.5, 0.1, 0.0 } }; + const geode::Point3D b{ { 0.6, 0.3 * geode::GLOBAL_EPSILON, 0.0 } }; + const geode::Point3D c{ { 1.0, 0.0, 0.0 } }; - const geode::Vector3D answer{ { 0, 0, -1 } }; + const geode::Vector3D answer{ { 0, 0, -1 } }; - const geode::Triangle3D triangle_ref{ a, b_ref, c }; - const auto normal_ref = triangle_ref.normal(); - geode::OpenGeodeGeometryException::test( normal_ref.has_value(), - "Normal of reference triangle should be calculated" ); - geode::OpenGeodeGeometryException::test( normal_ref.value() == answer, - "Wrong normal of reference triangle" ); - const auto pivot_ref = triangle_ref.pivot(); - geode::OpenGeodeGeometryException::test( pivot_ref.has_value(), - "Pivot of reference triangle should be calculated" ); - geode::OpenGeodeGeometryException::test( - pivot_ref.value() == 0, "Wrong pivot of reference triangle" ); + const geode::Triangle3D triangle_ref{ a, b_ref, c }; + const auto normal_ref = triangle_ref.normal(); + geode::OpenGeodeGeometryException::test( normal_ref.has_value(), + "Normal of reference triangle should be calculated" ); + geode::OpenGeodeGeometryException::test( + normal_ref.value() == answer, "Wrong normal of reference triangle" ); + const auto pivot_ref = triangle_ref.pivot(); + geode::OpenGeodeGeometryException::test( pivot_ref.has_value(), + "Pivot of reference triangle should be calculated" ); + geode::OpenGeodeGeometryException::test( + pivot_ref.value() == 0, "Wrong pivot of reference triangle" ); - const geode::Triangle3D triangle2{ a, b, c }; - const auto normal2 = triangle2.normal(); - geode::OpenGeodeGeometryException::test( - !normal2, "Normal of triangle2 should not be calculated" ); - const auto pivot2 = triangle2.pivot(); - geode::OpenGeodeGeometryException::test( - !pivot2, "Pivot of triangle2 should not be calculated" ); - test_degenerated_consistency(); - test_degenerated_triangle_vertex_order(); - test_degenerated_tetrahedron_vertex_order(); - } + const geode::Triangle3D triangle2{ a, b, c }; + const auto normal2 = triangle2.normal(); + geode::OpenGeodeGeometryException::test( + !normal2, "Normal of triangle2 should not be calculated" ); + const auto pivot2 = triangle2.pivot(); + geode::OpenGeodeGeometryException::test( + !pivot2, "Pivot of triangle2 should not be calculated" ); + test_degenerated_consistency(); + test_degenerated_triangle_vertex_order(); + test_degenerated_tetrahedron_vertex_order(); +} - OPENGEODE_TEST( "distance" ) +OPENGEODE_TEST( "distance" ) From f54694fd496dfab6670faa2fa15853fcc821a342 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 22:17:59 +0200 Subject: [PATCH 50/52] Add test for point skewed tetrahedron distance --- tests/geometry/test-distance.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index f2222f8cf..d2e6d8e9d 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1857,7 +1857,9 @@ void test_point_cylinder_distance() geode::point_cylinder_distance( axis_closest, cylinder ) ) <= geode::GLOBAL_EPSILON, "Wrong point_cylinder_distance: on the axis" ); - void test_point_skewed_tetrahedron_distance() + } + +void test_point_skewed_tetrahedron_distance() { const geode::Point3D a{ { 0, 0, 0 } }; const geode::Point3D b{ { 4, 0.2, 0.1 } }; From bbf3d609e559fc3027fac4a1bcc279ee722164d5 Mon Sep 17 00:00:00 2001 From: BotellaA <3213882+BotellaA@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:19:49 +0000 Subject: [PATCH 51/52] Apply prepare changes --- tests/geometry/test-distance.cpp | 135 +++++++++++++++---------------- 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index d2e6d8e9d..76ab6938a 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -1857,82 +1857,81 @@ void test_point_cylinder_distance() geode::point_cylinder_distance( axis_closest, cylinder ) ) <= geode::GLOBAL_EPSILON, "Wrong point_cylinder_distance: on the axis" ); - } +} void test_point_skewed_tetrahedron_distance() +{ + const geode::Point3D a{ { 0, 0, 0 } }; + const geode::Point3D b{ { 4, 0.2, 0.1 } }; + const geode::Point3D c{ { 3.5, 1, 0.3 } }; + const geode::Point3D d{ { 0.5, 0.3, 0.8 } }; + const geode::Tetrahedron tetra{ a, b, c, d }; + const auto& vertices = tetra.vertices(); + std::mt19937 generator{ 42 }; + std::uniform_real_distribution< double > distribution{ -2, 6 }; + for( const auto unused : geode::Range{ 1000 } ) { - const geode::Point3D a{ { 0, 0, 0 } }; - const geode::Point3D b{ { 4, 0.2, 0.1 } }; - const geode::Point3D c{ { 3.5, 1, 0.3 } }; - const geode::Point3D d{ { 0.5, 0.3, 0.8 } }; - const geode::Tetrahedron tetra{ a, b, c, d }; - const auto& vertices = tetra.vertices(); - std::mt19937 generator{ 42 }; - std::uniform_real_distribution< double > distribution{ -2, 6 }; - for( const auto unused : geode::Range{ 1000 } ) + geode_unused( unused ); + const geode::Point3D query{ { distribution( generator ), + distribution( generator ), distribution( generator ) } }; + const auto [distance, closest_point] = + geode::point_tetrahedron_distance( query, tetra ); + if( distance == 0 ) { - geode_unused( unused ); - const geode::Point3D query{ { distribution( generator ), - distribution( generator ), distribution( generator ) } }; - const auto [distance, closest_point] = - geode::point_tetrahedron_distance( query, tetra ); - if( distance == 0 ) - { - geode::OpenGeodeGeometryException::test( - geode::point_tetrahedron_position( query, tetra ) - != geode::POSITION::outside, - "Wrong null result for point_tetrahedron_distance with " - "query ", - query.string() ); - continue; - } - auto brute_force = std::numeric_limits< double >::max(); - for( const auto& facet : - geode::Tetrahedron::tetrahedron_facet_vertex ) - { - brute_force = std::min( brute_force, - std::get< 0 >( geode::point_triangle_distance( query, - geode::Triangle3D{ vertices[facet[0]], - vertices[facet[1]], vertices[facet[2]] } ) ) ); - } - geode::OpenGeodeGeometryException::test( - std::fabs( distance - brute_force ) < geode::GLOBAL_EPSILON, - "Wrong result for point_tetrahedron_distance with query ", - query.string(), ": ", distance, " | expected: ", brute_force ); geode::OpenGeodeGeometryException::test( - std::fabs( geode::point_point_distance( query, closest_point ) - - distance ) - < geode::GLOBAL_EPSILON, - "Wrong closest point for point_tetrahedron_distance with " + geode::point_tetrahedron_position( query, tetra ) + != geode::POSITION::outside, + "Wrong null result for point_tetrahedron_distance with " "query ", query.string() ); + continue; } + auto brute_force = std::numeric_limits< double >::max(); + for( const auto& facet : geode::Tetrahedron::tetrahedron_facet_vertex ) + { + brute_force = std::min( brute_force, + std::get< 0 >( geode::point_triangle_distance( + query, geode::Triangle3D{ vertices[facet[0]], + vertices[facet[1]], vertices[facet[2]] } ) ) ); + } + geode::OpenGeodeGeometryException::test( + std::fabs( distance - brute_force ) < geode::GLOBAL_EPSILON, + "Wrong result for point_tetrahedron_distance with query ", + query.string(), ": ", distance, " | expected: ", brute_force ); + geode::OpenGeodeGeometryException::test( + std::fabs( + geode::point_point_distance( query, closest_point ) - distance ) + < geode::GLOBAL_EPSILON, + "Wrong closest point for point_tetrahedron_distance with " + "query ", + query.string() ); } +} - void test() - { - test_point_segment_distance(); - test_segment_segment_distance(); - test_segment_line_distance(); - test_point_line_distance(); - test_point_triangle_distance(); - test_point_tetrahedron_distance(); - test_point_skewed_tetrahedron_distance(); - test_point_plane_distance(); - test_point_sphere_distance(); - test_point_circle_distance(); - test_point_circle_signed_distance_far_from_origin(); - test_line_triangle_distance(); - test_segment_triangle_distance(); - test_triangle_triangle_distance(); - test_point_ellipse_distance(); - test_point_cylinder_distance(); - test_degenerated_segment_triangle_distance(); - test_degenerated_triangle_triangle_distance(); - test_random_triangle_triangle_distance(); - test_nan_point_triangle_distance(); - test_degenerated_point_triangle_distance_2d(); - test_point_segment_line_distance_accuracy(); - } +void test() +{ + test_point_segment_distance(); + test_segment_segment_distance(); + test_segment_line_distance(); + test_point_line_distance(); + test_point_triangle_distance(); + test_point_tetrahedron_distance(); + test_point_skewed_tetrahedron_distance(); + test_point_plane_distance(); + test_point_sphere_distance(); + test_point_circle_distance(); + test_point_circle_signed_distance_far_from_origin(); + test_line_triangle_distance(); + test_segment_triangle_distance(); + test_triangle_triangle_distance(); + test_point_ellipse_distance(); + test_point_cylinder_distance(); + test_degenerated_segment_triangle_distance(); + test_degenerated_triangle_triangle_distance(); + test_random_triangle_triangle_distance(); + test_nan_point_triangle_distance(); + test_degenerated_point_triangle_distance_2d(); + test_point_segment_line_distance_accuracy(); +} - OPENGEODE_TEST( "distance" ) +OPENGEODE_TEST( "distance" ) From 4427dd834a7a77183aaba56e947c737f65278a3d Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 22:37:21 +0200 Subject: [PATCH 52/52] fix --- tests/geometry/test-basic-objects.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/geometry/test-basic-objects.cpp b/tests/geometry/test-basic-objects.cpp index 5ef806670..26f241ae8 100644 --- a/tests/geometry/test-basic-objects.cpp +++ b/tests/geometry/test-basic-objects.cpp @@ -353,8 +353,6 @@ void test() test_tetrahedron(); test_sphere(); test_circle(); - test_polygon_minimum_height(); - test_circle_bounding_box(); test_polygon(); }