From 2e087354111f41ff333a390064df33995ea0a7da Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:54:09 +0200 Subject: [PATCH 01/20] fix(Manifold): edges shared by more than 255 polygons are not manifold The number of polygons around an edge was stored in a local_index_t: an edge shared by 257 polygons wrapped to 1 and was seen as a border edge. Co-Authored-By: Claude Opus 5.5 --- .../manifold/surface_edge_manifold.cpp | 6 ++--- tests/inspection/test-surface-manifold.cpp | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp index bc798394..cbfdcfeb 100644 --- a/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp @@ -37,10 +37,10 @@ namespace template < geode::index_t dimension > [[nodiscard]] absl::linked_hash_map< Edge, - std::pair< geode::local_index_t, bool > > + std::pair< geode::index_t, bool > > edge_to_polygons_around( const geode::SurfaceMesh< dimension >& mesh ) { - absl::linked_hash_map< Edge, std::pair< geode::local_index_t, bool > > + absl::linked_hash_map< Edge, std::pair< geode::index_t, bool > > polygons_around_edges; for( const auto polygon_id : geode::Range{ mesh.nb_polygons() } ) { @@ -53,7 +53,7 @@ namespace }; if( !polygons_around_edges .try_emplace( polygon_edge_vertex_cycle, - std::pair< geode::local_index_t, bool >{ + std::pair< geode::index_t, bool >{ 1, false } ) .second ) { diff --git a/tests/inspection/test-surface-manifold.cpp b/tests/inspection/test-surface-manifold.cpp index a11f34f9..114ec37e 100644 --- a/tests/inspection/test-surface-manifold.cpp +++ b/tests/inspection/test-surface-manifold.cpp @@ -137,6 +137,31 @@ void check_edge_non_manifold2D() "Surface edges are shown non manifold whereas they are." ); } +void check_edge_with_many_polygons() +{ + // 257 triangles around edge (0, 1) + auto surface = geode::TriangulatedSurface2D::create(); + auto builder = geode::TriangulatedSurfaceBuilder2D::create( *surface ); + builder->create_point( geode::Point2D{ { 0., 0. } } ); + builder->create_point( geode::Point2D{ { 1., 0. } } ); + constexpr geode::index_t NB_TRIANGLES{ 257 }; + for( const auto t : geode::Range{ NB_TRIANGLES } ) + { + const auto vertex = builder->create_point( + geode::Point2D{ { 0.5, 1. + static_cast< double >( t ) } } ); + builder->create_triangle( { 0, 1, vertex } ); + } + const geode::SurfaceMeshEdgeManifold2D manifold_inspector{ *surface }; + geode::OpenGeodeInspectorInspectionException::test( + !manifold_inspector.mesh_edges_are_manifold(), + "[Test] Surface with an edge shared by ", NB_TRIANGLES, + " triangles is shown as manifold." ); + geode::OpenGeodeInspectorInspectionException::test( + manifold_inspector.non_manifold_edges().nb_issues() == 1, + "[Test] Surface with an edge shared by ", NB_TRIANGLES, + " triangles should have one non manifold edge." ); +} + int main() { try @@ -146,6 +171,7 @@ int main() check_vertex_non_manifold2D(); check_edge_manifold2D(); check_edge_non_manifold2D(); + check_edge_with_many_polygons(); geode::Logger::info( "TEST SUCCESS" ); return 0; From 684af55a257a1bf36cadb521fe363ec44d162d31 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:55:38 +0200 Subject: [PATCH 02/20] fix(Adjacency): out of range adjacencies are reported instead of crashing The adjacent polygon/polyhedron index was used without bounds check (OpenGeode only asserts it in debug): a stale adjacency read out of bounds and crashed in release. It is now reported as a wrong adjacency. Co-Authored-By: Claude Opus 5.5 --- .../criterion/adjacency/solid_adjacency.cpp | 4 +++ .../criterion/adjacency/surface_adjacency.cpp | 4 +++ tests/inspection/test-solid-adjacency.cpp | 26 +++++++++++++++++++ tests/inspection/test-surface-adjacency.cpp | 23 ++++++++++++++++ 4 files changed, 57 insertions(+) diff --git a/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp b/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp index 5762d376..734210a6 100644 --- a/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp @@ -92,6 +92,10 @@ namespace geode const auto adjacent_polyhedron = mesh_.polyhedron_adjacent( polyhedron_facet ); const auto polyhedron_adj_id = adjacent_polyhedron.value(); + if( polyhedron_adj_id >= mesh_.nb_polyhedra() ) + { + return false; + } const auto facet_vertices = detail::OrientedVertexCycle< PolyhedronFacetVertices >( mesh_.polyhedron_facet_vertices( polyhedron_facet ) ); diff --git a/src/geode/inspector/inspection/criterion/adjacency/surface_adjacency.cpp b/src/geode/inspector/inspection/criterion/adjacency/surface_adjacency.cpp index 2be4f71f..ad0c8458 100644 --- a/src/geode/inspector/inspection/criterion/adjacency/surface_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/adjacency/surface_adjacency.cpp @@ -87,6 +87,10 @@ namespace geode const auto adjacent_polygon = mesh_.polygon_adjacent( polygon_edge ); const auto polygon_adj_id = adjacent_polygon.value(); + if( polygon_adj_id >= mesh_.nb_polygons() ) + { + return false; + } const auto v0 = mesh_.polygon_edge_vertex( polygon_edge, 0 ); const auto v1 = mesh_.polygon_edge_vertex( polygon_edge, 1 ); for( const auto edge_id : diff --git a/tests/inspection/test-solid-adjacency.cpp b/tests/inspection/test-solid-adjacency.cpp index 76fba129..c8e8f2d4 100644 --- a/tests/inspection/test-solid-adjacency.cpp +++ b/tests/inspection/test-solid-adjacency.cpp @@ -30,6 +30,7 @@ #include #include +#include #include void check_adjacency() @@ -166,6 +167,29 @@ void check_non_adjacency_inversed_tetrahedron() "an inversed tetrahedron." ); } +void check_adjacency_out_of_range() +{ + auto solid = geode::TetrahedralSolid3D::create(); + auto builder = geode::TetrahedralSolidBuilder3D::create( *solid ); + builder->create_vertices( 4 ); + builder->set_point( 0, geode::Point3D{ { 0., 0., 2. } } ); + builder->set_point( 1, geode::Point3D{ { 3., .5, 0. } } ); + builder->set_point( 2, geode::Point3D{ { .5, 3., .5 } } ); + builder->set_point( 3, geode::Point3D{ { 2., 1.5, 3. } } ); + builder->create_tetrahedron( { 0, 1, 2, 3 } ); + // Stale adjacency to a polyhedron which does not exist + builder->set_polyhedron_adjacent( { 0, 0 }, 1000000 ); + + const geode::SolidMeshAdjacency3D adjacency_inspector{ *solid }; + geode::OpenGeodeInspectorInspectionException::test( + adjacency_inspector.mesh_has_wrong_adjacencies(), + "Solid with out of range adjacency should have wrong adjacencies." ); + geode::OpenGeodeInspectorInspectionException::test( + adjacency_inspector.polyhedron_facets_with_wrong_adjacency().nb_issues() + == 1, + "Solid with out of range adjacency should have one wrong facet." ); +} + int main() { try @@ -177,6 +201,8 @@ int main() check_non_adjacency_wrong_facet(); check_non_adjacency_inversed_tetrahedron(); + check_adjacency_out_of_range(); + geode::Logger::info( "TEST SUCCESS" ); return 0; } diff --git a/tests/inspection/test-surface-adjacency.cpp b/tests/inspection/test-surface-adjacency.cpp index 8cc7a74e..56e0da86 100644 --- a/tests/inspection/test-surface-adjacency.cpp +++ b/tests/inspection/test-surface-adjacency.cpp @@ -294,6 +294,27 @@ void check_non_adjacency_inversed_triangle3D() "to an inversed triangle.." ); } +void check_adjacency_out_of_range() +{ + auto surface = geode::TriangulatedSurface2D::create(); + auto builder = geode::TriangulatedSurfaceBuilder2D::create( *surface ); + builder->create_point( geode::Point2D{ { 0., 0. } } ); + builder->create_point( geode::Point2D{ { 1., 0. } } ); + builder->create_point( geode::Point2D{ { 0., 1. } } ); + builder->create_triangle( { 0, 1, 2 } ); + // Stale adjacency to a polygon which does not exist + builder->set_polygon_adjacent( { 0, 0 }, 1000000 ); + + const geode::SurfaceMeshAdjacency2D adjacency_inspector{ *surface }; + geode::OpenGeodeInspectorInspectionException::test( + adjacency_inspector.mesh_has_wrong_adjacencies(), + "Surface with out of range adjacency should have wrong adjacencies." ); + geode::OpenGeodeInspectorInspectionException::test( + adjacency_inspector.polygon_edges_with_wrong_adjacency().nb_issues() + == 1, + "Surface with out of range adjacency should have one wrong edge." ); +} + int main() { try @@ -308,6 +329,8 @@ int main() check_non_adjacency_wrong_edge3D(); check_non_adjacency_inversed_triangle3D(); + check_adjacency_out_of_range(); + geode::Logger::info( "TEST SUCCESS" ); return 0; } From 488cf683ef73da0d48bdd839a244e6545fe82a14 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:57:07 +0200 Subject: [PATCH 03/20] fix(Manifold): model edges without unique vertices are not compared Surface edges whose vertices are not linked to unique vertices all became the model edge (NO_ID, NO_ID), shared by every such surface, and were reported as non manifold. They are skipped: unlinked vertices are reported by the topology inspection. Co-Authored-By: Claude Opus 5.5 --- .../manifold/brep_meshes_manifold.cpp | 6 ++ tests/inspection/CMakeLists.txt | 10 +++ .../inspection/test-brep-meshes-criteria.cpp | 89 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 tests/inspection/test-brep-meshes-criteria.cpp diff --git a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp index 8f35aa8e..8fe82952 100644 --- a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp @@ -178,6 +178,12 @@ namespace geode vertices[edge_id == vertices.size() - 1 ? 0 : edge_id + 1] } ); + // Unlinked vertices are reported by the topology + // inspection, they cannot be compared here + if( vertex0 == NO_ID || vertex1 == NO_ID ) + { + continue; + } const auto info = edges.try_emplace( Edge{ std::array{ vertex0, vertex1 } }, std::vector{ surface.id() } ); diff --git a/tests/inspection/CMakeLists.txt b/tests/inspection/CMakeLists.txt index f51c1a42..4b1b8f2c 100755 --- a/tests/inspection/CMakeLists.txt +++ b/tests/inspection/CMakeLists.txt @@ -143,6 +143,16 @@ add_geode_test( ${PROJECT_NAME}::inspection ) +add_geode_test( + SOURCE "test-brep-meshes-criteria.cpp" + DEPENDENCIES + OpenGeode::basic + OpenGeode::geometry + OpenGeode::mesh + OpenGeode::model + ${PROJECT_NAME}::inspection +) + add_geode_test( SOURCE "test-solid-negative-elements.cpp" DEPENDENCIES diff --git a/tests/inspection/test-brep-meshes-criteria.cpp b/tests/inspection/test-brep-meshes-criteria.cpp new file mode 100644 index 00000000..d6b1433d --- /dev/null +++ b/tests/inspection/test-brep-meshes-criteria.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019 - 2026 Geode-solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace +{ + // Two triangles sharing an edge, vertices not linked to unique vertices + const geode::Surface3D& add_square_surface( + const geode::BRep& brep, geode::BRepBuilder& builder, double z ) + { + const auto& surface = brep.surface( builder.add_surface() ); + auto mesh_builder = builder.surface_mesh_builder( surface ); + mesh_builder->create_point( geode::Point3D{ { 0, 0, z } } ); + mesh_builder->create_point( geode::Point3D{ { 1, 0, z } } ); + mesh_builder->create_point( geode::Point3D{ { 0, 1, z } } ); + mesh_builder->create_point( geode::Point3D{ { 1, 1, z } } ); + mesh_builder->create_polygon( { 0, 1, 2 } ); + mesh_builder->create_polygon( { 1, 3, 2 } ); + mesh_builder->compute_polygon_adjacencies(); + return surface; + } +} // namespace + +void check_model_edges_without_unique_vertices() +{ + geode::BRep brep; + geode::BRepBuilder builder{ brep }; + add_square_surface( brep, builder, 0 ); + add_square_surface( brep, builder, 1 ); + const geode::BRepComponentMeshesManifold inspector{ brep }; + const auto result = inspector.inspect_brep_manifold(); + geode::OpenGeodeInspectorInspectionException::test( + result.brep_non_manifold_edges.nb_issues() == 0, + "[Test] Edges without unique vertices should not be seen as shared " + "by several surfaces: ", + result.brep_non_manifold_edges.string() ); +} + +int main() +{ + try + { + geode::OpenGeodeInspectorInspectionLibrary::initialize(); + check_model_edges_without_unique_vertices(); + + geode::Logger::info( "TEST SUCCESS" ); + return 0; + } + catch( ... ) + { + return geode::geode_lippincott(); + } +} From df1365a5e886840bafa9995b8696dacdc014337d Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:57:45 +0200 Subject: [PATCH 04/20] fix(Manifold): single element components without unique vertices no longer crash several_cmvs_on_one_vertex called component_mesh_vertices(NO_ID), only guarded by a debug assertion: unlinked vertices of a single edge internal Line (or single polygon internal Surface) read out of bounds in release. Unlinked vertices are skipped, they are reported by the topology inspection. Co-Authored-By: Claude Opus 5.5 --- .../manifold/brep_meshes_manifold.cpp | 7 +++--- .../inspection/test-brep-meshes-criteria.cpp | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp index 8fe82952..a244752e 100644 --- a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp @@ -285,11 +285,12 @@ namespace geode [[nodiscard]] bool several_cmvs_on_one_vertex( absl::Span< const index_t > unique_vertices ) const { - std::vector< uuid > surfaces; for( const auto unique_vertex : unique_vertices ) { - if( model().component_mesh_vertices( unique_vertex ).size() - > 1 ) + // Unlinked vertices are reported by the topology inspection + if( unique_vertex == NO_ID + || model().component_mesh_vertices( unique_vertex ).size() + > 1 ) { return true; } diff --git a/tests/inspection/test-brep-meshes-criteria.cpp b/tests/inspection/test-brep-meshes-criteria.cpp index d6b1433d..35b2bdba 100644 --- a/tests/inspection/test-brep-meshes-criteria.cpp +++ b/tests/inspection/test-brep-meshes-criteria.cpp @@ -72,12 +72,34 @@ void check_model_edges_without_unique_vertices() result.brep_non_manifold_edges.string() ); } +void check_single_edge_line_without_unique_vertices() +{ + geode::BRep brep; + geode::BRepBuilder builder{ brep }; + const auto& surface = add_square_surface( brep, builder, 0 ); + const auto& line = brep.line( builder.add_line() ); + auto mesh_builder = builder.line_mesh_builder( line ); + mesh_builder->create_point( geode::Point3D{ { 0, 0, 0 } } ); + mesh_builder->create_point( geode::Point3D{ { 1, 1, 0 } } ); + mesh_builder->create_edge( 0, 1 ); + builder.add_line_surface_internal_relationship( line, surface ); + builder.create_unique_vertices( 1 ); + const geode::BRepComponentMeshesManifold inspector{ brep }; + const auto result = inspector.inspect_brep_manifold(); + geode::OpenGeodeInspectorInspectionException::test( + result.brep_non_manifold_edges.nb_issues() == 0, + "[Test] Single edge line without unique vertices should not be " + "checked: ", + result.brep_non_manifold_edges.string() ); +} + int main() { try { geode::OpenGeodeInspectorInspectionLibrary::initialize(); check_model_edges_without_unique_vertices(); + check_single_edge_line_without_unique_vertices(); geode::Logger::info( "TEST SUCCESS" ); return 0; From e4d4427bd0a1b0f3c142d8a746dffb5e1a443ee6 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 18:58:44 +0200 Subject: [PATCH 05/20] fix(Manifold): broken adjacencies make solid edges non manifold instead of throwing mesh_edges_are_manifold let the polyhedra_around_edge exception escape on broken adjacencies, while non_manifold_edges already reports them. Co-Authored-By: Claude Opus 5.5 --- .../manifold/solid_edge_manifold.cpp | 18 +++++++++++---- tests/inspection/test-solid-manifold.cpp | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp index 23f1296d..6358afca 100644 --- a/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp @@ -105,11 +105,21 @@ namespace geode { continue; } - const auto polyhedra_around = mesh_.polyhedra_around_edge( - polyhedron_edge_vertices, polyhedron_id ); - if( !polyhedra_around_edge_are_the_same( polyhedra_around, - polyhedra_around_edges_.at( polyhedron_edge ) ) ) + try + { + const auto polyhedra_around = + mesh_.polyhedra_around_edge( + polyhedron_edge_vertices, polyhedron_id ); + if( !polyhedra_around_edge_are_the_same( + polyhedra_around, polyhedra_around_edges_.at( + polyhedron_edge ) ) ) + { + return false; + } + } + catch( const OpenGeodeException& ) { + // Broken adjacencies around the edge return false; } } diff --git a/tests/inspection/test-solid-manifold.cpp b/tests/inspection/test-solid-manifold.cpp index 7d3c6143..278c36fe 100644 --- a/tests/inspection/test-solid-manifold.cpp +++ b/tests/inspection/test-solid-manifold.cpp @@ -186,6 +186,27 @@ void check_facet_non_manifold() "Solid shows wrong non manifold facet id." ); } +void check_edge_manifold_broken_adjacency() +{ + auto solid = geode::TetrahedralSolid3D::create(); + auto builder = geode::TetrahedralSolidBuilder3D::create( *solid ); + builder->create_vertices( 5 ); + builder->set_point( 0, geode::Point3D{ { 0., 0., 2. } } ); + builder->set_point( 1, geode::Point3D{ { 3., .5, 0. } } ); + builder->set_point( 2, geode::Point3D{ { .5, 3., .5 } } ); + builder->set_point( 3, geode::Point3D{ { 2., 1.5, 3. } } ); + builder->set_point( 4, geode::Point3D{ { 3.5, 2.5, -.5 } } ); + builder->create_tetrahedron( { 0, 1, 2, 3 } ); + builder->create_tetrahedron( { 1, 4, 2, 3 } ); + // One-way adjacency + builder->set_polyhedron_adjacent( { 0, 0 }, 1 ); + + const geode::SolidMeshEdgeManifold3D manifold_inspector{ *solid }; + geode::OpenGeodeInspectorInspectionException::test( + !manifold_inspector.mesh_edges_are_manifold(), + "[Test] Solid with broken adjacency is shown as manifold." ); +} + int main() { try @@ -198,6 +219,8 @@ int main() check_facet_manifold(); check_facet_non_manifold(); + check_edge_manifold_broken_adjacency(); + geode::Logger::info( "TEST SUCCESS" ); return 0; } From 5b984afa0b026bb7305f9d1b1629e3e2d9521216 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:02:47 +0200 Subject: [PATCH 06/20] perf(Manifold): solid vertex manifold without polyhedra around vertex cache Each vertex called SolidMesh::polyhedra_around_vertex, which allocates a hash set and a stack, fills the mesh lazy cache (racy when other inspections run concurrently) and throws on broken adjacencies. The same propagation through facet adjacencies is now done locally, without allocation for usual vertices, in parallel over vertices. It starts from the first polyhedron containing the vertex instead of the mesh stored one. Solid vertex manifold: cva/brep_from_topo blocks 0.37s -> 0.15s, corbi_tet_mesh 0.11s -> 0.03s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../manifold/solid_vertex_manifold.cpp | 102 ++++++++++++++---- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/solid_vertex_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/solid_vertex_manifold.cpp index 7d767281..7c78a404 100644 --- a/src/geode/inspector/inspection/criterion/manifold/solid_vertex_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/solid_vertex_manifold.cpp @@ -24,6 +24,9 @@ #include #include +#include + +#include #include @@ -34,8 +37,8 @@ namespace { bool polyhedra_around_vertex_are_the_same( - const geode::PolyhedraAroundVertex& pav1, - const geode::PolyhedraAroundVertex& pav2 ) + absl::Span< const geode::PolyhedronVertex > pav1, + absl::Span< const geode::PolyhedronVertex > pav2 ) { if( pav1.size() != pav2.size() ) { @@ -70,6 +73,81 @@ namespace } return polyhedra_around_vertices_list; } + + // Propagates around the vertex from its first polyhedron through the + // adjacencies of the facets containing it, like + // SolidMesh::polyhedra_around_vertex but without its cache: the vertex is + // manifold if every polyhedron containing it is reached. + template < geode::index_t dimension > + bool vertex_is_manifold( const geode::SolidMesh< dimension >& mesh, + geode::index_t vertex_id, + absl::Span< const geode::PolyhedronVertex > polyhedron_vertices ) + { + if( polyhedron_vertices.empty() ) + { + return true; + } + absl::InlinedVector< geode::PolyhedronVertex, 32 > reached; + absl::InlinedVector< geode::index_t, 32 > visited; + absl::InlinedVector< geode::PolyhedronVertex, 32 > to_visit; + to_visit.push_back( polyhedron_vertices.front() ); + visited.push_back( polyhedron_vertices.front().polyhedron_id ); + while( !to_visit.empty() ) + { + const auto polyhedron_vertex = to_visit.back(); + to_visit.pop_back(); + reached.push_back( polyhedron_vertex ); + if( reached.size() > polyhedron_vertices.size() ) + { + return false; + } + for( const auto& polyhedron_facet : + mesh.polyhedron_vertex_facets( polyhedron_vertex ) ) + { + const auto adjacent = + mesh.polyhedron_adjacent( polyhedron_facet ); + if( !adjacent ) + { + continue; + } + const auto adjacent_id = adjacent.value(); + if( adjacent_id >= mesh.nb_polyhedra() ) + { + return false; + } + if( absl::c_contains( visited, adjacent_id ) ) + { + continue; + } + visited.push_back( adjacent_id ); + if( const auto vertex_in_adjacent = + mesh.vertex_in_polyhedron( adjacent_id, vertex_id ) ) + { + to_visit.emplace_back( + adjacent_id, vertex_in_adjacent.value() ); + } + } + } + return polyhedra_around_vertex_are_the_same( + polyhedron_vertices, reached ); + } + + template < geode::index_t dimension > + std::vector< char > non_manifold_vertices_flags( + const geode::SolidMesh< dimension >& mesh ) + { + const auto polyhedra_around_vertices_list = + polyhedra_around_vertices( mesh ); + std::vector< char > non_manifold( mesh.nb_vertices(), false ); + async::parallel_for( + async::irange( geode::index_t{ 0 }, mesh.nb_vertices() ), + [&mesh, &polyhedra_around_vertices_list, &non_manifold]( + geode::index_t vertex_id ) { + non_manifold[vertex_id] = !vertex_is_manifold( mesh, vertex_id, + polyhedra_around_vertices_list[vertex_id] ); + } ); + return non_manifold; + } } // namespace namespace geode @@ -82,32 +160,18 @@ namespace geode bool mesh_vertices_are_manifold() const { - const auto polyhedra_around_vertices_list = - polyhedra_around_vertices( mesh_ ); - for( const auto vertex_id : geode::Range{ mesh_.nb_vertices() } ) - { - if( !polyhedra_around_vertex_are_the_same( - polyhedra_around_vertices_list[vertex_id], - mesh_.polyhedra_around_vertex( vertex_id ) ) ) - { - return false; - } - } - return true; + return !absl::c_contains( non_manifold_vertices_flags( mesh_ ), 1 ); } InspectionIssues< index_t > non_manifold_vertices() const { - const auto polyhedra_around_vertices_list = - polyhedra_around_vertices( mesh_ ); + const auto non_manifold = non_manifold_vertices_flags( mesh_ ); InspectionIssues< geode::index_t > non_manifold_vertices{ "non manifold vertices" }; for( const auto vertex_id : geode::Range{ mesh_.nb_vertices() } ) { - if( !polyhedra_around_vertex_are_the_same( - polyhedra_around_vertices_list[vertex_id], - mesh_.polyhedra_around_vertex( vertex_id ) ) ) + if( non_manifold[vertex_id] ) { non_manifold_vertices.add_issue( vertex_id, absl::StrCat( "vertex ", vertex_id, ", at position [", From 21203271e699163b26ff10499d5d3475ac5aa7c9 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:04:01 +0200 Subject: [PATCH 07/20] perf(Manifold): faster solid edges manifold - the edge to polyhedra map stored a heap vector per edge, did two hash lookups per edge occurrence and was built in the constructor, paid by every SolidMeshInspector even if edges were never inspected (and stale if the mesh changed); unique edges are now listed once per query with inline storage - the checked_edges hash set is replaced by the unique edges order - polyhedra around each edge are propagated in parallel Solid edge manifold: cva/brep_from_topo blocks 2.31s -> 0.57s, corbi_tet_mesh 0.32s -> 0.08s (identical results, same issues order). Co-Authored-By: Claude Opus 5.5 --- .../manifold/solid_edge_manifold.cpp | 167 +++++++++--------- 1 file changed, 82 insertions(+), 85 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp index 6358afca..4b8d3705 100644 --- a/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/solid_edge_manifold.cpp @@ -23,8 +23,11 @@ #include +#include #include -#include +#include + +#include #include #include @@ -55,28 +58,72 @@ namespace return true; } + struct EdgePolyhedra + { + // Vertices of the edge as first found in a polyhedron + std::array< geode::index_t, 2 > vertices; + absl::InlinedVector< geode::index_t, 8 > polyhedra; + }; + + // Unique edges in order of first appearance, with their polyhedra template < geode::index_t dimension > - absl::flat_hash_map< Edge, std::vector< geode::index_t > > - edges_to_polyhedra_around( const geode::SolidMesh< dimension >& mesh ) + std::vector< EdgePolyhedra > edges_to_polyhedra_around( + const geode::SolidMesh< dimension >& mesh ) { - absl::flat_hash_map< Edge, std::vector< geode::index_t > > - polyhedra_around_edges; + std::vector< EdgePolyhedra > edges; + absl::flat_hash_map< Edge, geode::index_t > edge_ids; + edge_ids.reserve( 2 * mesh.nb_polyhedra() ); for( const auto polyhedron_id : geode::Range{ mesh.nb_polyhedra() } ) { - for( const auto polyhedron_edge_vertices : + for( const auto& polyhedron_edge_vertices : mesh.polyhedron_edges_vertices( polyhedron_id ) ) { - const Edge polyhedron_edge{ polyhedron_edge_vertices }; - if( !polyhedra_around_edges - .try_emplace( polyhedron_edge, 1, polyhedron_id ) - .second ) + const auto [it, inserted] = edge_ids.try_emplace( + Edge{ polyhedron_edge_vertices }, edges.size() ); + if( inserted ) { - polyhedra_around_edges[polyhedron_edge].push_back( - polyhedron_id ); + edges.push_back( { polyhedron_edge_vertices, {} } ); } + edges[it->second].polyhedra.push_back( polyhedron_id ); } } - return polyhedra_around_edges; + return edges; + } + + enum struct EDGE_STATUS : char + { + manifold, + non_manifold, + broken_adjacencies + }; + + template < geode::index_t dimension > + std::vector< EDGE_STATUS > edges_status( + const geode::SolidMesh< dimension >& mesh, + absl::Span< const EdgePolyhedra > edges ) + { + std::vector< EDGE_STATUS > status( edges.size() ); + async::parallel_for( + async::irange( geode::index_t{ 0 }, + static_cast< geode::index_t >( edges.size() ) ), + [&mesh, &edges, &status]( geode::index_t edge_id ) { + const auto& edge = edges[edge_id]; + try + { + status[edge_id] = + polyhedra_around_edge_are_the_same( + mesh.polyhedra_around_edge( + edge.vertices, edge.polyhedra.front() ), + edge.polyhedra ) + ? EDGE_STATUS::manifold + : EDGE_STATUS::non_manifold; + } + catch( const geode::OpenGeodeException& ) + { + status[edge_id] = EDGE_STATUS::broken_adjacencies; + } + } ); + return status; } } // namespace @@ -86,45 +133,15 @@ namespace geode class SolidMeshEdgeManifold< dimension >::Impl { public: - Impl( const SolidMesh< dimension >& mesh ) - : mesh_( mesh ), - polyhedra_around_edges_( edges_to_polyhedra_around( mesh ) ) - { - } + Impl( const SolidMesh< dimension >& mesh ) : mesh_( mesh ) {} bool mesh_edges_are_manifold() const { - absl::flat_hash_set< Edge > checked_edges; - for( const auto polyhedron_id : Range{ mesh_.nb_polyhedra() } ) - { - for( const auto polyhedron_edge_vertices : - mesh_.polyhedron_edges_vertices( polyhedron_id ) ) - { - Edge polyhedron_edge{ polyhedron_edge_vertices }; - if( !checked_edges.insert( polyhedron_edge ).second ) - { - continue; - } - try - { - const auto polyhedra_around = - mesh_.polyhedra_around_edge( - polyhedron_edge_vertices, polyhedron_id ); - if( !polyhedra_around_edge_are_the_same( - polyhedra_around, polyhedra_around_edges_.at( - polyhedron_edge ) ) ) - { - return false; - } - } - catch( const OpenGeodeException& ) - { - // Broken adjacencies around the edge - return false; - } - } - } - return true; + const auto edges = edges_to_polyhedra_around( mesh_ ); + return absl::c_all_of( + edges_status( mesh_, edges ), []( EDGE_STATUS status ) { + return status == EDGE_STATUS::manifold; + } ); } InspectionIssues< std::array< index_t, 2 > > non_manifold_edges() const @@ -132,42 +149,24 @@ namespace geode InspectionIssues< std::array< index_t, 2 > > non_manifold_edges{ "Non manifold edges" }; - absl::flat_hash_set< Edge > checked_edges; - for( const auto polyhedron_id : Range{ mesh_.nb_polyhedra() } ) + const auto edges = edges_to_polyhedra_around( mesh_ ); + const auto status = edges_status( mesh_, edges ); + for( const auto edge_id : Indices{ edges } ) { - for( const auto polyhedron_edge_vertices : - mesh_.polyhedron_edges_vertices( polyhedron_id ) ) + const auto& vertices = edges[edge_id].vertices; + if( status[edge_id] == EDGE_STATUS::non_manifold ) + { + non_manifold_edges.add_issue( vertices, + absl::StrCat( "Non manifold edge between vertices ", + vertices[0], " and ", vertices[1] ) ); + } + else if( status[edge_id] == EDGE_STATUS::broken_adjacencies ) { - Edge polyhedron_edge{ polyhedron_edge_vertices }; - if( !checked_edges.insert( polyhedron_edge ).second ) - { - continue; - } - try - { - if( !polyhedra_around_edge_are_the_same( - mesh_.polyhedra_around_edge( - polyhedron_edge_vertices, polyhedron_id ), - polyhedra_around_edges_.at( - polyhedron_edge ) ) ) - { - non_manifold_edges.add_issue( - polyhedron_edge_vertices, - absl::StrCat( - "Non manifold edge between vertices ", - polyhedron_edge_vertices[0], " and ", - polyhedron_edge_vertices[1] ) ); - } - } - catch( const OpenGeodeException& ) - { - non_manifold_edges.add_issue( polyhedron_edge_vertices, - absl::StrCat( "Could not check manifold on edge " - "between vertices with index ", - polyhedron_edge_vertices[0], " and index ", - polyhedron_edge_vertices[1], - "; Check issues with solid adjacencies." ) ); - } + non_manifold_edges.add_issue( vertices, + absl::StrCat( "Could not check manifold on edge " + "between vertices with index ", + vertices[0], " and index ", vertices[1], + "; Check issues with solid adjacencies." ) ); } } return non_manifold_edges; @@ -175,8 +174,6 @@ namespace geode private: const SolidMesh< dimension >& mesh_; - absl::flat_hash_map< Edge, std::vector< geode::index_t > > - polyhedra_around_edges_; }; template < index_t dimension > From ae22521a258a11d2c455dd5a22035fa6daf293fc Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:05:05 +0200 Subject: [PATCH 08/20] perf(Manifold): count polyhedra around facets with a reserved flat hash map The facet counts used an absl::linked_hash_map (a list node per facet) and up to two lookups per facet occurrence. A reserved flat_hash_map with a single try_emplace is used, non manifold facets are sorted back in order of first appearance. Solid facet manifold: cva/brep_from_topo blocks 1.41s -> 0.96s; whole BRep manifold inspection 3.8s -> 1.87s with the previous commits (identical results). Co-Authored-By: Claude Opus 5.5 --- .../manifold/solid_facet_manifold.cpp | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/solid_facet_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/solid_facet_manifold.cpp index 23e16818..a01e4a03 100644 --- a/src/geode/inspector/inspection/criterion/manifold/solid_facet_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/solid_facet_manifold.cpp @@ -23,8 +23,8 @@ #include -#include -#include +#include +#include #include #include @@ -36,28 +36,52 @@ namespace { using Facet = geode::detail::VertexCycle< geode::PolyhedronFacetVertices >; + struct FacetCount + { + geode::index_t first_occurrence; + geode::index_t nb_polyhedra; + }; + + // Facets shared by more than two polyhedra, in order of first appearance template < geode::index_t dimension > - [[nodiscard]] absl::linked_hash_map< Facet, geode::index_t > - facets_to_nb_adjacent_polyhedra( - const geode::SolidMesh< dimension >& mesh ) + [[nodiscard]] std::vector< Facet > non_manifold_facets_list( + const geode::SolidMesh< dimension >& mesh ) { - absl::linked_hash_map< Facet, geode::index_t > - nb_polyhedra_adjacent_to_facets; + absl::flat_hash_map< Facet, FacetCount > facet_counts; + facet_counts.reserve( 3 * mesh.nb_polyhedra() ); + geode::index_t occurrence{ 0 }; for( const auto polyhedron_id : geode::Range{ mesh.nb_polyhedra() } ) { for( const auto facet_id : geode::LRange{ mesh.nb_polyhedron_facets( polyhedron_id ) } ) { - const Facet facet{ mesh.polyhedron_facet_vertices( - { polyhedron_id, facet_id } ) }; - if( !nb_polyhedra_adjacent_to_facets.try_emplace( facet, 1 ) - .second ) + auto [it, inserted] = facet_counts.try_emplace( + Facet{ mesh.polyhedron_facet_vertices( + { polyhedron_id, facet_id } ) }, + FacetCount{ occurrence, 1 } ); + if( !inserted ) { - nb_polyhedra_adjacent_to_facets[facet] += 1; + it->second.nb_polyhedra++; } + occurrence++; + } + } + std::vector< std::pair< geode::index_t, const Facet* > > non_manifold; + for( const auto& [facet, count] : facet_counts ) + { + if( count.nb_polyhedra > 2 ) + { + non_manifold.emplace_back( count.first_occurrence, &facet ); } } - return nb_polyhedra_adjacent_to_facets; + absl::c_sort( non_manifold ); + std::vector< Facet > result; + result.reserve( non_manifold.size() ); + for( const auto& facet : non_manifold ) + { + result.push_back( *facet.second ); + } + return result; } } // namespace @@ -71,42 +95,24 @@ namespace geode [[nodiscard]] bool mesh_facets_are_manifold() const { - const auto nb_polyhedra_adjacent_to_facets = - facets_to_nb_adjacent_polyhedra( mesh_ ); - for( const auto& nb_adjacent_polyhedra : - nb_polyhedra_adjacent_to_facets ) - { - if( nb_adjacent_polyhedra.second > 2 ) - { - return false; - } - } - return true; + return non_manifold_facets_list( mesh_ ).empty(); } [[nodiscard]] InspectionIssues< PolyhedronFacetVertices > non_manifold_facets() const { - const auto nb_polyhedra_adjacent_to_facets = - facets_to_nb_adjacent_polyhedra( mesh_ ); InspectionIssues< PolyhedronFacetVertices > non_manifold_facets{ "non manifold facets" }; - for( const auto& nb_adjacent_polyhedra : - nb_polyhedra_adjacent_to_facets ) + for( const auto& facet : non_manifold_facets_list( mesh_ ) ) { - if( nb_adjacent_polyhedra.second > 2 ) + std::string message{ "facet made of vertices with index " }; + for( const auto vertex_id : facet.vertices() ) { - std::string message{ "facet made of vertices with index " }; - for( const auto vertex_id : - nb_adjacent_polyhedra.first.vertices() ) - { - absl::StrAppend( &message, vertex_id, ", " ); - } - absl::StrAppend( &message, "is not manifold." ); - non_manifold_facets.add_issue( - nb_adjacent_polyhedra.first.vertices(), message ); + absl::StrAppend( &message, vertex_id, ", " ); } + absl::StrAppend( &message, "is not manifold." ); + non_manifold_facets.add_issue( facet.vertices(), message ); } return non_manifold_facets; } From ecfd3a36156f691ae735e8428854d11217c1de68 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:07:05 +0200 Subject: [PATCH 09/20] fix(Manifold): surface vertices with broken adjacencies are not manifold When polygons_around_vertex threw (broken adjacencies, or over 1000 polygons around the vertex), the vertex was silently skipped and counted as manifold. It is now reported as a vertex which could not be checked. Co-Authored-By: Claude Opus 5.5 --- .../manifold/surface_vertex_manifold.cpp | 9 +++++-- tests/inspection/test-surface-manifold.cpp | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp index 8d179d77..004df8ed 100644 --- a/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp @@ -96,7 +96,8 @@ namespace geode } catch( const geode::OpenGeodeException& ) { - continue; + // Broken adjacencies around the vertex + return false; } } return true; @@ -125,7 +126,11 @@ namespace geode } catch( const geode::OpenGeodeException& ) { - continue; + non_manifold_vertices.add_issue( vertex_id, + absl::StrCat( "vertex ", vertex_id, ", at position [", + mesh_.point( vertex_id ).string(), + "] could not be checked; check issues with " + "surface adjacencies" ) ); } } return non_manifold_vertices; diff --git a/tests/inspection/test-surface-manifold.cpp b/tests/inspection/test-surface-manifold.cpp index 114ec37e..7041f09a 100644 --- a/tests/inspection/test-surface-manifold.cpp +++ b/tests/inspection/test-surface-manifold.cpp @@ -137,6 +137,32 @@ void check_edge_non_manifold2D() "Surface edges are shown non manifold whereas they are." ); } +void check_vertex_broken_adjacency() +{ + auto surface = geode::TriangulatedSurface2D::create(); + auto builder = geode::TriangulatedSurfaceBuilder2D::create( *surface ); + builder->create_point( geode::Point2D{ { 0., 0. } } ); + builder->create_point( geode::Point2D{ { 1., 0. } } ); + builder->create_point( geode::Point2D{ { 0., 1. } } ); + builder->create_point( geode::Point2D{ { 5., 0. } } ); + builder->create_point( geode::Point2D{ { 6., 0. } } ); + builder->create_point( geode::Point2D{ { 5., 1. } } ); + builder->create_triangle( { 0, 1, 2 } ); + builder->create_triangle( { 3, 4, 5 } ); + // Adjacency between triangles without common vertices + builder->set_polygon_adjacent( { 0, 0 }, 1 ); + builder->set_polygon_adjacent( { 1, 0 }, 0 ); + + const geode::SurfaceMeshVertexManifold2D manifold_inspector{ *surface }; + geode::OpenGeodeInspectorInspectionException::test( + !manifold_inspector.mesh_vertices_are_manifold(), + "[Test] Surface with broken adjacency is shown as manifold." ); + geode::OpenGeodeInspectorInspectionException::test( + manifold_inspector.non_manifold_vertices().nb_issues() != 0, + "[Test] Surface with broken adjacency should have vertices which " + "could not be checked." ); +} + void check_edge_with_many_polygons() { // 257 triangles around edge (0, 1) @@ -172,6 +198,7 @@ int main() check_edge_manifold2D(); check_edge_non_manifold2D(); check_edge_with_many_polygons(); + check_vertex_broken_adjacency(); geode::Logger::info( "TEST SUCCESS" ); return 0; From 129c360f7cc84f9c84a4ca6ba12a7f7cde930bc1 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:08:20 +0200 Subject: [PATCH 10/20] perf(Manifold): surface vertex manifold without polygons around vertex cache Each vertex called SurfaceMesh::polygons_around_vertex, which fills the mesh lazy cache: this raced with the topology inspection reading the same cache (is_vertex_on_border) in BRep/Section inspections. The same walk around the vertex is now done locally, bounded by the number of polygons containing the vertex, checking adjacency indices, and in parallel over vertices. Surface vertex manifold: cva/brep_from_topo surfaces 0.040s -> 0.013s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../manifold/surface_vertex_manifold.cpp | 188 ++++++++++++++---- 1 file changed, 153 insertions(+), 35 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp index 004df8ed..bc332405 100644 --- a/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/surface_vertex_manifold.cpp @@ -24,6 +24,9 @@ #include #include +#include + +#include #include @@ -34,8 +37,8 @@ namespace { bool polygons_around_vertex_are_the_same( - const geode::PolygonsAroundVertex& pav1, - const geode::PolygonsAroundVertex& pav2 ) + absl::Span< const geode::PolygonVertex > pav1, + absl::Span< const geode::PolygonVertex > pav2 ) { if( pav1.size() != pav2.size() ) { @@ -69,6 +72,143 @@ namespace } return polygons_list; } + + enum struct VERTEX_STATUS : char + { + manifold, + non_manifold, + broken_adjacencies + }; + + // Same walk as SurfaceMesh::polygons_around_vertex, without its cache. + // Returns std::nullopt on broken adjacencies. + template < geode::index_t dimension > + std::optional< std::optional< geode::PolygonVertex > > + next_polygon_vertex_around( const geode::SurfaceMesh< dimension >& mesh, + const geode::PolygonVertex& polygon_vertex, + bool& vertex_is_next ) + { + const auto exit_vertex = + vertex_is_next ? polygon_vertex + : mesh.previous_polygon_vertex( polygon_vertex ); + const geode::PolygonEdge exit_edge{ exit_vertex }; + const auto adjacent = mesh.polygon_adjacent( exit_edge ); + if( !adjacent ) + { + return std::optional< geode::PolygonVertex >{}; + } + if( adjacent.value() >= mesh.nb_polygons() ) + { + return std::nullopt; + } + const auto adjacent_edge = mesh.polygon_adjacent_edge( exit_edge ); + if( !adjacent_edge ) + { + return std::nullopt; + } + if( mesh.polygon_edge_vertex( exit_edge, 0 ) + == mesh.polygon_edge_vertex( adjacent_edge.value(), 0 ) + && mesh.polygon_edge_vertex( exit_edge, 1 ) + == mesh.polygon_edge_vertex( adjacent_edge.value(), 1 ) ) + { + vertex_is_next = !vertex_is_next; + } + return std::optional< geode::PolygonVertex >{ + vertex_is_next ? mesh.next_polygon_vertex( + geode::PolygonVertex{ adjacent_edge.value() } ) + : geode::PolygonVertex{ adjacent_edge.value() } + }; + } + + template < geode::index_t dimension > + VERTEX_STATUS vertex_status( const geode::SurfaceMesh< dimension >& mesh, + absl::Span< const geode::PolygonVertex > polygon_vertices ) + { + if( polygon_vertices.empty() ) + { + return VERTEX_STATUS::manifold; + } + const auto& first = polygon_vertices.front(); + absl::InlinedVector< geode::PolygonVertex, 16 > reached{ first }; + try + { + bool vertex_is_next{ false }; + auto current = first; + bool closed{ false }; + while( true ) + { + const auto next = + next_polygon_vertex_around( mesh, current, vertex_is_next ); + if( !next ) + { + return VERTEX_STATUS::broken_adjacencies; + } + if( !next.value() ) + { + break; + } + if( next.value() == first ) + { + closed = true; + break; + } + reached.push_back( next.value().value() ); + if( reached.size() > polygon_vertices.size() ) + { + return VERTEX_STATUS::non_manifold; + } + current = next.value().value(); + } + if( !closed ) + { + vertex_is_next = true; + current = first; + while( true ) + { + const auto next = next_polygon_vertex_around( + mesh, current, vertex_is_next ); + if( !next ) + { + return VERTEX_STATUS::broken_adjacencies; + } + if( !next.value() ) + { + break; + } + reached.push_back( next.value().value() ); + if( reached.size() > polygon_vertices.size() ) + { + return VERTEX_STATUS::non_manifold; + } + current = next.value().value(); + } + } + } + catch( const geode::OpenGeodeException& ) + { + return VERTEX_STATUS::broken_adjacencies; + } + return polygons_around_vertex_are_the_same( polygon_vertices, reached ) + ? VERTEX_STATUS::manifold + : VERTEX_STATUS::non_manifold; + } + + template < geode::index_t dimension > + std::vector< VERTEX_STATUS > vertices_status( + const geode::SurfaceMesh< dimension >& mesh ) + { + const auto polygons_around_vertices_list = + polygons_around_vertices( mesh ); + std::vector< VERTEX_STATUS > status( mesh.nb_vertices() ); + async::parallel_for( + async::irange( geode::index_t{ 0 }, mesh.nb_vertices() ), + [&mesh, &polygons_around_vertices_list, &status]( + geode::index_t vertex_id ) { + status[vertex_id] = vertex_status( + mesh, polygons_around_vertices_list[vertex_id] ); + } ); + return status; + } } // namespace namespace geode @@ -81,50 +221,28 @@ namespace geode bool mesh_vertices_are_manifold() const { - const auto polygons_around_vertices_list = - polygons_around_vertices( mesh_ ); - for( const auto vertex_id : geode::Range{ mesh_.nb_vertices() } ) - { - try - { - if( !polygons_around_vertex_are_the_same( - polygons_around_vertices_list[vertex_id], - mesh_.polygons_around_vertex( vertex_id ) ) ) - { - return false; - } - } - catch( const geode::OpenGeodeException& ) - { - // Broken adjacencies around the vertex - return false; - } - } - return true; + return absl::c_all_of( + vertices_status( mesh_ ), []( VERTEX_STATUS status ) { + return status == VERTEX_STATUS::manifold; + } ); } InspectionIssues< index_t > non_manifold_vertices() const { - const auto polygons_around_vertices_list = - polygons_around_vertices( mesh_ ); + const auto status = vertices_status( mesh_ ); InspectionIssues< geode::index_t > non_manifold_vertices{ "non manifold vertices" }; for( const auto vertex_id : geode::Range{ mesh_.nb_vertices() } ) { - try + if( status[vertex_id] == VERTEX_STATUS::non_manifold ) { - if( !polygons_around_vertex_are_the_same( - polygons_around_vertices_list[vertex_id], - mesh_.polygons_around_vertex( vertex_id ) ) ) - { - non_manifold_vertices.add_issue( vertex_id, - absl::StrCat( "vertex ", vertex_id, - ", at position [", - mesh_.point( vertex_id ).string(), "]" ) ); - } + non_manifold_vertices.add_issue( vertex_id, + absl::StrCat( "vertex ", vertex_id, ", at position [", + mesh_.point( vertex_id ).string(), "]" ) ); } - catch( const geode::OpenGeodeException& ) + else if( status[vertex_id] + == VERTEX_STATUS::broken_adjacencies ) { non_manifold_vertices.add_issue( vertex_id, absl::StrCat( "vertex ", vertex_id, ", at position [", From fc32557b006556837749be5e566887ac6df5ab82 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:09:40 +0200 Subject: [PATCH 11/20] perf(Manifold): move block issues into the issues maps instead of copying Co-Authored-By: Claude Opus 5.5 --- .../inspection/criterion/manifold/brep_meshes_manifold.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp index a244752e..05c54ca7 100644 --- a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp @@ -132,7 +132,7 @@ namespace geode block.name().value_or( block.id().string() ), " (", block.id().string(), ") non manifold edges" ) ); components_non_manifold_edges.add_issues_to_map( - block.id(), non_manifold_edges ); + block.id(), std::move( non_manifold_edges ) ); } } @@ -148,7 +148,7 @@ namespace geode block.name().value_or( block.id().string() ), " (", block.id().string(), ") non manifold facets" ) ); components_non_manifold_facets.add_issues_to_map( - block.id(), non_manifold_facets ); + block.id(), std::move( non_manifold_facets ) ); } } From 5dc9b56be784b224ac46720575a40c9bf2170004 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:10:26 +0200 Subject: [PATCH 12/20] perf(Manifold): run the model manifold inspections in parallel The vertices, edges, facets and model edges/facets manifold inspections of a BRep (vertices and edges for a Section) ran one after the other. They fill different results and no longer write to the mesh caches, so they now run concurrently. inspect_brep_manifold: cva/brep_from_topo 1.87s -> 1.08s, corbi_tet_mesh 0.29s -> 0.16s, fimiston 0.68s -> 0.48s (identical results). Overall with the previous commits: cva/brep_from_topo 3.8s -> 1.08s. Co-Authored-By: Claude Opus 5.5 --- .../manifold/brep_meshes_manifold.cpp | 31 ++++++++++++++----- .../manifold/section_meshes_manifold.cpp | 15 ++++++--- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp index 05c54ca7..ba15ed84 100644 --- a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp @@ -23,6 +23,8 @@ #include +#include + #include #include #include @@ -311,14 +313,27 @@ namespace geode BRepComponentMeshesManifold::inspect_brep_manifold() const { BRepMeshesManifoldInspectionResult result; - impl_->add_component_meshes_non_manifold_vertices( - result.meshes_non_manifold_vertices ); - impl_->add_component_meshes_non_manifold_edges( - result.meshes_non_manifold_edges ); - impl_->add_component_meshes_non_manifold_facets( - result.meshes_non_manifold_facets ); - impl_->add_model_non_manifold_edges( result.brep_non_manifold_edges ); - impl_->add_model_non_manifold_facets( result.brep_non_manifold_facets ); + async::parallel_invoke( + [this, &result] { + impl_->add_component_meshes_non_manifold_vertices( + result.meshes_non_manifold_vertices ); + }, + [this, &result] { + impl_->add_component_meshes_non_manifold_edges( + result.meshes_non_manifold_edges ); + }, + [this, &result] { + impl_->add_component_meshes_non_manifold_facets( + result.meshes_non_manifold_facets ); + }, + [this, &result] { + impl_->add_model_non_manifold_edges( + result.brep_non_manifold_edges ); + }, + [this, &result] { + impl_->add_model_non_manifold_facets( + result.brep_non_manifold_facets ); + } ); return result; } } // namespace geode diff --git a/src/geode/inspector/inspection/criterion/manifold/section_meshes_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/section_meshes_manifold.cpp index b50bf980..a0d225df 100644 --- a/src/geode/inspector/inspection/criterion/manifold/section_meshes_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/section_meshes_manifold.cpp @@ -23,6 +23,8 @@ #include +#include + #include #include @@ -83,10 +85,15 @@ namespace geode SectionComponentMeshesManifold::inspect_section_manifold() const { SectionMeshesManifoldInspectionResult result; - impl_->add_surfaces_meshes_non_manifold_vertices( - result.meshes_non_manifold_vertices ); - impl_->add_surfaces_meshes_non_manifold_edges( - result.meshes_non_manifold_edges ); + async::parallel_invoke( + [this, &result] { + impl_->add_surfaces_meshes_non_manifold_vertices( + result.meshes_non_manifold_vertices ); + }, + [this, &result] { + impl_->add_surfaces_meshes_non_manifold_edges( + result.meshes_non_manifold_edges ); + } ); return result; } } // namespace geode From 958093c5a61e43efb5c13b7a119a45f2c9a56f63 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:11:19 +0200 Subject: [PATCH 13/20] perf(Manifold): lighter BRep model edges manifold Each surface edge made two model.unique_vertex() calls (uuid hash lookup) and the edges map was an absl::linked_hash_map with a std::vector per edge. Surface unique vertices are fetched once per vertex, the map is a flat_hash_map with inline storage and non manifold edges are sorted back in order of first appearance. inspect_brep_manifold: fimiston 0.48s -> 0.32s, model_D 0.023s -> 0.011s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../manifold/brep_meshes_manifold.cpp | 64 ++++++++++++------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp index ba15ed84..7eb98e51 100644 --- a/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/brep_meshes_manifold.cpp @@ -23,6 +23,10 @@ #include +#include +#include +#include + #include #include @@ -158,14 +162,26 @@ namespace geode InspectionIssues< BRepNonManifoldEdge >& issues ) const { using Edge = detail::VertexCycle< std::array< index_t, 2 > >; - absl::linked_hash_map< Edge, std::vector< uuid > > edges; + struct EdgeSurfaces + { + index_t first_occurrence; + absl::InlinedVector< uuid, 2 > surfaces; + }; + absl::flat_hash_map< Edge, EdgeSurfaces > edges_map; + index_t occurrence{ 0 }; for( const auto& surface : model().active_surfaces() ) { const auto& mesh = surface.mesh(); + std::vector< index_t > unique_vertices( mesh.nb_vertices() ); + for( const auto v : Indices{ unique_vertices } ) + { + unique_vertices[v] = + model().unique_vertex( { surface.component_id(), v } ); + } for( const auto polygon_id : Range{ mesh.nb_polygons() } ) { - const auto vertices = mesh.polygon_vertices( polygon_id ); - for( const auto edge_id : LIndices{ vertices } ) + for( const auto edge_id : + LRange{ mesh.nb_polygon_edges( polygon_id ) } ) { const auto adj = mesh.polygon_adjacent( { polygon_id, edge_id } ); @@ -173,40 +189,43 @@ namespace geode { continue; } - const auto vertex0 = model().unique_vertex( - { surface.component_id(), vertices[edge_id] } ); - const auto vertex1 = - model().unique_vertex( { surface.component_id(), - vertices[edge_id == vertices.size() - 1 - ? 0 - : edge_id + 1] } ); + const auto edge_vertices = mesh.polygon_edge_vertices( + { polygon_id, edge_id } ); + const auto vertex0 = unique_vertices[edge_vertices[0]]; + const auto vertex1 = unique_vertices[edge_vertices[1]]; // Unlinked vertices are reported by the topology // inspection, they cannot be compared here if( vertex0 == NO_ID || vertex1 == NO_ID ) { continue; } - const auto info = edges.try_emplace( + auto [it, inserted] = edges_map.try_emplace( Edge{ std::array{ vertex0, vertex1 } }, - std::vector{ surface.id() } ); - if( !info.second ) - { - info.first->second.push_back( surface.id() ); - } + EdgeSurfaces{ occurrence, {} } ); + it->second.surfaces.push_back( surface.id() ); + occurrence++; } } } - for( auto& edge : edges ) + std::vector< std::pair< Edge, EdgeSurfaces > > edges; + for( auto& edge : edges_map ) { - if( edge.second.size() <= 1 ) + if( edge.second.surfaces.size() > 1 ) { - continue; + edges.emplace_back( edge.first, std::move( edge.second ) ); } + } + absl::c_sort( edges, []( const auto& lhs, const auto& rhs ) { + return lhs.second.first_occurrence + < rhs.second.first_occurrence; + } ); + for( auto& edge : edges ) + { std::string message = absl::StrCat( "Model edge between unique vertices ", edge.first.vertices()[0], " and ", edge.first.vertices()[1], " is not manifold: it is on several Surfaces: " ); - for( const auto surface_uuid : edge.second ) + for( const auto& surface_uuid : edge.second.surfaces ) { absl::StrAppend( &message, model() @@ -216,8 +235,9 @@ namespace geode " (", surface_uuid.string(), "), " ); } absl::StrAppend( &message, "but not on their border" ); - issues.add_issue( - BRepNonManifoldEdge{ edge.first.vertices(), edge.second }, + issues.add_issue( BRepNonManifoldEdge{ edge.first.vertices(), + { edge.second.surfaces.begin(), + edge.second.surfaces.end() } }, message ); } for( const auto& line : model().active_lines() ) From a78cd0f447c71e4431d40b68cc5fbf87884fa14f Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:13:15 +0200 Subject: [PATCH 14/20] perf(Adjacency): direct check of surface border edges on lines For every surface border edge, detail::line_component_mesh_edges built hash maps of component mesh vertex pairs and of line edges only to test whether the result was empty, after two unique_vertex() hash lookups; exceptions were used for unlinked vertices with a warning logged per edge. The line edge is now looked up directly from the component mesh vertices, unique vertices are fetched once per surface vertex, unlinked vertices are checked explicitly (same issue message) and surfaces are inspected in parallel. inspect_brep_meshes_adjacencies: fimiston 0.098s -> 0.034s, model_D 0.009s -> 0.003s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../internal/component_meshes_adjacency.cpp | 161 +++++++++++------- 1 file changed, 100 insertions(+), 61 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/internal/component_meshes_adjacency.cpp b/src/geode/inspector/inspection/criterion/internal/component_meshes_adjacency.cpp index a16f6342..f1062763 100644 --- a/src/geode/inspector/inspection/criterion/internal/component_meshes_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/internal/component_meshes_adjacency.cpp @@ -23,13 +23,16 @@ #include +#include + #include #include +#include #include -#include +#include #include #include #include @@ -38,28 +41,92 @@ namespace { + // True if both unique vertices are linked to the vertices of an edge of a + // same Line template < typename Model > - bool polygon_edge_is_on_a_line( const Model& model, - const geode::Surface< Model::dim >& surface, - const geode::PolygonEdge& polygon_edge ) + bool unique_vertices_are_on_a_line_edge( const Model& model, + geode::index_t unique_vertex0, + geode::index_t unique_vertex1 ) { - const auto edge_unique_vertices = - geode::edge_unique_vertices( model, surface, polygon_edge ); - geode::OpenGeodeInspectorInspectionException::check_exception( - edge_unique_vertices[0] != geode::NO_ID - && edge_unique_vertices[1] != geode::NO_ID, - surface.mesh().edge_barycenter( polygon_edge ), - geode::OpenGeodeException::TYPE::data, - "[ComponentMeshesAdjacency] Missing unique_vertices" ); - geode::OpenGeodeInspectorInspectionException::check_exception( - edge_unique_vertices[0] < model.nb_unique_vertices() - && edge_unique_vertices[1] < model.nb_unique_vertices(), - surface.mesh().edge_barycenter( polygon_edge ), - geode::OpenGeodeException::TYPE::data, - "[ComponentMeshesAdjacency] Wrong unique_vertices" ); - return !geode::detail::line_component_mesh_edges( - model, edge_unique_vertices ) - .empty(); + if( unique_vertex0 == unique_vertex1 ) + { + return false; + } + const auto& cmvs1 = model.component_mesh_vertices( unique_vertex1 ); + for( const auto& cmv0 : + model.component_mesh_vertices( unique_vertex0 ) ) + { + if( cmv0.component_id.type + != geode::Line< Model::dim >::component_type_static() ) + { + continue; + } + for( const auto& cmv1 : cmvs1 ) + { + if( cmv1.component_id.id != cmv0.component_id.id ) + { + continue; + } + const auto& mesh = model.line( cmv0.component_id.id ).mesh(); + for( const auto& edge_vertex : + mesh.edges_around_vertex( cmv0.vertex ) ) + { + if( mesh.edge_vertex( edge_vertex.opposite() ) + == cmv1.vertex ) + { + return true; + } + } + } + } + return false; + } + + template < typename Model > + geode::InspectionIssues< geode::PolygonEdge > surface_wrong_adjacencies( + const Model& model, const geode::Surface< Model::dim >& surface ) + { + const auto& mesh = surface.mesh(); + const geode::SurfaceMeshAdjacency< Model::dim > inspector{ mesh }; + auto issues = inspector.polygon_edges_with_wrong_adjacency(); + issues.set_description( absl::StrCat( "Surface ", + surface.name().value_or( surface.id().string() ), " (", + surface.id().string(), ") polygon edges adjacency issues" ) ); + std::vector< geode::index_t > unique_vertices( mesh.nb_vertices() ); + for( const auto v : geode::Indices{ unique_vertices } ) + { + unique_vertices[v] = + model.unique_vertex( { surface.component_id(), v } ); + } + for( const auto polygon_id : geode::Range{ mesh.nb_polygons() } ) + { + for( const auto edge_id : + geode::LRange{ mesh.nb_polygon_edges( polygon_id ) } ) + { + const geode::PolygonEdge polygon_edge{ polygon_id, edge_id }; + if( !mesh.is_edge_on_border( polygon_edge ) ) + { + continue; + } + const auto edge_vertices = + mesh.polygon_edge_vertices( polygon_edge ); + const auto unique_vertex0 = unique_vertices[edge_vertices[0]]; + const auto unique_vertex1 = unique_vertices[edge_vertices[1]]; + // Vertices without (valid) unique vertex cannot be on a Line + if( unique_vertex0 >= model.nb_unique_vertices() + || unique_vertex1 >= model.nb_unique_vertices() + || !unique_vertices_are_on_a_line_edge( + model, unique_vertex0, unique_vertex1 ) ) + { + issues.add_issue( polygon_edge, + absl::StrCat( "edge ", edge_id, " of polygon ", + polygon_id, + " has no adjacencies but is not part of a model " + "Line." ) ); + } + } + } + return issues; } } // namespace @@ -80,49 +147,21 @@ namespace geode InspectionIssuesMap< PolygonEdge >& components_wrong_adjacencies ) const { + std::vector< async::task< + std::pair< uuid, InspectionIssues< PolygonEdge > > > > + tasks; for( const auto& surface : model_.active_surfaces() ) { - const SurfaceMeshAdjacency< Model::dim > inspector{ - surface.mesh() - }; - auto issues = inspector.polygon_edges_with_wrong_adjacency(); - issues.set_description( absl::StrCat( "Surface ", - surface.name().value_or( surface.id().string() ), " (", - surface.id().string(), - ") polygon edges adjacency issues" ) ); - const auto& mesh = surface.mesh(); - for( const auto polygon_id : Range{ mesh.nb_polygons() } ) - { - for( const auto edge_id : - LRange{ mesh.nb_polygon_edges( polygon_id ) } ) - { - const PolygonEdge polygon_edge{ polygon_id, edge_id }; - try - { - if( mesh.is_edge_on_border( polygon_edge ) - && !polygon_edge_is_on_a_line( - model_, surface, polygon_edge ) ) - { - issues.add_issue( polygon_edge, - absl::StrCat( "edge ", edge_id, - " of polygon ", polygon_id, - " has no adjacencies but is not part " - "of a model Line." ) ); - } - } - catch( const OpenGeodeException& e ) - { - Logger::warning( e.what() ); - issues.add_issue( polygon_edge, - absl::StrCat( "edge ", edge_id, " of polygon ", - polygon_id, - " has no adjacencies but is not part of a " - "model Line." ) ); - } - } - } + tasks.emplace_back( async::spawn( [this, &surface] { + return std::pair{ surface.id(), + surface_wrong_adjacencies( model_, surface ) }; + } ) ); + } + for( auto& task : async::when_all( tasks ).get() ) + { + auto [surface_id, issues] = task.get(); components_wrong_adjacencies.add_issues_to_map( - surface.id(), std::move( issues ) ); + surface_id, std::move( issues ) ); } } From 7d518353e9f2e0f9ce92942524d9a59e66256e14 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:14:28 +0200 Subject: [PATCH 15/20] fix(Adjacency): block border facets with unlinked vertices are reported A block border facet whose vertices were not linked to unique vertices was silently skipped, while the same case is reported for surface border edges. Co-Authored-By: Claude Opus 5.5 --- .../adjacency/brep_meshes_adjacency.cpp | 5 ++-- .../inspection/test-brep-meshes-criteria.cpp | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp b/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp index ca957e18..d9c7eb39 100644 --- a/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp @@ -109,8 +109,9 @@ namespace geode const auto is_on_surface = polyhedron_facet_is_on_a_surface( block, polyhedron_facet ); - if( is_on_surface.has_value() - && !is_on_surface.value() ) + // Facets with unlinked vertices cannot be on a + // Surface + if( !is_on_surface.value_or( false ) ) { wrong_adjacencies.add_issue( polyhedron_facet, absl::StrCat( "facet ", facet_id, diff --git a/tests/inspection/test-brep-meshes-criteria.cpp b/tests/inspection/test-brep-meshes-criteria.cpp index 35b2bdba..1e3a4bc6 100644 --- a/tests/inspection/test-brep-meshes-criteria.cpp +++ b/tests/inspection/test-brep-meshes-criteria.cpp @@ -27,15 +27,19 @@ #include #include +#include #include #include +#include #include +#include #include #include #include #include +#include #include namespace @@ -93,6 +97,29 @@ void check_single_edge_line_without_unique_vertices() result.brep_non_manifold_edges.string() ); } +void check_block_facets_without_unique_vertices() +{ + geode::BRep brep; + geode::BRepBuilder builder{ brep }; + const auto& block = brep.block( builder.add_block() ); + auto mesh_builder = builder.block_mesh_builder( block ); + mesh_builder->create_point( geode::Point3D{ { 0, 0, 0 } } ); + mesh_builder->create_point( geode::Point3D{ { 1, 0, 0 } } ); + mesh_builder->create_point( geode::Point3D{ { 0, 1, 0 } } ); + mesh_builder->create_point( geode::Point3D{ { 0, 0, 1 } } ); + mesh_builder->create_polyhedron( { 0, 1, 2, 3 }, + { { 1, 3, 2 }, { 0, 2, 3 }, { 3, 1, 0 }, { 0, 1, 2 } } ); + const geode::BRepComponentMeshesAdjacency inspector{ brep }; + const auto result = inspector.inspect_brep_meshes_adjacencies(); + const auto& issues_map = + result.blocks_facets_with_wrong_adjacencies.issues_map(); + geode::OpenGeodeInspectorInspectionException::test( + issues_map.contains( block.id() ) + && issues_map.at( block.id() ).nb_issues() == 4, + "[Test] Block border facets without unique vertices should be " + "reported as not on a Surface" ); +} + int main() { try @@ -100,6 +127,7 @@ int main() geode::OpenGeodeInspectorInspectionLibrary::initialize(); check_model_edges_without_unique_vertices(); check_single_edge_line_without_unique_vertices(); + check_block_facets_without_unique_vertices(); geode::Logger::info( "TEST SUCCESS" ); return 0; From 4b91d448e02983756d59c6b373d6a4ad12f11ac4 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:15:52 +0200 Subject: [PATCH 16/20] perf(Adjacency): look up block border facets in a surface polygons set For every block border facet, polygon_unique_vertices made one unique_vertex hash lookup per vertex and detail::surface_component_mesh_polygons built hash maps and called SurfaceMesh::polygons_around_vertex, which fills the mesh lazy cache (racy with the other inspections running concurrently). The sorted unique vertices of every surface polygon are now gathered once, in parallel, and each border facet is a single set lookup; blocks are inspected in parallel. inspect_brep_meshes_adjacencies: corbi_tet_mesh 0.29s -> 0.07s, mss 0.077s -> 0.034s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../adjacency/brep_meshes_adjacency.cpp | 159 +++++++++++++----- 1 file changed, 118 insertions(+), 41 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp b/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp index d9c7eb39..75d7d4f5 100644 --- a/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/adjacency/brep_meshes_adjacency.cpp @@ -23,6 +23,9 @@ #include +#include +#include + #include #include @@ -30,9 +33,10 @@ #include #include +#include -#include #include +#include #include #include @@ -84,62 +88,135 @@ namespace geode InspectionIssuesMap< PolyhedronFacet >& components_wrong_adjacencies ) const { + if( model().nb_active_blocks() == 0 ) + { + return; + } + const auto surface_polygons = surfaces_polygons_unique_vertices(); + std::vector< async::task< + std::pair< uuid, InspectionIssues< PolyhedronFacet > > > > + tasks; for( const auto& block : model().active_blocks() ) { - const geode::SolidMeshAdjacency3D inspector{ block.mesh() }; - auto wrong_adjacencies = - inspector.polyhedron_facets_with_wrong_adjacency(); - wrong_adjacencies.set_description( absl::StrCat( "Block ", - block.name().value_or( block.id().string() ), " (", - block.id().string(), - ") polyhedron facets adjacencies issues" ) ); - const auto& mesh = block.mesh(); - for( const auto polyhedron_id : Range{ mesh.nb_polyhedra() } ) - { - for( const auto facet_id : - LRange{ mesh.nb_polyhedron_facets( polyhedron_id ) } ) + tasks.emplace_back( async::spawn( [this, &block, + &surface_polygons] { + return std::pair{ block.id(), + block_wrong_adjacencies( block, surface_polygons ) }; + } ) ); + } + for( auto& task : async::when_all( tasks ).get() ) + { + auto [block_id, issues] = task.get(); + components_wrong_adjacencies.add_issues_to_map( + block_id, std::move( issues ) ); + } + } + + private: + using SortedUniqueVertices = PolygonVertices; + + std::vector< index_t > mesh_unique_vertices( + const ComponentID& component_id, index_t nb_vertices ) const + { + std::vector< index_t > unique_vertices( nb_vertices ); + for( const auto v : Indices{ unique_vertices } ) + { + unique_vertices[v] = + model().unique_vertex( { component_id, v } ); + } + return unique_vertices; + } + + // Sorted unique vertices of every surface polygon + absl::flat_hash_set< SortedUniqueVertices > + surfaces_polygons_unique_vertices() const + { + std::vector< async::task< std::vector< SortedUniqueVertices > > > + tasks; + for( const auto& surface : model().surfaces() ) + { + tasks.emplace_back( async::spawn( [this, &surface] { + const auto& mesh = surface.mesh(); + const auto unique_vertices = mesh_unique_vertices( + surface.component_id(), mesh.nb_vertices() ); + std::vector< SortedUniqueVertices > surface_polygons; + surface_polygons.reserve( mesh.nb_polygons() ); + for( const auto polygon_id : Range{ mesh.nb_polygons() } ) { - const PolyhedronFacet polyhedron_facet{ polyhedron_id, - facet_id }; - if( !mesh.is_polyhedron_facet_on_border( - polyhedron_facet ) ) + auto vertices = mesh.polygon_vertices( polygon_id ); + for( auto& vertex : vertices ) { - continue; + vertex = unique_vertices[vertex]; } - const auto is_on_surface = - polyhedron_facet_is_on_a_surface( - block, polyhedron_facet ); - // Facets with unlinked vertices cannot be on a - // Surface - if( !is_on_surface.value_or( false ) ) + if( absl::c_contains( vertices, NO_ID ) ) { - wrong_adjacencies.add_issue( polyhedron_facet, - absl::StrCat( "facet ", facet_id, - " of polyhedron ", polyhedron_id, - " has no adjacencies but is not part of a " - "model Surface." ) ); + continue; } + absl::c_sort( vertices ); + surface_polygons.emplace_back( std::move( vertices ) ); } + return surface_polygons; + } ) ); + } + absl::flat_hash_set< SortedUniqueVertices > polygons; + for( auto& task : async::when_all( tasks ).get() ) + { + for( auto& polygon : task.get() ) + { + polygons.emplace( std::move( polygon ) ); } - components_wrong_adjacencies.add_issues_to_map( - block.id(), std::move( wrong_adjacencies ) ); } + return polygons; } - private: - absl::optional< bool > polyhedron_facet_is_on_a_surface( + InspectionIssues< PolyhedronFacet > block_wrong_adjacencies( const Block3D& block, - const PolyhedronFacet& polyhedron_facet ) const + const absl::flat_hash_set< SortedUniqueVertices >& + surface_polygons ) const { - const auto facet_unique_vertices = - polygon_unique_vertices( model(), block, polyhedron_facet ); - if( absl::c_contains( facet_unique_vertices, geode::NO_ID ) ) + const auto& mesh = block.mesh(); + const geode::SolidMeshAdjacency3D inspector{ mesh }; + auto wrong_adjacencies = + inspector.polyhedron_facets_with_wrong_adjacency(); + wrong_adjacencies.set_description( absl::StrCat( "Block ", + block.name().value_or( block.id().string() ), " (", + block.id().string(), + ") polyhedron facets adjacencies issues" ) ); + const auto unique_vertices = mesh_unique_vertices( + block.component_id(), mesh.nb_vertices() ); + for( const auto polyhedron_id : Range{ mesh.nb_polyhedra() } ) { - return std::nullopt; + for( const auto facet_id : + LRange{ mesh.nb_polyhedron_facets( polyhedron_id ) } ) + { + const PolyhedronFacet polyhedron_facet{ polyhedron_id, + facet_id }; + if( !mesh.is_polyhedron_facet_on_border( + polyhedron_facet ) ) + { + continue; + } + SortedUniqueVertices facet_unique_vertices; + for( const auto vertex : + mesh.polyhedron_facet_vertices( polyhedron_facet ) ) + { + facet_unique_vertices.push_back( + unique_vertices[vertex] ); + } + absl::c_sort( facet_unique_vertices ); + // Facets with unlinked vertices cannot be on a Surface + if( absl::c_contains( facet_unique_vertices, NO_ID ) + || !surface_polygons.contains( facet_unique_vertices ) ) + { + wrong_adjacencies.add_issue( polyhedron_facet, + absl::StrCat( "facet ", facet_id, " of polyhedron ", + polyhedron_id, + " has no adjacencies but is not part of a " + "model Surface." ) ); + } + } } - return !detail::surface_component_mesh_polygons( - model(), facet_unique_vertices ) - .empty(); + return wrong_adjacencies; } }; From ff55a5685924428cd0cdadb7dcda91975fcef8e0 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:16:45 +0200 Subject: [PATCH 17/20] perf(Adjacency): check adjacency back-link before comparing facet cycles For each interior facet, an OrientedVertexCycle was built for every facet of the adjacent polyhedron before testing whether that facet points back. The back-link and the number of vertices are now tested first, so usually a single cycle comparison is made. inspect_brep_meshes_adjacencies: cva/brep_from_topo 0.83s -> 0.58s, corbi_tet_mesh 0.07s -> 0.05s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../criterion/adjacency/solid_adjacency.cpp | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp b/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp index 734210a6..5e581cdd 100644 --- a/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp @@ -96,19 +96,32 @@ namespace geode { return false; } - const auto facet_vertices = - detail::OrientedVertexCycle< PolyhedronFacetVertices >( - mesh_.polyhedron_facet_vertices( polyhedron_facet ) ); + const auto nb_facet_vertices = + mesh_.nb_polyhedron_facet_vertices( polyhedron_facet ); + std::optional< + detail::OrientedVertexCycle< PolyhedronFacetVertices > > + facet_vertices; for( const auto facet_id : LRange{ mesh_.nb_polyhedron_facets( polyhedron_adj_id ) } ) { const PolyhedronFacet adj_facet{ polyhedron_adj_id, facet_id }; - const auto adj_facet_vertices = - detail::OrientedVertexCycle< PolyhedronFacetVertices >( - mesh_.polyhedron_facet_vertices( adj_facet ) ); - if( facet_vertices.is_opposite( adj_facet_vertices ) - && mesh_.polyhedron_adjacent( adj_facet ) - == polyhedron_facet.polyhedron_id ) + // Cheap checks before comparing the vertex cycles + if( mesh_.polyhedron_adjacent( adj_facet ) + != polyhedron_facet.polyhedron_id + || mesh_.nb_polyhedron_facet_vertices( adj_facet ) + != nb_facet_vertices ) + { + continue; + } + if( !facet_vertices ) + { + facet_vertices.emplace( + mesh_.polyhedron_facet_vertices( polyhedron_facet ) ); + } + const detail::OrientedVertexCycle< PolyhedronFacetVertices > + adj_facet_vertices{ mesh_.polyhedron_facet_vertices( + adj_facet ) }; + if( facet_vertices->is_opposite( adj_facet_vertices ) ) { return true; } From b22acab06fd13019b6bcc2335018316f2661fa42 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:17:09 +0200 Subject: [PATCH 18/20] perf(Adjacency): check solid facets adjacencies in parallel polyhedron_facets_with_wrong_adjacency checked every polyhedron serially. Polyhedra are now checked in parallel and issues added in the same order. inspect_brep_meshes_adjacencies: cva/brep_from_topo 0.58s -> 0.15s, mss 0.028s -> 0.008s (identical results). Co-Authored-By: Claude Opus 5.5 --- .../criterion/adjacency/solid_adjacency.cpp | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp b/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp index 5e581cdd..3b0b43f3 100644 --- a/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp +++ b/src/geode/inspector/inspection/criterion/adjacency/solid_adjacency.cpp @@ -23,6 +23,10 @@ #include +#include + +#include + #include #include @@ -65,27 +69,47 @@ namespace geode mesh_.name().value_or( mesh_.id().string() ), " polyhedron facets adjacencies issues" ) }; + const auto wrong_facets = facets_with_wrong_adjacency(); for( const auto polyhedron_id : Range{ mesh_.nb_polyhedra() } ) { - for( const auto facet_id : - LRange{ mesh_.nb_polyhedron_facets( polyhedron_id ) } ) + for( const auto facet_id : wrong_facets[polyhedron_id] ) { - const PolyhedronFacet polyhedron_facet{ polyhedron_id, - facet_id }; - if( !mesh_.is_polyhedron_facet_on_border( polyhedron_facet ) - && !mesh_polyhedron_facet_has_right_adjacency( - polyhedron_facet ) ) - { - wrong_adjacency_facets.add_issue( polyhedron_facet, - absl::StrCat( "facet ", facet_id, " of polyhedron ", - polyhedron_id, " has wrong adjacencies" ) ); - } + wrong_adjacency_facets.add_issue( + PolyhedronFacet{ polyhedron_id, facet_id }, + absl::StrCat( "facet ", facet_id, " of polyhedron ", + polyhedron_id, " has wrong adjacencies" ) ); } } return wrong_adjacency_facets; } private: + // Facets with wrong adjacency of each polyhedron + std::vector< absl::InlinedVector< local_index_t, 1 > > + facets_with_wrong_adjacency() const + { + std::vector< absl::InlinedVector< local_index_t, 1 > > wrong_facets( + mesh_.nb_polyhedra() ); + async::parallel_for( + async::irange( index_t{ 0 }, mesh_.nb_polyhedra() ), + [this, &wrong_facets]( index_t polyhedron_id ) { + for( const auto facet_id : + LRange{ mesh_.nb_polyhedron_facets( polyhedron_id ) } ) + { + const PolyhedronFacet polyhedron_facet{ polyhedron_id, + facet_id }; + if( !mesh_.is_polyhedron_facet_on_border( + polyhedron_facet ) + && !mesh_polyhedron_facet_has_right_adjacency( + polyhedron_facet ) ) + { + wrong_facets[polyhedron_id].push_back( facet_id ); + } + } + } ); + return wrong_facets; + } + bool mesh_polyhedron_facet_has_right_adjacency( const PolyhedronFacet& polyhedron_facet ) const { From 20df9c5c188d8444037b690fd4410edc3a61cc12 Mon Sep 17 00:00:00 2001 From: Arnaud Botella Date: Wed, 14 Oct 2026 19:19:15 +0200 Subject: [PATCH 19/20] perf(NegativeElements): no polygon allocation for triangles mesh.polygon() builds a std::vector of points per polygon; triangles now use the exact triangle_area_sign on their three points directly. Co-Authored-By: Claude Opus 5.5 --- .../surface_negative_elements.cpp | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/negative_elements/surface_negative_elements.cpp b/src/geode/inspector/inspection/criterion/negative_elements/surface_negative_elements.cpp index 9ca2c615..e8921ce2 100644 --- a/src/geode/inspector/inspection/criterion/negative_elements/surface_negative_elements.cpp +++ b/src/geode/inspector/inspection/criterion/negative_elements/surface_negative_elements.cpp @@ -48,8 +48,7 @@ namespace geode { for( const auto polygon_id : Range{ mesh_.nb_polygons() } ) { - if( polygon_area_sign( mesh_.polygon( polygon_id ) ) - == SIGN::negative ) + if( polygon_is_negative( polygon_id ) ) { return true; } @@ -67,8 +66,7 @@ namespace geode { for( const auto polygon_id : Range{ mesh_.nb_polygons() } ) { - if( polygon_area_sign( mesh_.polygon( polygon_id ) ) - == SIGN::negative ) + if( polygon_is_negative( polygon_id ) ) { wrong_polygons.add_issue( polygon_id, absl::StrCat( "negative polygon ", polygon_id ) ); @@ -78,6 +76,26 @@ namespace geode return wrong_polygons; } + private: + bool polygon_is_negative( index_t polygon_id ) const + { + if constexpr( dimension == 2 ) + { + if( mesh_.nb_polygon_vertices( polygon_id ) == 3 ) + { + const auto vertices = mesh_.polygon_vertices( polygon_id ); + return triangle_area_sign( + Triangle2D{ mesh_.point( vertices[0] ), + mesh_.point( vertices[1] ), + mesh_.point( vertices[2] ) } ) + == SIGN::negative; + } + return polygon_area_sign( mesh_.polygon( polygon_id ) ) + == SIGN::negative; + } + return false; + } + private: const SurfaceMesh< dimension >& mesh_; }; From bc55f48e0e4f919f9341cc64e561e853e228f73b Mon Sep 17 00:00:00 2001 From: BotellaA <3213882+BotellaA@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:33:28 +0000 Subject: [PATCH 20/20] Apply prepare changes --- .../inspection/criterion/manifold/surface_edge_manifold.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp b/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp index cbfdcfeb..5467ba18 100644 --- a/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp +++ b/src/geode/inspector/inspection/criterion/manifold/surface_edge_manifold.cpp @@ -53,8 +53,7 @@ namespace }; if( !polygons_around_edges .try_emplace( polygon_edge_vertex_cycle, - std::pair< geode::index_t, bool >{ - 1, false } ) + std::pair< geode::index_t, bool >{ 1, false } ) .second ) { polygons_around_edges[polygon_edge_vertex_cycle].first += 1;