Skip to content

fix(Geometry): tier 1 robustness fixes - #1349

Merged
BotellaA merged 33 commits into
nextfrom
fix/geometry-robustness-tier1
Oct 15, 2026
Merged

BotellaA merged 33 commits into
nextfrom
fix/geometry-robustness-tier1

Conversation

@BotellaA

@BotellaA BotellaA commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Tier 1 of the geometry robustness review: fixes for functions that returned wrong results on ordinary (non-degenerate) inputs. Each fix is its own commit and comes with a test that fails on next and passes here (checked for every commit).

Fixes (one commit each)

Area Bug on next Fix
CoordinateSystem set_directions / set_direction kept a stale cached inverse matrix Reset the cache on every direction change and after deserialization; validate the frame
Frame max/min_elongation_direction compared vectors lexicographically Compare by length
BoundingBox::add_box Adding an empty box turned the result into ±DBL_MAX Per-axis min/max
AABBTree::closest_element_box A NaN distance on the starting element stopped the search Ignore a non-finite starting distance
Triangle::set_point Cached normal and pivot were not reset Reset the cache
line_line_intersection Parallel test fabs(x) < 0. was never true, so parallel lines gave (-inf, NaN) == 0.
point_circle_signed_distance Sign taken relative to the world origin Sign relative to the circle plane
segment_triangle_intersection Segments with an endpoint on the triangle dropped 25-35% of the time (1 ulp) GLOBAL_EPSILON tolerance on the segment parameter, then clamp
Polygon::normal Sum of unit fan normals, wrong or missing for concave polygons Area-weighted (Newell) sum
polygon_area_sign Sign of the first fan triangle, wrong for concave polygons Exact orient_2d at a convex-hull vertex
point_triangle_position 3D Fixed (1,1,1) projection: every point returned parallel for planes containing (1,1,1) (e.g. internal faces of a cube cut into tets) Exact 2D test after dropping the dominant normal axis
segment_segment_intersection_detection 3D Dropped the smallest-extent axis, so real crossings were reported parallel Project along the plane normal (shared helper)
triangle_area Heron with an eps test on p - l_i: needle triangles of height 1000·eps returned 0 Cross product; 0 only when height <= GLOBAL_EPSILON
tetrahedron_signed_volume Angular tolerance zeroed valid tets (V = -3.33 returned 0) Triple product; 0 only when height <= GLOBAL_EPSILON
point_tetrahedron_distance Only the facet with the smallest barycentric coordinate was tested (errors up to 0.96) Test every facet the point is outside of
point_ellipse_distance Unsquared numerator, sort that never reordered, wrong un-permutation: 95% of random queries wrong Follows Eberly's algorithm
segment_segment_distance Near-parallel fallback returned the same point twice and threw on short segments Cancellation-free determinants and a single polishing step; distance and points always agree
FrameTransform Greedy axis matching could map two axes to one, or leave the mapping uninitialized True permutation maximizing the sum of |cos|, deterministic ties

Plus two perf(Mensuration) commits that make the triangle_area cross product cheaper than Heron, and a LOCAL benchmark (tests/geometry/benchmark-tier1-robustness.cpp).

Behavior changes to check downstream

  • triangle_area and tetrahedron_signed_volume now return non-zero values for thin (but not degenerate) elements. Code using area == 0 as a degeneracy test (Background, Common, Simplex, Conversion, Geosciences) will flag fewer elements.
  • segment_segment_distance and point_tetrahedron_distance now return correct (sometimes smaller) distances, with consistent closest points.
  • polygon_area_sign is correct for concave polygons: Inspector's surface_negative_elements results may change.
  • line_line_intersection / segment_segment_intersection report parallel instead of incorrect. All callers found only test operator bool.
  • CoordinateSystem::set_directions now throws on degenerate directions.

Deliberately not changed

FrameTransform::apply(Point/Vector) gathers (result[d] = p[dir[d]]) while apply(Frame) scatters, so the two disagree for 3-cycle permutations. Switching to scatter would silently break Simplex frontal metric coordinates and Hybrid/Numerics PGP, which rely on the gather convention. This needs a design decision, so it is left as is (the test documents the current convention).

Benchmark (ns/call, best of 7, pinned to one core, next 670d96d vs this branch, same clang -O3 flags)

Significant speedups: segment_segment_distance (2.3x random, 23x near-parallel), Polygon::normal (14x), triangulate (2.1x), triangle_area 2D (1.9x) and 3D (1.25x), triangle_signed_area (1.7x).

The slowdowns pay for correctness:

  • set_directions+coordinates and set_point+normal: next returned a stale cached value (the bug).
  • segment_triangle_intersection with an endpoint on the triangle: next wrongly rejected most of these segments early.
  • Needle triangle_area: next returned 0 early.
  • point_tetrahedron_distance: every visible facet is now evaluated.
  • point_ellipse_distance: now converges to the right root.
  • polygon_area_sign: about 3 ns for the minimum-vertex scan.

Everything else is within noise.

Full table
Function Input next (ns/call) branch (ns/call) speedup
CoordinateSystem3D::coordinates random points 5.7 5.7 1.00x
CoordinateSystem3D::set_directions+coordinates random scales 20.8 86.4 0.24x
Frame3D::max_elongation_direction random frames 9.6 7.8 1.23x
Frame3D::min_elongation_direction random frames 9.6 7.6 1.27x
FrameTransform3D::ctor+apply(Vector) random frames 128.7 108.3 1.19x
NormalFrameTransform3D::ctor+apply(Vector) random frames 162.7 144.4 1.13x
BoundingBox3D::add_box random boxes 7.8 7.5 1.04x
AABBTree3D::closest_element_box 10k random boxes 1467.2 1576.8 0.93x
OwnerTriangle3D::set_point+normal random triangles 12.3 72.5 0.17x
triangle_area(Triangle3D) random 11.6 9.3 1.25x
triangle_area(Triangle3D) needles h~1e-3 8.0 9.3 0.85x
triangle_area(Triangle2D) random 10.5 5.6 1.87x
triangle_signed_area(Triangle3D) random 19.0 11.2 1.70x
point_triangle_position(3D) points on triangle plane 662.5 642.7 1.03x
point_triangle_position(3D) random points 37.3 37.6 0.99x
segment_triangle_intersection segments through triangle 135.0 137.7 0.98x
segment_triangle_intersection segments ending on triangle 93.7 125.0 0.75x
segment_triangle_intersection_detection coplanar segments 1179.9 1169.9 1.01x
segment_segment_intersection(2D) random 107.6 107.1 1.00x
line_line_intersection(2D) random 76.7 76.7 1.00x
segment_segment_distance(3D) random 88.5 38.9 2.27x
segment_segment_distance(3D) near parallel 752.8 32.9 22.87x
segment_segment_intersection_detection(3D) coplanar 600.6 562.3 1.07x
segment_segment_intersection_detection(3D) random 26.0 26.0 1.00x
polygon_area_sign concave stars (8 vertices) 27.6 31.0 0.89x
OwnerPolygon3D::normal concave stars (8 vertices) 422.0 29.3 14.42x
OwnerPolygon3D::triangulate concave stars (8 vertices) 1435.9 673.7 2.13x
tetrahedron_signed_volume random 16.3 13.9 1.17x
tetrahedron_signed_volume flat (h~1e-3) 16.2 14.0 1.16x
point_tetrahedron_distance random 123.5 183.1 0.67x
point_circle_signed_distance random 21.8 22.0 0.99x
point_ellipse_distance(2D) random 464.2 573.2 0.81x
point_ellipse_distance(3D) random 446.8 575.7 0.78x

Testing

ctest -L essential: 102/102 (new test-frame).

🤖 Generated with Claude Code

