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/include/geode/geometry/internal/predicates.hpp b/include/geode/geometry/detail/predicates.hpp similarity index 97% rename from include/geode/geometry/internal/predicates.hpp rename to include/geode/geometry/detail/predicates.hpp index 617821515..a4043ed7e 100644 --- a/include/geode/geometry/internal/predicates.hpp +++ b/include/geode/geometry/detail/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 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/include/geode/geometry/internal/intersection_from_sides.hpp b/include/geode/geometry/internal/intersection_from_sides.hpp index 068d19c03..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 44ff770e0..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/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/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/CMakeLists.txt b/src/geode/geometry/CMakeLists.txt index b70775c6e..205eb8017 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" + "detail/predicates.cpp" "point.cpp" "projection.cpp" "quality.cpp" @@ -105,11 +105,12 @@ 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" - "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/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/src/geode/geometry/basic_objects/polygon.cpp b/src/geode/geometry/basic_objects/polygon.cpp index cc7a44bbd..b39ae1611 100644 --- a/src/geode/geometry/basic_objects/polygon.cpp +++ b/src/geode/geometry/basic_objects/polygon.cpp @@ -304,17 +304,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/src/geode/geometry/basic_objects/tetrahedron.cpp b/src/geode/geometry/basic_objects/tetrahedron.cpp index 82c95c590..f34e5c9b0 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,30 @@ 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 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]; + 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 Vector3D edge23{ point2, point3 }; + const auto six_volume = edge01.dot( edge02.cross( edge03 ) ); + 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_cross2; } template < typename PointType > diff --git a/src/geode/geometry/basic_objects/triangle.cpp b/src/geode/geometry/basic_objects/triangle.cpp index 87ba90d79..b43150cb0 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 @@ -295,16 +310,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/src/geode/geometry/bounding_box.cpp b/src/geode/geometry/bounding_box.cpp index 1fa15b510..dc732812a 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 > @@ -160,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 > @@ -195,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 > @@ -485,58 +520,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 <> @@ -556,58 +540,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/src/geode/geometry/common.cpp b/src/geode/geometry/common.cpp index 140ba2310..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/internal/predicates.cpp b/src/geode/geometry/detail/predicates.cpp similarity index 64% rename from src/geode/geometry/internal/predicates.cpp rename to src/geode/geometry/detail/predicates.cpp index 10ffacaf2..76be019ad 100644 --- a/src/geode/geometry/internal/predicates.cpp +++ b/src/geode/geometry/detail/predicates.cpp @@ -21,7 +21,10 @@ * */ -#include +#include + +#include +#include /* * Copyright (c) 2012-2014, Bruno Levy @@ -875,8 +878,12 @@ 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. + 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 ========================= @@ -1406,177 +1413,731 @@ 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 ) ) ) + 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 ) { - max2 = fabs( a23 ); + return FPG_UNCERTAIN_VALUE; } - double lower_bound_1; - double upper_bound_1; - lower_bound_1 = max1; - upper_bound_1 = max1; - if( ( max2 < lower_bound_1 ) ) + const double eps = 8.88720573725927976811e-16 * ( max1 * max2 ); + if( Delta > eps ) { - lower_bound_1 = max2; + return 1; } - else + if( Delta < -eps ) { - if( ( max2 > upper_bound_1 ) ) - { - upper_bound_1 = max2; - } + return -1; } - if( ( lower_bound_1 < 5.00368081960964635413e-147 ) ) + 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 ) { - return FPG_UNCERTAIN_VALUE; + return false; } - else - { - if( ( upper_bound_1 > 1.67597599124282407923e+153 ) ) + return std::fabs( delta ) + > 8.88720573725927976811e-16 * ( max1 * max2 ); + } + + // 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 bool is_certainly_not_aligned_3d( 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 ) ); + 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 *******/ + + // 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 ) ); + } + + /******* adaptive stages *******/ + + // 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, stage D adds exactly all the remaining terms to the stage B + // result. + namespace adaptive + { + 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 = + ( 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] ); + } + + // 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 ) + { + 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 ) ) { - return FPG_UNCERTAIN_VALUE; + Q = enow; + next_e(); } - eps = ( 8.88720573725927976811e-16 * ( max1 * max2 ) ); - if( ( delta1 > eps ) ) + else { - int_tmp_result = 1; + Q = fnow; + next_f(); } - else + if( eindex < elen && findex < flen ) { - if( ( delta1 < -eps ) ) + if( ( fnow > enow ) == ( fnow > -enow ) ) { - int_tmp_result = -1; + fast_two_sum( enow, Q, Qnew, hh ); + next_e(); } else { - return FPG_UNCERTAIN_VALUE; + 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; + } } } - } - 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 ) ) + while( eindex < elen ) { - upper_bound_1 = max2; + 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; } - if( ( lower_bound_1 < 5.00368081960964635413e-147 ) ) - { - return FPG_UNCERTAIN_VALUE; - } - else + + // e and h cannot be the same. + inline int scale_expansion_zeroelim( + int elen, const double* e, double b, double* h ) { - if( ( upper_bound_1 > 1.67597599124282407923e+153 ) ) + int hindex = 0; + double Q; + double hh; + two_product( e[0], b, Q, hh ); + if( hh != 0. ) { - return FPG_UNCERTAIN_VALUE; + 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; + } } - eps = ( 8.88720573725927976811e-16 * ( max2 * max3 ) ); - if( ( delta2 > eps ) ) + if( Q != 0. || hindex == 0 ) { - int_tmp_result_FFWKCAA = 1; + h[hindex++] = Q; } - else + return hindex; + } + + inline double estimate( int elen, const double* e ) + { + double Q = e[0]; + for( int eindex = 1; eindex < elen; eindex++ ) { - if( ( delta2 < -eps ) ) + 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. ) { - int_tmp_result_FFWKCAA = -1; + return positive; } - else + if( e[eindex] < 0. ) { - return FPG_UNCERTAIN_VALUE; + return negative; } } + return zero; } - lower_bound_1 = max1; - upper_bound_1 = max1; - if( ( max3 < lower_bound_1 ) ) + + inline SIGN sign_of( double value ) { - lower_bound_1 = max3; + if( value > 0. ) + { + return positive; + } + return value < 0. ? negative : zero; } - else + + // Exact (a1 + a0) * b as a 4 components expansion + inline void two_one_product( + double a1, double a0, double b, double x[4] ) { - if( ( max3 > upper_bound_1 ) ) + 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. ) { - upper_bound_1 = max3; + 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; } - if( ( lower_bound_1 < 5.00368081960964635413e-147 ) ) + + // Sign of (p1-p0)x(p2-p0) + inline SIGN orient_2d( const geode::Point2D& p0, + const geode::Point2D& p1, + const geode::Point2D& p2 ) { - return FPG_UNCERTAIN_VALUE; - } - else + 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 ); + + // 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 ) + { + return sign_of( det ); + } + + // Stage C + 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. ) + { + 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 ) + { + return sign_of( det ); + } + + // 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 ); + } + + // 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 ) { - if( ( upper_bound_1 > 1.67597599124282407923e+153 ) ) + 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 ); + + // Stage B + 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 fin1[192]; + double fin2[192]; + auto finlength = + fast_expansion_sum_zeroelim( ablen, abdet, clen, cdet, fin1 ); + + double det = estimate( finlength, fin1 ); + double errbound = O3D_ERRBOUND_B * permanent; + if( det >= errbound || -det >= errbound ) { - return FPG_UNCERTAIN_VALUE; + return sign_of( det ); } - eps = ( 8.88720573725927976811e-16 * ( max3 * max1 ) ); - if( ( delta3 > eps ) ) + + // Stage C + 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. ) { - int_tmp_result_k60Ocge = 1; + return expansion_sign( finlength, fin1 ); } - else + + 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 ) { - if( ( delta3 < -eps ) ) + return sign_of( det ); + } + + // 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. ) { - int_tmp_result_k60Ocge = -1; + two_one_product( product1, product0, ztail, u ); + add( 4, u ); } - else + }; + if( adxtail != 0. ) + { + if( bdytail != 0. ) { - return FPG_UNCERTAIN_VALUE; + 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 ); } - return ( - ( ( ( int_tmp_result == 0 ) && ( int_tmp_result_FFWKCAA == 0 ) ) - && ( int_tmp_result_k60Ocge == 0 ) ) - ? 0 - : 1 ); - } + } // namespace adaptive } // namespace GEO /******* extracted from predicates.cpp *******/ @@ -1604,46 +2165,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, @@ -1734,12 +2257,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 adaptive::orient_2d( p0, p1, p2 ); } SIGN orient_3d( const geode::Point3D& p0, @@ -1747,38 +2274,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 ) { - result = orient_3d_exact( p0, p1, p2, p3 ); + return result; } - return result; + if( is_orient_3d_zero( p0, p1, p2, p3 ) ) + { + return zero; + } + return adaptive::orient_3d( 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 ) { - result = det_3d_exact( p0, p1, p2 ); + return result; + } + if( is_det_3d_zero( 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( is_certainly_not_aligned_3d( p0, p1, p2 ) ) + { + return false; + } + if( is_aligned_3d_zero( p0, p1, p2 ) ) + { + return true; } - */ return aligned_3d_exact( p0, p1, p2 ); } @@ -1786,18 +2321,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/src/geode/geometry/distance.cpp b/src/geode/geometry/distance.cpp index 9cf97f5de..d45350af3 100644 --- a/src/geode/geometry/distance.cpp +++ b/src/geode/geometry/distance.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -68,33 +69,6 @@ namespace && signed_area_3 >= 0 ); } - 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; @@ -176,7 +150,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 } ) { @@ -439,26 +413,20 @@ 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( const auto j : + geode::Range{ i + 1, non_colocated_points.size() } ) { - 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 ) { - 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] }; @@ -794,36 +762,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 > @@ -942,16 +882,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( @@ -982,7 +914,8 @@ namespace geode opengeode_geometry_api point_triangle_distance( const Point2D& point, const Triangle2D& triangle ) { - if( may_point_be_in_triangle( point, triangle ) ) + if( !triangle.is_degenerated() + && may_point_be_in_triangle( point, triangle ) ) { return { 0.0, point }; } @@ -1118,6 +1051,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 ); @@ -1470,6 +1410,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 a2c037dc6..885add986 100644 --- a/src/geode/geometry/intersection.cpp +++ b/src/geode/geometry/intersection.cpp @@ -97,6 +97,67 @@ namespace size_t numIntersections; std::array< double, 2 > parameter; }; + + /* + * 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_on_first, + double distance, + const PointType& point_on_second ) + { + if( distance > geode::GLOBAL_EPSILON ) + { + return { geode::INTERSECTION_TYPE::none }; + } + typename geode::CorrectnessInfo< Intersection >::Correctness + first_correctness{ true, Intersection{ point_on_first } }; + typename geode::CorrectnessInfo< Intersection >::Correctness + second_correctness{ true, Intersection{ point_on_second } }; + return { Intersection{ point_on_first }, + { first_correctness, second_correctness } }; + } + + /* + * 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 @@ -133,26 +194,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 = @@ -182,11 +245,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 = @@ -211,26 +274,21 @@ 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 ) { - 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; } @@ -238,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 ) @@ -457,6 +523,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 ) @@ -508,6 +588,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 ) @@ -534,6 +621,22 @@ 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; + } + }; // 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 @@ -546,7 +649,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(); @@ -554,6 +657,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() ); @@ -561,28 +700,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 @@ -594,147 +725,63 @@ 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 ) { - // 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 }; } + for( const auto parameter : wall_parameters( D ) ) + { + add_parameter( parameter ); + } } - - // 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 ) + else { - // Plane intersection inside the top cylinder end disk. - result.parameter[result.numIntersections++] = t1; - } + // 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 ) - { - // 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; - if( discr > 0 ) + // Test for intersections with the planes of the end disks. + const auto t0 = ( -halfHeight - P.value( 2 ) ) / D.value( 2 ); + if( is_inside_radius( P.value( 0 ) + t0 * D.value( 0 ), + P.value( 1 ) + t0 * D.value( 1 ) ) ) { - 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; - } - } + // Plane intersection inside the bottom cylinder end disk. + add_parameter( t0 ); + } - 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. + const auto t1 = ( halfHeight - P.value( 2 ) ) / D.value( 2 ); + 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 ); } - else if( discr == 0 ) + + if( result.numIntersections < 2 ) { - auto tValue = -a1 / a2; - if( t0 <= t1 ) - { - if( t0 <= tValue && tValue <= t1 ) - { - result.parameter[result.numIntersections++] = - tValue; - } - } - else + // 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( t1 <= tValue && tValue <= t0 ) + if( min_parameter <= parameter + && parameter <= max_parameter ) { - result.parameter[result.numIntersections++] = - tValue; + add_parameter( parameter ); } } } - // 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 ) { @@ -765,19 +812,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 @@ -785,40 +827,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 ) { - const auto distance = point_point_distance( - results[r], cylinder.axis().vertices()[v].get() ); - if( distance <= GLOBAL_EPSILON ) + cur_correctness = true; + break; + } + if( distance < cylinder.radius() ) + { + 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 } }; @@ -828,26 +867,21 @@ namespace geode opengeode_geometry_api segment_cylinder_intersection( const Segment3D& segment, const Cylinder& cylinder ) { + 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, distance, projection ); + } auto line_intersections = 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; } @@ -872,7 +906,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 } ) { @@ -927,21 +962,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 ) ) ); }; @@ -1109,26 +1146,21 @@ 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 ) { - 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/src/geode/geometry/intersection_detection.cpp b/src/geode/geometry/intersection_detection.cpp index 2116826a1..7ac67b704 100644 --- a/src/geode/geometry/intersection_detection.cpp +++ b/src/geode/geometry/intersection_detection.cpp @@ -28,8 +28,8 @@ #include #include #include +#include #include -#include #include #include #include diff --git a/src/geode/geometry/is_point_inside.cpp b/src/geode/geometry/is_point_inside.cpp index ac9db7c91..7c88ede5e 100644 --- a/src/geode/geometry/is_point_inside.cpp +++ b/src/geode/geometry/is_point_inside.cpp @@ -24,19 +24,36 @@ #include #include +#include #include namespace { - bool is_left( const geode::Point2D& p0, - const geode::Point2D& p1, - const geode::Point2D& p2 ) + // 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& edge_vertex0, + const geode::Point2D& edge_vertex1, + GEO::SIGN point_side ) { - 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; + 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 } ) + { + 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; + } + } + return true; } } // namespace @@ -45,35 +62,43 @@ 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 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 ) ) { - if( v1.get().value( 1 ) > point.value( 1 ) ) - { - if( is_left( v0, v1, point ) ) - { - winding_number++; - } - } + // The edge neither crosses the point row nor contains it + continue; + } + const auto side = GEO::PCK::orient_2d( v0, v1, point ); + if( is_point_on_edge( point, v0, v1, side ) ) + { + return true; } - else + if( y0 <= y ) { - if( v1.get().value( 1 ) <= point.value( 1 ) ) + if( y1 > y && side == GEO::SIGN::positive ) { - if( !is_left( v0, v1, point ) ) - { - winding_number--; - } + winding_number++; } } + else if( y1 <= y && side == GEO::SIGN::negative ) + { + winding_number--; + } } return winding_number != 0; } -} // namespace geode \ No newline at end of file +} // namespace geode 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/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/src/geode/geometry/position.cpp b/src/geode/geometry/position.cpp index 5dcdca3d3..841e66101 100644 --- a/src/geode/geometry/position.cpp +++ b/src/geode/geometry/position.cpp @@ -33,9 +33,9 @@ #include #include #include +#include #include #include -#include #include #include 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/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/src/geode/geometry/radial_sort.cpp b/src/geode/geometry/radial_sort.cpp index 1fce1a600..c8c99b62f 100644 --- a/src/geode/geometry/radial_sort.cpp +++ b/src/geode/geometry/radial_sort.cpp @@ -28,8 +28,8 @@ #include #include #include +#include #include -#include #include #include diff --git a/src/geode/geometry/rotation.cpp b/src/geode/geometry/rotation.cpp index e56f1fe93..72ffa8703 100644 --- a/src/geode/geometry/rotation.cpp +++ b/src/geode/geometry/rotation.cpp @@ -88,38 +88,30 @@ 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" ); + 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 ); 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/src/geode/geometry/sign.cpp b/src/geode/geometry/sign.cpp index 98432bc04..a8fa91379 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/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/CMakeLists.txt b/tests/geometry/CMakeLists.txt index df3826317..e0710c712 100644 --- a/tests/geometry/CMakeLists.txt +++ b/tests/geometry/CMakeLists.txt @@ -136,6 +136,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 @@ -178,4 +184,3 @@ add_geode_test( ${PROJECT_NAME}::basic ${PROJECT_NAME}::geometry ) - diff --git a/tests/geometry/test-basic-objects.cpp b/tests/geometry/test-basic-objects.cpp index 2b4667b36..26f241ae8 100644 --- a/tests/geometry/test-basic-objects.cpp +++ b/tests/geometry/test-basic-objects.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include diff --git a/tests/geometry/test-bounding-box.cpp b/tests/geometry/test-bounding-box.cpp index f0522f706..0c70b2281 100644 --- a/tests/geometry/test-bounding-box.cpp +++ b/tests/geometry/test-bounding-box.cpp @@ -354,6 +354,75 @@ 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.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 } }; + 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_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_add_empty_box() { geode::BoundingBox3D box; @@ -384,6 +453,8 @@ void test() test_tetra_intersections(); test_triangle2d_intersections(); test_triangle3d_intersections(); + test_large_triangle3d_epsilon_intersections(); + test_1d_line_intersections(); } OPENGEODE_TEST( "bounding-box" ) diff --git a/tests/geometry/test-coordinate-system.cpp b/tests/geometry/test-coordinate-system.cpp index e04e06846..a2edc3f70 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 modified_system2D() { geode::Logger::info( "Modified system 2D" ); @@ -157,6 +166,7 @@ void test() orthonormal_system3D(); acute_system3D(); optuse_system3D(); + test_system1D(); modified_system2D(); } diff --git a/tests/geometry/test-distance.cpp b/tests/geometry/test-distance.cpp index f99ccf972..76ab6938a 100644 --- a/tests/geometry/test-distance.cpp +++ b/tests/geometry/test-distance.cpp @@ -21,6 +21,8 @@ * */ +#include + #include #include @@ -28,6 +30,7 @@ #include #include +#include #include #include #include @@ -1568,6 +1571,294 @@ 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 : + { 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 } }; + 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_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_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 ) <= 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 ) + <= geode::GLOBAL_EPSILON, + "Wrong triangle_triangle_distance closest points" ); + } +} + +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_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 ); +} + +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 ) + <= geode::GLOBAL_EPSILON, + "Inconsistent point_line_distance and point_line_projection" ); + geode::OpenGeodeGeometryException::test( + std::fabs( line_distance - line_reference ) < geode::GLOBAL_EPSILON, + "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 ) + <= geode::GLOBAL_EPSILON, + "Inconsistent point_segment_distance and " + "point_segment_projection" ); + geode::OpenGeodeGeometryException::test( + 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 + 0.5 * geode::GLOBAL_EPSILON } }; + 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_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_point_skewed_tetrahedron_distance() { const geode::Point3D a{ { 0, 0, 0 } }; @@ -1611,7 +1902,8 @@ void test_point_skewed_tetrahedron_distance() std::fabs( geode::point_point_distance( query, closest_point ) - distance ) < geode::GLOBAL_EPSILON, - "Wrong closest point for point_tetrahedron_distance with query ", + "Wrong closest point for point_tetrahedron_distance with " + "query ", query.string() ); } } @@ -1633,6 +1925,13 @@ 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(); + test_nan_point_triangle_distance(); + test_degenerated_point_triangle_distance_2d(); + test_point_segment_line_distance_accuracy(); } OPENGEODE_TEST( "distance" ) diff --git a/tests/geometry/test-intersection.cpp b/tests/geometry/test-intersection.cpp index 694584e6e..611ba8336 100644 --- a/tests/geometry/test-intersection.cpp +++ b/tests/geometry/test-intersection.cpp @@ -1079,6 +1079,344 @@ 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 : + { 0.4 * geode::GLOBAL_EPSILON, 0.1 * geode::GLOBAL_EPSILON } ) + { + 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 ); + } +} + +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., 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 } }; + 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_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., 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 } }; + 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 ) +{ + 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_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(); @@ -1096,6 +1434,11 @@ void test() test_triangle_circle_intersection(); test_line_ellipse_intersection(); 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(); } OPENGEODE_TEST( "intersection" ) 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 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 diff --git a/tests/geometry/test-position.cpp b/tests/geometry/test-position.cpp index ed7dfef09..eddc4c320 100644 --- a/tests/geometry/test-position.cpp +++ b/tests/geometry/test-position.cpp @@ -21,14 +21,20 @@ * */ +#include +#include +#include + #include #include #include +#include #include #include #include #include +#include #include #include @@ -569,6 +575,168 @@ 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 } }; + 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" ); + } + + // 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() +{ + 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, next_double_above( x2 ) } }, segment ); + const auto side_down = geode::point_side_to_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, + "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, next_double_above( x3 ), z3 } }, triangle ); + const auto side_below = geode::point_side_to_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, + "Wrong side for points one ulp away from the plane" ); + } +} + +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, next_double_above( y2 ) } }, + geode::Segment2D{ p0, p1 } ); + const auto side_down = geode::point_side_to_segment( + geode::Point2D{ { x2, next_double_below( y2 ) } }, + 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(); @@ -577,6 +745,9 @@ void test() test_point_segment_position(); test_point_triangle_position(); 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 diff --git a/tests/geometry/test-predicates.cpp b/tests/geometry/test-predicates.cpp new file mode 100644 index 000000000..3f2e4c3ea --- /dev/null +++ b/tests/geometry/test-predicates.cpp @@ -0,0 +1,2180 @@ +/* + * 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 + +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" ); + // 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 } }; + 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" ); + } + + // 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" ); + + // 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 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" ); + } + + // 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() + { + 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" ); + } + } + } + + // 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() +{ + test_orient_2d(); + test_orient_3d(); + test_det_3d(); + test_aligned_3d(); + test_dot_3d(); + test_dot_2d(); + test_exact_degenerate(); + test_underflow(); + test_nearly_degenerate_consistency(); + test_integer_configurations(); + test_regression_cases(); + + geode::Logger::info( "TEST SUCCESS" ); +} + +OPENGEODE_TEST( "predicates" ) diff --git a/tests/geometry/test-quality.cpp b/tests/geometry/test-quality.cpp index 61f96bc79..6fe3a85fe 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 ) < geode::GLOBAL_EPSILON + && std::fabs( small[q] - reference[q] ) < geode::GLOBAL_EPSILON, + "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 diff --git a/tests/geometry/test-rotation.cpp b/tests/geometry/test-rotation.cpp index 972aa7c11..91fbff55c 100644 --- a/tests/geometry/test-rotation.cpp +++ b/tests/geometry/test-rotation.cpp @@ -30,6 +30,43 @@ #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" ); + 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. } }; + 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 +85,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" ) 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 diff --git a/tests/geometry/test-triangle.cpp b/tests/geometry/test-triangle.cpp index 1dd027ecd..65455ccb8 100644 --- a/tests/geometry/test-triangle.cpp +++ b/tests/geometry/test-triangle.cpp @@ -21,17 +21,184 @@ * */ +#include +#include +#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.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(), + "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, 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( + 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" ); + + // 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. }; + 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() ); + } + } +} + +// 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_set_point() { geode::OwnerTriangle3D triangle{ geode::Point3D{ { 0, 0, 0 } }, @@ -84,6 +251,9 @@ void test() 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" )