BotellaA and others added 20 commits October 13, 2026 21:19
The global-to-local matrix was cached and never invalidated, so
coordinates() kept using the old directions after set_directions() or
Frame::set_direction() (used by Grid when rescaling cells).
Add CoordinateSystem::set_direction resetting the cache, reset it in
set_directions and after deserialization, and validate the frame in
set_directions and in the points constructor (the directions constructor
no longer checks a moved-from array).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
max/min_elongation_direction used the lexicographic operator< of Vector,
returning the direction with the largest/smallest first coordinate
instead of the longest/shortest one. Compare squared lengths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
add_box added the other box's min and max as points; for an empty box
(min = DBL_MAX, max = lowest) this blew the box up to the whole space.
Take the per-axis min of mins and max of maxs instead, which also fixes
AABBTree bounding boxes when some elements have empty boxes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the action returned NaN (or inf) for the hinted element, every
comparison against the squared distance failed and the hint was
returned with a NaN distance. Restart from an empty best candidate
when the hint distance is not finite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_point modified a vertex without invalidating the cached pivot and
normal, so normal()/pivot() kept returning values of the old triangle
(also on copies, which copy the cache).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parallel test was fabs(x) < 0., which is never true: parallel lines
went through a division by zero and returned an 'incorrect' result with
an infinite/NaN point. Test the dot perpendicular against exact zero so
parallel lines and segments are reported as parallel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sign was computed from the dot product of the normal with the point
position vector, i.e. relative to the world origin instead of the
circle plane. Use the vector from the circle center to the point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
segment_triangle_intersection compared the plane parameter against the
exact half extent, so a segment ending on the triangle was randomly
rejected depending on rounding. Accept parameters up to the half length
plus GLOBAL_EPSILON (a length) and clamp the result onto the segment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
normal() summed unit normals of the fan triangles, so for concave
polygons triangles of opposite orientation cancelled out regardless of
their area (e.g. a dart gave no normal, and triangulate() then picked an
arbitrary orientation). Sum the un-normalized fan cross products
(Newell) and return nullopt when the polygon height is below
GLOBAL_EPSILON instead of relying on an exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
polygon_area_sign returned the sign of the first non-degenerate fan
triangle, which is wrong for concave polygons (a dart gave the opposite
sign). Use the exact orientation at the lexicographically smallest
vertex, which lies on the convex hull, skipping duplicated neighbours,
and keep the fan loop as a fallback (now using index_t so polygons with
more than 255 vertices are supported).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exact 3D point/triangle position used determinants with the fixed
(1,1,1) vector, which are all zero when the triangle plane contains that
direction, giving wrong results (inside points reported as outside).
After the exact coplanarity test, drop the coordinate where the triangle
normal is the largest (exact) and use the exact 2D predicate; exactly
collinear triangles are handled by testing the point against their
edges (keeping the 'parallel' convention).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plane normal

segment_segment_intersection_detection in 3D (and the degenerate
triangle fallback of segment/triangle detection) dropped the axis with
the smallest bounding box extent. For primitives in a plane containing
that axis (e.g. a vertical plane) the projection collapsed the plane to
a line and crossing segments were reported as parallel or disjoint ones
as overlapping. Drop the largest component of the plane normal computed
from the longest segment, or, for collinear inputs, the smallest
component of its direction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Heron's formula with an epsilon on p - l_i returned 0 for valid needle
triangles (a unit base with a 1e-3 height gave 0, since p - l_i is
about h^2/L). Compute the area from the cross product of the two edges
adjacent to the vertex opposite to the longest edge, and return 0 only
when the triangle height is below GLOBAL_EPSILON (compared on squared
quantities). triangle_signed_area in 3D reuses the same cross product
for the sign. Re-enable the existing triangle area tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tetrahedron_signed_volume returned 0 as soon as no vertex had two edges
making an angle above GLOBAL_ANGULAR_EPSILON, so valid flat tetrahedra
(e.g. 2000 long and 0.07 high) got a null volume. Use the plain triple
product (same sign convention) and return 0 only when the tetrahedron
height relative to its largest facet is below GLOBAL_EPSILON (compared
on squared values). Re-enable the existing signed volume test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ance

Only the facet with the most negative barycentric coordinate was
tested, which is not always the closest one for skewed tetrahedra.
Return the minimal distance over all facets with a negative coordinate
(all facets when the tetrahedron is degenerated).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several deviations from Eberly's point/hyperellipsoid algorithm made
point_ellipse_distance return points off the ellipse for most queries:
- the bisection numerator used p * z instead of p^2 * z;
- the axes were sorted by index instead of by decreasing extent;
- the closest point coordinates were not un-permuted and the reflection
  signs were applied to the wrong axes;
- zeroed coordinates were written at the wrong index.
Also compute the axis lengths and unit directions only once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… segments

Nearly parallel segments bailed out of Eberly's algorithm (log2 > 20
tests) into a dichotomic search returning the same point on both
segments with a distance inconsistent with them, and throwing for
segments shorter than GLOBAL_EPSILON. The post-processing could also
return points that did not match the distance.
- compute ac - bb, be - cd and ae - bd with cross products
  (Binet-Cauchy identity) to avoid cancellations, and use the parallel
  branch only when sin^2(angle) is below 1e-20;
- handle segments shorter than GLOBAL_EPSILON as points;
- polish the result once (project P(s) on segment1 then Q(t) on
  segment0) and compute the distance from the returned points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each 'from' direction was greedily matched with its most aligned 'to'
direction, so two directions could map to the same one (e.g. on ties
or with non-orthogonal frames), and the arrays stayed uninitialized if
no dot product was positive. Choose the permutation maximizing the sum
of |cos| (first one in lexicographic order on ties). Also remove unused
members of NormalFrameTransform::Impl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compute the edges, their squared lengths and the cross product of the
edges adjacent to the vertex opposite to the longest edge on scalar
coordinates with branchless selections, avoiding temporary vectors and
store-forwarding stalls of the previous implementation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BotellaA
BotellaA requested a review from panquez October 13, 2026 20:44
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Cpp-Linter Report ⚠️

Some files did not pass the configured checks!

clang-tidy (v21.1.8) reports: 1742 concern(s)
  • include/geode/geometry/basic_objects/tetrahedron.hpp:46:11: warning: [cppcoreguidelines-special-member-functions]

    class 'GenericTetrahedron' defines a copy constructor, a copy assignment operator, a move constructor and a move assignment operator but does not define a destructor

       46 |     class GenericTetrahedron
          |           ^
  • include/geode/geometry/basic_objects/tetrahedron.hpp:50:26: warning: [readability-redundant-inline-specifier]

    variable 'tetrahedron_facet_vertex' has inline specifier but is implicitly inlined

       50 |         static constexpr inline std::array< std::array< index_t, 3 >, 4 >
          |                          ^~~~~~
  • include/geode/geometry/basic_objects/tetrahedron.hpp:51:13: warning: [readability-identifier-naming]

    invalid case style for constexpr variable 'tetrahedron_facet_vertex'

       51 |             tetrahedron_facet_vertex{ { { { 1, 3, 2 } }, { { 0, 2, 3 } },
          |             ^~~~~~~~~~~~~~~~~~~~~~~~
          |             TETRAHEDRON_FACET_VERTEX
  • include/geode/geometry/basic_objects/tetrahedron.hpp:80:34: warning: [cppcoreguidelines-special-member-functions]

    class 'OwnerTetrahedron' defines a copy constructor, a copy assignment operator, a move constructor and a move assignment operator but does not define a destructor

       80 |     class opengeode_geometry_api OwnerTetrahedron
          |                                  ^
  • include/geode/geometry/basic_objects/tetrahedron.hpp:91:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

       91 |         OwnerTetrahedron( const Tetrahedron& other ) noexcept;
          |         ^
          |         explicit 
  • include/geode/geometry/basic_objects/tetrahedron.hpp:98:34: warning: [cppcoreguidelines-special-member-functions]

    class 'Tetrahedron' defines a copy constructor, a copy assignment operator, a move constructor and a move assignment operator but does not define a destructor

       98 |     class opengeode_geometry_api Tetrahedron
          |                                  ^
  • include/geode/geometry/basic_objects/tetrahedron.hpp:110:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

      110 |         Tetrahedron( const OwnerTetrahedron& other ) noexcept;
          |         ^
          |         explicit 
  • include/geode/geometry/basic_objects/triangle.hpp:59:11: warning: [cppcoreguidelines-special-member-functions]

    class 'GenericTriangle' defines a copy constructor, a copy assignment operator, a move constructor and a move assignment operator but does not define a destructor

       59 |     class GenericTriangle
          |           ^
  • include/geode/geometry/basic_objects/triangle.hpp:116:11: warning: [cppcoreguidelines-special-member-functions]

    class 'OwnerTriangle' defines a copy constructor, a copy assignment operator, a move constructor and a move assignment operator but does not define a destructor

      116 |     class OwnerTriangle
          |           ^
  • include/geode/geometry/basic_objects/triangle.hpp:135:11: warning: [cppcoreguidelines-special-member-functions]

    class 'Triangle' defines a copy constructor, a copy assignment operator, a move constructor and a move assignment operator but does not define a destructor

      135 |     class Triangle : public GenericTriangle< RefPoint< dimension >, dimension >
          |           ^
  • include/geode/geometry/basic_objects/triangle.hpp:145:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

      145 |         Triangle( const OwnerTriangle< dimension >& other ) noexcept;
          |         ^
          |         explicit 
  • include/geode/geometry/detail/aabb_impl.hpp:101:9: warning: [google-explicit-constructor]

    single-argument constructors must be marked explicit to avoid unintentional implicit conversions

      101 |         Impl( absl::Span< const BoundingBox< dimension > > bboxes )
          |         ^
          |         explicit 
  • include/geode/geometry/detail/aabb_impl.hpp:107:31: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '-'; add parentheses to explicitly specify the order of operations

      107 |                 tree_.resize( 2 * bboxes.size() - 1 );
          |                               ^~~~~~~~~~~~~~~~~
          |                               (                )
  • include/geode/geometry/detail/aabb_impl.hpp:125:57: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'get_recursive_iterators' of similar type ('index_t') are easily swapped by mistake

      125 |         [[nodiscard]] Iterator get_recursive_iterators( index_t node_index,
          |                                                         ^~~~~~~~~~~~~~~~~~~
      126 |             index_t element_begin,
          |             ~~~~~~~~~~~~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:125:65: note: the first parameter in the range is 'node_index'
      125 |         [[nodiscard]] Iterator get_recursive_iterators( index_t node_index,
          |                                                                 ^~~~~~~~~~
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:126:21: note: the last parameter in the range is 'element_begin'
      126 |             index_t element_begin,
          |                     ^~~~~~~~~~~~~
  • include/geode/geometry/detail/aabb_impl.hpp:129:22: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      129 |             Iterator it;
          |                      ^
  • include/geode/geometry/detail/aabb_impl.hpp:131:33: warning: [readability-math-missing-parentheses]

    '/' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      131 |                 element_begin + ( element_end - element_begin ) / 2;
          |                                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                 (                                  )
  • include/geode/geometry/detail/aabb_impl.hpp:174:33: warning: [readability-math-missing-parentheses]

    '/' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      174 |                 element_begin + ( element_end - element_begin ) / 2;
          |                                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                 (                                  )
  • include/geode/geometry/detail/aabb_impl.hpp:194:14: warning: [readability-function-cognitive-complexity]

    function 'closest_element_box_recursive' has cognitive complexity of 13 (threshold 10)

      194 |         void closest_element_box_recursive( const Point< dimension >& query,
          |              ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:210:13: note: +1, including nesting penalty of 0, nesting level increased to 1
      210 |             if( is_leaf( element_begin, element_end ) )
          |             ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:215:17: note: +2, including nesting penalty of 1, nesting level increased to 2
      215 |                 if( cur_squared_distance < squared_distance )
          |                 ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:231:13: note: +1, including nesting penalty of 0, nesting level increased to 1
      231 |             if( squared_distance_left < squared_distance_right )
          |             ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:233:17: note: +2, including nesting penalty of 1, nesting level increased to 2
      233 |                 if( squared_distance_left < squared_distance )
          |                 ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:239:17: note: +2, including nesting penalty of 1, nesting level increased to 2
      239 |                 if( squared_distance_right < squared_distance )
          |                 ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:246:13: note: +1, nesting level increased to 1
      246 |             else
          |             ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:248:17: note: +2, including nesting penalty of 1, nesting level increased to 2
      248 |                 if( squared_distance_right < squared_distance )
          |                 ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:254:17: note: +2, including nesting penalty of 1, nesting level increased to 2
      254 |                 if( squared_distance_left < squared_distance )
          |                 ^
  • include/geode/geometry/detail/aabb_impl.hpp:194:14: warning: [readability-function-size]

    function 'closest_element_box_recursive' exceeds recommended size/complexity thresholds

      194 |         void closest_element_box_recursive( const Point< dimension >& query,
          |              ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:194:14: note: 7 parameters (threshold 4)
  • include/geode/geometry/detail/aabb_impl.hpp:222:24: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      222 |             const auto it = get_recursive_iterators(
          |                        ^
  • include/geode/geometry/detail/aabb_impl.hpp:329:28: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      329 |                 const auto it = get_recursive_iterators(
          |                            ^
  • include/geode/geometry/detail/aabb_impl.hpp:346:14: warning: [readability-function-size]

    function 'leaf_self_intersect_recursive' exceeds recommended size/complexity thresholds

      346 |         void leaf_self_intersect_recursive( index_t leaf_node_index,
          |              ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:346:14: note: 7 parameters (threshold 4)
  • include/geode/geometry/detail/aabb_impl.hpp:347:13: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'leaf_self_intersect_recursive' of similar type ('index_t') are easily swapped by mistake

      347 |             index_t leaf_position,
          |             ^~~~~~~~~~~~~~~~~~~~~~
      348 |             index_t node_index,
          |             ~~~~~~~~~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:347:21: note: the first parameter in the range is 'leaf_position'
      347 |             index_t leaf_position,
          |                     ^~~~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:348:21: note: the last parameter in the range is 'node_index'
      348 |             index_t node_index,
          |                     ^~~~~~~~~~
  • include/geode/geometry/detail/aabb_impl.hpp:373:24: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      373 |             const auto it = get_recursive_iterators(
          |                        ^
  • include/geode/geometry/detail/aabb_impl.hpp:383:14: warning: [readability-function-size]

    function 'leaf_other_intersect_recursive' exceeds recommended size/complexity thresholds

      383 |         void leaf_other_intersect_recursive( index_t leaf_node_index,
          |              ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:383:14: note: 8 parameters (threshold 4)
  • include/geode/geometry/detail/aabb_impl.hpp:407:24: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      407 |             const auto it = other.get_recursive_iterators(
          |                        ^
  • include/geode/geometry/detail/aabb_impl.hpp:418:14: warning: [readability-function-size]

    function 'generic_intersect_recursive' exceeds recommended size/complexity thresholds

      418 |         bool generic_intersect_recursive( const BOX_FILTER& box_filter,
          |              ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:418:14: note: 5 parameters (threshold 4)
  • include/geode/geometry/detail/aabb_impl.hpp:441:24: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      441 |             const auto it = get_recursive_iterators(
          |                        ^
  • include/geode/geometry/detail/aabb_impl.hpp:460:28: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      460 |                 const auto it = get_recursive_iterators(
          |                            ^
  • include/geode/geometry/detail/aabb_impl.hpp:478:14: warning: [readability-function-size]

    function 'containing_boxes_recursive' exceeds recommended size/complexity thresholds

      478 |         void containing_boxes_recursive( index_t node_index,
          |              ^
    /__w/OpenGeode/OpenGeode/include/geode/geometry/detail/aabb_impl.hpp:478:14: note: 5 parameters (threshold 4)
  • include/geode/geometry/detail/aabb_impl.hpp:498:24: warning: [readability-identifier-length]

    variable name 'it' is too short, expected at least 3 characters

      498 |             const auto it = get_recursive_iterators(
          |                        ^
  • include/geode/geometry/nn_search.hpp:41:11: warning: [cppcoreguidelines-special-member-functions]

    class 'NNSearch' defines a destructor, a copy constructor, a copy assignment operator and a move constructor but does not define a move assignment operator

       41 |     class NNSearch
          |           ^
  • src/geode/basic/assert.cpp:115:9: warning: [bugprone-empty-catch]

    empty catch statements hide issues; to handle exceptions appropriately, consider re-throwing, handling, or avoiding catch altogether

      115 |         catch( ... )
          |         ^
  • src/geode/geometry/basic_objects/circle.cpp:62:24: warning: [readability-identifier-length]

    variable name 'dP' is too short, expected at least 3 characters

       62 |             const auto dP = plane_.normal().dot( direction );
          |                        ^
  • src/geode/geometry/basic_objects/circle.cpp:62:24: warning: [readability-identifier-naming]

    invalid case style for variable 'dP'

       62 |             const auto dP = plane_.normal().dot( direction );
          |                        ^~
          |                        d_p
       63 |             if( std::fabs( dP ) >= 1.0 )
          |                            ~~
          |                            d_p
       64 |             {
       65 |                 return 0.;
       66 |             }
       67 |             return std::sqrt( 1 - dP * dP );
          |                                   ~~   ~~
          |                                   d_p  d_p
  • src/geode/geometry/basic_objects/circle.cpp:67:35: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '-'; add parentheses to explicitly specify the order of operations

       67 |             return std::sqrt( 1 - dP * dP );
          |                                   ^~~~~~~
          |                                   (      )
  • src/geode/geometry/basic_objects/circle.cpp:83:18: warning: [hicpp-use-equals-default]

    use '= default' to define a trivial copy constructor

       83 |     OwnerCircle::OwnerCircle( const OwnerCircle& other ) : Base( other ) {}
          |                  ^                                         ~~~~~~~~~~~~~~~~
          |                                                            = default;
  • src/geode/geometry/basic_objects/circle.cpp:84:31: warning: [cert-oop54-cpp]

    operator=() does not handle self-assignment properly

       84 |     OwnerCircle& OwnerCircle::operator=( const OwnerCircle& other )
          |                               ^
  • src/geode/geometry/basic_objects/circle.cpp:84:31: warning: [hicpp-use-equals-default]

    use '= default' to define a trivial copy-assignment operator

       84 |     OwnerCircle& OwnerCircle::operator=( const OwnerCircle& other )
          |                               ^
       85 |     {
          |     ~
          |     = default;
       86 |         Base::operator=( other );
          |         ~~~~~~~~~~~~~~~~~~~~~~~~~
       87 |         return *this;
          |         ~~~~~~~~~~~~~
       88 |     }
          |     ~
  • src/geode/geometry/basic_objects/polygon.cpp:279:52: warning: [readability-function-cognitive-complexity]

    function 'minimum_height' has cognitive complexity of 11 (threshold 10)

      279 |     double GenericPolygon< PointType, dimension >::minimum_height() const
          |                                                    ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:284:9: note: +1, including nesting penalty of 0, nesting level increased to 1
      284 |         for( const auto e : geode::Range{ nb_vertices } )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:288:48: note: +2, including nesting penalty of 1, nesting level increased to 2
      288 |                 vertices_[e == nb_vertices - 1 ? 0 : e + 1];
          |                                                ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:291:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      291 |             if( cur_length > max_length )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:297:9: note: +1, including nesting penalty of 0, nesting level increased to 1
      297 |         if( max_length < GLOBAL_EPSILON )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:302:48: note: +1, including nesting penalty of 0, nesting level increased to 1
      302 |             max_length_edge + 1 == nb_vertices ? 0 : max_length_edge + 1;
          |                                                ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:308:9: note: +1, including nesting penalty of 0, nesting level increased to 1
      308 |         for( const auto vertex : geode::Range{ nb_vertices } )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:310:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      310 |             if( vertex == max_length_edge || vertex == next )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/polygon.cpp:310:43: note: +1
      310 |             if( vertex == max_length_edge || vertex == next )
          |                                           ^
  • src/geode/geometry/basic_objects/tetrahedron.cpp:61:56: warning: [cppcoreguidelines-avoid-magic-numbers]

    1. is a magic number; consider replacing it with a named constant
       61 |         return ( point0 + point1 + point2 + point3 ) / 4.;
          |                                                        ^
  • src/geode/geometry/basic_objects/triangle.cpp:43:57: warning: [readability-function-cognitive-complexity]

    function 'simple_pivot_and_normal' has cognitive complexity of 11 (threshold 10)

       43 |     std::optional< geode::internal::PivotNormalResult > simple_pivot_and_normal(
          |                                                         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/triangle.cpp:49:9: note: +1, including nesting penalty of 0, nesting level increased to 1
       49 |         for( const auto pivot : geode::LRange{ 3 } )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/triangle.cpp:51:46: note: +2, including nesting penalty of 1, nesting level increased to 2
       51 |             const auto next = pivot + 1 == 3 ? 0 : pivot + 1;
          |                                              ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/triangle.cpp:54:13: note: +2, including nesting penalty of 1, nesting level increased to 2
       54 |             if( result->lengths[pivot] < geode::GLOBAL_EPSILON )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/triangle.cpp:59:42: note: +2, including nesting penalty of 1, nesting level increased to 2
       59 |             const auto prev = pivot == 0 ? 2 : pivot - 1;
          |                                          ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/triangle.cpp:62:13: note: +2, including nesting penalty of 1, nesting level increased to 2
       62 |             if( length1 < geode::GLOBAL_EPSILON )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/basic_objects/triangle.cpp:69:13: note: +2, including nesting penalty of 1, nesting level increased to 2
       69 |             if( length > geode::GLOBAL_ANGULAR_EPSILON )
          |             ^
  • src/geode/geometry/basic_objects/triangle.cpp:127:47: warning: [cppcoreguidelines-avoid-magic-numbers]

    1. is a magic number; consider replacing it with a named constant
      127 |         return ( point0 + point1 + point2 ) / 3.;
          |                                               ^
  • src/geode/geometry/basic_objects/triangle.cpp:199:9: warning: [llvm-qualified-auto]

    'const auto max' can be declared as 'const auto *const max'

      199 |         const auto max = absl::c_max_element( result->lengths );
          |         ^~~~~~~~~~
          |         const auto *const 
  • src/geode/geometry/basic_objects/triangle.cpp:203:20: warning: [readability-identifier-length]

    variable name 'e1' is too short, expected at least 3 characters

      203 |         const auto e1 = longest_e == 2 ? 0 : longest_e + 1;
          |                    ^
  • src/geode/geometry/basic_objects/triangle.cpp:205:20: warning: [readability-identifier-length]

    variable name 'e2' is too short, expected at least 3 characters

      205 |         const auto e2 = e1 == 2 ? 0 : e1 + 1;
          |                    ^
  • src/geode/geometry/bounding_box.cpp:43:10: warning: [readability-function-size]

    function 'is_separating_axis' exceeds recommended size/complexity thresholds

       43 |     bool is_separating_axis( const geode::Vector3D& axis,
          |          ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/bounding_box.cpp:43:10: note: 5 parameters (threshold 4)
  • src/geode/geometry/bounding_box.cpp:45:9: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'is_separating_axis' of similar type ('const geode::Vector3D &') are easily swapped by mistake

       45 |         const geode::Vector3D& box_center,
          |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       46 |         const geode::Vector3D& box_half_diagonal,
          |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/bounding_box.cpp:45:32: note: the first parameter in the range is 'box_center'
       45 |         const geode::Vector3D& box_center,
          |                                ^~~~~~~~~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/bounding_box.cpp:46:32: note: the last parameter in the range is 'box_half_diagonal'
       46 |         const geode::Vector3D& box_half_diagonal,
          |                                ^~~~~~~~~~~~~~~~~
  • src/geode/geometry/bounding_box.cpp:94:25: warning: [readability-identifier-length]

    variable name 'i0' is too short, expected at least 3 characters

       94 |         for( const auto i0 : geode::LRange{ 3 } )
          |                         ^
  • src/geode/geometry/bounding_box.cpp:96:29: warning: [readability-identifier-length]

    variable name 'i1' is too short, expected at least 3 characters

       96 |             for( const auto i1 : geode::LRange{ 3 } )
          |                             ^
  • src/geode/geometry/bounding_box.cpp:143:19: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      143 |                 + segment_extent * std::fabs( segment_direction.value( i ) );
          |                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                   (                                                         )
  • src/geode/geometry/bounding_box.cpp:173:24: warning: [readability-identifier-length]

    variable name 'ii' is too short, expected at least 3 characters

      173 |             const auto ii = i == 2 ? 0 : i + 1;
          |                        ^
  • src/geode/geometry/bounding_box.cpp:198:26: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      198 |         const auto rhs = box_half_extent.value( 0 )
          |                          ^~~~~~~~~~~~~~~~~~~~~~~~~~
          |                          (
      199 |                              * std::fabs( line.direction().value( 1 ) )
          |                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                                        )
  • src/geode/geometry/bounding_box.cpp:200:28: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      200 |                          + box_half_extent.value( 1 )
          |                            ^~~~~~~~~~~~~~~~~~~~~~~~~~
          |                            (
      201 |                                * std::fabs( line.direction().value( 0 ) );
          |                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |                                                                          )
  • src/geode/geometry/bounding_box.cpp:273:9: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'BoundingBox' of similar type ('Point') are easily swapped by mistake

      273 |         Point< dimension > min, Point< dimension > max )
          |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/bounding_box.cpp:273:28: note: the first parameter in the range is 'min'
      273 |         Point< dimension > min, Point< dimension > max )
          |                            ^~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/bounding_box.cpp:273:52: note: the last parameter in the range is 'max'
      273 |         Point< dimension > min, Point< dimension > max )
          |                                                    ^~~
  • src/geode/geometry/bounding_box.cpp:653:34: warning: [cppcoreguidelines-avoid-magic-numbers]

    1. is a magic number; consider replacing it with a named constant
      653 |         return ( min_ + max_ ) / 2.;
          |                                  ^
  • src/geode/geometry/bounding_box.cpp:718:25: warning: [readability-identifier-length]

    variable name 'c' is too short, expected at least 3 characters

      718 |         for( const auto c : LRange{ dimension } )
          |                         ^
  • src/geode/geometry/coordinate_system.cpp:49:25: warning: [readability-identifier-length]

    variable name 'd0' is too short, expected at least 3 characters

       49 |         for( const auto d0 : geode::LRange{ 2 } )
          |                         ^
  • src/geode/geometry/coordinate_system.cpp:51:29: warning: [readability-identifier-length]

    variable name 'd1' is too short, expected at least 3 characters

       51 |             for( const auto d1 : geode::LRange{ d0 + 1, 3 } )
          |                             ^
  • src/geode/geometry/coordinate_system.cpp:78:20: warning: [readability-identifier-length]

    variable name 'OA' is too short, expected at least 3 characters

       78 |         const auto OA = frame.direction( 0 );
          |                    ^
  • src/geode/geometry/coordinate_system.cpp:78:20: warning: [readability-identifier-naming]

    invalid case style for variable 'OA'

       78 |         const auto OA = frame.direction( 0 );
          |                    ^~
          |                    oa
       79 |         const auto OB = frame.direction( 1 );
       80 |         const geode::SquareMatrix< 2 > system_matrix{
       81 |             { geode::Vector2D{ { OA.value( 0 ), OB.value( 0 ) } },
          |                                  ~~
          |                                  oa
       82 |                 geode::Vector2D{ { OA.value( 1 ), OB.value( 1 ) } } }
          |                                    ~~
          |                                    oa
  • src/geode/geometry/coordinate_system.cpp:79:20: warning: [readability-identifier-length]

    variable name 'OB' is too short, expected at least 3 characters

       79 |         const auto OB = frame.direction( 1 );
          |                    ^
  • src/geode/geometry/coordinate_system.cpp:79:20: warning: [readability-identifier-naming]

    invalid case style for variable 'OB'

       79 |         const auto OB = frame.direction( 1 );
          |                    ^~
          |                    ob
       80 |         const geode::SquareMatrix< 2 > system_matrix{
       81 |             { geode::Vector2D{ { OA.value( 0 ), OB.value( 0 ) } },
          |                                                 ~~
          |                                                 ob
       82 |                 geode::Vector2D{ { OA.value( 1 ), OB.value( 1 ) } } }
          |                                                   ~~
          |                                                   ob
  • src/geode/geometry/coordinate_system.cpp:90:20: warning: [readability-identifier-length]

    variable name 'OA' is too short, expected at least 3 characters

       90 |         const auto OA = frame.direction( 0 );
          |                    ^
  • src/geode/geometry/coordinate_system.cpp:90:20: warning: [readability-identifier-naming]

    invalid case style for variable 'OA'

       90 |         const auto OA = frame.direction( 0 );
          |                    ^~
          |                    oa
       91 |         const auto OB = frame.direction( 1 );
       92 |         const auto OC = frame.direction( 2 );
       93 |         const geode::SquareMatrix< 3 > system_matrix{
       94 |             { geode::Vector3D{
       95 |                   { OA.value( 0 ), OB.value( 0 ), OC.value( 0 ) } },
          |                     ~~
          |                     oa
       96 |                 geode::Vector3D{
       97 |                     { OA.value( 1 ), OB.value( 1 ), OC.value( 1 ) } },
          |                       ~~
          |                       oa
       98 |                 geode::Vector3D{
       99 |                     { OA.value( 2 ), OB.value( 2 ), OC.value( 2 ) } } }
          |                       ~~
          |                       oa
  • src/geode/geometry/coordinate_system.cpp:91:20: warning: [readability-identifier-length]

    variable name 'OB' is too short, expected at least 3 characters

       91 |         const auto OB = frame.direction( 1 );
          |                    ^
  • src/geode/geometry/coordinate_system.cpp:91:20: warning: [readability-identifier-naming]

    invalid case style for variable 'OB'

       91 |         const auto OB = frame.direction( 1 );
          |                    ^~
          |                    ob
       92 |         const auto OC = frame.direction( 2 );
       93 |         const geode::SquareMatrix< 3 > system_matrix{
       94 |             { geode::Vector3D{
       95 |                   { OA.value( 0 ), OB.value( 0 ), OC.value( 0 ) } },
          |                                    ~~
          |                                    ob
       96 |                 geode::Vector3D{
       97 |                     { OA.value( 1 ), OB.value( 1 ), OC.value( 1 ) } },
          |                                      ~~
          |                                      ob
       98 |                 geode::Vector3D{
       99 |                     { OA.value( 2 ), OB.value( 2 ), OC.value( 2 ) } } }
          |                                      ~~
          |                                      ob
  • src/geode/geometry/coordinate_system.cpp:92:20: warning: [readability-identifier-length]

    variable name 'OC' is too short, expected at least 3 characters

       92 |         const auto OC = frame.direction( 2 );
          |                    ^
  • src/geode/geometry/coordinate_system.cpp:92:20: warning: [readability-identifier-naming]

    invalid case style for variable 'OC'

       92 |         const auto OC = frame.direction( 2 );
          |                    ^~
          |                    oc
       93 |         const geode::SquareMatrix< 3 > system_matrix{
       94 |             { geode::Vector3D{
       95 |                   { OA.value( 0 ), OB.value( 0 ), OC.value( 0 ) } },
          |                                                   ~~
          |                                                   oc
       96 |                 geode::Vector3D{
       97 |                     { OA.value( 1 ), OB.value( 1 ), OC.value( 1 ) } },
          |                                                     ~~
          |                                                     oc
       98 |                 geode::Vector3D{
       99 |                     { OA.value( 2 ), OB.value( 2 ), OC.value( 2 ) } } }
          |                                                     ~~
          |                                                     oc
  • src/geode/geometry/coordinate_system.cpp:173:35: warning: [readability-identifier-length]

    variable name 'OP' is too short, expected at least 3 characters

      173 |         const Vector< dimension > OP{ origin_, global_coordinates };
          |                                   ^
  • src/geode/geometry/coordinate_system.cpp:173:35: warning: [readability-identifier-naming]

    invalid case style for variable 'OP'

      173 |         const Vector< dimension > OP{ origin_, global_coordinates };
          |                                   ^~
          |                                   op
      174 |         return global_to_local_matrix_( frame_inverse_matrix, *this ) * OP;
          |                                                                         ~~
          |                                                                         op
  • src/geode/geometry/distance.cpp:72:45: warning: [readability-identifier-length]

    parameter name 'b1' is too short, expected at least 3 characters

       72 |     void get_min_edge02( double a11, double b1, std::array< double, 2 >& p )
          |                                             ^
  • src/geode/geometry/distance.cpp:72:74: warning: [readability-identifier-length]

    parameter name 'p' is too short, expected at least 3 characters

       72 |     void get_min_edge02( double a11, double b1, std::array< double, 2 >& p )
          |                                                                          ^
  • src/geode/geometry/distance.cpp:89:10: warning: [readability-function-size]

    function 'get_min_edge12' exceeds recommended size/complexity thresholds

       89 |     void get_min_edge12( double a01,
          |          ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:89:10: note: 6 parameters (threshold 4)
  • src/geode/geometry/distance.cpp:89:26: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'get_min_edge12' of similar type ('double') are easily swapped by mistake

       89 |     void get_min_edge12( double a01,
          |                          ^~~~~~~~~~~
       90 |         double a11,
          |         ~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:89:33: note: the first parameter in the range is 'a01'
       89 |     void get_min_edge12( double a01,
          |                                 ^~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:90:16: note: the last parameter in the range is 'a11'
       90 |         double a11,
          |                ^~~
  • src/geode/geometry/distance.cpp:91:16: warning: [readability-identifier-length]

    parameter name 'b1' is too short, expected at least 3 characters

       91 |         double b1,
          |                ^
  • src/geode/geometry/distance.cpp:92:9: warning: [bugprone-easily-swappable-parameters]

    2 adjacent parameters of 'get_min_edge12' of similar type ('double') are easily swapped by mistake

       92 |         double f10,
          |         ^~~~~~~~~~~
       93 |         double f01,
          |         ~~~~~~~~~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:92:16: note: the first parameter in the range is 'f10'
       92 |         double f10,
          |                ^~~
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:93:16: note: the last parameter in the range is 'f01'
       93 |         double f01,
          |                ^~~
  • src/geode/geometry/distance.cpp:94:34: warning: [readability-identifier-length]

    parameter name 'p' is too short, expected at least 3 characters

       94 |         std::array< double, 2 >& p )
          |                                  ^
  • src/geode/geometry/distance.cpp:96:20: warning: [readability-identifier-length]

    variable name 'h0' is too short, expected at least 3 characters

       96 |         const auto h0 = a01 + b1 - f10;
          |                    ^
  • src/geode/geometry/distance.cpp:103:24: warning: [readability-identifier-length]

    variable name 'h1' is too short, expected at least 3 characters

      103 |             const auto h1 = a11 + b1 - f01;
          |                        ^
  • src/geode/geometry/distance.cpp:116:10: warning: [readability-function-size]

    function 'get_min_interior' exceeds recommended size/complexity thresholds

      116 |     void get_min_interior( const std::array< double, 2 >& p0,
          |          ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:116:10: note: 5 parameters (threshold 4)
  • src/geode/geometry/distance.cpp:116:59: warning: [readability-identifier-length]

    parameter name 'p0' is too short, expected at least 3 characters

      116 |     void get_min_interior( const std::array< double, 2 >& p0,
          |                                                           ^
  • src/geode/geometry/distance.cpp:117:16: warning: [readability-identifier-length]

    parameter name 'h0' is too short, expected at least 3 characters

      117 |         double h0,
          |                ^
  • src/geode/geometry/distance.cpp:118:40: warning: [readability-identifier-length]

    parameter name 'p1' is too short, expected at least 3 characters

      118 |         const std::array< double, 2 >& p1,
          |                                        ^
  • src/geode/geometry/distance.cpp:119:16: warning: [readability-identifier-length]

    parameter name 'h1' is too short, expected at least 3 characters

      119 |         double h1,
          |                ^
  • src/geode/geometry/distance.cpp:120:34: warning: [readability-identifier-length]

    parameter name 'p' is too short, expected at least 3 characters

      120 |         std::array< double, 2 >& p )
          |                                  ^
  • src/geode/geometry/distance.cpp:124:16: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      124 |         p[0] = omz * p0[0] + z * p1[0];
          |                ^~~~~~~~~~~
          |                (          )
  • src/geode/geometry/distance.cpp:124:30: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      124 |         p[0] = omz * p0[0] + z * p1[0];
          |                              ^~~~~~~~~~
          |                              (        )
  • src/geode/geometry/distance.cpp:125:16: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      125 |         p[1] = omz * p0[1] + z * p1[1];
          |                ^~~~~~~~~~~
          |                (          )
  • src/geode/geometry/distance.cpp:125:30: warning: [readability-math-missing-parentheses]

    '*' has higher precedence than '+'; add parentheses to explicitly specify the order of operations

      125 |         p[1] = omz * p0[1] + z * p1[1];
          |                              ^~~~~~~~~~
          |                              (        )
  • src/geode/geometry/distance.cpp:170:42: warning: [readability-function-cognitive-complexity]

    function 'pivot_point_triangle_distance' has cognitive complexity of 44 (threshold 10)

      170 |     std::tuple< double, geode::Point3D > pivot_point_triangle_distance(
          |                                          ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:175:33: note: +1, including nesting penalty of 0, nesting level increased to 1
      175 |         const auto v1 = v0 == 2 ? 0 : v0 + 1;
          |                                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:176:33: note: +1, including nesting penalty of 0, nesting level increased to 1
      176 |         const auto v2 = v1 == 2 ? 0 : v1 + 1;
          |                                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:194:9: note: +1, including nesting penalty of 0, nesting level increased to 1
      194 |         if( f00 >= 0 )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:196:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      196 |             if( f01 >= 0 )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:200:13: note: +1, nesting level increased to 2
      200 |             else
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:209:17: note: +3, including nesting penalty of 2, nesting level increased to 3
      209 |                 if( h0 >= 0 )
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:213:17: note: +1, nesting level increased to 3
      213 |                 else
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:216:21: note: +4, including nesting penalty of 3, nesting level increased to 4
      216 |                     if( h1 <= 0 )
          |                     ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:220:21: note: +1, nesting level increased to 4
      220 |                     else
          |                     ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:227:14: note: +1, nesting level increased to 1
      227 |         else if( f01 <= 0 )
          |              ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:229:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      229 |             if( f10 <= 0 )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:234:13: note: +1, nesting level increased to 2
      234 |             else
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:242:17: note: +3, including nesting penalty of 2, nesting level increased to 3
      242 |                 if( h0 >= 0 )
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:246:17: note: +1, nesting level increased to 3
      246 |                 else
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:249:21: note: +4, including nesting penalty of 3, nesting level increased to 4
      249 |                     if( h1 <= 0 )
          |                     ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:253:21: note: +1, nesting level increased to 4
      253 |                     else
          |                     ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:260:14: note: +1, nesting level increased to 1
      260 |         else if( f10 <= 0 )
          |              ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:270:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      270 |             if( h0 >= 0 )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:274:13: note: +1, nesting level increased to 2
      274 |             else
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:277:17: note: +3, including nesting penalty of 2, nesting level increased to 3
      277 |                 if( h1 <= 0 )
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:281:17: note: +1, nesting level increased to 3
      281 |                 else
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:287:9: note: +1, nesting level increased to 1
      287 |         else
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:295:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      295 |             if( h0 >= 0 )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:299:13: note: +1, nesting level increased to 2
      299 |             else
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:302:17: note: +3, including nesting penalty of 2, nesting level increased to 3
      302 |                 if( h1 <= 0 )
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:306:17: note: +1, nesting level increased to 3
      306 |                 else
          |                 ^
  • src/geode/geometry/distance.cpp:173:30: warning: [readability-identifier-length]

    parameter name 'v0' is too short, expected at least 3 characters

      173 |         geode::local_index_t v0 )
          |                              ^
  • src/geode/geometry/distance.cpp:175:20: warning: [readability-identifier-length]

    variable name 'v1' is too short, expected at least 3 characters

      175 |         const auto v1 = v0 == 2 ? 0 : v0 + 1;
          |                    ^
  • src/geode/geometry/distance.cpp:176:20: warning: [readability-identifier-length]

    variable name 'v2' is too short, expected at least 3 characters

      176 |         const auto v2 = v1 == 2 ? 0 : v1 + 1;
          |                    ^
  • src/geode/geometry/distance.cpp:184:20: warning: [readability-identifier-length]

    variable name 'b0' is too short, expected at least 3 characters

      184 |         const auto b0 = -diff.dot( edge0 );
          |                    ^
  • src/geode/geometry/distance.cpp:185:20: warning: [readability-identifier-length]

    variable name 'b1' is too short, expected at least 3 characters

      185 |         const auto b1 = -diff.dot( edge1 );
          |                    ^
  • src/geode/geometry/distance.cpp:191:9: warning: [cppcoreguidelines-pro-type-member-init]

    uninitialized record type: 'p'

      191 |         std::array< double, 2 > p0, p1, p;
          |         ^
    note: this fix will not be applied because it overlaps with another fix
  • src/geode/geometry/distance.cpp:191:9: warning: [cppcoreguidelines-pro-type-member-init]

    uninitialized record type: 'p0'

    note: this fix will not be applied because it overlaps with another fix
  • src/geode/geometry/distance.cpp:191:9: warning: [cppcoreguidelines-pro-type-member-init]

    uninitialized record type: 'p1'

    note: this fix will not be applied because it overlaps with another fix
  • src/geode/geometry/distance.cpp:191:9: warning: [readability-isolate-declaration]

    multiple declarations in a single statement reduces readability

      191 |         std::array< double, 2 > p0, p1, p;
          |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • src/geode/geometry/distance.cpp:191:33: warning: [readability-identifier-length]

    variable name 'p0' is too short, expected at least 3 characters

      191 |         std::array< double, 2 > p0, p1, p;
          |                                 ^
  • src/geode/geometry/distance.cpp:191:37: warning: [readability-identifier-length]

    variable name 'p1' is too short, expected at least 3 characters

      191 |         std::array< double, 2 > p0, p1, p;
          |                                     ^
  • src/geode/geometry/distance.cpp:192:9: warning: [readability-isolate-declaration]

    multiple declarations in a single statement reduces readability

      192 |         double dt1, h0, h1;
          |         ^~~~~~~~~~~~~~~~~~~
  • src/geode/geometry/distance.cpp:192:16: warning: [cppcoreguidelines-init-variables]

    variable 'dt1' is not initialized

      192 |         double dt1, h0, h1;
          |                ^
    note: this fix will not be applied because it overlaps with another fix
  • src/geode/geometry/distance.cpp:192:21: warning: [cppcoreguidelines-init-variables]

    variable 'h0' is not initialized

      192 |         double dt1, h0, h1;
          |                     ^
    note: this fix will not be applied because it overlaps with another fix
  • src/geode/geometry/distance.cpp:192:21: warning: [readability-identifier-length]

    variable name 'h0' is too short, expected at least 3 characters

  • src/geode/geometry/distance.cpp:192:25: warning: [cppcoreguidelines-init-variables]

    variable 'h1' is not initialized

      192 |         double dt1, h0, h1;
          |                         ^
    note: this fix will not be applied because it overlaps with another fix
  • src/geode/geometry/distance.cpp:192:25: warning: [readability-identifier-length]

    variable name 'h1' is too short, expected at least 3 characters

  • src/geode/geometry/distance.cpp:322:9: warning: [readability-function-cognitive-complexity]

    function 'find_non_colocated_triangles_points' has cognitive complexity of 11 (threshold 10)

      322 |         find_non_colocated_triangles_points( const geode::Triangle3D& triangle0,
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:330:9: note: +1, including nesting penalty of 0, nesting level increased to 1
      330 |         for( const auto vertex0 : geode::LRange{ 3 } )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:332:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      332 |             for( const auto vertex1 : geode::LRange{ 3 } )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:334:17: note: +3, including nesting penalty of 2, nesting level increased to 3
      334 |                 if( geode::point_point_distance(
          |                 ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:347:9: note: +1, including nesting penalty of 0, nesting level increased to 1
      347 |         for( const auto v : geode::LRange{ 3 } )
          |         ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:349:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      349 |             if( !colocated.first[v] )
          |             ^
    /__w/OpenGeode/OpenGeode/src/geode/geometry/distance.cpp:353:13: note: +2, including nesting penalty of 1, nesting level increased to 2
      353 |             if( !colocated.second[v] )
          |             ^
  • src/geode/geometry/distance.cpp:373:25: warning: [readability-identifier-length]

    variable name 'v0' is too short, expected at least 3 characters

      373 |         for( const auto v0 : geode::LRange{ 3 } )
          |                         ^
  • src/geode/geometry/distance.cpp:381:29: warning: [readability-identifier-length]

    variable name 'v1' is too short, expected at least 3 characters

      381 |             for( const auto v1 : geode::LRange{ 3 } )
          |                             ^
  • src/geode/geometry/distance.cpp:463:71: warning: [readability-identifier-length]

    parameter name 'u' is too short, expected at least 3 characters

      463 |     SegmentSegmentTerms segment_segment_terms( const geode::Vector2D& u,
          |                                                                       ^
  • src/geode/geometry/distance.cpp:476:20: warning: [readability-identifier-length]

    variable name 'uv' is too short, expected at least 3 characters

      476 |         const auto uv = u.cross( v );
          |                    ^

Have any feedback or feature suggestions? Share it here.

Choosing the edge pair adjacent to the vertex opposite the longest edge
only improves precision below GLOBAL_EPSILON, and its data-dependent
selects compiled to unpredictable branches: triangle_area was 1.8x slower
than the Heron version on random triangles. Use the edges at vertex 0.

Benchmark vs next (ns/call): 3D random 11.6 -> 9.3, 2D random 10.5 -> 5.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@panquez panquez left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the most important thing is to think about making comparison with epsilon.

Comment thread src/geode/geometry/bounding_box.cpp
Comment thread src/geode/geometry/sign.cpp Outdated
Comment thread src/geode/geometry/distance.cpp Outdated
Comment thread src/geode/geometry/distance.cpp
Comment thread tests/geometry/test-basic-objects.cpp
Comment thread tests/geometry/test-distance.cpp
Comment thread tests/geometry/test-distance.cpp Outdated
Comment thread tests/geometry/test-mensuration.cpp Outdated
Comment thread tests/geometry/test-mensuration.cpp Outdated
Comment thread tests/geometry/test-mensuration.cpp Outdated
When the corner at the lexicographically smallest vertex was flat (spike),
polygon_area_sign fell back to the first non-null fan triangle, which is
wrong for concave polygons. Such a vertex does not change the polygon signed
area: remove it and look at the new smallest vertex.
Document the result for self-intersecting polygons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BotellaA and others added 4 commits October 14, 2026 12:31
Replace the dedicated 1e-20 relative threshold on sin^2(angle) by a
geometric one: segments are parallel when each one deviates from the
direction of the other by less than GLOBAL_EPSILON
(det < GLOBAL_EPSILON^2 * min(a, c)). Segment distance tests now check
results up to GLOBAL_EPSILON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the hard-coded 1e-9 / 1e-12 tolerances by GLOBAL_EPSILON, scaled by
the longest edge for areas: below GLOBAL_EPSILON, values are considered
equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- rename the needle triangle to cap (one angle close to 180 degrees) and
  test a segment crossing it in its plane;
- test the polygon normal on a flat polygon not aligned with the axes;
- test segments crossing a triangle close to their first vertex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BotellaA
BotellaA requested a review from panquez October 14, 2026 10:41
BotellaA and others added 4 commits October 14, 2026 23:07
* fix(Intersection): handle degenerated segments as points

segment_triangle_distance and the segment_{plane,sphere,cylinder,ellipse}
intersections, as well as 2D segment_segment and segment_line
intersections, built an InfiniteLine from the segment, which throws from
Segment::direction() when the segment length is below GLOBAL_EPSILON.
Such segments are now treated as their barycenter: the result is the
point-to-object distance, or a single intersection point when the point
lies on the object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Distance): skip degenerated edges in triangle_triangle_distance

test_close_triangles printed leftover DEBUG/SDEBUG output when two
vertices of a triangle were colocated, then still computed the distance
from that zero-length edge (which used to throw from
Segment::direction()). Degenerated edges are now skipped: their
vertices are covered by the other edges of the triangle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(Distance): test each triangle edge once in triangle_triangle_distance

test_close_triangles iterated over all ordered vertex pairs, computing
segment_triangle_distance twice per edge. Only unordered pairs are now
tested, halving the work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Distance): avoid out-of-bounds access on NaN in point_triangle_distance

For degenerated triangles, no_pivot_point_triangle_distance initialized
the selected edge to NO_LID and only updated it on a strictly smaller
distance. With NaN coordinates no comparison succeeds and the edge array
was read out of bounds. The selected edge now defaults to the first one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Distance): handle flat triangles in 2D point_triangle_distance

The in-triangle test relies on the signs of three signed areas, which
are all zero for any point on the supporting line of a collinear (or
point) triangle, so such points were reported at distance 0. Triangles
whose minimum height is below GLOBAL_EPSILON (compared with squared
quantities) now skip the inside test and use the edge distances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Triangle): base is_degenerated on the minimum height

Triangle::is_degenerated only measured the distance from the third
vertex to the line of the first edge, so a triangle whose minimum height
is below GLOBAL_EPSILON could be reported valid while normal() returns
nullopt. Tetrahedron::is_degenerated then built a Plane from that
triangle and threw bad_optional_access.

Both now compare their minimum height (2*area / longest edge for
triangles, 3*volume / largest facet area for tetrahedra) to
GLOBAL_EPSILON, using squared quantities to avoid square roots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Polygon): use the farthest vertex in minimum_height

minimum_height measured the distance from the longest edge line to an
arbitrary vertex (the last one not on that edge), so a valid polygon
with one vertex near the longest edge line, e.g. (0,0),(10,0),(5,5),
(-1,0), was reported with height 0 and as degenerated. The height is now
the maximum distance over all vertices not on the longest edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(NNSearch): handle empty point sets and zero neighbor requests

closest_neighbor called front() on an empty result when the search had
no point, and neighbors(point, 0) passed &vector[0] of empty vectors to
nanoflann, whose k-NN result set writes at index k-1. closest_neighbor
now returns NO_ID on an empty set (as AABBTree::closest_element_box
does), and neighbors returns an empty vector for k = 0 or an empty set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(SquareMatrix): implement 1x1 determinant and inverse

SquareMatrix<1> is explicitly instantiated but determinant() and
inverse() threw "Not implemented", so CoordinateSystem1D::coordinates
always threw. Both are now implemented for dimension 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Rotation): avoid infinite loop on huge angles in rotate

rotate normalized the angle with while loops adding or removing 2*PI,
which never terminate for infinite angles or angles whose ulp exceeds
2*PI (e.g. 1e17). The normalization is useless since the rotation
matrix is quadratic in the quaternion, so it is removed. The axis check
compared a squared length to GLOBAL_EPSILON, rejecting valid axes
shorter than 1e-3; any non-null finite axis is now accepted. The
homogeneous coordinate is always 1, so its check and division are
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Intersection): fix line_cylinder_intersection perpendicular case

When the line was perpendicular to the cylinder axis, the computed wall
intersections fell through to the end-disk code, dividing by D.z == 0
and appending parameters past the 2-element array. The perpendicular
case is now handled on its own, using the line-to-axis distance with an
epsilon tolerance so tangent lines give a single point. Parameter
insertion is bounded.

Correctness bookkeeping is also fixed: the line correctness flag was
overwritten instead of AND-accumulated and both correctness lists
received extra points; each now holds exactly one entry per result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Intersection): keep correctness in sync with filtered results

segment_sphere, segment_cylinder and segment_ellipse intersections
filtered the line intersections lying on the segment but returned the
unfiltered correctness lists, so correctness points no longer matched
the result points. Correctness entries are now filtered in lockstep.

plane_circle_intersection overwrote its correctness flags for each
point instead of AND-accumulating them, and triangle_circle_intersection
ignored the circle correctness flag of the plane-circle result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Intersection): stable discriminant in line_sphere_intersection

The discriminant |P-C|^2 - (D.(P-C))^2 - r^2 suffered catastrophic
cancellation when the line origin was far from the sphere (no or wrong
intersections for an origin at 1e8), and compared an area to
GLOBAL_EPSILON, reporting tangency for lines 0.4e-4 away from a sphere
of radius 1e-4. The sphere center is now projected on the line, the
distance h is computed directly, the tangency test compares the length
r - h to GLOBAL_EPSILON, and the roots use (r-h)*(r+h) from the
projected point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Circle): fix bounding_box for normals opposite to an axis

The sine of the angle between the circle normal and an axis returned
M_PI instead of 0 when the normal was opposite to that axis, inflating
the bounding box along that axis by PI times the radius.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(BoundingBox): scale-independent triangle epsilon_intersects

The separating axis tests of BoundingBox3D::intersects and
epsilon_intersects with a Triangle3D used unnormalized axes (triangle
normal, edge x box axis cross products), so GLOBAL_EPSILON was compared
to projections scaled by |axis| (up to edge length squared): large
triangles 0.5e-6 away from a box were not epsilon-intersecting. The
tolerance is now scaled by the axis length. The triangle normal test
projects all three vertices instead of only the first one, which is
wrong when the normal is noisy, and the box face tests, redundant with
the triangle bounding box test, are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(BoundingBox): fix 1D ray and line intersections

The 1D line test returned true only when the origin was before the box
in the line direction, so rays starting inside the box were reported as
not intersecting, and infinite lines (which cover the whole axis) as
well. 1D lines now always intersect; rays pointing away from the box are
rejected by the existing ray quick-skip test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Quality): scale-invariant degeneracy in tetrahedron qualities

tetrahedron_aspect_ratio and tetrahedron_collapse_aspect_ratio compared
a determinant / volume (cubic in length) to GLOBAL_EPSILON, so a regular
tetrahedron with edge 0.01 was reported as degenerated. The volume ratio
qualities returned NaN (0/0) when vertices were coincident. All four now
use Tetrahedron::is_degenerated (minimum height below GLOBAL_EPSILON):
degenerated tetrahedra get the worst value (max for aspect ratios, 0 for
volume ratios).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(IsPointInside): exact predicates and consistent boundary handling

is_point_inside_polygon used an inexact floating-point orientation test,
and boundary points were inside or outside depending on which edge they
lay on (e.g. bottom edge inside, top edge outside for a unit square).
The winding number now uses GEO::PCK::orient_2d, and points on the
polygon boundary are always considered inside, independently of the
polygon orientation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Distance): compute point-line/segment distances by projection

point_line_distance and point_segment_distance used Heron's formula on
the three side lengths, whose cancellation gives errors above
GLOBAL_EPSILON (4e-6 measured) for points far along the line. Both now
measure the distance to the projection, so they are accurate and
consistent with point_line_projection / point_segment_projection.
point_segment_projection computes the clamped parameter directly
instead of normalizing the direction, and degenerated segments use
their barycenter for both distance and projection (distance used the
first vertex).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Point): combine coordinates hashes with absl::Hash

std::hash<Point2D/3D> XOR-ed the per-coordinate hashes, so every point
with equal coordinates hashed to 0 and permuted coordinates collided
((1,2) and (2,1)). They now delegate to absl::Hash<Point>, which
combines the coordinate array through AbslHashValue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Predicates): statically initialize expansion constants

The exact arithmetic splitter and epsilon were zero until
GEO::PCK::initialize() ran (through OpenGeodeGeometryLibrary::initialize).
Without it, two_product produced wrong error terms and exact orient_2d /
orient_3d returned zero or wrong signs for near-degenerate inputs; this
was the case in geometry unit tests, which only initialize the basic
library. The constants are now statically initialized to their IEEE-754
values (2^27 + 1 and 2^-53).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Predicates): disable FP contraction for exact predicates

The exact arithmetic of predicates.cpp (two_product, expansion sums)
relies on every operation being rounded; contracting a*b+c into fused
multiply-add breaks it. Only MSVC was protected by a pragma. The file is
now compiled with -ffp-contract=off on GCC and Clang (and excluded from
unity builds so the option applies to it alone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(Geometry): add tier2-robustness benchmark

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(Distance): reuse Triangle::is_degenerated in 2D point_triangle_distance

is_triangle_flat duplicated the Triangle2D::is_degenerated formula.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Intersection): report the closest cylinder point for degenerated segments

point_cylinder_distance moves to the public distance API and returns the
closest point on the cylinder surface, so the degenerated segment/cylinder
correctness point lies on the cylinder like for the other objects.
degenerated_segment_intersection parameters are renamed to match all
call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Intersection): length tolerances in line_cylinder_intersection

All branches now compare lengths against GLOBAL_EPSILON (wall, end disks,
parallel case), and parameters closer than GLOBAL_EPSILON are merged: a
line through an end disk rim reported the rim point twice and lost its
second intersection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Rotation): throw on non-finite angle in rotate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(IsPointInside): run exact orientation only on edges spanning the point row

20-vertex star: 157 ns -> 74 ns per call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(Geometry): remove tier2-robustness benchmark

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Tetrahedron): include opposite edges in is_degenerated

A tetrahedron is degenerated if its vertices lie in a slab of thickness
GLOBAL_EPSILON. The thinnest slab may be bounded by planes containing two
opposite edges instead of a facet: slivers of thickness below
GLOBAL_EPSILON have heights up to twice as large and were not reported.
Document the strip/slab definitions of Triangle and Tetrahedron.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(Geometry): add zero-filter and missing filters to exact predicates

Exactly degenerate inputs (duplicated points, points sharing coordinates)
always fell back to expansion arithmetic since the floating-point filters
cannot certify a zero sign. A zero-filter (Bartels et al. 2022) now
decides them before the exact computation.

aligned_3d had no filter and dot_2d had none: both always used exact
arithmetic. aligned_3d now concludes as soon as one cross product
component is certified non-zero; dot_2d gets the orient_2d error bound.

Speedups: aligned_3d ~350 -> ~10 ns, dot_2d ~110 -> ~10 ns,
orient_3d on exactly coplanar points ~380 -> ~25 ns. Generic inputs
unchanged, results identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Geometry): exact predicates no longer depend on library initialization

The splitter and epsilon used by the expansion arithmetic were only set
by GEO::PCK::initialize(), called by OpenGeodeGeometryLibrary::initialize().
Code using the geometry library without initializing it (e.g. geometry
tests, which only initialize the basic library) silently got wrong exact
signs: two_product() splits with a zero splitter. They are now constant
initialized with their IEEE 754 double values.

The new consistency test (orient_2d/orient_3d under point permutations on
nearly degenerate inputs) failed before this fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(Geometry): add Shewchuk adaptive stages to orient_2d and orient_3d

When the floating-point filter is uncertain, stages B and C of
Shewchuk's adaptive predicates (public domain predicates.c) are now
evaluated before the full exact computation: stage B computes exactly
the determinant of the rounded coordinate differences with short fixed
size expansions, stage C concludes if these differences were exact or
adds the first order corrections of their roundoff errors.

Nearly degenerate and exactly degenerate (without shared coordinates)
configurations are 2.5x to 4x faster, e.g. orient_3d on nearly coplanar
points 660 -> 270 ns, orient_2d on nearly collinear points 128 -> 41 ns.
Configurations still undecided after stage C pay the stages overhead
(~10% slower) as stage D is not ported. Generic inputs unchanged; the
signs of all benchmark calls are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(Geometry): add Shewchuk stage D to orient_2d and orient_3d

When stages B and C cannot decide, stage D now adds exactly all the
remaining roundoff error terms to the stage B expansion, instead of
restarting a full exact evaluation. orient_2d_exact and orient_3d_exact
are no longer used and removed.

This removes the slowdown of configurations undecided after stage C:
orient_3d on nearly collinear points 617 -> 473 ns (574 ns before the
adaptive stages). Other timings unchanged. Signs checked against
Shewchuk's original predicates.c on 20M hard cases (mixed magnitudes,
exact and nearly degenerate configurations): no mismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(Geometry): exhaustive non-regression tests for exact predicates

test-predicates now checks every predicate against references that do not
depend on the implementation:
- integer configurations (random, exactly degenerate, one unit away from
  degenerate, shared coordinates, duplicated points), at usual scales and
  at scales below the floating-point filters underflow guard, compared
  with exact 64 bits integer determinants;
- a table of configurations decided by each evaluation stage (filter,
  zero-filter, adaptive stages B, C and D, exact evaluation), including
  configurations where plain floating-point evaluation gives a wrong sign,
  with signs checked with exact rational arithmetic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Geometry): adaptive predicates return zero on a zero estimate

sign_of() returned negative for a zero estimate, reachable when all the
products underflow. Also share the double epsilon constant, return a
bool from the aligned_3d filter and document the array expansion
helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(Geometry): export exact predicates for Windows test linking

test-predicates calls GEO::PCK functions directly: they need to be
exported from the geometry DLL on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(Geometry): move exact predicates out of internal

The GEO::PCK predicates are now exported: predicates.hpp becomes a
public header and predicates.cpp leaves the internal folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(Geometry): move exact predicates to detail

The exported GEO::PCK predicates embed Geogram definitions: they are
advanced API, not public API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(Distance): iterate edge vertex pairs with geode::Range

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(IsPointInside): check collinearity inside is_point_on_edge

is_point_on_edge only checked the edge bounding box and relied on its
caller to have checked that the point is on the edge line. It now takes
the exact orientation and checks both, so the function does what its name
says. Tests cover points inside the bounding box of a diagonal edge but
off it, and name the one-ulp moves in the exact predicate tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(Geometry): check is_degenerated against vertex order

Triangles (cap, obtuse, needle) and tetrahedra (cap, wedge, sliver) of
thickness 0.5, 0.9, 1.1 and 2 GLOBAL_EPSILON, in a rotated frame offset by
up to 1e6, give the expected result for every vertex permutation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(Geometry): express tolerances with GLOBAL_EPSILON

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(Intersection): split degenerated segment tests by object

One function per intersected object, with lengths and offsets expressed
with GLOBAL_EPSILON. The degenerated segment is closer than GLOBAL_EPSILON
to the cylinder, so its correctness point is compared exactly to the
closest point on the cylinder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Replace internal predicates include with detail predicates

* Refactor double initialization in predicates.cpp

Replaced static double initialization with constexpr for DOUBLE_EPSILON and updated expansion_splitter and expansion_epsilon to use these constants.

* Apply prepare changes

* Add test_set_point function to test-triangle.cpp

* Apply prepare changes

* Add test for point skewed tetrahedron distance

* Apply prepare changes

* fix

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The merge from next moved predicates.cpp from internal/ to detail/, but
SKIP_UNITY and the -ffp-contract=off source property still named the old
path, so both silently applied to nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BotellaA
BotellaA merged commit ca7ae14 into next Oct 15, 2026
14 of 17 checks passed
@BotellaA
BotellaA deleted the fix/geometry-robustness-tier1 branch October 15, 2026 14:04
@BotellaA

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 19.0.1-rc.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants