From 27fc341d3deee02e085955046cb6572756d617ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Fri, 4 Sep 2026 18:05:03 -0700 Subject: [PATCH] Sync with 1cd15ed --- .github/workflows/wheel.yaml | 25 +- .gitignore | 1 + CMakeLists.txt | 2 +- cmake/lagrange/lagrange_add_module.cmake | 84 ++- cmake/lagrange/lagrange_include_modules.cmake | 87 ++- cmake/recipes/external/clipper2.cmake | 16 +- cmake/recipes/external/nanobind.cmake | 2 +- cmake/recipes/external/python.cmake | 8 +- cmake/recipes/external/quadprog.cmake | 3 +- .../external/texture_signal_processing.cmake | 2 +- cmake/recipes/external/xatlas.cmake | 2 +- docs/cpp/CMakeLists.txt | 1 + docs/cpp/Doxyfile | 8 +- docs/cpp/doxyfile-predefined.txt | 50 ++ modules/core/CMakeLists.txt | 29 +- modules/core/include/lagrange/Attribute.h | 6 +- modules/core/include/lagrange/Components.h | 2 +- .../core/include/lagrange/ExactPredicates.h | 2 +- modules/core/include/lagrange/Logger.h | 2 +- modules/core/include/lagrange/Mesh.h | 4 +- .../core/include/lagrange/MeshNavigation.h | 4 +- modules/core/include/lagrange/MeshTopology.h | 2 +- .../lagrange/attributes/attribute_utils.h | 2 +- .../attributes/condense_indexed_attribute.h | 2 +- .../lagrange/attributes/eval_as_attribute.h | 2 +- .../lagrange/attributes/map_attributes.h | 2 +- .../attributes/map_corner_attributes.h | 2 +- .../attributes/map_facet_attributes.h | 2 +- .../attributes/map_indexed_attributes.h | 2 +- .../attributes/map_vertex_attributes.h | 2 +- .../lagrange/attributes/rename_attribute.h | 2 +- .../attributes/unify_corner_indices.h | 2 +- .../lagrange/attributes/unify_index_buffer.h | 2 +- .../lagrange/chain_corners_around_vertices.h | 2 +- modules/core/include/lagrange/chain_edges.h | 2 +- .../lagrange/chain_edges_into_simple_loops.h | 2 +- .../core/include/lagrange/combine_mesh_list.h | 2 +- .../lagrange/compute_bordered_components.h | 2 +- .../include/lagrange/compute_corner_normal.h | 2 +- .../lagrange/compute_dihedral_angles.h | 2 +- .../lagrange/compute_dijkstra_distance.h | 5 +- .../include/lagrange/compute_edge_lengths.h | 2 +- modules/core/include/lagrange/compute_euler.h | 2 +- .../include/lagrange/compute_facet_area.h | 2 +- .../lagrange/compute_greedy_coloring.h | 4 +- .../include/lagrange/compute_lift_operator.h | 6 +- .../include/lagrange/compute_mesh_centroid.h | 2 +- .../lagrange/compute_mesh_covariance.h | 2 +- .../core/include/lagrange/compute_normal.h | 2 +- .../include/lagrange/compute_pointcloud_pca.h | 2 +- .../lagrange/compute_tangent_bitangent.h | 2 +- .../lagrange/compute_triangle_normal.h | 2 +- .../core/include/lagrange/compute_uv_charts.h | 2 +- .../include/lagrange/compute_uv_distortion.h | 2 +- .../include/lagrange/compute_vertex_normal.h | 2 +- .../include/lagrange/compute_vertex_valence.h | 2 +- .../include/lagrange/corner_to_edge_mapping.h | 29 +- .../include/lagrange/extract_boundary_loops.h | 2 +- .../core/include/lagrange/extract_submesh.h | 2 +- .../core/include/lagrange/foreach_attribute.h | 2 +- .../core/include/lagrange/internal/dijkstra.h | 2 +- .../lagrange/internal/invert_mapping.h | 1 + .../lagrange/internal/map_attributes.h | 5 +- .../corner_to_edge_mapping.h} | 23 +- modules/core/include/lagrange/legacy/inline.h | 2 +- .../include/lagrange/marching_triangles.h | 2 +- .../lagrange/mesh_cleanup/close_small_holes.h | 2 +- .../detect_degenerate_triangles.h | 2 +- .../mesh_cleanup/is_vertex_manifold.h | 2 +- .../remove_degenerate_triangles.h | 2 +- .../mesh_cleanup/remove_duplicate_facets.h | 2 +- .../mesh_cleanup/remove_duplicate_vertices.h | 2 +- .../mesh_cleanup/remove_isolated_vertices.h | 2 +- .../mesh_cleanup/remove_null_area_triangles.h | 2 +- .../mesh_cleanup/remove_short_edges.h | 2 +- ...emove_topologically_degenerate_triangles.h | 2 +- .../mesh_cleanup/resolve_nonmanifoldness.h | 2 +- .../resolve_vertex_nonmanifoldness.h | 2 +- .../lagrange/mesh_cleanup/split_long_edges.h | 2 +- .../lagrange/mesh_cleanup/split_triangle.h | 2 +- modules/core/include/lagrange/mesh_convert.h | 10 +- .../core/include/lagrange/normalize_meshes.h | 2 +- .../core/include/lagrange/orient_outward.h | 2 +- modules/core/include/lagrange/quad_to_tri.h | 2 +- .../include/lagrange/reorder_mesh_vertices.h | 2 +- .../select_facets_by_normal_similarity.h | 12 +- .../lagrange/select_facets_in_frustum.h | 2 +- .../include/lagrange/thicken_and_close_mesh.h | 2 +- .../core/include/lagrange/transform_mesh.h | 2 +- .../include/lagrange/unify_index_buffer.h | 2 +- .../include/lagrange/utils/ProgressCallback.h | 2 + .../core/include/lagrange/utils/chain_edges.h | 4 +- modules/core/include/lagrange/utils/utils.h | 2 +- modules/core/include/lagrange/utils/warnoff.h | 1 + modules/core/include/lagrange/uv_mesh.h | 2 +- modules/core/js/src/core.cpp | 597 +++++++++++++++++- modules/core/js/test/attribute.test.ts | 579 +++++++++++++++++ modules/core/js/ts/core.ts | 173 ++++- .../include/lagrange/python/utils/StubType.h | 2 +- .../lagrange/python/utils/bind_safe_vector.h | 9 +- modules/core/python/src/bind_utilities.h | 41 +- modules/core/python/src/core.cpp | 1 + modules/core/python/src/logging.cpp | 4 +- .../python/tests/test_compute_uv_tile_list.py | 41 ++ modules/core/python/tests/test_mesh_bbox.py | 46 ++ modules/core/src/Mesh.cpp | 7 +- modules/core/src/compute_components.cpp | 21 + .../core/src/compute_dijkstra_distance.cpp | 2 + modules/core/src/compute_mesh_covariance.cpp | 4 + modules/core/src/compute_normal.cpp | 20 +- .../core/src/compute_tangent_bitangent.cpp | 2 + modules/core/src/compute_uv_tile_list.cpp | 2 +- modules/core/src/compute_vertex_normal.cpp | 1 + modules/core/src/compute_vertex_valence.cpp | 1 + modules/core/src/create_mesh.cpp | 14 +- modules/core/src/extract_submesh.cpp | 11 + modules/core/src/isoline.cpp | 1 + .../src/mesh_cleanup/close_small_holes.cpp | 1 + .../mesh_cleanup/remove_duplicate_facets.cpp | 1 + .../remove_duplicate_vertices.cpp | 2 + .../mesh_cleanup/remove_isolated_vertices.cpp | 1 + .../mesh_cleanup/resolve_nonmanifoldness.cpp | 2 + .../resolve_vertex_nonmanifoldness.cpp | 2 + .../src/mesh_cleanup/split_long_edges.cpp | 1 + modules/core/src/normalize_meshes.cpp | 1 + modules/core/src/orient_outward.cpp | 2 + .../select_facets_by_normal_similarity.cpp | 4 + modules/core/src/select_facets_in_frustum.cpp | 1 + modules/core/src/separate_by_facet_groups.cpp | 4 + modules/core/src/thicken_and_close_mesh.cpp | 1 + .../tests/attributes/test_attribute_utils.cpp | 18 +- .../test_condense_indexed_attribute.cpp | 18 +- .../attributes/test_unify_index_buffer.cpp | 30 +- .../core/tests/compile_errors/CMakeLists.txt | 6 +- .../compile_errors/test_mesh_attribute.cpp | 2 +- .../compute_tangent_bitangent_mikktspace.h | 1 + .../experimental/test_indexed_attributes.cpp | 14 +- .../test_detect_degenerate_triangles.cpp | 14 +- .../test_remove_degenerate_facets.cpp | 1 + .../test_remove_degenerate_triangles.cpp | 20 +- .../test_remove_isolated_vertices.cpp | 143 ++++- .../mesh_cleanup/test_remove_short_edges.cpp | 11 +- ...ove_topologically_degenerate_triangles.cpp | 14 +- .../test_resolve_nonmanifoldness.cpp | 5 +- .../test_resolve_vertex_nonmanifoldness.cpp | 5 +- modules/core/tests/test_Connectivity.cpp | 18 +- modules/core/tests/test_EdgeFacetMap.cpp | 18 +- modules/core/tests/test_chain_edges.cpp | 14 +- modules/core/tests/test_combine_mesh_list.cpp | 14 +- modules/core/tests/test_combine_meshes.cpp | 9 + modules/core/tests/test_common.cpp | 9 +- .../core/tests/test_compute_components.cpp | 15 + .../core/tests/test_compute_corner_normal.cpp | 20 +- .../tests/test_compute_dijkstra_distance.cpp | 17 + .../core/tests/test_compute_facet_area.cpp | 11 +- .../core/tests/test_compute_mesh_centroid.cpp | 3 +- .../tests/test_compute_mesh_covariance.cpp | 6 +- modules/core/tests/test_compute_normal.cpp | 17 +- .../tests/test_compute_tangent_bitangent.cpp | 5 + .../core/tests/test_compute_vertex_normal.cpp | 7 +- .../test_compute_weighted_corner_normal.cpp | 7 +- .../tests/test_corner_to_edge_mapping.cpp | 12 +- modules/core/tests/test_create_mesh.cpp | 14 +- .../tests/test_extract_boundary_loops.cpp | 5 +- modules/core/tests/test_extract_submesh.cpp | 12 + modules/core/tests/test_isoline.cpp | 2 + .../core/tests/test_marching_triangles.cpp | 26 +- modules/core/tests/test_mesh.cpp | 16 +- modules/core/tests/test_mesh_convert.cpp | 36 +- modules/core/tests/test_mesh_trait.cpp | 10 +- modules/core/tests/test_orient_outward.cpp | 2 + modules/core/tests/test_quad_to_tri.cpp | 18 +- .../core/tests/test_reorder_mesh_vertices.cpp | 14 +- .../tests/test_sample_points_on_surface.cpp | 18 +- ...est_select_facets_by_normal_similarity.cpp | 20 + .../tests/test_select_facets_in_frustum.cpp | 1 + modules/core/tests/test_select_vertices.cpp | 16 +- .../tests/test_separate_by_facet_groups.cpp | 32 + .../core/tests/test_unify_index_buffer.cpp | 7 +- modules/core/tests/test_utils_chain_edges.cpp | 14 +- modules/core/tests/test_utils_hash.cpp | 3 + modules/core/tests/test_utils_range.cpp | 16 +- .../tests/test_weld_indexed_attribute.cpp | 2 + modules/geodesic/CMakeLists.txt | 3 +- .../extras/geometrycentral/CMakeLists.txt | 17 + .../geometrycentral}/GeodesicEngineHeat.cpp | 0 .../geometrycentral}/GeodesicEngineMMP.cpp | 0 .../geometrycentral}/geometry_central_utils.h | 0 modules/geodesic/geodesic.md | 13 +- .../lagrange/geodesic/GeodesicEngineHeat.h | 4 + .../lagrange/geodesic/GeodesicEngineMMP.h | 4 + modules/geodesic/python/CMakeLists.txt | 2 + modules/geodesic/src/GeodesicEngineDGPC.cpp | 21 +- modules/geodesic/tests/CMakeLists.txt | 8 +- .../include/lagrange/image/image_filters.h | 27 +- .../include/lagrange/image/image_utils.h | 14 +- .../image_io/include/lagrange/image_io/exr.h | 5 +- modules/io/include/lagrange/io/load_mesh.h | 8 +- .../io/include/lagrange/io/load_mesh.impl.h | 2 +- .../io/include/lagrange/io/load_mesh_assimp.h | 8 +- .../io/include/lagrange/io/load_mesh_ext.h | 2 +- .../io/include/lagrange/io/load_mesh_fbx.h | 6 +- .../io/include/lagrange/io/load_mesh_gltf.h | 6 +- .../io/include/lagrange/io/load_mesh_msh.h | 6 +- .../io/include/lagrange/io/load_mesh_obj.h | 6 +- .../io/include/lagrange/io/load_mesh_ply.h | 6 +- .../io/include/lagrange/io/load_mesh_stl.h | 6 +- modules/io/include/lagrange/io/load_scene.h | 6 +- .../include/lagrange/io/load_scene_assimp.h | 6 +- .../io/include/lagrange/io/load_scene_fbx.h | 6 +- .../io/include/lagrange/io/load_scene_gltf.h | 6 +- .../io/include/lagrange/io/load_scene_obj.h | 6 +- .../include/lagrange/io/load_simple_scene.h | 6 +- .../lagrange/io/load_simple_scene_assimp.h | 8 +- .../lagrange/io/load_simple_scene_fbx.h | 8 +- .../lagrange/io/load_simple_scene_gltf.h | 8 +- modules/io/include/lagrange/io/save_mesh.h | 8 +- .../io/include/lagrange/io/save_mesh_gltf.h | 6 +- .../io/include/lagrange/io/save_mesh_msh.h | 6 +- .../io/include/lagrange/io/save_mesh_obj.h | 6 +- .../io/include/lagrange/io/save_mesh_ply.h | 6 +- modules/io/include/lagrange/io/save_scene.h | 6 +- .../io/include/lagrange/io/save_scene_gltf.h | 6 +- .../io/include/lagrange/io/save_scene_obj.h | 6 +- .../include/lagrange/io/save_simple_scene.h | 6 +- .../lagrange/io/save_simple_scene_gltf.h | 6 +- .../lagrange/io/save_simple_scene_obj.h | 6 +- modules/io/include/lagrange/io/types.h | 4 + modules/io/js/src/io.cpp | 1 + modules/io/js/ts/io.ts | 2 + modules/io/python/src/io.cpp | 22 +- modules/io/src/legacy_load_mesh.cpp | 8 +- modules/io/src/load_mesh_stl.cpp | 5 + modules/io/src/load_obj.cpp | 21 +- modules/io/tests/test_obj.cpp | 18 + .../packing/compute_rectangle_packing.h | 2 +- .../lagrange/packing/repack_uv_charts.h | 6 +- .../lagrange/poisson/AttributeEvaluator.h | 7 +- .../poisson/mesh_from_oriented_points.h | 3 +- modules/poisson/src/AttributeEvaluator.cpp | 12 +- .../poisson/src/mesh_from_oriented_points.cpp | 6 +- .../include/lagrange/primitive/SweepPath.h | 2 +- .../lagrange/primitive/generate_disc.h | 4 +- .../lagrange/primitive/generate_hexahedron.h | 2 +- .../lagrange/primitive/generate_icosahedron.h | 4 +- .../lagrange/primitive/generate_octahedron.h | 6 +- .../primitive/generate_rounded_cone.h | 6 +- .../primitive/generate_rounded_cube.h | 6 +- .../primitive/generate_rounded_cylinder.h | 2 +- .../primitive/generate_rounded_plane.h | 6 +- .../lagrange/primitive/generate_sphere.h | 6 +- .../primitive/generate_subdivided_sphere.h | 6 +- .../primitive/generate_swept_surface.h | 6 +- .../lagrange/primitive/generate_torus.h | 6 +- .../lagrange/primitive/generation_utils.h | 2 +- .../primitive/src/generate_swept_surface.cpp | 4 + modules/python/CMakeLists.txt | 3 +- .../lagrange/raycasting/ClosestPointResult.h | 2 +- .../lagrange/raycasting/EmbreeHelper.h | 2 +- .../lagrange/raycasting/EmbreeRayCaster.h | 2 +- .../lagrange/raycasting/RayCasterMesh.h | 2 +- .../lagrange/raycasting/create_ray_caster.h | 2 +- .../raycasting/embree_closest_point.h | 2 +- .../lagrange/raycasting/project_attributes.h | 2 +- .../project_attributes_closest_point.h | 2 +- .../project_attributes_directional.h | 2 +- .../lagrange/raycasting/project_options.h | 2 +- .../project_particles_directional.h | 2 +- .../src/compute_local_feature_size.cpp | 1 + .../raycasting/src/legacy/EmbreeHelper.cpp | 26 +- modules/scene/python/src/bind_value.h | 6 +- .../lagrange/subdivision/compute_sharpness.h | 7 +- .../lagrange/subdivision/mesh_subdivision.h | 6 +- .../subdivision/midpoint_subdivision.h | 7 +- .../lagrange/subdivision/sqrt_subdivision.h | 7 +- modules/subdivision/src/compute_sharpness.cpp | 1 + .../subdivision/src/midpoint_subdivision.cpp | 1 + .../subdivision/src/subdivide_adaptive.cpp | 5 + modules/subdivision/src/subdivide_uniform.cpp | 4 + .../testing/include/lagrange/testing/common.h | 6 +- .../include/lagrange/testing/require_approx.h | 2 +- modules/testing/src/common.cpp | 8 +- modules/texproc/examples/CMakeLists.txt | 2 +- .../texproc/examples/texture_compositing.cpp | 430 ++++++++++++- .../lagrange/texproc/TextureRasterizer.h | 5 + .../lagrange/texproc/texture_compositing.h | 46 +- .../lagrange/texproc/texture_filtering.h | 4 + .../lagrange/texproc/texture_stitching.h | 4 + .../examples/plot_convergence_benchmark.py | 283 +++++++++ .../python/scripts/texture_from_multiview.py | 100 ++- modules/texproc/python/src/texproc.cpp | 58 +- modules/texproc/python/tests/test_texproc.py | 8 +- modules/texproc/src/TextureRasterizer.cpp | 16 +- modules/texproc/src/geodesic_dilation.cpp | 3 +- modules/texproc/src/mesh_utils.h | 12 +- modules/texproc/src/texture_compositing.cpp | 543 ++++++++++------ modules/texproc/src/texture_filtering.cpp | 6 +- modules/texproc/src/texture_stitching.cpp | 6 +- modules/texproc/tests/CMakeLists.txt | 3 +- ...est_texture_compositing_regularization.cpp | 363 +++++++++++ modules/volume/examples/CMakeLists.txt | 4 + modules/volume/examples/grid_viewer.cpp | 14 +- modules/volume/examples/register_grid.h | 21 +- modules/volume/examples/voxelize_cli.cpp | 11 +- modules/volume/examples/voxelize_gui.cpp | 19 +- .../volume/SmoothedMeshGradientField.h | 82 +++ .../include/lagrange/volume/mesh_to_volume.h | 8 +- .../include/lagrange/volume/volume_to_mesh.h | 2 +- modules/volume/python/src/volume.cpp | 10 + .../volume/src/SmoothedMeshGradientField.cpp | 111 ++++ modules/volume/src/mesh_to_volume.cpp | 17 +- modules/volume/tests/CMakeLists.txt | 4 +- .../test_smoothed_mesh_gradient_field.cpp | 254 ++++++++ modules/volume/tests/test_voxelization.cpp | 35 + pyproject.toml | 3 + 315 files changed, 5217 insertions(+), 825 deletions(-) create mode 100644 docs/cpp/doxyfile-predefined.txt rename modules/core/include/lagrange/{corner_to_edge_mapping.impl.h => legacy/corner_to_edge_mapping.h} (80%) create mode 100644 modules/core/js/test/attribute.test.ts create mode 100644 modules/core/python/tests/test_compute_uv_tile_list.py create mode 100644 modules/core/python/tests/test_mesh_bbox.py create mode 100644 modules/geodesic/extras/geometrycentral/CMakeLists.txt rename modules/geodesic/{src => extras/geometrycentral}/GeodesicEngineHeat.cpp (100%) rename modules/geodesic/{src => extras/geometrycentral}/GeodesicEngineMMP.cpp (100%) rename modules/geodesic/{src => extras/geometrycentral}/geometry_central_utils.h (100%) create mode 100644 modules/texproc/python/examples/plot_convergence_benchmark.py create mode 100644 modules/texproc/tests/test_texture_compositing_regularization.cpp create mode 100644 modules/volume/include/lagrange/volume/SmoothedMeshGradientField.h create mode 100644 modules/volume/src/SmoothedMeshGradientField.cpp create mode 100644 modules/volume/tests/test_smoothed_mesh_gradient_field.cpp diff --git a/.github/workflows/wheel.yaml b/.github/workflows/wheel.yaml index abf00595..bef18857 100644 --- a/.github/workflows/wheel.yaml +++ b/.github/workflows/wheel.yaml @@ -47,16 +47,35 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v3.4.1 env: + CIBW_BUILD: "cp310-*" CIBW_BUILD_FRONTEND: "build[uv]" CIBW_ARCHS_LINUX: auto64 CIBW_ARCHS_WINDOWS: auto64 CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - CIBW_SKIP: "*-musllinux* pp*" - CIBW_TEST_COMMAND: "pytest -s --pdb {project}/modules" - CIBW_TEST_REQUIRES: pytest + CIBW_SKIP: "*-musllinux*" MACOSX_DEPLOYMENT_TARGET: 13.00 CIBW_BUILD_VERBOSITY: 2 + - name: Test wheel on supported Python versions + shell: bash + run: | + set -euxo pipefail + wheels=(wheelhouse/*.whl) + if [[ ${#wheels[@]} -ne 1 || ! -f "${wheels[0]}" ]]; then + echo "Expected exactly one wheel, found: ${wheels[*]}" >&2 + exit 1 + fi + + for python_version in 3.10 3.11 3.12 3.13 3.14; do + uv run \ + --isolated \ + --no-project \ + --python "$python_version" \ + --with pytest \ + --with "${wheels[0]}" \ + python -m pytest -s modules + done + - uses: actions/upload-artifact@v4 with: name: wheels-${{matrix.os}} diff --git a/.gitignore b/.gitignore index a2678c70..f4dc84b6 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,7 @@ LagrangeOptions.cmake # The MetaBuild equivalent /META/META.user.yaml /META/META.lock +/META/build /external /dist diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a1faa68..56321d21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ else() endif() # Check required CMake version -cmake_minimum_required(VERSION 3.25.0) +cmake_minimum_required(VERSION 3.26.0) cmake_policy(SET CMP0054 NEW) # Only interpret if() arguments as variables or keywords when unquoted. cmake_policy(SET CMP0076 NEW) # target_sources() command converts relative paths to absolute. diff --git a/cmake/lagrange/lagrange_add_module.cmake b/cmake/lagrange/lagrange_add_module.cmake index ab3dc08c..a6cfd34a 100644 --- a/cmake/lagrange/lagrange_add_module.cmake +++ b/cmake/lagrange/lagrange_add_module.cmake @@ -9,6 +9,86 @@ # OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. # +function(lagrange_add_module_component module_name) + get_filename_component(component_dir "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) + get_filename_component(component "${component_dir}" NAME) + get_filename_component(extras_dir "${component_dir}" DIRECTORY) + get_filename_component(module_dir "${extras_dir}" DIRECTORY) + get_filename_component(detected_module_name "${module_dir}" NAME) + if(NOT detected_module_name STREQUAL module_name) + message(FATAL_ERROR + "Component '${component}' is under module '${detected_module_name}', not '${module_name}'" + ) + endif() + + set(module_target "lagrange_${module_name}") + if(NOT TARGET ${module_target}) + message(FATAL_ERROR "Cannot add component '${component}': lagrange::${module_name} does not exist") + endif() + if(TARGET lagrange::${module_name}::${component}) + return() + endif() + if(NOT component MATCHES "^[A-Za-z_]+$") + message(FATAL_ERROR + "Invalid component name '${component}' for lagrange::${module_name}; " + "component names may contain letters and underscores only" + ) + endif() + + file(GLOB_RECURSE component_sources CONFIGURE_DEPENDS "*.cpp" "*.h") + if(NOT component_sources) + message(FATAL_ERROR "Component '${component}' for lagrange::${module_name} has no sources") + endif() + + get_target_property(module_type ${module_target} TYPE) + if(module_type STREQUAL "STATIC_LIBRARY") + set(component_type STATIC) + elseif(module_type STREQUAL "SHARED_LIBRARY") + set(component_type SHARED) + else() + message(FATAL_ERROR "Compiled components require a static or shared base module") + endif() + + string(TOUPPER "${module_name}" uc_module_name) + string(TOUPPER "${component}" uc_component) + set(component_target "lagrange_${module_name}_${component}") + add_library(${component_target} ${component_type} ${component_sources}) + add_library(lagrange::${module_name}::${component} ALIAS ${component_target}) + target_link_libraries(${component_target} PUBLIC ${module_target}) + target_include_directories(${component_target} PRIVATE "${module_dir}/src") + target_compile_definitions( + ${component_target} + PUBLIC "LAGRANGE_${uc_module_name}_WITH_${uc_component}" + ) + if(module_type STREQUAL "SHARED_LIBRARY") + target_compile_definitions(${component_target} PRIVATE "lagrange_${module_name}_EXPORTS") + endif() + target_compile_features(${component_target} PUBLIC cxx_std_17) + lagrange_add_warnings(${component_target}) + target_code_coverage(${component_target} PUBLIC AUTO ALL EXCLUDE "${FETCHCONTENT_BASE_DIR}/*") + + set_target_properties( + ${component_target} + PROPERTIES + POSITION_INDEPENDENT_CODE ON + FOLDER "${LAGRANGE_IDE_PREFIX}Lagrange/Modules/Components" + ) + get_target_property(warnings_as_errors ${module_target} COMPILE_WARNING_AS_ERROR) + if(NOT warnings_as_errors STREQUAL "warnings_as_errors-NOTFOUND") + set_target_properties( + ${component_target} + PROPERTIES COMPILE_WARNING_AS_ERROR "${warnings_as_errors}" + ) + endif() + + get_target_property(install_components ${module_target} LAGRANGE_MODULE_INSTALL_COMPONENTS) + if(install_components) + lagrange_install(${component_target}) + endif() + set_target_properties(${component_target} PROPERTIES EXPORT_NAME "${module_name}::${component}") + message(STATUS "Lagrange: enabling component 'lagrange::${module_name}::${component}'") +endfunction() + function(lagrange_add_module) # Retrieve module name get_filename_component(module_path "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) @@ -80,7 +160,9 @@ function(lagrange_add_module) # Create install rules if(LAGRANGE_INSTALL AND NOT OPTIONS_NO_INSTALL) + set_target_properties(lagrange_${module_name} PROPERTIES LAGRANGE_MODULE_INSTALL_COMPONENTS TRUE) lagrange_install(lagrange_${module_name}) + else() + set_target_properties(lagrange_${module_name} PROPERTIES LAGRANGE_MODULE_INSTALL_COMPONENTS FALSE) endif() - endfunction() diff --git a/cmake/lagrange/lagrange_include_modules.cmake b/cmake/lagrange/lagrange_include_modules.cmake index 9c89fad9..0f411ad3 100644 --- a/cmake/lagrange/lagrange_include_modules.cmake +++ b/cmake/lagrange/lagrange_include_modules.cmake @@ -9,21 +9,86 @@ # OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. # -function(lagrange_include_modules) +# Includes one module and any requested optional components. +# Usage: lagrange_include_module( [COMPONENTS ...]). +function(lagrange_include_module name) + set(options "") + set(oneValueArgs "") + set(multiValueArgs COMPONENTS) + cmake_parse_arguments(PARSE_ARGV 1 OPTIONS "${options}" "${oneValueArgs}" "${multiValueArgs}") + if(OPTIONS_UNPARSED_ARGUMENTS) + message(FATAL_ERROR + "Unexpected arguments for lagrange_include_module(${name}): ${OPTIONS_UNPARSED_ARGUMENTS}" + ) + endif() + + if(LAGRANGE_ALLOWLIST AND NOT (${name} IN_LIST LAGRANGE_ALLOWLIST)) + message(FATAL_ERROR "Lagrange module '${name}' is not allowed in this project") + endif() + if(${name} IN_LIST LAGRANGE_BLOCKLIST) + message(FATAL_ERROR "Lagrange module '${name}' is excluded from this project") + endif() + get_property(lagrange_module_path GLOBAL PROPERTY __lagrange_module_path) + get_property(lagrange_source_dir GLOBAL PROPERTY __lagrange_source_dir) + get_property(lagrange_binary_dir GLOBAL PROPERTY __lagrange_binary_dir) set(CMAKE_MODULE_PATH ${lagrange_module_path}) - foreach(name IN ITEMS ${ARGN}) - if(LAGRANGE_ALLOWLIST AND NOT (${name} IN_LIST LAGRANGE_ALLOWLIST)) - message(FATAL_ERROR "Lagrange module '${name}' is not allowed in this project") + if(NOT TARGET lagrange::${name}) + add_subdirectory( + ${lagrange_source_dir}/modules/${name} + ${lagrange_binary_dir}/modules/lagrange_${name} + ) + endif() + + foreach(component IN LISTS OPTIONS_COMPONENTS) + if(TARGET lagrange::${name}::${component}) + continue() endif() - if(${name} IN_LIST LAGRANGE_BLOCKLIST) - message(FATAL_ERROR "Lagrange module '${name}' is excluded from this project") + if(NOT component MATCHES "^[A-Za-z_]+$") + message(FATAL_ERROR + "Invalid component name '${component}' for lagrange::${name}; " + "component names may contain letters and underscores only" + ) endif() - - if(NOT TARGET lagrange::${name}) - get_property(lagrange_source_dir GLOBAL PROPERTY __lagrange_source_dir) - get_property(lagrange_binary_dir GLOBAL PROPERTY __lagrange_binary_dir) - add_subdirectory(${lagrange_source_dir}/modules/${name} ${lagrange_binary_dir}/modules/lagrange_${name}) + set(component_source_dir "${lagrange_source_dir}/modules/${name}/extras/${component}") + if(NOT EXISTS "${component_source_dir}/CMakeLists.txt") + file(GLOB available_component_dirs LIST_DIRECTORIES TRUE + "${lagrange_source_dir}/modules/${name}/extras/*" + ) + set(available_components "") + foreach(component_dir IN LISTS available_component_dirs) + if(IS_DIRECTORY "${component_dir}") + get_filename_component(available_component "${component_dir}" NAME) + list(APPEND available_components "${available_component}") + endif() + endforeach() + message(FATAL_ERROR + "Unknown component '${component}' for lagrange::${name}. " + "Available components: ${available_components}" + ) endif() + add_subdirectory( + "${component_source_dir}" + "${lagrange_binary_dir}/modules/lagrange_${name}/extras/${component}" + ) + if(NOT TARGET lagrange::${name}::${component}) + message(FATAL_ERROR + "Component '${component}' did not create lagrange::${name}::${component}" + ) + endif() + endforeach() +endfunction() + +# Includes multiple base modules; use lagrange_include_module() for optional components. +function(lagrange_include_modules) + set(module_names ${ARGN}) + if("COMPONENTS" IN_LIST module_names) + message(FATAL_ERROR + "lagrange_include_modules() accepts module names only; use " + "lagrange_include_module( COMPONENTS ...) for components" + ) + endif() + foreach(name IN LISTS module_names) + lagrange_include_module(${name}) endforeach() endfunction() diff --git a/cmake/recipes/external/clipper2.cmake b/cmake/recipes/external/clipper2.cmake index a2a644ce..5383a97f 100644 --- a/cmake/recipes/external/clipper2.cmake +++ b/cmake/recipes/external/clipper2.cmake @@ -20,12 +20,16 @@ option(CLIPPER2_EXAMPLES "Build examples" OFF) option(CLIPPER2_TESTS "Build tests" OFF) include(CPM) -CPMAddPackage( - NAME clipper2 - GITHUB_REPOSITORY AngusJohnson/Clipper2 - GIT_TAG Clipper2_1.4.0 - SOURCE_SUBDIR CPP -) +block() + set(BUILD_SHARED_LIBS OFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + CPMAddPackage( + NAME clipper2 + GITHUB_REPOSITORY AngusJohnson/Clipper2 + GIT_TAG Clipper2_1.4.0 + SOURCE_SUBDIR CPP + ) +endblock() add_library(Clipper2::Clipper2 ALIAS Clipper2) diff --git a/cmake/recipes/external/nanobind.cmake b/cmake/recipes/external/nanobind.cmake index fbb2207e..d793e98a 100644 --- a/cmake/recipes/external/nanobind.cmake +++ b/cmake/recipes/external/nanobind.cmake @@ -19,7 +19,7 @@ include(CPM) CPMAddPackage( NAME nanobind GITHUB_REPOSITORY wjakob/nanobind - GIT_TAG v2.13.0 + GIT_TAG v3.0.0 DOWNLOAD_ONLY ON ) diff --git a/cmake/recipes/external/python.cmake b/cmake/recipes/external/python.cmake index 919c4bc6..cd8b7b99 100644 --- a/cmake/recipes/external/python.cmake +++ b/cmake/recipes/external/python.cmake @@ -9,9 +9,13 @@ # OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. # -if(TARGET Python::Module) +if(TARGET Python::Module AND TARGET Python::SABIModule) return() endif() set(Python_FIND_VIRTUALENV FIRST) -find_package(Python 3.9 COMPONENTS Interpreter Development.Module REQUIRED) +find_package( + Python 3.10 + COMPONENTS Interpreter Development.Module Development.SABIModule + REQUIRED +) diff --git a/cmake/recipes/external/quadprog.cmake b/cmake/recipes/external/quadprog.cmake index 377b43aa..402d4b02 100644 --- a/cmake/recipes/external/quadprog.cmake +++ b/cmake/recipes/external/quadprog.cmake @@ -22,12 +22,13 @@ CPMAddPackage( GIT_TAG 8e6e38ad5b257d1795a9ef995e4fd7db72ccf551 ) -add_library(quadprog +add_library(quadprog STATIC ${quadprog_SOURCE_DIR}/QuadProgPP.cpp ${quadprog_SOURCE_DIR}/QuadProgPP.h ) add_library(quadprog::quadprog ALIAS quadprog) set_target_properties(quadprog PROPERTIES FOLDER third_party) +set_target_properties(quadprog PROPERTIES POSITION_INDEPENDENT_CODE ON) include(GNUInstallDirs) target_include_directories(quadprog SYSTEM INTERFACE diff --git a/cmake/recipes/external/texture_signal_processing.cmake b/cmake/recipes/external/texture_signal_processing.cmake index e8e606a8..cb4e6f1b 100644 --- a/cmake/recipes/external/texture_signal_processing.cmake +++ b/cmake/recipes/external/texture_signal_processing.cmake @@ -19,7 +19,7 @@ include(CPM) CPMAddPackage( NAME texture_signal_processing GITHUB_REPOSITORY jdumas/TextureSignalProcessing - GIT_TAG 4689f674deadde94511b6f4c00b6dedf3790527e # adobe/lagrange branch + GIT_TAG 72de1412d5a579f83407eac86dd428a7032d4e34 # adobe/lagrange branch ) add_library(texture_signal_processing::texture_signal_processing INTERFACE IMPORTED GLOBAL) diff --git a/cmake/recipes/external/xatlas.cmake b/cmake/recipes/external/xatlas.cmake index d31b7211..600c017f 100644 --- a/cmake/recipes/external/xatlas.cmake +++ b/cmake/recipes/external/xatlas.cmake @@ -19,7 +19,7 @@ include(CPM) CPMAddPackage( NAME xatlas GITHUB_REPOSITORY jdumas/xatlas - GIT_TAG 649fae81dd20801db55338bc86e0a7eab745ddf4 + GIT_TAG 3d2c0a44096feb59e20b05a8a399169f3829f210 DOWNLOAD_ONLY ON ) diff --git a/docs/cpp/CMakeLists.txt b/docs/cpp/CMakeLists.txt index 3f6ef10f..328744b7 100644 --- a/docs/cpp/CMakeLists.txt +++ b/docs/cpp/CMakeLists.txt @@ -56,6 +56,7 @@ set(DOXYGEN_EXCLUDE_FOLDERS ) list(JOIN DOXYGEN_EXCLUDE_FOLDERS " " DOXYGEN_EXCLUDE_FOLDERS) +set(DOXYGEN_PREDEFINED_FILE ${CMAKE_CURRENT_SOURCE_DIR}/doxyfile-predefined.txt) set(DOXYGEN_LAYOUT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/DoxygenLayout.xml) set(DOXYGEN_WORKING_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..) set(DOXYGEN_INPUT_DIR modules) diff --git a/docs/cpp/Doxyfile b/docs/cpp/Doxyfile index 80814432..37a6ce2a 100644 --- a/docs/cpp/Doxyfile +++ b/docs/cpp/Doxyfile @@ -2484,9 +2484,13 @@ INCLUDE_FILE_PATTERNS = # is assumed. To prevent a macro definition from being undefined via #undef or # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. +# +# The PREDEFINED tag itself is defined in doxyfile-predefined.txt, which is +# generated by scripts/generate_export_headers.py (via @INCLUDE below) so it +# always lists every module's LA__API export macro without requiring +# fragile in-place edits of this file. -PREDEFINED = LA_IGNORE_DOCUMENTATION_WARNING_BEGIN= \ - LA_IGNORE_DOCUMENTATION_WARNING_END= +@INCLUDE = @DOXYGEN_PREDEFINED_FILE@ # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/docs/cpp/doxyfile-predefined.txt b/docs/cpp/doxyfile-predefined.txt new file mode 100644 index 00000000..00e722db --- /dev/null +++ b/docs/cpp/doxyfile-predefined.txt @@ -0,0 +1,50 @@ +PREDEFINED = LA_IGNORE_DOCUMENTATION_WARNING_BEGIN= \ + LA_IGNORE_DOCUMENTATION_WARNING_END= \ + LA_ANORIGAMI_API= \ + LA_BAKING_API= \ + LA_BVH_API= \ + LA_CAD_IO_API= \ + LA_CAGE_API= \ + LA_CLIPPING_API= \ + LA_CONTOURING_API= \ + LA_CORE_API= \ + LA_DECAL_API= \ + LA_DECIMATION_API= \ + LA_DEFORMERS_API= \ + LA_DISPLACEMENT_API= \ + LA_FILTERING_API= \ + LA_FS_API= \ + LA_GEODESIC_API= \ + LA_HULL_API= \ + LA_IMAGE_API= \ + LA_IMAGE_IO_API= \ + LA_IO_API= \ + LA_NEO_API= \ + LA_NEO_IO_API= \ + LA_PACKING_API= \ + LA_PARTITIONING_API= \ + LA_PHYSICS_API= \ + LA_PLANAR_API= \ + LA_POISSON_API= \ + LA_POLYDDG_API= \ + LA_POLYSCOPE_API= \ + LA_PRIMITIVE_API= \ + LA_QUAD_LAYOUT_API= \ + LA_QUADGEN_API= \ + LA_QUADRANGULATION_API= \ + LA_RAYCASTING_API= \ + LA_REMESHING_API= \ + LA_REMESHING_IM_API= \ + LA_SAMPLING_API= \ + LA_SCENE_API= \ + LA_SEGMENTATION_API= \ + LA_SERIALIZATION2_API= \ + LA_SOLVER_API= \ + LA_SUBDIVISION_API= \ + LA_TESTING_API= \ + LA_TEXPROC_API= \ + LA_UI_API= \ + LA_USD_API= \ + LA_VOLUME_API= \ + LA_WINDING_API= \ + LA_XATLAS_API= diff --git a/modules/core/CMakeLists.txt b/modules/core/CMakeLists.txt index 45f8dded..a32eebb6 100644 --- a/modules/core/CMakeLists.txt +++ b/modules/core/CMakeLists.txt @@ -52,22 +52,19 @@ if(LAGRANGE_ASSERT_DEBUG_BREAK) target_compile_definitions(lagrange_core PRIVATE -DLA_ENABLE_ASSERT_DEBUG_BREAK) endif() -# TODO: Setup CI/CD to check that we can disable legacy functions. -# -# We probably need to split this in two options: -# - LAGRANGE_ENABLE_LEGACY_REDIRECTION: To enable/disable inclusion of legacy headers from new -# headers (and also inlines the `lagrange::legacy::` namespace). -# - LAGRANGE_ENABLE_LEGACY_FUNCTIONS: To enable/disable compilation of legacy functions altogether. -# -# For now this option is commented out because disabling it causes compilation errors due to missing -# headers. Let's re-enable it once we add some CI/CD checks for it. -# -# option(LAGRANGE_ENABLE_LEGACY_FUNCTIONS "Enable redirection for legacy functions." ON) -# if(LAGRANGE_ENABLE_LEGACY_FUNCTIONS) - target_compile_definitions(lagrange_core PUBLIC - -DLAGRANGE_ENABLE_LEGACY_FUNCTIONS - ) -# endif() +# LAGRANGE_ENABLE_LEGACY_FUNCTIONS compiles legacy code; LAGRANGE_ENABLE_LEGACY_HEADERS redirects +# legacy headers from new headers and inlines the legacy namespace (requires the former). +option(LAGRANGE_ENABLE_LEGACY_FUNCTIONS "Compile legacy functions." ON) +include(CMakeDependentOption) +cmake_dependent_option(LAGRANGE_ENABLE_LEGACY_HEADERS + "Redirect legacy headers and inline the legacy namespace." ON + "LAGRANGE_ENABLE_LEGACY_FUNCTIONS" OFF) +if(LAGRANGE_ENABLE_LEGACY_FUNCTIONS) + target_compile_definitions(lagrange_core PUBLIC -DLAGRANGE_ENABLE_LEGACY_FUNCTIONS) +endif() +if(LAGRANGE_ENABLE_LEGACY_HEADERS) + target_compile_definitions(lagrange_core PUBLIC -DLAGRANGE_ENABLE_LEGACY_HEADERS) +endif() if(LAGRANGE_FMT_EIGEN_FIX) target_compile_definitions(lagrange_core PUBLIC diff --git a/modules/core/include/lagrange/Attribute.h b/modules/core/include/lagrange/Attribute.h index 2ce1f7c6..46e17ebf 100644 --- a/modules/core/include/lagrange/Attribute.h +++ b/modules/core/include/lagrange/Attribute.h @@ -533,7 +533,7 @@ class Attribute : public AttributeBase [[nodiscard]] bool is_read_only() const { return m_is_read_only; } /// - /// Gets an entry for element @p i, at channel @p p. Note that due to internal checks, it is + /// Gets an entry for element @p i, at channel @p c. Note that due to internal checks, it is /// more efficient to first retrieve a span() of the whole buffer, then do access operations on /// this span inside a for loop. /// @@ -545,7 +545,7 @@ class Attribute : public AttributeBase ValueType get(size_t i, size_t c) const; /// - /// Gets a writable reference to the entry for element @p i, at channel @p p. Note that due to + /// Gets a writable reference to the entry for element @p i, at channel @p c. Note that due to /// internal checks, it is more efficient to first retrieve a span() of the whole buffer, then /// do access operations on this span inside a for loop. /// @@ -568,7 +568,7 @@ class Attribute : public AttributeBase ValueType get(size_t i) const; /// - /// Gets a writable reference to a scalar element @p i, at channel @p p. Note that due to + /// Gets a writable reference to a scalar element @p i. Note that due to /// internal checks, it is more efficient to first retrieve a span() of the whole buffer, then /// do access operations on this span inside a for loop. /// diff --git a/modules/core/include/lagrange/Components.h b/modules/core/include/lagrange/Components.h index 21cbb40c..ca81d083 100644 --- a/modules/core/include/lagrange/Components.h +++ b/modules/core/include/lagrange/Components.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/ExactPredicates.h b/modules/core/include/lagrange/ExactPredicates.h index 6c930b1a..2a29f24c 100644 --- a/modules/core/include/lagrange/ExactPredicates.h +++ b/modules/core/include/lagrange/ExactPredicates.h @@ -92,7 +92,7 @@ class LA_CORE_API ExactPredicates /// @param p1 First 2D point. /// @param p2 Second 2D point. /// @param p3 Third 2D point. - /// @param p4 Fourth 3D point. + /// @param p4 Fourth 2D point. /// /// @return Return a positive value if the point p4 lies inside the circle passing through /// p1, p2, and p3; a negative value if it lies outside; and zero if the four points diff --git a/modules/core/include/lagrange/Logger.h b/modules/core/include/lagrange/Logger.h index 641a2dac..58114225 100644 --- a/modules/core/include/lagrange/Logger.h +++ b/modules/core/include/lagrange/Logger.h @@ -33,7 +33,7 @@ namespace lagrange { /// #include /// /// lagrange::logger().info("This is a message"); -/// lagrange::logger().warning("Invalid value for x: {}", x); +/// lagrange::logger().warn("Invalid value for x: {}", x); /// /// // set logger verbosity /// lagrange::logger().set_level(spdlog::level::debug); diff --git a/modules/core/include/lagrange/Mesh.h b/modules/core/include/lagrange/Mesh.h index 79a15710..04c0c6fb 100644 --- a/modules/core/include/lagrange/Mesh.h +++ b/modules/core/include/lagrange/Mesh.h @@ -873,7 +873,7 @@ class Mesh : public MeshBase /// Applies a function to each corner around a prescribed vertex. /// /// @param[in] v Queried vertex index. - /// @param[in] func Callback to apply to each incident facet. + /// @param[in] func Callback to apply to each incident corner. /// /// @tparam Func A callable function of type Index -> void. /// @@ -888,7 +888,7 @@ class Mesh : public MeshBase /// Applies a function to each corner around a prescribed edge. /// /// @param[in] e Queried edge index. - /// @param[in] func Callback to apply to each incident facet. + /// @param[in] func Callback to apply to each incident corner. /// /// @tparam Func A callable function of type Index -> void. /// diff --git a/modules/core/include/lagrange/MeshNavigation.h b/modules/core/include/lagrange/MeshNavigation.h index d57026a4..429d031f 100644 --- a/modules/core/include/lagrange/MeshNavigation.h +++ b/modules/core/include/lagrange/MeshNavigation.h @@ -283,7 +283,7 @@ class MeshNavigation /// Applies a function to each corner around a prescribed vertex. /// /// @param[in] v Queried vertex index. - /// @param[in] func Callback to apply to each incident facet. + /// @param[in] func Callback to apply to each incident corner. /// /// @tparam Func A callable function of type Index -> void. /// @@ -300,7 +300,7 @@ class MeshNavigation /// Applies a function to each corner around a prescribed edge. /// /// @param[in] e Queried edge index. - /// @param[in] func Callback to apply to each incident facet. + /// @param[in] func Callback to apply to each incident corner. /// /// @tparam Func A callable function of type Index -> void. /// diff --git a/modules/core/include/lagrange/MeshTopology.h b/modules/core/include/lagrange/MeshTopology.h index ffc7f183..2b629971 100644 --- a/modules/core/include/lagrange/MeshTopology.h +++ b/modules/core/include/lagrange/MeshTopology.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Use `topology.h` instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/attribute_utils.h b/modules/core/include/lagrange/attributes/attribute_utils.h index 37972fe1..ea1ff122 100644 --- a/modules/core/include/lagrange/attributes/attribute_utils.h +++ b/modules/core/include/lagrange/attributes/attribute_utils.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::map_attribute instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/condense_indexed_attribute.h b/modules/core/include/lagrange/attributes/condense_indexed_attribute.h index d127dd5b..ce92b553 100644 --- a/modules/core/include/lagrange/attributes/condense_indexed_attribute.h +++ b/modules/core/include/lagrange/attributes/condense_indexed_attribute.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::weld_indexed_attribute instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/eval_as_attribute.h b/modules/core/include/lagrange/attributes/eval_as_attribute.h index 042c3f78..e8084ffe 100644 --- a/modules/core/include/lagrange/attributes/eval_as_attribute.h +++ b/modules/core/include/lagrange/attributes/eval_as_attribute.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/attributes/map_attributes.h b/modules/core/include/lagrange/attributes/map_attributes.h index 8aa3b057..007dc79f 100644 --- a/modules/core/include/lagrange/attributes/map_attributes.h +++ b/modules/core/include/lagrange/attributes/map_attributes.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::internal::map_attributes instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/map_corner_attributes.h b/modules/core/include/lagrange/attributes/map_corner_attributes.h index cb794fa1..77f0d6c4 100644 --- a/modules/core/include/lagrange/attributes/map_corner_attributes.h +++ b/modules/core/include/lagrange/attributes/map_corner_attributes.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::internal::map_attributes instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/map_facet_attributes.h b/modules/core/include/lagrange/attributes/map_facet_attributes.h index b1f86598..76311883 100644 --- a/modules/core/include/lagrange/attributes/map_facet_attributes.h +++ b/modules/core/include/lagrange/attributes/map_facet_attributes.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::internal::map_attributes instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/map_indexed_attributes.h b/modules/core/include/lagrange/attributes/map_indexed_attributes.h index cd7bc922..e8e7d7ad 100644 --- a/modules/core/include/lagrange/attributes/map_indexed_attributes.h +++ b/modules/core/include/lagrange/attributes/map_indexed_attributes.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::internal::map_attributes instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/map_vertex_attributes.h b/modules/core/include/lagrange/attributes/map_vertex_attributes.h index 3588ce07..c3dde3b5 100644 --- a/modules/core/include/lagrange/attributes/map_vertex_attributes.h +++ b/modules/core/include/lagrange/attributes/map_vertex_attributes.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::internal::map_attributes instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/rename_attribute.h b/modules/core/include/lagrange/attributes/rename_attribute.h index 1b4ca07e..9ed5b747 100644 --- a/modules/core/include/lagrange/attributes/rename_attribute.h +++ b/modules/core/include/lagrange/attributes/rename_attribute.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::SurfaceMesh::rename_attribute instead. #include #endif diff --git a/modules/core/include/lagrange/attributes/unify_corner_indices.h b/modules/core/include/lagrange/attributes/unify_corner_indices.h index 1678d22f..a985602f 100644 --- a/modules/core/include/lagrange/attributes/unify_corner_indices.h +++ b/modules/core/include/lagrange/attributes/unify_corner_indices.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/attributes/unify_index_buffer.h b/modules/core/include/lagrange/attributes/unify_index_buffer.h index 007e65d2..46e57d0e 100644 --- a/modules/core/include/lagrange/attributes/unify_index_buffer.h +++ b/modules/core/include/lagrange/attributes/unify_index_buffer.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use lagrange::unify_index_buffer instead. #include #endif diff --git a/modules/core/include/lagrange/chain_corners_around_vertices.h b/modules/core/include/lagrange/chain_corners_around_vertices.h index 736d4004..6987ea34 100644 --- a/modules/core/include/lagrange/chain_corners_around_vertices.h +++ b/modules/core/include/lagrange/chain_corners_around_vertices.h @@ -28,7 +28,7 @@ namespace lagrange { /// necessary since there may be isolated vertices, and thus /// computing it from the facet array may be misleading. /// @param[in] facets #F x k array of facet indices. -/// @param[out] vertex_to_corner #E x 1 array of first facet corner in the chain starting +/// @param[out] vertex_to_corner #V x 1 array of first facet corner in the chain starting /// from a given vertex. /// @param[out] next_corner_around_vertex #C x 1 array of next facet corner in the chain at a given /// facet corner. diff --git a/modules/core/include/lagrange/chain_edges.h b/modules/core/include/lagrange/chain_edges.h index b96a0c4e..bf460c08 100644 --- a/modules/core/include/lagrange/chain_edges.h +++ b/modules/core/include/lagrange/chain_edges.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/chain_edges_into_simple_loops.h b/modules/core/include/lagrange/chain_edges_into_simple_loops.h index 6e696993..06d295f3 100644 --- a/modules/core/include/lagrange/chain_edges_into_simple_loops.h +++ b/modules/core/include/lagrange/chain_edges_into_simple_loops.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/combine_mesh_list.h b/modules/core/include/lagrange/combine_mesh_list.h index 3b80e745..82d3b785 100644 --- a/modules/core/include/lagrange/combine_mesh_list.h +++ b/modules/core/include/lagrange/combine_mesh_list.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_bordered_components.h b/modules/core/include/lagrange/compute_bordered_components.h index 20a369f4..863e5626 100644 --- a/modules/core/include/lagrange/compute_bordered_components.h +++ b/modules/core/include/lagrange/compute_bordered_components.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Replaced by compute_components.h #include #endif diff --git a/modules/core/include/lagrange/compute_corner_normal.h b/modules/core/include/lagrange/compute_corner_normal.h index c1d82c2b..212c217c 100644 --- a/modules/core/include/lagrange/compute_corner_normal.h +++ b/modules/core/include/lagrange/compute_corner_normal.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_dihedral_angles.h b/modules/core/include/lagrange/compute_dihedral_angles.h index 1d5e1919..d2c4e7d6 100644 --- a/modules/core/include/lagrange/compute_dihedral_angles.h +++ b/modules/core/include/lagrange/compute_dihedral_angles.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_dijkstra_distance.h b/modules/core/include/lagrange/compute_dijkstra_distance.h index 1266e7a5..f37f2583 100644 --- a/modules/core/include/lagrange/compute_dijkstra_distance.h +++ b/modules/core/include/lagrange/compute_dijkstra_distance.h @@ -11,13 +11,16 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif #include #include +#include #include +#include +#include namespace lagrange { /// diff --git a/modules/core/include/lagrange/compute_edge_lengths.h b/modules/core/include/lagrange/compute_edge_lengths.h index bc4f9247..627394d4 100644 --- a/modules/core/include/lagrange/compute_edge_lengths.h +++ b/modules/core/include/lagrange/compute_edge_lengths.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_euler.h b/modules/core/include/lagrange/compute_euler.h index 1a9c816a..e0ee0864 100644 --- a/modules/core/include/lagrange/compute_euler.h +++ b/modules/core/include/lagrange/compute_euler.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Use `topology.h` instead. #include #endif diff --git a/modules/core/include/lagrange/compute_facet_area.h b/modules/core/include/lagrange/compute_facet_area.h index f85bd5ad..11e296a4 100644 --- a/modules/core/include/lagrange/compute_facet_area.h +++ b/modules/core/include/lagrange/compute_facet_area.h @@ -12,7 +12,7 @@ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Replaced by compute_area.h. #include #endif diff --git a/modules/core/include/lagrange/compute_greedy_coloring.h b/modules/core/include/lagrange/compute_greedy_coloring.h index 0d318be2..c98531c5 100644 --- a/modules/core/include/lagrange/compute_greedy_coloring.h +++ b/modules/core/include/lagrange/compute_greedy_coloring.h @@ -24,7 +24,7 @@ namespace lagrange { /// @{ /// -/// Option struct for computing dihedral angles. +/// Option struct for computing greedy coloring. /// struct GreedyColoringOptions { @@ -34,7 +34,7 @@ struct GreedyColoringOptions /// Element type to be colored. Can be either Vertex or Facet. AttributeElement element_type = AttributeElement::Facet; - /// Minimum number of colors to use. The algorithm will cycle through them but may use more. + /// Initial color palette size. The algorithm allocates more colors when neighbors exhaust the palette, and may use fewer. size_t num_color_used = 8; }; diff --git a/modules/core/include/lagrange/compute_lift_operator.h b/modules/core/include/lagrange/compute_lift_operator.h index eda3092d..cc775bda 100644 --- a/modules/core/include/lagrange/compute_lift_operator.h +++ b/modules/core/include/lagrange/compute_lift_operator.h @@ -22,9 +22,9 @@ namespace lagrange { /// It uses barycentric coordinates on each triangle to fill in coeffs of a sparse matrix which is then returned. /// This bilinear sparse mapping (a.k.a. the returned sparse matrix) can used extend mesh defined fields to fields defined in R^3. /// This is useful for interpolating positions or scalar curvatures to a sampled point cloud for example. -/// This is suited for use with sampling functions like random_sample_uniform that naturally build the third arg. +/// This is suited for use with sampling functions like sample_uniform that naturally build the third arg. /// -/// @return Sparse n*m matrix, where n is the number of vertex in the input mesh and m is the number of vertex in the input point cloud. +/// @return Sparse n*m matrix, where n is the number of points in the input point cloud and m is the number of vertices in the input mesh. /// template auto compute_lift_operator_from_sampling(const MeshType& mesh /**< [in] Input triangular mesh.*/, const Cloud& closest_points /**< [in] Input point cloud. should lie on input mesh.*/, const Indices& element_indices /**< [in] Input mesh triangle indices for each point of the input point cloud.*/) @@ -84,7 +84,7 @@ auto compute_lift_operator_from_sampling(const MeshType& mesh /**< [in] Input tr /// This is useful for interpolating positions or scalar curvatures to a sampled point cloud for example. /// This is suited for use with bvh structure whose data return by batch_query can be used as the second arg. /// -/// @return Sparse n*m matrix, where n is the number of vertex in the input mesh and m is the number of vertex in the input point cloud. +/// @return Sparse n*m matrix, where n is the number of points in the input point cloud and m is the number of vertices in the input mesh. /// template auto compute_lift_operator_from_projections( diff --git a/modules/core/include/lagrange/compute_mesh_centroid.h b/modules/core/include/lagrange/compute_mesh_centroid.h index 1bc75b4b..78fa1ab5 100644 --- a/modules/core/include/lagrange/compute_mesh_centroid.h +++ b/modules/core/include/lagrange/compute_mesh_centroid.h @@ -12,7 +12,7 @@ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Replaced by compute_centroid.h. #include #endif diff --git a/modules/core/include/lagrange/compute_mesh_covariance.h b/modules/core/include/lagrange/compute_mesh_covariance.h index 26d67817..b3c18458 100644 --- a/modules/core/include/lagrange/compute_mesh_covariance.h +++ b/modules/core/include/lagrange/compute_mesh_covariance.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_normal.h b/modules/core/include/lagrange/compute_normal.h index a119ca3a..02d89316 100644 --- a/modules/core/include/lagrange/compute_normal.h +++ b/modules/core/include/lagrange/compute_normal.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_pointcloud_pca.h b/modules/core/include/lagrange/compute_pointcloud_pca.h index 06c06d7d..b2c25aa1 100644 --- a/modules/core/include/lagrange/compute_pointcloud_pca.h +++ b/modules/core/include/lagrange/compute_pointcloud_pca.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_tangent_bitangent.h b/modules/core/include/lagrange/compute_tangent_bitangent.h index 968a092f..7e0227e0 100644 --- a/modules/core/include/lagrange/compute_tangent_bitangent.h +++ b/modules/core/include/lagrange/compute_tangent_bitangent.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_triangle_normal.h b/modules/core/include/lagrange/compute_triangle_normal.h index 15914bad..b7bd1e8b 100644 --- a/modules/core/include/lagrange/compute_triangle_normal.h +++ b/modules/core/include/lagrange/compute_triangle_normal.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_uv_charts.h b/modules/core/include/lagrange/compute_uv_charts.h index 28a43d38..d86919a9 100644 --- a/modules/core/include/lagrange/compute_uv_charts.h +++ b/modules/core/include/lagrange/compute_uv_charts.h @@ -39,7 +39,7 @@ struct UVChartOptions /** * Compute UV charts of an input mesh. * - * This method will create a per-vertex chart id in an attribute named + * This method will create a per-facet chart id in an attribute named * `UVChartOptions::output_attribute_name`. Each chart id is in [0, num_charts-1]. * * @param mesh Input mesh. diff --git a/modules/core/include/lagrange/compute_uv_distortion.h b/modules/core/include/lagrange/compute_uv_distortion.h index 493b8068..6a80a470 100644 --- a/modules/core/include/lagrange/compute_uv_distortion.h +++ b/modules/core/include/lagrange/compute_uv_distortion.h @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_vertex_normal.h b/modules/core/include/lagrange/compute_vertex_normal.h index fdecfac2..1e960fda 100644 --- a/modules/core/include/lagrange/compute_vertex_normal.h +++ b/modules/core/include/lagrange/compute_vertex_normal.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/compute_vertex_valence.h b/modules/core/include/lagrange/compute_vertex_valence.h index 77861583..1b3e181d 100644 --- a/modules/core/include/lagrange/compute_vertex_valence.h +++ b/modules/core/include/lagrange/compute_vertex_valence.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/corner_to_edge_mapping.h b/modules/core/include/lagrange/corner_to_edge_mapping.h index bee79494..425c4787 100644 --- a/modules/core/include/lagrange/corner_to_edge_mapping.h +++ b/modules/core/include/lagrange/corner_to_edge_mapping.h @@ -9,30 +9,9 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -//////////////////////////////////////////////////////////////////////////////// #pragma once -#include -//////////////////////////////////////////////////////////////////////////////// -namespace lagrange { - -/// -/// Computes a mapping from mesh corners (k*f+i) to unique edge ids. -/// -/// @param[in] F #F x k matrix of facet indices. -/// @param[out] C2E #F*k vector of unique edge ids per corner. -/// -/// @tparam DerivedF Type of facet array. -/// @tparam DerivedC Type of corner to edge vector. -/// -/// @return Number of unique edges created. -/// -template -Eigen::Index corner_to_edge_mapping( - const Eigen::MatrixBase& F, - Eigen::PlainObjectBase& C2E); - -} // namespace lagrange - -// Templated definitions -#include +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS + // Replaced by SurfaceMesh::initialize_edges() and SurfaceMesh::get_corner_edge(). + #include +#endif diff --git a/modules/core/include/lagrange/extract_boundary_loops.h b/modules/core/include/lagrange/extract_boundary_loops.h index bfef8668..e6493c62 100644 --- a/modules/core/include/lagrange/extract_boundary_loops.h +++ b/modules/core/include/lagrange/extract_boundary_loops.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/extract_submesh.h b/modules/core/include/lagrange/extract_submesh.h index e1fd337e..ed501815 100644 --- a/modules/core/include/lagrange/extract_submesh.h +++ b/modules/core/include/lagrange/extract_submesh.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/foreach_attribute.h b/modules/core/include/lagrange/foreach_attribute.h index ed6d7020..c66e7074 100644 --- a/modules/core/include/lagrange/foreach_attribute.h +++ b/modules/core/include/lagrange/foreach_attribute.h @@ -396,7 +396,7 @@ void par_foreach_named_attribute_write(SurfaceMesh& mesh, Visitor /// lambda. /// /// @tparam mask Bit field mask to filter attribute based on their element types. -/// @tparam Visitor Type of the visitor function, which should be `template &);` +/// @tparam Visitor Type of the visitor function, which should be `template void(Attribute &);` /// @tparam Scalar Mesh scalar type. /// @tparam Index Mesh index type. /// diff --git a/modules/core/include/lagrange/internal/dijkstra.h b/modules/core/include/lagrange/internal/dijkstra.h index 63e4de6c..a48871b5 100644 --- a/modules/core/include/lagrange/internal/dijkstra.h +++ b/modules/core/include/lagrange/internal/dijkstra.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/internal/invert_mapping.h b/modules/core/include/lagrange/internal/invert_mapping.h index ac764a5e..e9671320 100644 --- a/modules/core/include/lagrange/internal/invert_mapping.h +++ b/modules/core/include/lagrange/internal/invert_mapping.h @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +#pragma once #include #include diff --git a/modules/core/include/lagrange/internal/map_attributes.h b/modules/core/include/lagrange/internal/map_attributes.h index ab09e3de..9b468dc7 100644 --- a/modules/core/include/lagrange/internal/map_attributes.h +++ b/modules/core/include/lagrange/internal/map_attributes.h @@ -39,6 +39,7 @@ struct MapAttributesOptions /// in `options` define the behavior when multiple source vertices are mapped to the same target /// vertex. /// +/// @tparam element The attribute element type to map. /// @tparam Scalar The scalar type. /// @tparam Index The index type. /// @@ -46,8 +47,8 @@ struct MapAttributesOptions /// @param target_mesh The target mesh. /// @param mapping_data A flat array of source element indices. /// @param mapping_offsets The offset index array into the `mapping_data`. Source element with -/// index listed from `mapping_data[mapping_offset[i]]` to -/// `mapping_data[mapping_offset[i+1]]` are mapped to target element `i`. +/// index listed from `mapping_data[mapping_offsets[i]]` to +/// `mapping_data[mapping_offsets[i+1]]` are mapped to target element `i`. /// If empty, source element with index `mapping_data[i]` is mapped to /// target element `i`. /// @param options Option settings. diff --git a/modules/core/include/lagrange/corner_to_edge_mapping.impl.h b/modules/core/include/lagrange/legacy/corner_to_edge_mapping.h similarity index 80% rename from modules/core/include/lagrange/corner_to_edge_mapping.impl.h rename to modules/core/include/lagrange/legacy/corner_to_edge_mapping.h index 9db5ae2a..21647fd4 100644 --- a/modules/core/include/lagrange/corner_to_edge_mapping.impl.h +++ b/modules/core/include/lagrange/legacy/corner_to_edge_mapping.h @@ -11,10 +11,8 @@ */ //////////////////////////////////////////////////////////////////////////////// #pragma once -#include -#include -#include +#include #include // clang-format off @@ -31,7 +29,23 @@ //////////////////////////////////////////////////////////////////////////////// namespace lagrange { - +LAGRANGE_LEGACY_INLINE +namespace legacy { + +/// +/// Computes a mapping from mesh corners (k*f+i) to unique edge ids. +/// +/// @param[in] F #F x k matrix of facet indices. +/// @param[out] C2E #F*k vector of unique edge ids per corner. +/// +/// @tparam DerivedF Type of facet array. +/// @tparam DerivedC Type of corner to edge vector. +/// +/// @return Number of unique edges created. +/// +/// @deprecated This function operates on a raw facet matrix and predates SurfaceMesh. Use +/// SurfaceMesh::initialize_edges() together with SurfaceMesh::get_corner_edge() instead. +/// template Eigen::Index corner_to_edge_mapping( const Eigen::MatrixBase& F, @@ -88,4 +102,5 @@ Eigen::Index corner_to_edge_mapping( return num_edges; } +} // namespace legacy } // namespace lagrange diff --git a/modules/core/include/lagrange/legacy/inline.h b/modules/core/include/lagrange/legacy/inline.h index 4159f9f8..c963cf16 100644 --- a/modules/core/include/lagrange/legacy/inline.h +++ b/modules/core/include/lagrange/legacy/inline.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #define LAGRANGE_LEGACY_INLINE inline #else #define LAGRANGE_LEGACY_INLINE diff --git a/modules/core/include/lagrange/marching_triangles.h b/modules/core/include/lagrange/marching_triangles.h index ebc8cf50..7d0d944a 100644 --- a/modules/core/include/lagrange/marching_triangles.h +++ b/modules/core/include/lagrange/marching_triangles.h @@ -14,7 +14,7 @@ #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #pragma message( \ "This header is deprecated. Use `trim_by_isoline` or `extract_isoline` from `` instead.") #include diff --git a/modules/core/include/lagrange/mesh_cleanup/close_small_holes.h b/modules/core/include/lagrange/mesh_cleanup/close_small_holes.h index c985010f..31f76518 100644 --- a/modules/core/include/lagrange/mesh_cleanup/close_small_holes.h +++ b/modules/core/include/lagrange/mesh_cleanup/close_small_holes.h @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/detect_degenerate_triangles.h b/modules/core/include/lagrange/mesh_cleanup/detect_degenerate_triangles.h index ae397d5b..75beb732 100644 --- a/modules/core/include/lagrange/mesh_cleanup/detect_degenerate_triangles.h +++ b/modules/core/include/lagrange/mesh_cleanup/detect_degenerate_triangles.h @@ -10,6 +10,6 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h b/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h index 28cfd60f..0106103f 100644 --- a/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h +++ b/modules/core/include/lagrange/mesh_cleanup/is_vertex_manifold.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Use `topology.h` instead. #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_degenerate_triangles.h b/modules/core/include/lagrange/mesh_cleanup/remove_degenerate_triangles.h index 335fcfdc..a04bb4af 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_degenerate_triangles.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_degenerate_triangles.h @@ -10,6 +10,6 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_facets.h b/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_facets.h index bc340190..c56c5917 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_facets.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_facets.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_vertices.h b/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_vertices.h index 401b2eb9..70be48f1 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_vertices.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_duplicate_vertices.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_isolated_vertices.h b/modules/core/include/lagrange/mesh_cleanup/remove_isolated_vertices.h index 1048eb5b..e941f552 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_isolated_vertices.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_isolated_vertices.h @@ -13,7 +13,7 @@ #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_null_area_triangles.h b/modules/core/include/lagrange/mesh_cleanup/remove_null_area_triangles.h index 7ce29771..147481ee 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_null_area_triangles.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_null_area_triangles.h @@ -10,6 +10,6 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_short_edges.h b/modules/core/include/lagrange/mesh_cleanup/remove_short_edges.h index 7db99fd1..fe132815 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_short_edges.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_short_edges.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/remove_topologically_degenerate_triangles.h b/modules/core/include/lagrange/mesh_cleanup/remove_topologically_degenerate_triangles.h index b2f7895b..18b8d94a 100644 --- a/modules/core/include/lagrange/mesh_cleanup/remove_topologically_degenerate_triangles.h +++ b/modules/core/include/lagrange/mesh_cleanup/remove_topologically_degenerate_triangles.h @@ -10,6 +10,6 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/resolve_nonmanifoldness.h b/modules/core/include/lagrange/mesh_cleanup/resolve_nonmanifoldness.h index e66a8b8d..42c21707 100644 --- a/modules/core/include/lagrange/mesh_cleanup/resolve_nonmanifoldness.h +++ b/modules/core/include/lagrange/mesh_cleanup/resolve_nonmanifoldness.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h b/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h index 3eb1b5c3..c3ace11d 100644 --- a/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h +++ b/modules/core/include/lagrange/mesh_cleanup/resolve_vertex_nonmanifoldness.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/split_long_edges.h b/modules/core/include/lagrange/mesh_cleanup/split_long_edges.h index f9fc6d14..602fa6e2 100644 --- a/modules/core/include/lagrange/mesh_cleanup/split_long_edges.h +++ b/modules/core/include/lagrange/mesh_cleanup/split_long_edges.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_cleanup/split_triangle.h b/modules/core/include/lagrange/mesh_cleanup/split_triangle.h index b4edde5c..d163fa2f 100644 --- a/modules/core/include/lagrange/mesh_cleanup/split_triangle.h +++ b/modules/core/include/lagrange/mesh_cleanup/split_triangle.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/mesh_convert.h b/modules/core/include/lagrange/mesh_convert.h index 377fe98e..850151ae 100644 --- a/modules/core/include/lagrange/mesh_convert.h +++ b/modules/core/include/lagrange/mesh_convert.h @@ -11,8 +11,10 @@ */ #pragma once -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + + #include + #include namespace lagrange { /// @@ -69,4 +71,6 @@ std::unique_ptr to_legacy_mesh(const SurfaceMesh& mesh) /// @} } // namespace lagrange -#include + #include + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/include/lagrange/normalize_meshes.h b/modules/core/include/lagrange/normalize_meshes.h index 9168bf03..a22bc1e1 100644 --- a/modules/core/include/lagrange/normalize_meshes.h +++ b/modules/core/include/lagrange/normalize_meshes.h @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/orient_outward.h b/modules/core/include/lagrange/orient_outward.h index d1ce3ad3..b2bdac9a 100644 --- a/modules/core/include/lagrange/orient_outward.h +++ b/modules/core/include/lagrange/orient_outward.h @@ -13,7 +13,7 @@ #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/quad_to_tri.h b/modules/core/include/lagrange/quad_to_tri.h index 4a7c9fed..8e674018 100644 --- a/modules/core/include/lagrange/quad_to_tri.h +++ b/modules/core/include/lagrange/quad_to_tri.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please consider using `triangulate_polygoanl_facets`. #include #endif diff --git a/modules/core/include/lagrange/reorder_mesh_vertices.h b/modules/core/include/lagrange/reorder_mesh_vertices.h index de170d65..2416f8fd 100644 --- a/modules/core/include/lagrange/reorder_mesh_vertices.h +++ b/modules/core/include/lagrange/reorder_mesh_vertices.h @@ -12,7 +12,7 @@ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS // Please use remap_vertices.h instead. #include #endif diff --git a/modules/core/include/lagrange/select_facets_by_normal_similarity.h b/modules/core/include/lagrange/select_facets_by_normal_similarity.h index 5e8fa76d..ad080096 100644 --- a/modules/core/include/lagrange/select_facets_by_normal_similarity.h +++ b/modules/core/include/lagrange/select_facets_by_normal_similarity.h @@ -11,11 +11,16 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif + +#include +#include + #include // needed in options #include // needed in options +#include // needed in options namespace lagrange { @@ -77,7 +82,8 @@ struct SelectFacetsByNormalSimilarityOptions * * @param[in, out] mesh The input mesh. * @param[in] seed_facet_id The index of the seed facet - * @param[in] options Optional arguments (greedy, output_attribute_name). + * @param[in] options Optional arguments (flood_error_limit, search_type, + * output_attribute_name, etc.). * * @tparam Scalar Mesh scalar type. * @tparam Index Mesh index type. @@ -88,7 +94,7 @@ struct SelectFacetsByNormalSimilarityOptions * @note Currently only support triangular mesh and will throw error if * mesh.is_triangle_mesh() is false! The function will check if the mesh contains * facet normal by looking for options.facet_normal_attribute_name, and if not - * found, will call compute_facet_normal(meshm, + * found, will call compute_facet_normal(mesh, * options.facet_normal_attribute_name). * * @see @ref SelectFacetsByNormalSimilarityOptions diff --git a/modules/core/include/lagrange/select_facets_in_frustum.h b/modules/core/include/lagrange/select_facets_in_frustum.h index 2eab5c21..44e961c7 100644 --- a/modules/core/include/lagrange/select_facets_in_frustum.h +++ b/modules/core/include/lagrange/select_facets_in_frustum.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/thicken_and_close_mesh.h b/modules/core/include/lagrange/thicken_and_close_mesh.h index 9ede3f8e..984f47f6 100644 --- a/modules/core/include/lagrange/thicken_and_close_mesh.h +++ b/modules/core/include/lagrange/thicken_and_close_mesh.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/core/include/lagrange/transform_mesh.h b/modules/core/include/lagrange/transform_mesh.h index 84c7121e..c966c9fe 100644 --- a/modules/core/include/lagrange/transform_mesh.h +++ b/modules/core/include/lagrange/transform_mesh.h @@ -63,7 +63,7 @@ void transform_mesh( /// /// @todo Add an overload for 2D transforms. /// -/// @param[in] mesh Mesh to transform in-place. +/// @param[in] mesh Mesh to transform. /// @param[in] transform Affine transform to apply. /// @param[in] options Transform options. /// diff --git a/modules/core/include/lagrange/unify_index_buffer.h b/modules/core/include/lagrange/unify_index_buffer.h index 40dd26b7..f0a6ea2b 100644 --- a/modules/core/include/lagrange/unify_index_buffer.h +++ b/modules/core/include/lagrange/unify_index_buffer.h @@ -33,7 +33,7 @@ namespace lagrange { /** * Unify index buffers of the input `mesh` for all attributes specified in `attribute_ids`. * - * @post The attributes specified by `attribute_id` will become vertex + * @post The attributes specified by `attribute_ids` will become vertex * attributes in the output mesh under the same names. * * @param[in] mesh The input polygonal mesh. diff --git a/modules/core/include/lagrange/utils/ProgressCallback.h b/modules/core/include/lagrange/utils/ProgressCallback.h index e2f1f889..ca1b062f 100644 --- a/modules/core/include/lagrange/utils/ProgressCallback.h +++ b/modules/core/include/lagrange/utils/ProgressCallback.h @@ -75,6 +75,8 @@ class LA_CORE_API ProgressCallback /// Updates the current iteration number by a fixed amount, and sends a notification. It is safe /// to call this method from multiple threads. /// + /// @param[in] increment Amount to add to the current iteration number. + /// void advance(size_t increment); /// diff --git a/modules/core/include/lagrange/utils/chain_edges.h b/modules/core/include/lagrange/utils/chain_edges.h index e81cbfdd..31441f72 100644 --- a/modules/core/include/lagrange/utils/chain_edges.h +++ b/modules/core/include/lagrange/utils/chain_edges.h @@ -83,7 +83,7 @@ struct ChainEdgesResult /// Both loops and chains are stored as an array of edges indices if `options.output_edge_index` is /// true, otherwise, they are an array of vertex indices. /// -/// If `options.output_edge_vertex` is false and `options.close_loop_with_identical_vertices` is +/// If `options.output_edge_index` is false and `options.close_loop_with_identical_vertices` is /// true, the first and last vertices of a loop is identical. /// /// @see @ref chain_undirected_edges @@ -109,7 +109,7 @@ ChainEdgesResult chain_directed_edges( /// Both loops and chains are stored as an array of edges indices if `options.output_edge_index` is /// true, otherwise, they are an array of vertex indices. /// -/// If `options.output_edge_vertex` is false and `options.close_loop_with_identical_vertices` is +/// If `options.output_edge_index` is false and `options.close_loop_with_identical_vertices` is /// true, the first and last vertices of a loop is identical. /// /// @see @ref chain_directed_edges diff --git a/modules/core/include/lagrange/utils/utils.h b/modules/core/include/lagrange/utils/utils.h index 65ead0ac..2e986d22 100644 --- a/modules/core/include/lagrange/utils/utils.h +++ b/modules/core/include/lagrange/utils/utils.h @@ -31,7 +31,7 @@ Scalar to_degrees(Scalar rad = 1) } /// Convert degrees to radians. -/// Use either as to_degrees(x) or x * to_degrees(); +/// Use either as to_radians(x) or x * to_radians(); template Scalar to_radians(Scalar deg = 1) { diff --git a/modules/core/include/lagrange/utils/warnoff.h b/modules/core/include/lagrange/utils/warnoff.h index 4564228e..ba42b356 100644 --- a/modules/core/include/lagrange/utils/warnoff.h +++ b/modules/core/include/lagrange/utils/warnoff.h @@ -12,6 +12,7 @@ #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunknown-warning-option" + #pragma clang diagnostic ignored "-Wc++23-attribute-extensions" // Not all warnings are supported by older version of clang. // So the line above has to come first. #pragma clang diagnostic ignored "-Wdeprecated" diff --git a/modules/core/include/lagrange/uv_mesh.h b/modules/core/include/lagrange/uv_mesh.h index bb6f3b3d..d2dc149d 100644 --- a/modules/core/include/lagrange/uv_mesh.h +++ b/modules/core/include/lagrange/uv_mesh.h @@ -81,7 +81,7 @@ SurfaceMesh uv_mesh_ref( * @tparam Index Mesh index type. * @tparam UVScalar Target UV attribute value type. * - * @return The extracted UV mesh reference. + * @return The extracted UV mesh view. * * @see @ref UVMeshOptions */ diff --git a/modules/core/js/src/core.cpp b/modules/core/js/src/core.cpp index 50d85765..89b5cadb 100644 --- a/modules/core/js/src/core.cpp +++ b/modules/core/js/src/core.cpp @@ -13,18 +13,28 @@ #include "bind_types.h" #include +#include +#include +#include #include #include #include #include +#include +#include +#include +#include #include +#include #include namespace lagrange::js::bind { namespace { +using val = emscripten::val; + // Splits vertices at attribute discontinuities so all indexed attributes become per-vertex. // The returned mesh has a unified index buffer suitable for WebGL / zero-copy views. MeshType unify_index_buffer_js(const MeshType& mesh) @@ -95,6 +105,581 @@ bool mesh_has_attribute(const MeshType& mesh, const std::string& name) return mesh.has_attribute(name); } +// --- Attribute element / usage <-> string --- + +AttributeElement parse_element(const std::string& s) +{ + if (s == "vertex") return AttributeElement::Vertex; + if (s == "facet") return AttributeElement::Facet; + if (s == "edge") return AttributeElement::Edge; + if (s == "corner") return AttributeElement::Corner; + if (s == "value") return AttributeElement::Value; + if (s == "indexed") return AttributeElement::Indexed; + throw std::runtime_error("Unknown attribute element: " + s); +} + +std::string element_name(AttributeElement e) +{ + switch (e) { + case AttributeElement::Vertex: return "vertex"; + case AttributeElement::Facet: return "facet"; + case AttributeElement::Edge: return "edge"; + case AttributeElement::Corner: return "corner"; + case AttributeElement::Value: return "value"; + case AttributeElement::Indexed: return "indexed"; + default: return "unknown"; + } +} + +AttributeUsage parse_usage(const std::string& s) +{ + if (s == "vector") return AttributeUsage::Vector; + if (s == "scalar") return AttributeUsage::Scalar; + if (s == "position") return AttributeUsage::Position; + if (s == "normal") return AttributeUsage::Normal; + if (s == "tangent") return AttributeUsage::Tangent; + if (s == "bitangent") return AttributeUsage::Bitangent; + if (s == "color") return AttributeUsage::Color; + if (s == "uv") return AttributeUsage::UV; + if (s == "vertexIndex") return AttributeUsage::VertexIndex; + if (s == "facetIndex") return AttributeUsage::FacetIndex; + if (s == "cornerIndex") return AttributeUsage::CornerIndex; + if (s == "edgeIndex") return AttributeUsage::EdgeIndex; + if (s == "string") return AttributeUsage::String; + throw std::runtime_error("Unknown attribute usage: " + s); +} + +std::string usage_name(AttributeUsage u) +{ + switch (u) { + case AttributeUsage::Vector: return "vector"; + case AttributeUsage::Scalar: return "scalar"; + case AttributeUsage::Position: return "position"; + case AttributeUsage::Normal: return "normal"; + case AttributeUsage::Tangent: return "tangent"; + case AttributeUsage::Bitangent: return "bitangent"; + case AttributeUsage::Color: return "color"; + case AttributeUsage::UV: return "uv"; + case AttributeUsage::VertexIndex: return "vertexIndex"; + case AttributeUsage::FacetIndex: return "facetIndex"; + case AttributeUsage::CornerIndex: return "cornerIndex"; + case AttributeUsage::EdgeIndex: return "edgeIndex"; + case AttributeUsage::String: return "string"; + default: return "unknown"; + } +} + +template +const char* dtype_name(); +template <> +const char* dtype_name() +{ + return "int8"; +} +template <> +const char* dtype_name() +{ + return "int16"; +} +template <> +const char* dtype_name() +{ + return "int32"; +} +template <> +const char* dtype_name() +{ + return "int64"; +} +template <> +const char* dtype_name() +{ + return "uint8"; +} +template <> +const char* dtype_name() +{ + return "uint16"; +} +template <> +const char* dtype_name() +{ + return "uint32"; +} +template <> +const char* dtype_name() +{ + return "uint64"; +} +template <> +const char* dtype_name() +{ + return "float32"; +} +template <> +const char* dtype_name() +{ + return "float64"; +} + +template +const char* typed_array_ctor_name(); +template <> +const char* typed_array_ctor_name() +{ + return "Int8Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Int16Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Int32Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Uint8Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Uint16Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Uint32Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Float32Array"; +} +template <> +const char* typed_array_ctor_name() +{ + return "Float64Array"; +} + +// int64_t/uint64_t widen to Float64Array (no safe native JS typed-array counterpart) only if +// every value round-trips exactly; a lossy value throws instead of returning corrupted data. +template +val to_typed_array(lagrange::span data) +{ + if constexpr (std::is_same_v || std::is_same_v) { + std::vector buf(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + double d = static_cast(data[i]); + if (static_cast(d) != data[i]) { + throw std::runtime_error( + "Attribute value at position " + std::to_string(i) + + " cannot be represented exactly as a JS number"); + } + buf[i] = d; + } + return val::global("Float64Array") + .new_(emscripten::typed_memory_view(buf.size(), buf.data())); + } else { + std::vector buf(data.begin(), data.end()); + return val::global(typed_array_ctor_name()) + .new_(emscripten::typed_memory_view(buf.size(), buf.data())); + } +} + +// Rejects BigInt64Array/BigUint64Array inputs, since TypedArray.prototype.set() otherwise +// throws an opaque native TypeError when mixing BigInt and non-BigInt content types. +void reject_bigint_array(const val& arr) +{ + if (arr.isUndefined() || arr.isNull()) return; + std::string ctor_name = arr["constructor"]["name"].as(); + if (ctor_name == "BigInt64Array" || ctor_name == "BigUint64Array") { + throw std::runtime_error( + "BigInt64Array/BigUint64Array are not supported; pass a regular Array or " + "Float64Array instead"); + } +} + +// Bulk-copies a JS array-like into a native buffer via TypedArray.prototype.set() instead of +// one embind property lookup + val::as() round trip per element (which dominates at scale). +template +void bulk_copy_into(const val& source, T* dest, size_t count) +{ + if (count == 0) return; + val view(emscripten::typed_memory_view(count, dest)); + view.call("set", source); +} + +// Converts already-read values into mesh Index values, validating each is a non-negative +// integer representable by Index (casting an out-of-range double otherwise is UB). +std::vector to_index_vector(const std::vector& raw, const char* context) +{ + std::vector out(raw.size()); + for (size_t i = 0; i < raw.size(); ++i) { + double v = raw[i]; + if (!(v >= 0) || v > static_cast(std::numeric_limits::max()) || + v != std::floor(v)) { + throw std::runtime_error( + std::string(context) + ": value at position " + std::to_string(i) + + " is not a valid non-negative integer index"); + } + out[i] = static_cast(v); + } + return out; +} + +// Validates a double is finite and in range before casting (UB otherwise). `>=` on the upper +// bound matters: ValueType::max() (2^n - 1) rounds *up* to 2^n as a double, so `>` would admit it. +template +ValueType to_wide_int(double v, const char* context, size_t i) +{ + if (!std::isfinite(v) || v != std::floor(v) || + v < static_cast(std::numeric_limits::min()) || + v >= static_cast(std::numeric_limits::max())) { + throw std::runtime_error( + std::string(context) + ": value at position " + std::to_string(i) + + " is not a valid integer representable by " + dtype_name()); + } + return static_cast(v); +} + +// Mirrors AttributeBase's channel validation (Attribute.cpp) plus SurfaceMesh's +// dimension-specific checks, run before the core library registers the attribute's name. +void validate_usage_channels(AttributeUsage usage, size_t num_channels, Index dim) +{ + switch (usage) { + case AttributeUsage::Vector: + if (num_channels < 1) { + throw std::runtime_error("createAttribute: numChannels must be at least 1"); + } + break; + case AttributeUsage::Scalar: + case AttributeUsage::VertexIndex: + case AttributeUsage::FacetIndex: + case AttributeUsage::CornerIndex: + case AttributeUsage::EdgeIndex: + case AttributeUsage::String: + if (num_channels != 1) { + throw std::runtime_error( + "createAttribute: usage '" + usage_name(usage) + "' requires numChannels: 1"); + } + break; + case AttributeUsage::Position: + if (num_channels != static_cast(dim)) { + throw std::runtime_error( + "createAttribute: usage 'position' requires numChannels: " + std::to_string(dim)); + } + break; + case AttributeUsage::Normal: + case AttributeUsage::Tangent: + case AttributeUsage::Bitangent: + if (num_channels != static_cast(dim) && + num_channels != static_cast(dim) + 1) { + throw std::runtime_error( + "createAttribute: usage '" + usage_name(usage) + "' requires numChannels: " + + std::to_string(dim) + " or " + std::to_string(dim + 1)); + } + break; + case AttributeUsage::Color: + if (num_channels < 1 || num_channels > 4) { + throw std::runtime_error( + "createAttribute: usage 'color' requires numChannels between 1 and 4"); + } + break; + case AttributeUsage::UV: + if (num_channels != 2) { + throw std::runtime_error("createAttribute: usage 'uv' requires numChannels: 2"); + } + break; + } +} + +std::string infer_dtype(const val& data) +{ + std::string ctor_name = data["constructor"]["name"].as(); + if (ctor_name == "Int8Array") return "int8"; + if (ctor_name == "Int16Array") return "int16"; + if (ctor_name == "Int32Array") return "int32"; + if (ctor_name == "Uint8Array" || ctor_name == "Uint8ClampedArray") return "uint8"; + if (ctor_name == "Uint16Array") return "uint16"; + if (ctor_name == "Uint32Array") return "uint32"; + if (ctor_name == "Float32Array") return "float32"; + if (ctor_name == "Float64Array") return "float64"; + return "float64"; // Plain JS Array (or anything else array-like): default to float64. +} + +// Dispatches on the attribute's runtime value type, over all 10 lagrange attribute value types. +template +auto visit_attribute(const MeshType& mesh, AttributeId id, Func&& func) +{ + auto vt = mesh.get_attribute_base(id).get_value_type(); +#define LA_X_visit_attribute(_, ValueType) \ + if (vt == lagrange::make_attribute_value_type()) { \ + return func(mesh.template get_attribute(id)); \ + } + LA_ATTRIBUTE_X(visit_attribute, 0) +#undef LA_X_visit_attribute + throw std::runtime_error("Unsupported attribute value type"); +} + +template +auto visit_indexed_attribute(const MeshType& mesh, AttributeId id, Func&& func) +{ + auto vt = mesh.get_attribute_base(id).get_value_type(); +#define LA_X_visit_indexed_attribute(_, ValueType) \ + if (vt == lagrange::make_attribute_value_type()) { \ + return func(mesh.template get_indexed_attribute(id)); \ + } + LA_ATTRIBUTE_X(visit_indexed_attribute, 0) +#undef LA_X_visit_indexed_attribute + throw std::runtime_error("Unsupported attribute value type"); +} + +AttributeId create_attribute_js(MeshType& mesh, const std::string& name, val opts) +{ + if (opts.isUndefined() || opts.isNull()) { + throw std::runtime_error("createAttribute: options object with 'element' is required"); + } + auto element_val = opts["element"]; + if (element_val.isUndefined()) { + throw std::runtime_error("createAttribute: 'element' is required"); + } + AttributeElement element = parse_element(element_val.as()); + + size_t num_channels = 1; + auto num_channels_val = opts["numChannels"]; + if (!num_channels_val.isUndefined() && !num_channels_val.isNull()) { + double nc = num_channels_val.as(); + if (!std::isfinite(nc) || nc < 1 || nc != std::floor(nc) || + nc > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("createAttribute: numChannels must be a positive integer"); + } + num_channels = static_cast(nc); + } + + AttributeUsage usage = AttributeUsage::Vector; + auto usage_val = opts["usage"]; + if (!usage_val.isUndefined()) usage = parse_usage(usage_val.as()); + validate_usage_channels(usage, num_channels, mesh.get_dimension()); + + val data = opts["data"]; + std::string dtype; + auto dtype_val = opts["dtype"]; + if (!dtype_val.isUndefined()) { + dtype = dtype_val.as(); + } else if (!data.isUndefined() && !data.isNull()) { + dtype = infer_dtype(data); + } else { + dtype = "float64"; + } + + if ((usage == AttributeUsage::VertexIndex || usage == AttributeUsage::FacetIndex || + usage == AttributeUsage::CornerIndex || usage == AttributeUsage::EdgeIndex) && + dtype != dtype_name()) { + throw std::runtime_error( + "createAttribute: usage '" + usage_name(usage) + "' requires dtype '" + + dtype_name() + "'"); + } + + reject_bigint_array(data); + bool has_data = !data.isUndefined() && !data.isNull(); + size_t data_len = has_data ? data["length"].as() : 0; + + val indices_val = opts["indices"]; + reject_bigint_array(indices_val); + bool has_indices = !indices_val.isUndefined() && !indices_val.isNull(); + size_t indices_len = has_indices ? indices_val["length"].as() : 0; + + std::vector indices_buf; + if (has_indices) { + std::vector raw_indices(indices_len); + bulk_copy_into(indices_val, raw_indices.data(), indices_len); + indices_buf = to_index_vector(raw_indices, "createAttribute: indices"); + } + if (element == AttributeElement::Indexed && has_indices) { + if (!has_data || data_len == 0) { + throw std::runtime_error( + "createAttribute: 'indices' requires non-empty 'data' for an indexed attribute"); + } + if (data_len % num_channels != 0) { + throw std::runtime_error( + "createAttribute: data length is not a multiple of numChannels"); + } + size_t value_count = data_len / num_channels; + for (size_t i = 0; i < indices_buf.size(); ++i) { + if (static_cast(indices_buf[i]) >= value_count) { + throw std::runtime_error( + "createAttribute: index at position " + std::to_string(i) + + " is out of range for " + std::to_string(value_count) + " indexed value(s)"); + } + } + } + lagrange::span indices_span(indices_buf.data(), indices_buf.size()); + + // int64/uint64 have no safe native JS typed-array counterpart to bulk-copy into directly + // (see to_typed_array()), so they go through a validated double intermediate instead. + auto do_create = [&]() -> AttributeId { +#define LA_X_create_attribute(_, ValueType) \ + if (dtype == dtype_name()) { \ + std::vector values_buf(data_len); \ + if (has_data) { \ + if constexpr ( \ + std::is_same_v || std::is_same_v) { \ + std::vector raw_values(data_len); \ + bulk_copy_into(data, raw_values.data(), data_len); \ + for (size_t i = 0; i < data_len; ++i) \ + values_buf[i] = \ + to_wide_int(raw_values[i], "createAttribute: data", i); \ + } else { \ + bulk_copy_into(data, values_buf.data(), data_len); \ + } \ + } \ + lagrange::span values_span(values_buf.data(), values_buf.size()); \ + return mesh.template create_attribute( \ + name, \ + element, \ + num_channels, \ + usage, \ + values_span, \ + indices_span); \ + } + LA_ATTRIBUTE_X(create_attribute, 0) +#undef LA_X_create_attribute + throw std::runtime_error("createAttribute: unsupported dtype '" + dtype + "'"); + }; + + try { + return do_create(); + } catch (...) { + // AttributeManager::create/create_indexed reserves the name/id before constructing the + // Attribute object, so a failure partway through (e.g. a usage/channel mismatch) leaves + // `name` registered but unusable. Clean that up so the name can be reused. + if (mesh.has_attribute(name)) mesh.delete_attribute(name); + throw; + } +} + +val get_attribute_js(const MeshType& mesh, const std::string& name) +{ + AttributeId id = mesh.get_attribute_id(name); + if (mesh.is_attribute_indexed(id)) { + throw std::runtime_error( + "getAttribute: '" + name + "' is an indexed attribute; use getIndexedAttribute"); + } + return visit_attribute(mesh, id, [&](const auto& attr) -> val { + using ValueType = typename std::decay_t::ValueType; + val out = val::object(); + out.set("id", id); + out.set("name", name); + out.set("element", element_name(attr.get_element_type())); + out.set("usage", usage_name(attr.get_usage())); + out.set("numChannels", static_cast(attr.get_num_channels())); + out.set("numElements", static_cast(attr.get_num_elements())); + out.set("dtype", dtype_name()); + out.set("data", to_typed_array(attr.get_all())); + return out; + }); +} + +void set_attribute_js(MeshType& mesh, const std::string& name, val data) +{ + AttributeId id = mesh.get_attribute_id(name); + if (mesh.is_attribute_indexed(id)) { + throw std::runtime_error( + "setAttribute: '" + name + + "' is an indexed attribute and cannot be overwritten " + "in place; delete and recreate it instead"); + } + reject_bigint_array(data); + size_t data_len = data["length"].as(); + auto vt = mesh.get_attribute_base(id).get_value_type(); + // int64/uint64 have no safe native JS typed-array counterpart to bulk-copy into directly + // (see to_typed_array()), so they go through a double intermediate instead. +#define LA_X_set_attribute(_, ValueType) \ + if (vt == lagrange::make_attribute_value_type()) { \ + auto& attr = mesh.template ref_attribute(id); \ + auto out = attr.ref_all(); \ + if (data_len != out.size()) { \ + throw std::runtime_error( \ + "setAttribute: data length " + std::to_string(data_len) + \ + " does not match attribute size " + std::to_string(out.size())); \ + } \ + if constexpr (std::is_same_v || std::is_same_v) { \ + std::vector raw(data_len); \ + bulk_copy_into(data, raw.data(), data_len); \ + std::vector validated(data_len); \ + for (size_t i = 0; i < data_len; ++i) \ + validated[i] = to_wide_int(raw[i], "setAttribute: data", i); \ + for (size_t i = 0; i < out.size(); ++i) out[i] = validated[i]; \ + } else { \ + bulk_copy_into(data, out.data(), out.size()); \ + } \ + return; \ + } + LA_ATTRIBUTE_X(set_attribute, 0) +#undef LA_X_set_attribute + throw std::runtime_error("setAttribute: unsupported attribute value type"); +} + +void delete_attribute_js(MeshType& mesh, const std::string& name) +{ + mesh.delete_attribute(name); +} + +void rename_attribute_js(MeshType& mesh, const std::string& old_name, const std::string& new_name) +{ + mesh.rename_attribute(old_name, new_name); +} + +AttributeId +duplicate_attribute_js(MeshType& mesh, const std::string& old_name, const std::string& new_name) +{ + return mesh.duplicate_attribute(old_name, new_name); +} + +AttributeId get_attribute_id_js(const MeshType& mesh, const std::string& name) +{ + return mesh.get_attribute_id(name); +} + +std::string get_attribute_name_js(const MeshType& mesh, AttributeId id) +{ + return std::string(mesh.get_attribute_name(id)); +} + +bool is_attribute_indexed_js(const MeshType& mesh, const std::string& name) +{ + return mesh.is_attribute_indexed(mesh.get_attribute_id(name)); +} + +val get_indexed_attribute_js(const MeshType& mesh, const std::string& name) +{ + AttributeId id = mesh.get_attribute_id(name); + if (!mesh.is_attribute_indexed(id)) { + throw std::runtime_error( + "getIndexedAttribute: '" + name + "' is not an indexed attribute; use getAttribute"); + } + return visit_indexed_attribute(mesh, id, [&](const auto& iattr) -> val { + using ValueType = typename std::decay_t::ValueType; + val values_obj = val::object(); + values_obj.set("numElements", static_cast(iattr.values().get_num_elements())); + values_obj.set("data", to_typed_array(iattr.values().get_all())); + + val out = val::object(); + out.set("id", id); + out.set("name", name); + out.set("usage", usage_name(iattr.get_usage())); + out.set("numChannels", static_cast(iattr.get_num_channels())); + out.set("dtype", dtype_name()); + out.set("values", values_obj); + out.set("indices", to_typed_array(iattr.indices().get_all())); + return out; + }); +} + } // namespace } // namespace lagrange::js::bind @@ -200,7 +785,17 @@ EMSCRIPTEN_BINDINGS(lagrange_core) } mesh.flip_facets(lagrange::span(buf.data(), buf.size())); } - }); + }) + .function("createAttribute", &create_attribute_js) + .function("getAttribute", &get_attribute_js) + .function("setAttribute", &set_attribute_js) + .function("deleteAttribute", &delete_attribute_js) + .function("renameAttribute", &rename_attribute_js) + .function("duplicateAttribute", &duplicate_attribute_js) + .function("getAttributeId", &get_attribute_id_js) + .function("getAttributeName", &get_attribute_name_js) + .function("isAttributeIndexed", &is_attribute_indexed_js) + .function("getIndexedAttribute", &get_indexed_attribute_js); function("unifyIndexBuffer", &unify_index_buffer_js); } diff --git a/modules/core/js/test/attribute.test.ts b/modules/core/js/test/attribute.test.ts new file mode 100644 index 00000000..ec2b6675 --- /dev/null +++ b/modules/core/js/test/attribute.test.ts @@ -0,0 +1,579 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { describe, test, expect, beforeAll } from "vitest"; +import { loadLagrange } from "../src/loadLagrange.node.js"; +import type { Lagrange } from "../src/types.js"; +import type { AttributeDType } from "../src/modules/core.js"; + +var lagrange: Lagrange; + +beforeAll(async () => { + lagrange = await loadLagrange(); +}); + +/** Helper: create a simple triangle mesh (3 vertices, 1 facet, 3 corners). */ +function makeTriangleMesh() { + const mesh = new lagrange.core.SurfaceMesh(3); + mesh.addVertex([0, 0, 0]); + mesh.addVertex([1, 0, 0]); + mesh.addVertex([0, 1, 0]); + mesh.addTriangle(0, 1, 2); + return mesh; +} + +describe("attribute: create / read round trip", () => { + test("per-vertex float32 attribute round-trips", () => { + const mesh = makeTriangleMesh(); + const id = mesh.createAttribute("weight", { + element: "vertex", + usage: "scalar", + data: new Float32Array([1, 2, 3]), + }); + expect(typeof id).toBe("number"); + expect(mesh.hasAttribute("weight")).toBe(true); + + const attr = mesh.getAttribute("weight"); + expect(attr.id).toBe(id); + expect(attr.name).toBe("weight"); + expect(attr.element).toBe("vertex"); + expect(attr.usage).toBe("scalar"); + expect(attr.numChannels).toBe(1); + expect(attr.numElements).toBe(3); + expect(attr.dtype).toBe("float32"); + expect(attr.data).toBeInstanceOf(Float32Array); + expect(Array.from(attr.data)).toEqual([1, 2, 3]); + mesh.delete(); + }); + + test("dtype defaults to float64 for a plain number[]", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("plain", { + element: "vertex", + data: [1, 2, 3], + }); + const attr = mesh.getAttribute("plain"); + expect(attr.dtype).toBe("float64"); + expect(attr.data).toBeInstanceOf(Float64Array); + expect(Array.from(attr.data)).toEqual([1, 2, 3]); + mesh.delete(); + }); + + test("multi-channel color attribute (uint8, 3 channels)", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("color", { + element: "vertex", + usage: "color", + numChannels: 3, + data: new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255]), + }); + const attr = mesh.getAttribute("color"); + expect(attr.numChannels).toBe(3); + expect(attr.numElements).toBe(3); + expect(attr.dtype).toBe("uint8"); + expect(attr.data).toBeInstanceOf(Uint8Array); + expect(Array.from(attr.data)).toEqual([255, 0, 0, 0, 255, 0, 0, 0, 255]); + mesh.delete(); + }); + + test("Uint8ClampedArray data infers dtype uint8", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("clamped", { + element: "vertex", + data: new Uint8ClampedArray([1, 2, 3]), + }); + const attr = mesh.getAttribute("clamped"); + expect(attr.dtype).toBe("uint8"); + expect(Array.from(attr.data)).toEqual([1, 2, 3]); + mesh.delete(); + }); + + test("per-facet attribute with explicit dtype overrides inferred one", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("materialId", { + element: "facet", + usage: "scalar", + dtype: "int32", + data: [7], + }); + const attr = mesh.getAttribute("materialId"); + expect(attr.element).toBe("facet"); + expect(attr.numElements).toBe(1); + expect(attr.dtype).toBe("int32"); + expect(attr.data).toBeInstanceOf(Int32Array); + expect(Array.from(attr.data)).toEqual([7]); + mesh.delete(); + }); + + test("creating without data auto-sizes to the mesh's current element count", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("empty", { element: "vertex" }); + const attr = mesh.getAttribute("empty"); + expect(attr.numElements).toBe(3); + expect(attr.numChannels).toBe(1); + mesh.delete(); + }); + + test("value element is sized by the provided data, independent of mesh size", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("freeBuffer", { + element: "value", + numChannels: 2, + data: [1, 2, 3, 4, 5, 6], + }); + const attr = mesh.getAttribute("freeBuffer"); + expect(attr.element).toBe("value"); + expect(attr.numChannels).toBe(2); + expect(attr.numElements).toBe(3); + mesh.delete(); + }); + + test.each([ + "int8", + "int16", + "int32", + "uint8", + "uint16", + "uint32", + "float32", + "float64", + ])("round-trips a %s value-element attribute", (dtype) => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("buf", { + element: "value", + dtype, + data: [0, 1, 5, 10], + }); + const attr = mesh.getAttribute("buf"); + expect(attr.dtype).toBe(dtype); + expect(Array.from(attr.data)).toEqual([0, 1, 5, 10]); + mesh.delete(); + }); + + test("int64/uint64 attributes are widened to Float64Array on read", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("bigInts", { + element: "value", + dtype: "int64", + data: [10, -20, Number.MAX_SAFE_INTEGER], + }); + const attr = mesh.getAttribute("bigInts"); + expect(attr.dtype).toBe("int64"); + expect(attr.data).toBeInstanceOf(Float64Array); + expect(Array.from(attr.data)).toEqual([10, -20, Number.MAX_SAFE_INTEGER]); + + mesh.createAttribute("bigUints", { + element: "value", + dtype: "uint64", + data: [10, 20, 30], + }); + const uattr = mesh.getAttribute("bigUints"); + expect(uattr.dtype).toBe("uint64"); + expect(uattr.data).toBeInstanceOf(Float64Array); + expect(Array.from(uattr.data)).toEqual([10, 20, 30]); + mesh.delete(); + }); + + test("int64 value exactly at 2^53 still round-trips (it is exactly representable)", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("edge", { element: "value", dtype: "int64", data: [2 ** 53] }); + expect(Array.from(mesh.getAttribute("edge").data)).toEqual([2 ** 53]); + mesh.delete(); + }); + + test("rejects a fractional int64/uint64 value", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { element: "value", dtype: "int64", data: [1.5] }), + ).toThrow(); + mesh.delete(); + }); + + test("rejects int64/uint64 values exactly at the rounded double boundary", () => { + // INT64_MAX/UINT64_MAX round *up* to 2^63/2^64 as doubles, so those exact + // values must be rejected even though they equal the naively-converted max. + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad1", { element: "value", dtype: "int64", data: [2 ** 63] }), + ).toThrow(); + expect(() => + mesh.createAttribute("bad2", { element: "value", dtype: "uint64", data: [2 ** 64] }), + ).toThrow(); + mesh.delete(); + }); + + test("rejects BigInt64Array data", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { + element: "value", + data: new BigInt64Array([1n, 2n]) as unknown as number[], + }), + ).toThrow(); + mesh.delete(); + }); +}); + +describe("attribute: mutation", () => { + test("setAttribute overwrites values in place", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("weight", { + element: "vertex", + data: new Float32Array([1, 2, 3]), + }); + mesh.setAttribute("weight", [4, 5, 6]); + const attr = mesh.getAttribute("weight"); + expect(Array.from(attr.data)).toEqual([4, 5, 6]); + mesh.delete(); + }); + + test("setAttribute throws on length mismatch", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("weight", { + element: "vertex", + data: [1, 2, 3], + }); + expect(() => mesh.setAttribute("weight", [1, 2])).toThrow(); + mesh.delete(); + }); + + test("setAttribute on int64/uint64 is atomic: a bad value leaves data unchanged", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("bigInts", { element: "value", dtype: "int64", data: [100, 200, 300] }); + // The bad value (NaN) is in the middle; a non-atomic write would still change index 0. + expect(() => mesh.setAttribute("bigInts", [999, Number.NaN, 999])).toThrow(); + expect(Array.from(mesh.getAttribute("bigInts").data)).toEqual([100, 200, 300]); + mesh.delete(); + }); + + test("deleteAttribute removes it", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("weight", { element: "vertex", data: [1, 2, 3] }); + expect(mesh.hasAttribute("weight")).toBe(true); + mesh.deleteAttribute("weight"); + expect(mesh.hasAttribute("weight")).toBe(false); + expect(() => mesh.getAttribute("weight")).toThrow(); + mesh.delete(); + }); + + test("renameAttribute moves data under the new name", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("old", { element: "vertex", data: [1, 2, 3] }); + mesh.renameAttribute("old", "new"); + expect(mesh.hasAttribute("old")).toBe(false); + expect(mesh.hasAttribute("new")).toBe(true); + expect(Array.from(mesh.getAttribute("new").data)).toEqual([1, 2, 3]); + mesh.delete(); + }); + + test("duplicateAttribute creates an independent copy", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("src", { element: "vertex", data: [1, 2, 3] }); + const newId = mesh.duplicateAttribute("src", "dst"); + expect(typeof newId).toBe("number"); + expect(mesh.hasAttribute("dst")).toBe(true); + + // Mutating the copy must not affect the original (copy-on-write). + mesh.setAttribute("dst", [9, 9, 9]); + expect(Array.from(mesh.getAttribute("src").data)).toEqual([1, 2, 3]); + expect(Array.from(mesh.getAttribute("dst").data)).toEqual([9, 9, 9]); + mesh.delete(); + }); +}); + +describe("attribute: id / name lookup", () => { + test("getAttributeId and getAttributeName round-trip", () => { + const mesh = makeTriangleMesh(); + const id = mesh.createAttribute("weight", { + element: "vertex", + data: [1, 2, 3], + }); + expect(mesh.getAttributeId("weight")).toBe(id); + expect(mesh.getAttributeName(id)).toBe("weight"); + mesh.delete(); + }); + + test("getAttributeId throws for an unknown name", () => { + const mesh = makeTriangleMesh(); + expect(() => mesh.getAttributeId("nope")).toThrow(); + mesh.delete(); + }); + + test("getAttributeName throws for an invalid id", () => { + const mesh = makeTriangleMesh(); + expect(() => mesh.getAttributeName(0xffffffff)).toThrow(); + mesh.delete(); + }); +}); + +describe("attribute: indexed attributes", () => { + function makeIndexedUv(mesh: ReturnType) { + return mesh.createAttribute("uv", { + element: "indexed", + usage: "uv", + numChannels: 2, + data: new Float32Array([0, 0, 1, 0, 0, 1]), // 3 unique UV pairs + indices: new Uint32Array([0, 1, 2]), // one per corner + }); + } + + test("isAttributeIndexed reflects element type", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("weight", { element: "vertex", data: [1, 2, 3] }); + makeIndexedUv(mesh); + expect(mesh.isAttributeIndexed("weight")).toBe(false); + expect(mesh.isAttributeIndexed("uv")).toBe(true); + mesh.delete(); + }); + + test("getIndexedAttribute reads values and per-corner indices", () => { + const mesh = makeTriangleMesh(); + const id = makeIndexedUv(mesh); + const iattr = mesh.getIndexedAttribute("uv"); + expect(iattr.id).toBe(id); + expect(iattr.name).toBe("uv"); + expect(iattr.usage).toBe("uv"); + expect(iattr.numChannels).toBe(2); + expect(iattr.dtype).toBe("float32"); + expect(iattr.values.numElements).toBe(3); + expect(Array.from(iattr.values.data)).toEqual([0, 0, 1, 0, 0, 1]); + expect(iattr.indices).toBeInstanceOf(Uint32Array); + expect(Array.from(iattr.indices)).toEqual([0, 1, 2]); + mesh.delete(); + }); + + test("getAttribute rejects an indexed attribute", () => { + const mesh = makeTriangleMesh(); + makeIndexedUv(mesh); + expect(() => mesh.getAttribute("uv")).toThrow(); + mesh.delete(); + }); + + test("getIndexedAttribute rejects a non-indexed attribute", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("weight", { element: "vertex", data: [1, 2, 3] }); + expect(() => mesh.getIndexedAttribute("weight")).toThrow(); + mesh.delete(); + }); + + test("setAttribute rejects an indexed attribute", () => { + const mesh = makeTriangleMesh(); + makeIndexedUv(mesh); + expect(() => mesh.setAttribute("uv", [0, 0, 1, 0, 0, 1])).toThrow(); + mesh.delete(); + }); + + test("passing indices for a non-indexed element throws", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { + element: "vertex", + data: [1, 2, 3], + indices: [0, 1, 2], + }), + ).toThrow(); + mesh.delete(); + }); +}); + +describe("attribute: validation errors", () => { + test("createAttribute requires 'element'", () => { + const mesh = makeTriangleMesh(); + // @ts-expect-error missing required 'element' + expect(() => mesh.createAttribute("bad", {})).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects an unknown element", () => { + const mesh = makeTriangleMesh(); + expect(() => + // @ts-expect-error intentionally invalid element + mesh.createAttribute("bad", { element: "nope", data: [1] }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects an unknown usage", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { + element: "vertex", + // @ts-expect-error intentionally invalid usage + usage: "nope", + data: [1, 2, 3], + }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects an unknown dtype", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { + element: "value", + // @ts-expect-error intentionally invalid dtype + dtype: "nope", + data: [1, 2, 3], + }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects a usage/channel-count mismatch", () => { + const mesh = makeTriangleMesh(); + // "uv" usage requires exactly 2 channels. + expect(() => + mesh.createAttribute("bad", { + element: "vertex", + usage: "uv", + numChannels: 3, + data: [1, 2, 3, 4, 5, 6, 7, 8, 9], + }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects vertexIndex usage with a mismatched dtype", () => { + const mesh = makeTriangleMesh(); + // vertexIndex/facetIndex/cornerIndex/edgeIndex must use the mesh's Index dtype (uint32). + expect(() => + mesh.createAttribute("bad", { + element: "facet", + usage: "vertexIndex", + dtype: "int32", + data: [0], + }), + ).toThrow(); + mesh.createAttribute("good", { + element: "facet", + usage: "vertexIndex", + dtype: "uint32", + data: [0], + }); + mesh.delete(); + }); + + test("createAttribute rejects a reserved ('$'-prefixed) name by default", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("$reserved", { element: "vertex", data: [1, 2, 3] }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects a duplicate name", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("weight", { element: "vertex", data: [1, 2, 3] }); + expect(() => + mesh.createAttribute("weight", { element: "vertex", data: [4, 5, 6] }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects numChannels: 0 on an indexed attribute", () => { + // Regression test: the core library divides by numChannels while validating an + // indexed attribute's value buffer, so 0 must be rejected before reaching it. + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { + element: "indexed", + numChannels: 0, + data: [1, 2, 3], + indices: [0, 1, 2], + }), + ).toThrow(); + // The module must still be usable afterward. + expect(mesh.getNumVertices()).toBe(3); + mesh.delete(); + }); + + test.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY])( + "createAttribute rejects an invalid numChannels (%s)", + (badNumChannels) => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { element: "vertex", numChannels: badNumChannels, data: [1, 2, 3] }), + ).toThrow(); + mesh.delete(); + }, + ); + + test.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, 1e300])( + "createAttribute rejects a non-finite/out-of-range int64 value (%s)", + (bad) => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { element: "value", dtype: "int64", data: [0, bad] }), + ).toThrow(); + mesh.delete(); + }, + ); + + test("setAttribute rejects a non-finite int64 value", () => { + const mesh = makeTriangleMesh(); + mesh.createAttribute("bad", { element: "value", dtype: "int64", data: [0, 1, 2] }); + expect(() => mesh.setAttribute("bad", [0, Number.NaN, 2])).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects an index that is out of range for the value count", () => { + const mesh = makeTriangleMesh(); + // Only 3 UV pairs (indices 0-2) exist; index 3 does not. + expect(() => + mesh.createAttribute("bad", { + element: "indexed", + usage: "uv", + numChannels: 2, + data: new Float32Array([0, 0, 1, 0, 0, 1]), + indices: new Uint32Array([0, 1, 3]), + }), + ).toThrow(); + mesh.delete(); + }); + + test("createAttribute rejects non-empty indices without data", () => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { element: "indexed", numChannels: 2, indices: [0, 1, 2] }), + ).toThrow(); + mesh.delete(); + }); + + test("a failed createAttribute does not leave the name reserved", () => { + const mesh = makeTriangleMesh(); + // "uv" requires exactly 2 channels; this fails deep inside the core library. + expect(() => + mesh.createAttribute("uv", { element: "vertex", usage: "uv", numChannels: 3, data: [1, 2, 3, 4, 5, 6, 7, 8, 9] }), + ).toThrow(); + expect(mesh.hasAttribute("uv")).toBe(false); + // The name must be reusable afterward. + mesh.createAttribute("uv", { element: "vertex", usage: "uv", numChannels: 2, data: [0, 0, 1, 0, 0, 1] }); + expect(mesh.hasAttribute("uv")).toBe(true); + mesh.delete(); + }); + + test.each([-1, 1.5, Number.MAX_VALUE])( + "createAttribute rejects an invalid index value (%s)", + (badIndex) => { + const mesh = makeTriangleMesh(); + expect(() => + mesh.createAttribute("bad", { + element: "indexed", + numChannels: 2, + data: [0, 0, 1, 0, 0, 1], + indices: [0, 1, badIndex], + }), + ).toThrow(); + mesh.delete(); + }, + ); +}); diff --git a/modules/core/js/ts/core.ts b/modules/core/js/ts/core.ts index 9d7a3924..4f48c385 100644 --- a/modules/core/js/ts/core.ts +++ b/modules/core/js/ts/core.ts @@ -75,11 +75,52 @@ export interface SurfaceMesh { clearVertices(): void; /** Whether a named attribute exists on the mesh. */ hasAttribute(name: string): boolean; + /** + * Create a new attribute on this mesh and return its id. + * + * Throws if `name` already exists, or if `name` starts with `"$"` (the + * reserved-name prefix used by internal mesh attributes). + */ + createAttribute(name: string, opts: CreateAttributeOptions): number; + /** + * Read a (non-indexed) attribute's full data and metadata. Throws if + * `name` does not exist or is an indexed attribute — use + * {@link getIndexedAttribute} for those. + */ + getAttribute(name: string): AttributeData; + /** + * Overwrite a (non-indexed) attribute's values in place. `data` must be + * exactly `numElements * numChannels` long; values are cast to the + * attribute's existing {@link AttributeDType}. Throws if `name` does not + * exist, is indexed, or `data`'s length does not match. + */ + setAttribute(name: string, data: AttributeTypedArray | number[]): void; + /** Delete an attribute by name. Throws if it does not exist. */ + deleteAttribute(name: string): void; + /** Rename an attribute. Throws if `oldName` does not exist or `newName` is already taken. */ + renameAttribute(oldName: string, newName: string): void; + /** + * Shallow-copy an attribute under a new name (copy-on-write: the buffer is + * only duplicated once one of the two is mutated). Returns the new + * attribute's id. + */ + duplicateAttribute(oldName: string, newName: string): number; + /** Look up an attribute's id by name. Throws if not found. */ + getAttributeId(name: string): number; + /** Look up an attribute's name by id. Throws if not found. */ + getAttributeName(id: number): string; + /** Whether the named attribute is an indexed (per-corner-index) attribute. Throws if not found. */ + isAttributeIndexed(name: string): boolean; + /** + * Read an indexed attribute's value buffer and per-corner index buffer. + * Throws if `name` does not exist or is not indexed. + */ + getIndexedAttribute(name: string): IndexedAttributeData; /** * Append one vertex. `vertex.length` must equal {@link getDimension} * (typically `3`). */ - addVertex(vertex: Float32Array | ArrayLike): void; + addVertex(vertex: Float64Array | ArrayLike): void; /** Append a single triangle from three vertex ids. */ addTriangle(v0: number, v1: number, v2: number): void; /** @@ -153,8 +194,8 @@ export interface CoreModule { unifyIndexBuffer(mesh: SurfaceMesh): SurfaceMesh; /** * Fill topological holes whose boundary has at most `maxHoleSize` vertices. - * Modifies `mesh` in place. Set `triangulateHoles` to triangulate the new - * patches; otherwise they are inserted as a single polygon. + * Modifies `mesh` in place. New patches are triangulated by default; set + * `triangulateHoles` to `false` to insert each patch as a single polygon. */ closeSmallHoles( mesh: SurfaceMesh, @@ -272,7 +313,7 @@ export interface CoreModule { /** Remove facets whose area is below the threshold. In-place. */ removeNullAreaFacets(mesh: SurfaceMesh, opts?: RemoveNullAreaFacetsOptions): void; /** Collapse edges shorter than `threshold`. In-place. */ - removeShortEdges(mesh: SurfaceMesh, threshold?: number): void; + removeShortEdges(mesh: SurfaceMesh, threshold: number): void; /** Remove facets that repeat a vertex (e.g. `[a, b, a]`). In-place. */ removeTopologicallyDegenerateFacets(mesh: SurfaceMesh): void; /** @@ -402,7 +443,7 @@ export type ColoringElementType = "vertex" | "facet"; export interface GreedyColoringOptions { /** Element to color. Default: `"facet"`. */ elementType?: ColoringElementType; - /** Upper bound on the number of distinct colors. Default: `8`. */ + /** Initial color palette size; the algorithm allocates more colors when neighbors exhaust the palette, and may use fewer. Default: `8`. */ numColorUsed?: number; } @@ -420,3 +461,125 @@ export interface RemoveNullAreaFacetsOptions { /** Also remove vertices that become unreferenced after the removal. Default: `false`. */ removeIsolatedVertices?: boolean; } + +/** + * Which mesh element an attribute is attached to. `"value"` is a free + * buffer not tied to a specific element (used to store indexed-attribute + * values). `"indexed"` attaches a per-corner index into such a buffer — + * used for seamed UVs, hard-edge normals, etc. + */ +export type AttributeElementType = "vertex" | "facet" | "edge" | "corner" | "value" | "indexed"; + +/** + * Usage tag hinting how attribute values should behave under rigid + * transforms (e.g. normals get inverse-transposed, positions get + * transformed directly). Does not affect storage. + */ +export type AttributeUsageType = + | "vector" + | "scalar" + | "position" + | "normal" + | "tangent" + | "bitangent" + | "color" + | "uv" + | "vertexIndex" + | "facetIndex" + | "cornerIndex" + | "edgeIndex" + | "string"; + +/** + * Attribute value storage type. `"int64"`/`"uint64"` attributes are + * transported as `number` (double) since JS has no safe native 64-bit + * integer typed array. Reading or writing a value that cannot be + * represented exactly as a double (roughly beyond ±2^53) throws, rather + * than silently returning or storing a corrupted value. + */ +export type AttributeDType = + | "int8" + | "int16" + | "int32" + | "int64" + | "uint8" + | "uint16" + | "uint32" + | "uint64" + | "float32" + | "float64"; + +/** Typed arrays accepted/returned by the attribute API. `int64`/`uint64` data reads as `Float64Array` (see {@link AttributeDType}). */ +export type AttributeTypedArray = + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array + | Float32Array + | Float64Array; + +export interface CreateAttributeOptions { + /** Mesh element the attribute is attached to. */ + element: AttributeElementType; + /** + * Usage tag. Default: `"vector"` (any channel count). Others constrain + * {@link numChannels}: `"scalar"`/index usages/`"string"` require `1`, + * `"uv"` requires `2`, `"color"` requires `1`-`4`, `"position"` requires + * the mesh dimension, `"normal"`/`"tangent"`/`"bitangent"` require the + * mesh dimension or dimension + 1. The `numChannels` default of `1` is + * only valid for usages that accept `1`. + */ + usage?: AttributeUsageType; + /** Number of channels (columns) per element. Default: `1` — see {@link usage} for constraints. */ + numChannels?: number; + /** + * Initial values, flattened row-major (`numElements * numChannels` + * long, or the values buffer length when `element` is `"indexed"`). A + * typed array also selects {@link dtype} unless it is set explicitly. + * Omit to leave the buffer default-initialized. + */ + data?: AttributeTypedArray | number[]; + /** + * Value storage type. Inferred from {@link data}'s typed-array class + * when omitted; defaults to `"float64"` if `data` is a plain `number[]` + * or absent. + */ + dtype?: AttributeDType; + /** + * Per-corner indices into {@link data}. Only meaningful when `element` + * is `"indexed"`. + */ + indices?: Uint32Array | number[]; +} + +export interface AttributeData { + id: number; + name: string; + element: AttributeElementType; + usage: AttributeUsageType; + numChannels: number; + numElements: number; + dtype: AttributeDType; + /** Flattened row-major values, `numElements * numChannels` long. */ + data: AttributeTypedArray; +} + +export interface IndexedAttributeValues { + numElements: number; + /** Flattened row-major values, `numElements * numChannels` long. */ + data: AttributeTypedArray; +} + +export interface IndexedAttributeData { + id: number; + name: string; + usage: AttributeUsageType; + numChannels: number; + dtype: AttributeDType; + values: IndexedAttributeValues; + /** Per-corner index into {@link values}. Length equals the mesh's corner count. */ + indices: Uint32Array; +} diff --git a/modules/core/python/include/lagrange/python/utils/StubType.h b/modules/core/python/include/lagrange/python/utils/StubType.h index a1a210e5..c9f70393 100644 --- a/modules/core/python/include/lagrange/python/utils/StubType.h +++ b/modules/core/python/include/lagrange/python/utils/StubType.h @@ -55,7 +55,7 @@ struct type_caster> NB_TYPE_CASTER(Wrapper, const_name(Hint::value)) - bool from_python(handle src, uint8_t flags, cleanup_list* cleanup) noexcept + bool from_python(handle src, uint32_t flags, cleanup_list* cleanup) noexcept { TCaster caster; if (!caster.from_python(src, flags, cleanup)) return false; diff --git a/modules/core/python/include/lagrange/python/utils/bind_safe_vector.h b/modules/core/python/include/lagrange/python/utils/bind_safe_vector.h index bd3ba5b7..6268ada3 100644 --- a/modules/core/python/include/lagrange/python/utils/bind_safe_vector.h +++ b/modules/core/python/include/lagrange/python/utils/bind_safe_vector.h @@ -29,7 +29,10 @@ NAMESPACE_BEGIN(NB_NAMESPACE) -template +template < + typename Vector, + rv_policy::value Policy = rv_policy::automatic_reference_v, + typename... Args> class_ bind_safe_vector(handle scope, const char* name, Args&&... args) { using ValueRef = typename detail::iterator_access::result_type; @@ -43,7 +46,7 @@ class_ bind_safe_vector(handle scope, const char* name, Args&&... args) static_assert( !detail::is_base_caster_v> || detail::is_copy_constructible_v || - (Policy != rv_policy::automatic_reference && Policy != rv_policy::copy), + (Policy != rv_policy::automatic_reference_v && Policy != rv_policy::copy_v), "bind_safe_vector(): the generated __getitem__ would copy elements, so the " "element type must be copy-constructible"); @@ -83,7 +86,7 @@ class_ bind_safe_vector(handle scope, const char* name, Args&&... args) [](Vector& v, Py_ssize_t i) -> ValueRef { return v.Vector::Super::operator[](detail::wrap(i, v.size())); }, - Policy) + rv_policy::policy_tag{}) .def("clear", [](Vector& v) { v.clear(); }, "Remove all items from list."); diff --git a/modules/core/python/src/bind_utilities.h b/modules/core/python/src/bind_utilities.h index 8f7c5b75..5f050699 100644 --- a/modules/core/python/src/bind_utilities.h +++ b/modules/core/python/src/bind_utilities.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -386,7 +388,7 @@ Vertices listed in `cone_vertices` are considered as cone vertices, which is alw :param mesh: Input mesh. :param element_type: Element type to be colored. Can be either Vertex or Facet. -:param num_color_used: Minimum number of colors to use. The algorithm will cycle through them but may use more. +:param num_color_used: Initial color palette size. The algorithm allocates more colors when neighbors exhaust the palette, and may use fewer. :param output_attribute_name: Output attribute name. :returns: Color attribute id.)"); @@ -772,7 +774,7 @@ argument. Each component id is in [0, num_components-1] range. "mesh"_a, "output_attribute_name"_a = nb::none(), "induced_by_attribute"_a = nb::none(), - R"(Compute vertex valence); + R"(Compute vertex valence. :param mesh: The input mesh. :param output_attribute_name: The name of the output attribute. @@ -1361,6 +1363,7 @@ Basel: Birkhäuser Basel, 2008. 175-188. :param facet_group_indices: The group index for each facet. Each group index must be in the range of [0, max(facet_group_indices)] :param source_vertex_attr_name: The optional attribute name to track source vertices. :param source_facet_attr_name: The optional attribute name to track source facets. +:param map_attributes: Map attributes from the source to target meshes. :returns: A list of meshes, one for each facet group. )"); @@ -2402,6 +2405,40 @@ and k is the number of materials. The value at row i and column j indicates the i belonging to material j. The function will insert boundaries between different materials based on the material attribute. )"); + + m.def( + "mesh_bbox", + [](MeshType& mesh) -> std::tuple, Eigen::VectorX> { + const Index dim = mesh.get_dimension(); + if (dim == 2) { + auto box = mesh_bbox<2, Scalar, Index>(mesh); + return {Eigen::VectorX(box.min()), Eigen::VectorX(box.max())}; + } else if (dim == 3) { + auto box = mesh_bbox<3, Scalar, Index>(mesh); + return {Eigen::VectorX(box.min()), Eigen::VectorX(box.max())}; + } + throw nb::value_error("mesh_bbox only supports 2D or 3D meshes."); + }, + "mesh"_a, + R"(Compute the axis-aligned bounding box of a mesh. + +:param mesh: Input mesh (must be 2D or 3D). + +:returns: A tuple ``(min, max)`` of corner coordinates. For a mesh with no vertices, an empty box is returned, where ``min`` is component-wise greater than ``max``.)"); + + m.def( + "compute_uv_tile_list", + &compute_uv_tile_list, + "mesh"_a, + R"(Extract the list of all UV tiles that a mesh's parametrization spans. + +UV tiles are understood to be a regular unit grid in UV space. Tiles are unioned across +every attribute marked with UV usage (both indexed and per-vertex), adding an entry for +each distinct integer ``(floor(u), floor(v))`` pair found. + +:param mesh: Input mesh to be analyzed. + +:returns: A list of integer ``(u, v)`` coordinate pairs, one per UV tile.)"); } } // namespace lagrange::python diff --git a/modules/core/python/src/core.cpp b/modules/core/python/src/core.cpp index f007f7db..82da6244 100644 --- a/modules/core/python/src/core.cpp +++ b/modules/core/python/src/core.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +#include #include "bind_attribute.h" #include "bind_camera_transforms.h" #include "bind_enum.h" diff --git a/modules/core/python/src/logging.cpp b/modules/core/python/src/logging.cpp index 4de5cb50..5e3ee5fc 100644 --- a/modules/core/python/src/logging.cpp +++ b/modules/core/python/src/logging.cpp @@ -42,7 +42,7 @@ class PythonLoggingSink : public spdlog::sinks::base_sink void sink_it_(const spdlog::details::log_msg& msg) override { // Logging in python requires the current thread to hold the GIL. - if (!PyGILState_Check()) return; + if (!NB_CALL(gil_check)()) return; auto payload = msg.payload; auto res = nb::str(payload.data(), payload.size()); @@ -62,7 +62,7 @@ class PythonLoggingSink : public spdlog::sinks::base_sink void flush_() override { // Logging in python requires the current thread to hold the GIL. - if (!PyGILState_Check()) return; + if (!NB_CALL(gil_check)()) return; auto handlers = m_py_logger.attr("handlers"); for (auto handler : handlers) { diff --git a/modules/core/python/tests/test_compute_uv_tile_list.py b/modules/core/python/tests/test_compute_uv_tile_list.py new file mode 100644 index 00000000..9325504a --- /dev/null +++ b/modules/core/python/tests/test_compute_uv_tile_list.py @@ -0,0 +1,41 @@ +# +# Copyright 2026 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import numpy as np + + +class TestComputeUVTileList: + def test_single_tile(self, cube_with_uv): + tiles = lagrange.compute_uv_tile_list(cube_with_uv) + assert set(tiles) == {(0, 0), (0, 1)} + + def test_no_uv(self, cube): + tiles = lagrange.compute_uv_tile_list(cube) + assert tiles == [] + + def test_vertex_uv_last_element(self): + mesh = lagrange.SurfaceMesh() + mesh.add_vertex([0, 0, 0]) + mesh.add_vertex([1, 0, 0]) + mesh.add_vertex([1, 1, 0]) + mesh.add_vertex([0, 1, 0]) + mesh.add_triangle(0, 1, 2) + mesh.add_triangle(0, 2, 3) + mesh.create_attribute( + "uv", + element=lagrange.AttributeElement.Vertex, + usage=lagrange.AttributeUsage.UV, + initial_values=np.array([[0.1, 0.1], [0.2, 0.2], [0.3, 0.3], [2.5, 2.5]]), + ) + tiles = lagrange.compute_uv_tile_list(mesh) + assert set(tiles) == {(0, 0), (2, 2)} diff --git a/modules/core/python/tests/test_mesh_bbox.py b/modules/core/python/tests/test_mesh_bbox.py new file mode 100644 index 00000000..f6ba86ae --- /dev/null +++ b/modules/core/python/tests/test_mesh_bbox.py @@ -0,0 +1,46 @@ +# +# Copyright 2026 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +import lagrange + +import numpy as np +import pytest + + +class TestMeshBBox: + def test_cube(self, cube): + bbox_min, bbox_max = lagrange.mesh_bbox(cube) + assert bbox_min == pytest.approx([0, 0, 0]) + assert bbox_max == pytest.approx([1, 1, 1]) + + def test_single_triangle(self, single_triangle): + bbox_min, bbox_max = lagrange.mesh_bbox(single_triangle) + assert bbox_min == pytest.approx([0, 0, 0]) + assert bbox_max == pytest.approx([1, 1, 1]) + + def test_2d(self): + mesh = lagrange.SurfaceMesh(2) + mesh.add_vertex([0, 0]) + mesh.add_vertex([2, 1]) + mesh.add_vertex([1, 3]) + mesh.add_triangle(0, 1, 2) + bbox_min, bbox_max = lagrange.mesh_bbox(mesh) + assert bbox_min.size == 2 + assert bbox_max.size == 2 + assert bbox_min == pytest.approx([0, 0]) + assert bbox_max == pytest.approx([2, 3]) + + def test_empty(self): + mesh = lagrange.SurfaceMesh() + bbox_min, bbox_max = lagrange.mesh_bbox(mesh) + assert bbox_min.size == 3 + assert bbox_max.size == 3 + assert np.all(bbox_min > bbox_max) diff --git a/modules/core/src/Mesh.cpp b/modules/core/src/Mesh.cpp index c8449ba8..3a1df9a2 100644 --- a/modules/core/src/Mesh.cpp +++ b/modules/core/src/Mesh.cpp @@ -10,7 +10,10 @@ * governing permissions and limitations under the License. */ -#include +// The legacy Mesh<> class depends on legacy-only types (e.g. MeshTopology). +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + + #include namespace lagrange { @@ -24,3 +27,5 @@ template class Mesh; template class Mesh; } // namespace lagrange + +#endif diff --git a/modules/core/src/compute_components.cpp b/modules/core/src/compute_components.cpp index f443a9ff..87514cb3 100644 --- a/modules/core/src/compute_components.cpp +++ b/modules/core/src/compute_components.cpp @@ -135,6 +135,27 @@ size_t compute_components( span blocker_elements, ComponentOptions options) { + switch (options.connectivity_type) { + case ComponentOptions::ConnectivityType::Vertex: { + const auto num_vertices = mesh.get_num_vertices(); + for (auto vi : blocker_elements) { + la_runtime_assert( + vi < num_vertices, + "blocker_vertices contains an out-of-bound vertex index"); + } + break; + } + case ComponentOptions::ConnectivityType::Edge: { + mesh.initialize_edges(); + const auto num_edges = mesh.get_num_edges(); + for (auto ei : blocker_elements) { + la_runtime_assert(ei < num_edges, "blocker_edges contains an out-of-bound edge index"); + } + break; + } + default: throw Error("Unsupported connectivity type"); + } + AttributeId id; if (mesh.has_attribute(options.output_attribute_name)) { diff --git a/modules/core/src/compute_dijkstra_distance.cpp b/modules/core/src/compute_dijkstra_distance.cpp index 4118e9fb..d83e9361 100644 --- a/modules/core/src/compute_dijkstra_distance.cpp +++ b/modules/core/src/compute_dijkstra_distance.cpp @@ -25,6 +25,8 @@ std::optional> compute_dijkstra_distance( SurfaceMesh& mesh, const DijkstraDistanceOptions& options) { + la_runtime_assert(options.seed_facet < mesh.get_num_facets(), "seed_facet is out of bound"); + const auto dist_attr_id = internal::find_or_create_attribute( mesh, options.output_attribute_name, diff --git a/modules/core/src/compute_mesh_covariance.cpp b/modules/core/src/compute_mesh_covariance.cpp index 5e6d98d5..8b8184a5 100644 --- a/modules/core/src/compute_mesh_covariance.cpp +++ b/modules/core/src/compute_mesh_covariance.cpp @@ -11,6 +11,10 @@ */ #include +#include +#include +#include +#include #include #include diff --git a/modules/core/src/compute_normal.cpp b/modules/core/src/compute_normal.cpp index 787ae692..f0d5de4b 100644 --- a/modules/core/src/compute_normal.cpp +++ b/modules/core/src/compute_normal.cpp @@ -45,6 +45,14 @@ namespace lagrange { namespace { +template +void check_cone_vertices(span cone_vertices, Index num_vertices) +{ + for (auto vi : cone_vertices) { + la_runtime_assert(vi < num_vertices, "cone_vertices contains an out-of-bound vertex index"); + } +} + template DisjointSets compute_unified_indices( const SurfaceMesh& mesh, @@ -124,6 +132,10 @@ AttributeId compute_normal_internal( Func get_unified_indices) { la_runtime_assert(mesh.get_dimension() == 3, "Only 3D meshes are supported."); + + const auto num_vertices = mesh.get_num_vertices(); + check_cone_vertices(cone_vertices, num_vertices); + if (!mesh.has_edges()) mesh.initialize_edges(); auto [facet_normal_id, had_facet_normals] = internal::recompute_facet_normal_if_needed( @@ -132,10 +144,10 @@ AttributeId compute_normal_internal( options.recompute_facet_normals); auto facet_normal = attribute_matrix_view(mesh, facet_normal_id); - const auto num_vertices = mesh.get_num_vertices(); - std::vector is_cone_vertex(num_vertices, false); - for (auto vi : cone_vertices) is_cone_vertex[vi] = true; + for (auto vi : cone_vertices) { + is_cone_vertex[vi] = true; + } // Step 1: For each vertex v, iterate over its incident corners and // group corners sharing the same normal together. @@ -227,6 +239,8 @@ AttributeId compute_normal( { la_runtime_assert(mesh.get_dimension() == 3, "Only 3D meshes are supported."); + check_cone_vertices(cone_vertices, mesh.get_num_vertices()); + if (!mesh.has_edges()) mesh.initialize_edges(); auto [facet_normal_id, had_facet_normals] = internal::recompute_facet_normal_if_needed( diff --git a/modules/core/src/compute_tangent_bitangent.cpp b/modules/core/src/compute_tangent_bitangent.cpp index ae5f8c32..28969f13 100644 --- a/modules/core/src/compute_tangent_bitangent.cpp +++ b/modules/core/src/compute_tangent_bitangent.cpp @@ -10,6 +10,8 @@ * governing permissions and limitations under the License. */ #include +#include +#include #include #include diff --git a/modules/core/src/compute_uv_tile_list.cpp b/modules/core/src/compute_uv_tile_list.cpp index 60053b5f..84fd5f43 100644 --- a/modules/core/src/compute_uv_tile_list.cpp +++ b/modules/core/src/compute_uv_tile_list.cpp @@ -55,7 +55,7 @@ std::vector> compute_uv_tile_list( if constexpr (std::is_same_v>) { auto&& values = attr.get_all(); - for (size_t index = 0; index < attr.get_num_elements(); index += channelCount) { + for (size_t index = 0; index < values.size(); index += channelCount) { std::array const tile = { static_cast(std::floor(values[index])), static_cast(std::floor(values[index + 1]))}; diff --git a/modules/core/src/compute_vertex_normal.cpp b/modules/core/src/compute_vertex_normal.cpp index 7fb81748..ffde4f48 100644 --- a/modules/core/src/compute_vertex_normal.cpp +++ b/modules/core/src/compute_vertex_normal.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include diff --git a/modules/core/src/compute_vertex_valence.cpp b/modules/core/src/compute_vertex_valence.cpp index 5942c1ba..dbf33db5 100644 --- a/modules/core/src/compute_vertex_valence.cpp +++ b/modules/core/src/compute_vertex_valence.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include diff --git a/modules/core/src/create_mesh.cpp b/modules/core/src/create_mesh.cpp index 71b143c2..4f60f787 100644 --- a/modules/core/src/create_mesh.cpp +++ b/modules/core/src/create_mesh.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + + #include + #include + #include + #include namespace lagrange { @@ -159,3 +161,5 @@ std::unique_ptr create_sphere(double refine_order) } } // namespace lagrange + +#endif diff --git a/modules/core/src/extract_submesh.cpp b/modules/core/src/extract_submesh.cpp index fbd81dcf..af6bf0a3 100644 --- a/modules/core/src/extract_submesh.cpp +++ b/modules/core/src/extract_submesh.cpp @@ -15,7 +15,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -30,6 +33,14 @@ SurfaceMesh extract_submesh( { SurfaceMesh output_mesh(mesh.get_dimension()); + const Index num_facets = mesh.get_num_facets(); + la_runtime_assert( + std::all_of( + selected_facets.begin(), + selected_facets.end(), + [num_facets](Index fid) { return fid < num_facets; }), + "extract_submesh: facet index out of bound"); + // Compute vertex mapping. const auto num_vertices = mesh.get_num_vertices(); std::vector vertex_old2new(num_vertices, invalid()); diff --git a/modules/core/src/isoline.cpp b/modules/core/src/isoline.cpp index a8ca91c6..448a8c68 100644 --- a/modules/core/src/isoline.cpp +++ b/modules/core/src/isoline.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include diff --git a/modules/core/src/mesh_cleanup/close_small_holes.cpp b/modules/core/src/mesh_cleanup/close_small_holes.cpp index 0628328b..6bc9dd4d 100644 --- a/modules/core/src/mesh_cleanup/close_small_holes.cpp +++ b/modules/core/src/mesh_cleanup/close_small_holes.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include diff --git a/modules/core/src/mesh_cleanup/remove_duplicate_facets.cpp b/modules/core/src/mesh_cleanup/remove_duplicate_facets.cpp index edacdd33..a7c2c899 100644 --- a/modules/core/src/mesh_cleanup/remove_duplicate_facets.cpp +++ b/modules/core/src/mesh_cleanup/remove_duplicate_facets.cpp @@ -11,6 +11,7 @@ */ #include +#include #include "../internal/bucket_sort.h" diff --git a/modules/core/src/mesh_cleanup/remove_duplicate_vertices.cpp b/modules/core/src/mesh_cleanup/remove_duplicate_vertices.cpp index 32836889..15eb359e 100644 --- a/modules/core/src/mesh_cleanup/remove_duplicate_vertices.cpp +++ b/modules/core/src/mesh_cleanup/remove_duplicate_vertices.cpp @@ -10,11 +10,13 @@ * governing permissions and limitations under the License. */ +#include #include #include #include #include #include +#include #include // clang-format off diff --git a/modules/core/src/mesh_cleanup/remove_isolated_vertices.cpp b/modules/core/src/mesh_cleanup/remove_isolated_vertices.cpp index 024cd3cb..bf16a988 100644 --- a/modules/core/src/mesh_cleanup/remove_isolated_vertices.cpp +++ b/modules/core/src/mesh_cleanup/remove_isolated_vertices.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include diff --git a/modules/core/src/mesh_cleanup/resolve_nonmanifoldness.cpp b/modules/core/src/mesh_cleanup/resolve_nonmanifoldness.cpp index 63abad2f..9a186dc1 100644 --- a/modules/core/src/mesh_cleanup/resolve_nonmanifoldness.cpp +++ b/modules/core/src/mesh_cleanup/resolve_nonmanifoldness.cpp @@ -10,12 +10,14 @@ * governing permissions and limitations under the License. */ +#include #include #include #include #include #include #include +#include // clang-format off #include diff --git a/modules/core/src/mesh_cleanup/resolve_vertex_nonmanifoldness.cpp b/modules/core/src/mesh_cleanup/resolve_vertex_nonmanifoldness.cpp index 495bcefb..6c9cdfd5 100644 --- a/modules/core/src/mesh_cleanup/resolve_vertex_nonmanifoldness.cpp +++ b/modules/core/src/mesh_cleanup/resolve_vertex_nonmanifoldness.cpp @@ -10,11 +10,13 @@ * governing permissions and limitations under the License. */ +#include #include #include #include #include #include +#include // clang-format off #include diff --git a/modules/core/src/mesh_cleanup/split_long_edges.cpp b/modules/core/src/mesh_cleanup/split_long_edges.cpp index b507d7d1..ea608b38 100644 --- a/modules/core/src/mesh_cleanup/split_long_edges.cpp +++ b/modules/core/src/mesh_cleanup/split_long_edges.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include diff --git a/modules/core/src/normalize_meshes.cpp b/modules/core/src/normalize_meshes.cpp index 2c698c57..cdcca0a2 100644 --- a/modules/core/src/normalize_meshes.cpp +++ b/modules/core/src/normalize_meshes.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +#include #include LA_IGNORE_MAYBE_UNINITIALIZED_START diff --git a/modules/core/src/orient_outward.cpp b/modules/core/src/orient_outward.cpp index 9ea95216..db1530c9 100644 --- a/modules/core/src/orient_outward.cpp +++ b/modules/core/src/orient_outward.cpp @@ -11,6 +11,8 @@ */ #include +#include +#include #include #include diff --git a/modules/core/src/select_facets_by_normal_similarity.cpp b/modules/core/src/select_facets_by_normal_similarity.cpp index a829e92b..e1b780bf 100644 --- a/modules/core/src/select_facets_by_normal_similarity.cpp +++ b/modules/core/src/select_facets_by_normal_similarity.cpp @@ -11,6 +11,9 @@ */ #include +#include +#include +#include #include #include @@ -36,6 +39,7 @@ AttributeId select_facets_by_normal_similarity( using Vector3D = typename Eigen::Vector3; const Index num_facets = mesh.get_num_facets(); + la_runtime_assert(seed_facet_id < num_facets, "seed_facet_id is out of bound"); const Scalar flood_error_limit = static_cast(options.flood_error_limit); const Scalar flood_second_to_first_order_limit_ratio = static_cast(options.flood_second_to_first_order_limit_ratio); diff --git a/modules/core/src/select_facets_in_frustum.cpp b/modules/core/src/select_facets_in_frustum.cpp index df87de2c..892815e2 100644 --- a/modules/core/src/select_facets_in_frustum.cpp +++ b/modules/core/src/select_facets_in_frustum.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include diff --git a/modules/core/src/separate_by_facet_groups.cpp b/modules/core/src/separate_by_facet_groups.cpp index 6d4b1478..83041d27 100644 --- a/modules/core/src/separate_by_facet_groups.cpp +++ b/modules/core/src/separate_by_facet_groups.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +#include #include "internal/extract_submeshes_by_group.h" #include #include #include +#include #include #include @@ -30,9 +32,11 @@ std::vector> separate_by_facet_groups( const Index num_facets = mesh.get_num_facets(); if (num_facets == 0) return {}; la_runtime_assert(static_cast(facet_group_indices.size()) == num_facets); + la_runtime_assert(num_groups < std::numeric_limits::max(), "num_groups is too large"); std::vector facet_indices(num_facets); std::vector group_offsets(num_groups + 1, 0); for (auto i : facet_group_indices) { + la_runtime_assert(static_cast(i) < num_groups, "facet group index is out of bound"); group_offsets[i + 1]++; } std::partial_sum(group_offsets.begin(), group_offsets.end(), group_offsets.begin()); diff --git a/modules/core/src/thicken_and_close_mesh.cpp b/modules/core/src/thicken_and_close_mesh.cpp index 3c1a8d5d..a7cf7710 100644 --- a/modules/core/src/thicken_and_close_mesh.cpp +++ b/modules/core/src/thicken_and_close_mesh.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ #include +#include #include #include diff --git a/modules/core/tests/attributes/test_attribute_utils.cpp b/modules/core/tests/attributes/test_attribute_utils.cpp index a0abf003..56d1ce5a 100644 --- a/modules/core/tests/attributes/test_attribute_utils.cpp +++ b/modules/core/tests/attributes/test_attribute_utils.cpp @@ -9,15 +9,17 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include -#include + #include + #include + #include + #include + + #include TEST_CASE("AttributeUtils", "[attribute][conversion]") { @@ -277,3 +279,5 @@ TEST_CASE("AttributeConversions", "[attribute]") REQUIRE(uv2(i, 1) == vertices(i, 1)); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/attributes/test_condense_indexed_attribute.cpp b/modules/core/tests/attributes/test_condense_indexed_attribute.cpp index ff00e087..12ff5a00 100644 --- a/modules/core/tests/attributes/test_condense_indexed_attribute.cpp +++ b/modules/core/tests/attributes/test_condense_indexed_attribute.cpp @@ -9,14 +9,16 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include + #include + #include TEST_CASE("CondenseIndexedAttribute", "[attribute][condense][indexed]") { @@ -78,3 +80,5 @@ TEST_CASE("CondenseIndexedAttribute", "[attribute][condense][indexed]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/attributes/test_unify_index_buffer.cpp b/modules/core/tests/attributes/test_unify_index_buffer.cpp index db8d11d4..c21c09c5 100644 --- a/modules/core/tests/attributes/test_unify_index_buffer.cpp +++ b/modules/core/tests/attributes/test_unify_index_buffer.cpp @@ -9,19 +9,21 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include TEST_CASE("UnifyIndexBuffer", "[attribute][unify][indexed]" LA_SLOW_DEBUG_FLAG) { @@ -212,3 +214,5 @@ TEST_CASE("UnifyIndexBuffer", "[attribute][unify][indexed]" LA_SLOW_DEBUG_FLAG) "valence")); // Although, valence is no longer accurate. } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/compile_errors/CMakeLists.txt b/modules/core/tests/compile_errors/CMakeLists.txt index 82bfdd22..d3616eb6 100644 --- a/modules/core/tests/compile_errors/CMakeLists.txt +++ b/modules/core/tests/compile_errors/CMakeLists.txt @@ -12,8 +12,10 @@ lagrange_add_compile_test(test_assert.cpp) lagrange_add_compile_test(test_attribute_type.cpp) lagrange_add_compile_test(test_mesh_attribute.cpp) -lagrange_add_compile_test(test_mesh_convert1.cpp) -lagrange_add_compile_test(test_mesh_convert2.cpp) +if(LAGRANGE_ENABLE_LEGACY_FUNCTIONS) + lagrange_add_compile_test(test_mesh_convert1.cpp) + lagrange_add_compile_test(test_mesh_convert2.cpp) +endif() lagrange_add_compile_test(test_mesh_type.cpp) lagrange_add_compile_test(test_pimpl.cpp) lagrange_add_compile_test(test_safe_cast.cpp) diff --git a/modules/core/tests/compile_errors/test_mesh_attribute.cpp b/modules/core/tests/compile_errors/test_mesh_attribute.cpp index 9e04825f..9db8453a 100644 --- a/modules/core/tests/compile_errors/test_mesh_attribute.cpp +++ b/modules/core/tests/compile_errors/test_mesh_attribute.cpp @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include +#include void test_to_surface_mesh() { diff --git a/modules/core/tests/compute_tangent_bitangent_mikktspace.h b/modules/core/tests/compute_tangent_bitangent_mikktspace.h index 7e59a51e..e35c6c3a 100644 --- a/modules/core/tests/compute_tangent_bitangent_mikktspace.h +++ b/modules/core/tests/compute_tangent_bitangent_mikktspace.h @@ -20,6 +20,7 @@ #include #include + #include namespace lagrange { diff --git a/modules/core/tests/experimental/test_indexed_attributes.cpp b/modules/core/tests/experimental/test_indexed_attributes.cpp index b6f66813..1b2ff905 100644 --- a/modules/core/tests/experimental/test_indexed_attributes.cpp +++ b/modules/core/tests/experimental/test_indexed_attributes.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include namespace { @@ -434,3 +436,5 @@ TEST_CASE("experimental/IndexedAttributes", "[attribute][indexed]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/mesh_cleanup/test_detect_degenerate_triangles.cpp b/modules/core/tests/mesh_cleanup/test_detect_degenerate_triangles.cpp index b2d61fe4..8c1faf35 100644 --- a/modules/core/tests/mesh_cleanup/test_detect_degenerate_triangles.cpp +++ b/modules/core/tests/mesh_cleanup/test_detect_degenerate_triangles.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + + #include + #include + #include + #include TEST_CASE("DetectDegenerateTrianglesTest", "[degenerate][triangle_mesh]") { @@ -61,3 +63,5 @@ TEST_CASE("DetectDegenerateTrianglesTest", "[degenerate][triangle_mesh]") REQUIRE(is_degenerate(2, 0) == 1.0); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/mesh_cleanup/test_remove_degenerate_facets.cpp b/modules/core/tests/mesh_cleanup/test_remove_degenerate_facets.cpp index ac480c9f..fbf2a611 100644 --- a/modules/core/tests/mesh_cleanup/test_remove_degenerate_facets.cpp +++ b/modules/core/tests/mesh_cleanup/test_remove_degenerate_facets.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include diff --git a/modules/core/tests/mesh_cleanup/test_remove_degenerate_triangles.cpp b/modules/core/tests/mesh_cleanup/test_remove_degenerate_triangles.cpp index a2a386ce..f5a5549b 100644 --- a/modules/core/tests/mesh_cleanup/test_remove_degenerate_triangles.cpp +++ b/modules/core/tests/mesh_cleanup/test_remove_degenerate_triangles.cpp @@ -9,16 +9,18 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include -#include -#include -#include -#include -#include + #include + #include + + #include + #include + #include + #include + #include TEST_CASE("RemoveDegenerateTrianglesTest", "[degenerate][triangle_mesh][cleanup]") { @@ -199,3 +201,5 @@ TEST_CASE("RemoveDegenerateTrianglesTest", "[degenerate][triangle_mesh][cleanup] REQUIRE(is_uv_degenerate(0, 0) == 0.0); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/mesh_cleanup/test_remove_isolated_vertices.cpp b/modules/core/tests/mesh_cleanup/test_remove_isolated_vertices.cpp index a6d73eab..30ff89d3 100644 --- a/modules/core/tests/mesh_cleanup/test_remove_isolated_vertices.cpp +++ b/modules/core/tests/mesh_cleanup/test_remove_isolated_vertices.cpp @@ -10,13 +10,144 @@ * governing permissions and limitations under the License. */ #include -#include -#include -#include -#include +#include #include -#include + +#include + +TEST_CASE("remove_isolated_vertices", "[surface_mesh][mesh_cleanup]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + SECTION("No isolated vertices") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 1}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 3); + + remove_isolated_vertices(mesh); + REQUIRE(mesh.get_num_vertices() == 4); + REQUIRE(mesh.get_num_facets() == 2); + } + + SECTION("Single isolated vertex") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 1}); // isolated + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 0); + + remove_isolated_vertices(mesh); + REQUIRE(mesh.get_num_vertices() == 3); + REQUIRE(mesh.get_num_facets() == 2); + } + + SECTION("Multiple isolated vertices") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 1}); // isolated + mesh.add_vertex({1, 1, 1}); // isolated + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 0); + + remove_isolated_vertices(mesh); + REQUIRE(mesh.get_num_vertices() == 3); + REQUIRE(mesh.get_num_facets() == 2); + } + + SECTION("All vertices are isolated") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 1}); + + remove_isolated_vertices(mesh); + REQUIRE(mesh.get_num_vertices() == 0); + REQUIRE(mesh.get_num_facets() == 0); + } + + SECTION("Vertex attributes") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); // isolated + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 1}); + mesh.add_triangle(1, 3, 2); + mesh.add_triangle(2, 3, 1); + + std::vector vertex_index{0, 1, 2, 3}; + mesh.template create_attribute( + "index", + AttributeElement::Vertex, + 1, + AttributeUsage::Scalar, + {vertex_index.data(), vertex_index.size()}); + + remove_isolated_vertices(mesh); + REQUIRE(mesh.get_num_vertices() == 3); + REQUIRE(mesh.get_num_facets() == 2); + REQUIRE(mesh.has_attribute("index")); + + auto index = mesh.template get_attribute("index").get_all(); + REQUIRE(index.size() == 3); + // Vertex 0 was removed; remove_vertices preserves the order of survivors. + REQUIRE(index[0] == 1); + REQUIRE(index[1] == 2); + REQUIRE(index[2] == 3); + } + + SECTION("Facet attributes") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); // isolated + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_vertex({0, 0, 1}); + mesh.add_triangle(1, 3, 2); + mesh.add_triangle(2, 3, 1); + + std::vector facet_index{10, 20}; + mesh.template create_attribute( + "index", + AttributeElement::Facet, + 1, + AttributeUsage::Scalar, + {facet_index.data(), facet_index.size()}); + + remove_isolated_vertices(mesh); + REQUIRE(mesh.get_num_facets() == 2); + REQUIRE(mesh.has_attribute("index")); + + auto index = mesh.template get_attribute("index").get_all(); + REQUIRE(index.size() == 2); + REQUIRE(index[0] == 10); + REQUIRE(index[1] == 20); + } +} + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + + #include + #include + #include + #include TEST_CASE("RemoveIsolatedVertices", "[isolated_vertices][cleanup]") { @@ -145,3 +276,5 @@ TEST_CASE("RemoveIsolatedVertices", "[isolated_vertices][cleanup]") REQUIRE((index - attr2).norm() == Catch::Approx(0.0)); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/mesh_cleanup/test_remove_short_edges.cpp b/modules/core/tests/mesh_cleanup/test_remove_short_edges.cpp index 45f98322..0bb3916b 100644 --- a/modules/core/tests/mesh_cleanup/test_remove_short_edges.cpp +++ b/modules/core/tests/mesh_cleanup/test_remove_short_edges.cpp @@ -12,15 +12,20 @@ #include #include -#include +#include #include -#include +#include #include #include -#include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + #include +#endif + TEST_CASE("remove_short_edges", "[surface][cleanup]") { using namespace lagrange; diff --git a/modules/core/tests/mesh_cleanup/test_remove_topologically_degenerate_triangles.cpp b/modules/core/tests/mesh_cleanup/test_remove_topologically_degenerate_triangles.cpp index 86fa631f..fe2372e5 100644 --- a/modules/core/tests/mesh_cleanup/test_remove_topologically_degenerate_triangles.cpp +++ b/modules/core/tests/mesh_cleanup/test_remove_topologically_degenerate_triangles.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + + #include + #include + #include + #include TEST_CASE("RemoveTopologicallyDegenerateTriangles", "[degenerate][triangle_mesh][cleanup]") { @@ -79,3 +81,5 @@ TEST_CASE("RemoveTopologicallyDegenerateTriangles", "[degenerate][triangle_mesh] REQUIRE(attr(0, 0) == 1); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/mesh_cleanup/test_resolve_nonmanifoldness.cpp b/modules/core/tests/mesh_cleanup/test_resolve_nonmanifoldness.cpp index 76a8f999..c78c5abc 100644 --- a/modules/core/tests/mesh_cleanup/test_resolve_nonmanifoldness.cpp +++ b/modules/core/tests/mesh_cleanup/test_resolve_nonmanifoldness.cpp @@ -14,12 +14,15 @@ #include #include -#include #include #include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif + TEST_CASE("resolve_nonmanifoldness", "[nonmanifold][surface][cleanup]") { using namespace lagrange; diff --git a/modules/core/tests/mesh_cleanup/test_resolve_vertex_nonmanifoldness.cpp b/modules/core/tests/mesh_cleanup/test_resolve_vertex_nonmanifoldness.cpp index a3658814..956c61c3 100644 --- a/modules/core/tests/mesh_cleanup/test_resolve_vertex_nonmanifoldness.cpp +++ b/modules/core/tests/mesh_cleanup/test_resolve_vertex_nonmanifoldness.cpp @@ -14,13 +14,16 @@ #include #include -#include #include #include #include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif + #ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS TEST_CASE("legacy::resolve_vertex_nonmanifoldness", "[nonmanifold][Mesh][cleanup]") diff --git a/modules/core/tests/test_Connectivity.cpp b/modules/core/tests/test_Connectivity.cpp index dd877e44..ebff1137 100644 --- a/modules/core/tests/test_Connectivity.cpp +++ b/modules/core/tests/test_Connectivity.cpp @@ -9,14 +9,16 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + #include + + #include + #include + #include + #include TEST_CASE("ConnectivitySimpleTriangleMesh", "[connectivity][triangle_mesh][simple]") { @@ -183,3 +185,5 @@ TEST_CASE("ConnectivityQuadMesh", "[connectivity][quad_mesh][simple]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_EdgeFacetMap.cpp b/modules/core/tests/test_EdgeFacetMap.cpp index 6ad370df..be064c14 100644 --- a/modules/core/tests/test_EdgeFacetMap.cpp +++ b/modules/core/tests/test_EdgeFacetMap.cpp @@ -9,15 +9,17 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include -#include -#include -#include -#include -#include + #include + + #include + #include + #include + #include + #include namespace { @@ -185,3 +187,5 @@ TEST_CASE("EdgeFacetMap", "[EdgeFacetMap]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_chain_edges.cpp b/modules/core/tests/test_chain_edges.cpp index 35b03e58..6ac8fc72 100644 --- a/modules/core/tests/test_chain_edges.cpp +++ b/modules/core/tests/test_chain_edges.cpp @@ -9,13 +9,15 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include -#include -#include + #include + #include + + #include + #include TEST_CASE("chain_edges", "[core]") { @@ -104,3 +106,5 @@ TEST_CASE("chain_edges", "[core]") } LA_IGNORE_DEPRECATION_WARNING_END } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_combine_mesh_list.cpp b/modules/core/tests/test_combine_mesh_list.cpp index 7b4ee254..af2be0da 100644 --- a/modules/core/tests/test_combine_mesh_list.cpp +++ b/modules/core/tests/test_combine_mesh_list.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include TEST_CASE("combine_mesh_list", "[mesh][combine]") { @@ -186,3 +188,5 @@ TEST_CASE("combine_mesh_list", "[mesh][combine]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_combine_meshes.cpp b/modules/core/tests/test_combine_meshes.cpp index 522f0a3a..f9fb1ec5 100644 --- a/modules/core/tests/test_combine_meshes.cpp +++ b/modules/core/tests/test_combine_meshes.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS @@ -24,6 +25,8 @@ #include #endif +#include + using namespace lagrange; TEST_CASE("combine_meshes", "[surface][utilities]") @@ -411,12 +414,14 @@ TEST_CASE("combine_meshes benchmark", "[surface][utilities][!benchmark]") lagrange::testing::load_surface_mesh("open/core/poly/mixedFaringPart.obj"); REQUIRE(mesh.has_attribute("normal")); +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS using MeshType = TriangleMesh3D; auto legacy_mesh = to_legacy_mesh(mesh); std::vector meshes; meshes.push_back(legacy_mesh.get()); meshes.push_back(legacy_mesh.get()); +#endif SECTION("without attributes") { @@ -429,10 +434,12 @@ TEST_CASE("combine_meshes benchmark", "[surface][utilities][!benchmark]") return combine_meshes({&mesh, &poly, &mesh}, false); }; +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS BENCHMARK("legacy::combine_mesh_list without attributes") { return legacy::combine_mesh_list(meshes, false); }; +#endif } SECTION("with attributes") @@ -441,9 +448,11 @@ TEST_CASE("combine_meshes benchmark", "[surface][utilities][!benchmark]") { return combine_meshes({&mesh, &mesh}, true); }; +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS BENCHMARK("legacy::combine_mesh_list with attributes") { return legacy::combine_mesh_list(meshes, true); }; +#endif } } diff --git a/modules/core/tests/test_common.cpp b/modules/core/tests/test_common.cpp index 78478b13..d9229b3f 100644 --- a/modules/core/tests/test_common.cpp +++ b/modules/core/tests/test_common.cpp @@ -9,13 +9,19 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include + #include #include #include #include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif + #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS TEST_CASE("INVALID", "") { using namespace lagrange; @@ -31,6 +37,7 @@ TEST_CASE("INVALID", "") // different INVALID values. REQUIRE(std::is_arithmetic::value); } +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS TEST_CASE("MoveData", "[common][eigen]") { diff --git a/modules/core/tests/test_compute_components.cpp b/modules/core/tests/test_compute_components.cpp index 9b21082c..6f0f33f5 100644 --- a/modules/core/tests/test_compute_components.cpp +++ b/modules/core/tests/test_compute_components.cpp @@ -28,6 +28,8 @@ #include #include +#include + TEST_CASE("compute_components", "[surface][components][utilities]") { using namespace lagrange; @@ -299,6 +301,19 @@ TEST_CASE("compute_components", "[surface][components][utilities]") auto num_components = compute_components(mesh, blocker_vertex, opt); REQUIRE(num_components == 2); } + SECTION("With out-of-bound blocker vertex") + { + opt.connectivity_type = ComponentOptions::ConnectivityType::Vertex; + std::vector blocker_vertex = {mesh.get_num_vertices()}; + LA_REQUIRE_THROWS(compute_components(mesh, blocker_vertex, opt)); + } + SECTION("With out-of-bound blocker edge") + { + opt.connectivity_type = ComponentOptions::ConnectivityType::Edge; + mesh.initialize_edges(); + std::vector blocker_edge = {mesh.get_num_edges()}; + LA_REQUIRE_THROWS(compute_components(mesh, blocker_edge, opt)); + } } } diff --git a/modules/core/tests/test_compute_corner_normal.cpp b/modules/core/tests/test_compute_corner_normal.cpp index 9c699fb0..85b38c8a 100644 --- a/modules/core/tests/test_compute_corner_normal.cpp +++ b/modules/core/tests/test_compute_corner_normal.cpp @@ -9,19 +9,19 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS -#include -#include -#include -#include -#include + #include + #include + #include + #include + #include + #include + #include + #include + #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS TEST_CASE("legacy::compute_corner_normal", "[mesh][triangle][attribute][corner_normal][legacy]") { using namespace lagrange; diff --git a/modules/core/tests/test_compute_dijkstra_distance.cpp b/modules/core/tests/test_compute_dijkstra_distance.cpp index 84f85f43..f274a1fc 100644 --- a/modules/core/tests/test_compute_dijkstra_distance.cpp +++ b/modules/core/tests/test_compute_dijkstra_distance.cpp @@ -89,6 +89,23 @@ TEST_CASE("DijkstraDistance", "[dijkstra][surface][triangle]") REQUIRE_FALSE(result.has_value()); } + SECTION("invalid seed facet") + { + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({2, 0, 0}); + mesh.add_vertex({0, 2, 0}); + mesh.add_vertex({2, 2, 0}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(2, 1, 3); + + DijkstraDistanceOptions options; + options.seed_facet = mesh.get_num_facets(); + options.barycentric_coords = {1, 0, 0}; + + LA_REQUIRE_THROWS(compute_dijkstra_distance(mesh, options)); + } + SECTION("mixed") { SurfaceMesh mesh; diff --git a/modules/core/tests/test_compute_facet_area.cpp b/modules/core/tests/test_compute_facet_area.cpp index a648f785..d11e4e33 100644 --- a/modules/core/tests/test_compute_facet_area.cpp +++ b/modules/core/tests/test_compute_facet_area.cpp @@ -14,14 +14,19 @@ #include #include -#include #include #include #include -#include -#include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + #include +#endif + +#include + TEST_CASE("compute_facet_area", "[core][area][surface]") { using namespace lagrange; diff --git a/modules/core/tests/test_compute_mesh_centroid.cpp b/modules/core/tests/test_compute_mesh_centroid.cpp index 20533350..b3aafe7d 100644 --- a/modules/core/tests/test_compute_mesh_centroid.cpp +++ b/modules/core/tests/test_compute_mesh_centroid.cpp @@ -18,11 +18,12 @@ #include #include -#include #include #ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + TEST_CASE("ComputeMeshCentroid", "[mesh][centroid]") { using namespace lagrange; diff --git a/modules/core/tests/test_compute_mesh_covariance.cpp b/modules/core/tests/test_compute_mesh_covariance.cpp index 221bec23..87e0389e 100644 --- a/modules/core/tests/test_compute_mesh_covariance.cpp +++ b/modules/core/tests/test_compute_mesh_covariance.cpp @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ - +#include #ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS @@ -92,11 +92,15 @@ TEST_CASE("legacy::compute_mesh_covariance", "[mesh][covariance]") } // end of TEST #endif +#include #include #include +#include +#include #include #include +#include TEST_CASE("compute_mesh_covariance", "[mesh][covariance]") { diff --git a/modules/core/tests/test_compute_normal.cpp b/modules/core/tests/test_compute_normal.cpp index ce492da3..e09bbd47 100644 --- a/modules/core/tests/test_compute_normal.cpp +++ b/modules/core/tests/test_compute_normal.cpp @@ -17,16 +17,21 @@ #include #include -#include +#include #include #include #include #include -#include +#include #include #include #include #include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif #include #include #include @@ -272,6 +277,14 @@ TEST_CASE("compute_normal", "[surface][attribute][normal][utilities]") REQUIRE(n2[2] < 0.8); REQUIRE(n3[2] < 0.8); } + + SECTION("Invalid cone vertex") + { + std::vector cones; + cones.push_back(mesh.get_num_vertices()); + LA_REQUIRE_THROWS( + compute_normal(mesh, lagrange::internal::pi / 2 - 0.1, cones)); + } } } diff --git a/modules/core/tests/test_compute_tangent_bitangent.cpp b/modules/core/tests/test_compute_tangent_bitangent.cpp index 087699c3..c9586b9f 100644 --- a/modules/core/tests/test_compute_tangent_bitangent.cpp +++ b/modules/core/tests/test_compute_tangent_bitangent.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -38,6 +39,8 @@ #include +#include + #ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS #include #include @@ -91,6 +94,7 @@ auto indexed_tangent_bitangent( lagrange::matrix_view(bitangent.indices())); } +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS template auto corner_tangent_bitangent_legacy( MeshType& mesh, @@ -135,6 +139,7 @@ auto indexed_tangent_bitangent_legacy( mesh.remove_indexed_attribute("tangent"); mesh.remove_indexed_attribute("bitangent"); } +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS } // namespace diff --git a/modules/core/tests/test_compute_vertex_normal.cpp b/modules/core/tests/test_compute_vertex_normal.cpp index c267ce6e..8dc969cc 100644 --- a/modules/core/tests/test_compute_vertex_normal.cpp +++ b/modules/core/tests/test_compute_vertex_normal.cpp @@ -14,14 +14,17 @@ #include #include -#include #include #include -#include #include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif + namespace { template diff --git a/modules/core/tests/test_compute_weighted_corner_normal.cpp b/modules/core/tests/test_compute_weighted_corner_normal.cpp index 759bcc8c..9bc17cf1 100644 --- a/modules/core/tests/test_compute_weighted_corner_normal.cpp +++ b/modules/core/tests/test_compute_weighted_corner_normal.cpp @@ -14,13 +14,16 @@ #include #include -#include #include #include -#include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif + TEST_CASE("compute_weighted_corner_normal", "[surface][attribute][utilities][normal]") { using namespace lagrange; diff --git a/modules/core/tests/test_corner_to_edge_mapping.cpp b/modules/core/tests/test_corner_to_edge_mapping.cpp index 66d92b50..d49c4da3 100644 --- a/modules/core/tests/test_corner_to_edge_mapping.cpp +++ b/modules/core/tests/test_corner_to_edge_mapping.cpp @@ -9,11 +9,13 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + #include + + #include namespace { @@ -90,3 +92,5 @@ TEST_CASE("corner_to_edge_mapping: regression", "[core]") REQUIRE(c2e.size() == c2e_ref.size()); REQUIRE(c2e == c2e_ref); } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_create_mesh.cpp b/modules/core/tests/test_create_mesh.cpp index 0fd73ded..c982c1ec 100644 --- a/modules/core/tests/test_create_mesh.cpp +++ b/modules/core/tests/test_create_mesh.cpp @@ -9,12 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + + #include + #include + #include + #include namespace mesh_io_helper { @@ -109,3 +111,5 @@ TEST_CASE("wrap_with_mesh", "[mesh][io][wrap]") // facet_block.eval()); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_extract_boundary_loops.cpp b/modules/core/tests/test_extract_boundary_loops.cpp index 52ab939f..ae0d2f15 100644 --- a/modules/core/tests/test_extract_boundary_loops.cpp +++ b/modules/core/tests/test_extract_boundary_loops.cpp @@ -9,11 +9,14 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include #include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif + #include #include diff --git a/modules/core/tests/test_extract_submesh.cpp b/modules/core/tests/test_extract_submesh.cpp index cb2e89d2..00e11391 100644 --- a/modules/core/tests/test_extract_submesh.cpp +++ b/modules/core/tests/test_extract_submesh.cpp @@ -21,12 +21,14 @@ #include #include #include +#include #include #include #include #include #include +#include namespace { @@ -213,6 +215,16 @@ TEST_CASE("extract_submesh", "[core][utilities][submesh]") options.source_vertex_attr_name, options.source_facet_attr_name); } + + SECTION("Selected facet out of bound") + { + std::array selected_facets{mesh.get_num_facets()}; + LA_REQUIRE_THROWS( + lagrange::extract_submesh( + mesh, + {selected_facets.data(), selected_facets.size()}, + options)); + } } TEST_CASE("extract_submesh benchmark", "[core][utilities][submesh][!benchmark]") diff --git a/modules/core/tests/test_isoline.cpp b/modules/core/tests/test_isoline.cpp index b7b0a495..fd493d03 100644 --- a/modules/core/tests/test_isoline.cpp +++ b/modules/core/tests/test_isoline.cpp @@ -29,6 +29,8 @@ #include +#include + #include namespace { diff --git a/modules/core/tests/test_marching_triangles.cpp b/modules/core/tests/test_marching_triangles.cpp index 050d0876..c7764386 100644 --- a/modules/core/tests/test_marching_triangles.cpp +++ b/modules/core/tests/test_marching_triangles.cpp @@ -10,19 +10,21 @@ * governing permissions and limitations under the License. */ -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS -#include -#include + #include + #include -#include -#include -#include -#include -#include -#include -#include + #include + #include + + #include + #include + #include + #include + #include + #include + #include namespace { @@ -287,3 +289,5 @@ TEST_CASE("MarchingTriangles_PerimeterOfEllipse", "[marching_triangles]") io::save_mesh("isovalues_mesh.vtk", *mesh); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_mesh.cpp b/modules/core/tests/test_mesh.cpp index cf4f4e1e..d25df14f 100644 --- a/modules/core/tests/test_mesh.cpp +++ b/modules/core/tests/test_mesh.cpp @@ -9,13 +9,15 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include + #include TEST_CASE("MeshCreation", "[Mesh][Creation]") @@ -317,3 +319,5 @@ TEST_CASE("ConnectivityInit_slow", "[Mesh][Connectivity]" LA_SLOW_FLAG LA_CORP_F REQUIRE(!mesh->is_vertex_manifold()); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_mesh_convert.cpp b/modules/core/tests/test_mesh_convert.cpp index f9ea96dd..d5cb2b47 100644 --- a/modules/core/tests/test_mesh_convert.cpp +++ b/modules/core/tests/test_mesh_convert.cpp @@ -9,18 +9,20 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include // Include early so we can explicitly silence warnings from Eigen. // Note: This warning only shows up with GCC 13 when ASan is enabled, even though we include Eigen // headers via -isystem. It seems that with GCC 14+ -isystem also silences -Wmaybe-uninitialized. LA_IGNORE_MAYBE_UNINITIALIZED_START -#include + #include LA_IGNORE_MAYBE_UNINITIALIZED_END -#include -#include -#include + #include + #include + #include // clang-format off #include @@ -28,8 +30,8 @@ LA_IGNORE_MAYBE_UNINITIALIZED_END #include // clang-format on -#include -#include + #include + #include // clang-format off #define LA_LEGACY_MESH_X(mode, data) \ @@ -748,20 +750,20 @@ void test_edge_sort(const std::vector>& edges) TEST_CASE("mesh_convert: to_surface_mesh", "[core][mesh_convert]") { -#define LA_X_to_surface_mesh(MeshType, Scalar, Index) \ - test_to_surface_mesh(); \ - test_to_surface_mesh_empty(); \ - test_to_surface_duplicate(); -#define LA_X_to_surface_mesh_aux(_, MeshType) LA_SURFACE_MESH_X(to_surface_mesh, MeshType) + #define LA_X_to_surface_mesh(MeshType, Scalar, Index) \ + test_to_surface_mesh(); \ + test_to_surface_mesh_empty(); \ + test_to_surface_duplicate(); + #define LA_X_to_surface_mesh_aux(_, MeshType) LA_SURFACE_MESH_X(to_surface_mesh, MeshType) LA_LEGACY_MESH_X(to_surface_mesh_aux, 0) } TEST_CASE("mesh_convert: from_surface_mesh", "[core][mesh_convert]") { -#define LA_X_from_surface_mesh(MeshType, Scalar, Index) \ - test_from_surface_mesh(); \ - test_from_surface_mesh_empty(); -#define LA_X_from_surface_mesh_aux(_, MeshType) LA_SURFACE_MESH_X(from_surface_mesh, MeshType) + #define LA_X_from_surface_mesh(MeshType, Scalar, Index) \ + test_from_surface_mesh(); \ + test_from_surface_mesh_empty(); + #define LA_X_from_surface_mesh_aux(_, MeshType) LA_SURFACE_MESH_X(from_surface_mesh, MeshType) LA_LEGACY_MESH_X(from_surface_mesh_aux, 0) } @@ -785,3 +787,5 @@ TEST_CASE("mesh_convert: fast_edge_sort", "[core][mesh_convert]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_mesh_trait.cpp b/modules/core/tests/test_mesh_trait.cpp index eb793a8a..f7cfdfd1 100644 --- a/modules/core/tests/test_mesh_trait.cpp +++ b/modules/core/tests/test_mesh_trait.cpp @@ -9,10 +9,12 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + + #include + #include TEST_CASE("MeshTrait", "[mesh_trait][mesh]") { @@ -76,3 +78,5 @@ TEST_CASE("MeshTrait", "[mesh_trait][mesh]") assert_is_not_mesh_raw_ptr(12); assert_is_not_mesh_raw_ptr(&vertices); } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_orient_outward.cpp b/modules/core/tests/test_orient_outward.cpp index ee361300..f67aa2d7 100644 --- a/modules/core/tests/test_orient_outward.cpp +++ b/modules/core/tests/test_orient_outward.cpp @@ -26,6 +26,8 @@ #include #include +#include + TEST_CASE("orient_outward", "[mesh][orient]") { using Scalar = double; diff --git a/modules/core/tests/test_quad_to_tri.cpp b/modules/core/tests/test_quad_to_tri.cpp index fe055646..4ece83a1 100644 --- a/modules/core/tests/test_quad_to_tri.cpp +++ b/modules/core/tests/test_quad_to_tri.cpp @@ -9,13 +9,15 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include -#include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + #include + #include + #include + #include + #include namespace lagrange_test_internal { @@ -155,3 +157,5 @@ TEST_CASE("quad_to_tri attribute", "[mesh][quad_to_tri][attribute]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_reorder_mesh_vertices.cpp b/modules/core/tests/test_reorder_mesh_vertices.cpp index f71dd08a..a273aef3 100644 --- a/modules/core/tests/test_reorder_mesh_vertices.cpp +++ b/modules/core/tests/test_reorder_mesh_vertices.cpp @@ -9,15 +9,17 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS // Since test_remove_duplicate_vertices already tests this functionality, // it is only mildly tested here. -#include + #include -#include -#include -#include -#include + #include + #include + #include + #include TEST_CASE("ReorderMeshVertices", "[mesh][reorder_vertices]") { @@ -67,3 +69,5 @@ TEST_CASE("ReorderMeshVertices", "[mesh][reorder_vertices]") LA_REQUIRE_THROWS(reorder_mesh_vertices(*mesh, forward_mapping)); } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_sample_points_on_surface.cpp b/modules/core/tests/test_sample_points_on_surface.cpp index dbe3f1f1..53650b32 100644 --- a/modules/core/tests/test_sample_points_on_surface.cpp +++ b/modules/core/tests/test_sample_points_on_surface.cpp @@ -9,14 +9,16 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include + #include + #include TEST_CASE("SamplePointsOnSurface", "[sample_points_on_surface][triangle_mesh]" LA_SLOW_DEBUG_FLAG) { @@ -344,3 +346,5 @@ TEST_CASE("SamplePointsOnSurface", "[sample_points_on_surface][triangle_mesh]" L // Final todo: check agains SampleViaGrid } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_select_facets_by_normal_similarity.cpp b/modules/core/tests/test_select_facets_by_normal_similarity.cpp index 000055bd..c3ac0445 100644 --- a/modules/core/tests/test_select_facets_by_normal_similarity.cpp +++ b/modules/core/tests/test_select_facets_by_normal_similarity.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace { @@ -306,3 +307,22 @@ TEST_CASE("select_facets_by_normal_similarity", "[select_facets_by_norma { run(); } + +TEST_CASE( + "select_facets_by_normal_similarity: invalid seed facet", + "[select_facets_by_normal_similarity]") +{ + using namespace lagrange; + using Scalar = double; + using Index = uint32_t; + + SurfaceMesh mesh; + mesh.add_vertex({0, 0, 0}); + mesh.add_vertex({1, 0, 0}); + mesh.add_vertex({0, 1, 0}); + mesh.add_triangle(0, 1, 2); + + SelectFacetsByNormalSimilarityOptions options; + const Index seed_id = mesh.get_num_facets(); + LA_REQUIRE_THROWS(lagrange::select_facets_by_normal_similarity(mesh, seed_id, options)); +} diff --git a/modules/core/tests/test_select_facets_in_frustum.cpp b/modules/core/tests/test_select_facets_in_frustum.cpp index c9d38b26..a9d0345b 100644 --- a/modules/core/tests/test_select_facets_in_frustum.cpp +++ b/modules/core/tests/test_select_facets_in_frustum.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include diff --git a/modules/core/tests/test_select_vertices.cpp b/modules/core/tests/test_select_vertices.cpp index ea90d377..1a73d007 100644 --- a/modules/core/tests/test_select_vertices.cpp +++ b/modules/core/tests/test_select_vertices.cpp @@ -9,13 +9,15 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include -#include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + + #include + #include + #include + #include TEST_CASE("select_vertices", "[select_vertices]") @@ -74,3 +76,5 @@ TEST_CASE("select_vertices", "[select_vertices]") } } } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_separate_by_facet_groups.cpp b/modules/core/tests/test_separate_by_facet_groups.cpp index f9caeaec..33854613 100644 --- a/modules/core/tests/test_separate_by_facet_groups.cpp +++ b/modules/core/tests/test_separate_by_facet_groups.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -548,6 +549,37 @@ TEST_CASE( validate_submesh(mesh, result[1], "@sv", "@sf"); } +TEST_CASE("separate_by_facet_groups: out-of-bound group index", "[core][utilities][separate]") +{ + auto mesh = lagrange::testing::create_test_cube(); + const Index num_facets = mesh.get_num_facets(); + + // Claim only 2 groups, but one facet is assigned to group index 2 (out of bound). + std::vector group_ids(num_facets, 0); + group_ids[0] = 2; + + SECTION("span overload") + { + LA_REQUIRE_THROWS( + lagrange::separate_by_facet_groups( + mesh, + size_t{2}, + lagrange::span(group_ids), + lagrange::SeparateByFacetGroupsOptions{})); + } + + SECTION("function_ref overload") + { + LA_REQUIRE_THROWS( + lagrange::separate_by_facet_groups( + mesh, + size_t{2}, + lagrange::function_ref( + [&](Index fi) -> Index { return group_ids[fi]; }), + lagrange::SeparateByFacetGroupsOptions{})); + } +} + TEST_CASE( "separate_by_facet_groups: span overload (auto num_groups)", "[core][utilities][separate]") diff --git a/modules/core/tests/test_unify_index_buffer.cpp b/modules/core/tests/test_unify_index_buffer.cpp index 7ac7ba9c..b1edbcd3 100644 --- a/modules/core/tests/test_unify_index_buffer.cpp +++ b/modules/core/tests/test_unify_index_buffer.cpp @@ -12,13 +12,16 @@ #include #include #include -#include #include #include #include #include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif + #include #include @@ -435,6 +438,7 @@ TEST_CASE("unify_index_buffer", "[attribute][next][unify]") } } +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS SECTION("Ensure all index attribute are unified") { std::unique_ptr legacy = create_cube(); @@ -449,6 +453,7 @@ TEST_CASE("unify_index_buffer", "[attribute][next][unify]") REQUIRE(!AttributeType::IsIndexed); }); } +#endif SECTION("Value attributes") { diff --git a/modules/core/tests/test_utils_chain_edges.cpp b/modules/core/tests/test_utils_chain_edges.cpp index 11cdb10d..de484a53 100644 --- a/modules/core/tests/test_utils_chain_edges.cpp +++ b/modules/core/tests/test_utils_chain_edges.cpp @@ -9,11 +9,16 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include + #include #include -#include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include + #include +#endif + #include #include @@ -200,6 +205,7 @@ TEST_CASE("utils/chain_edges", "[core][utils]") } } +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS TEST_CASE("chain_edges benchmark", "[core][utils][!benchmark]") { LA_IGNORE_DEPRECATION_WARNING_BEGIN @@ -257,3 +263,5 @@ TEST_CASE("chain_edges benchmark", "[core][utils][!benchmark]") }; LA_IGNORE_DEPRECATION_WARNING_END } + +#endif // LAGRANGE_ENABLE_LEGACY_FUNCTIONS diff --git a/modules/core/tests/test_utils_hash.cpp b/modules/core/tests/test_utils_hash.cpp index 64df818d..0f76d73f 100644 --- a/modules/core/tests/test_utils_hash.cpp +++ b/modules/core/tests/test_utils_hash.cpp @@ -13,6 +13,9 @@ #include #include + +#include + #include using namespace lagrange; diff --git a/modules/core/tests/test_utils_range.cpp b/modules/core/tests/test_utils_range.cpp index 08210329..aa6305be 100644 --- a/modules/core/tests/test_utils_range.cpp +++ b/modules/core/tests/test_utils_range.cpp @@ -9,12 +9,16 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include -#include +#include +#include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif + using namespace lagrange; TEST_CASE("range", "[range]") @@ -26,7 +30,9 @@ TEST_CASE("range", "[range]") vertices << 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0; Triangles facets(2, 3); facets << 0, 1, 2, 2, 1, 3; +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS auto mesh = create_mesh(vertices, facets); +#endif std::vector active = {1}; @@ -72,6 +78,7 @@ TEST_CASE("range", "[range]") REQUIRE(sum == 1); } +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS SECTION("") { for (auto f : range(mesh->get_num_facets())) { @@ -81,6 +88,7 @@ TEST_CASE("range", "[range]") REQUIRE(count == 2); REQUIRE(sum == 1); // 0 + 1 } +#endif SECTION("") { @@ -102,6 +110,7 @@ TEST_CASE("range", "[range]") REQUIRE(sum == 1); } +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS SECTION("") { for (auto v : range(mesh->get_num_vertices())) { @@ -111,6 +120,7 @@ TEST_CASE("range", "[range]") REQUIRE(count == 4); REQUIRE(sum == 6); } +#endif SECTION("") { diff --git a/modules/core/tests/test_weld_indexed_attribute.cpp b/modules/core/tests/test_weld_indexed_attribute.cpp index 7cfe6ffc..0b66669a 100644 --- a/modules/core/tests/test_weld_indexed_attribute.cpp +++ b/modules/core/tests/test_weld_indexed_attribute.cpp @@ -24,6 +24,8 @@ #include +#include + TEST_CASE("weld_indexed_attribute", "[core][attribute][surface]") { using namespace lagrange; diff --git a/modules/geodesic/CMakeLists.txt b/modules/geodesic/CMakeLists.txt index ac94e3de..52e1b70a 100644 --- a/modules/geodesic/CMakeLists.txt +++ b/modules/geodesic/CMakeLists.txt @@ -17,8 +17,7 @@ if(LAGRANGE_TOPLEVEL_PROJECT) endif() # 2. dependencies -lagrange_find_package(geometry-central REQUIRED) -target_link_libraries(lagrange_geodesic PUBLIC lagrange::core geometry-central::geometry-central) +target_link_libraries(lagrange_geodesic PUBLIC lagrange::core) # 3. unit tests and examples if(LAGRANGE_UNIT_TESTS) diff --git a/modules/geodesic/extras/geometrycentral/CMakeLists.txt b/modules/geodesic/extras/geometrycentral/CMakeLists.txt new file mode 100644 index 00000000..279dd3ad --- /dev/null +++ b/modules/geodesic/extras/geometrycentral/CMakeLists.txt @@ -0,0 +1,17 @@ +# +# Copyright 2026 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +include(geometry-central) +lagrange_add_module_component(geodesic) +target_link_libraries( + lagrange_geodesic_geometrycentral + PRIVATE geometry-central::geometry-central +) diff --git a/modules/geodesic/src/GeodesicEngineHeat.cpp b/modules/geodesic/extras/geometrycentral/GeodesicEngineHeat.cpp similarity index 100% rename from modules/geodesic/src/GeodesicEngineHeat.cpp rename to modules/geodesic/extras/geometrycentral/GeodesicEngineHeat.cpp diff --git a/modules/geodesic/src/GeodesicEngineMMP.cpp b/modules/geodesic/extras/geometrycentral/GeodesicEngineMMP.cpp similarity index 100% rename from modules/geodesic/src/GeodesicEngineMMP.cpp rename to modules/geodesic/extras/geometrycentral/GeodesicEngineMMP.cpp diff --git a/modules/geodesic/src/geometry_central_utils.h b/modules/geodesic/extras/geometrycentral/geometry_central_utils.h similarity index 100% rename from modules/geodesic/src/geometry_central_utils.h rename to modules/geodesic/extras/geometrycentral/geometry_central_utils.h diff --git a/modules/geodesic/geodesic.md b/modules/geodesic/geodesic.md index 8b04cdff..d40b409e 100644 --- a/modules/geodesic/geodesic.md +++ b/modules/geodesic/geodesic.md @@ -6,5 +6,14 @@ Geodesic Module @defgroup module-geodesic Geodesic Module @brief Geodesic distance computation on meshes. -Multiple geodesic engines (DGPC, heat method, MMP) for computing geodesic -distances over surface meshes. +The base `lagrange::geodesic` target provides the generic and DGPC engines without a +geometry-central dependency. The heat-method and MMP engines are provided by the optional +`geometrycentral` component. + +@code{.cmake} +lagrange_include_module(geodesic COMPONENTS geometrycentral) +target_link_libraries(my_target PRIVATE lagrange::geodesic::geometrycentral) +@endcode + +Including `GeodesicEngineHeat.h` or `GeodesicEngineMMP.h` without linking the component produces a +compile-time error that names the required target. diff --git a/modules/geodesic/include/lagrange/geodesic/GeodesicEngineHeat.h b/modules/geodesic/include/lagrange/geodesic/GeodesicEngineHeat.h index 64d2d0b6..2e407845 100644 --- a/modules/geodesic/include/lagrange/geodesic/GeodesicEngineHeat.h +++ b/modules/geodesic/include/lagrange/geodesic/GeodesicEngineHeat.h @@ -11,6 +11,10 @@ */ #pragma once +#ifndef LAGRANGE_GEODESIC_WITH_GEOMETRYCENTRAL + #error "GeodesicEngineHeat requires the lagrange::geodesic::geometrycentral component" +#endif + #include namespace lagrange::geodesic { diff --git a/modules/geodesic/include/lagrange/geodesic/GeodesicEngineMMP.h b/modules/geodesic/include/lagrange/geodesic/GeodesicEngineMMP.h index ed46e017..d2e0723c 100644 --- a/modules/geodesic/include/lagrange/geodesic/GeodesicEngineMMP.h +++ b/modules/geodesic/include/lagrange/geodesic/GeodesicEngineMMP.h @@ -11,6 +11,10 @@ */ #pragma once +#ifndef LAGRANGE_GEODESIC_WITH_GEOMETRYCENTRAL + #error "GeodesicEngineMMP requires the lagrange::geodesic::geometrycentral component" +#endif + #include namespace lagrange::geodesic { diff --git a/modules/geodesic/python/CMakeLists.txt b/modules/geodesic/python/CMakeLists.txt index 0c3e99dd..1aadd730 100644 --- a/modules/geodesic/python/CMakeLists.txt +++ b/modules/geodesic/python/CMakeLists.txt @@ -10,4 +10,6 @@ # governing permissions and limitations under the License. # +lagrange_include_module(geodesic COMPONENTS geometrycentral) lagrange_add_python_binding() +target_link_libraries(lagrange_python PRIVATE lagrange::geodesic::geometrycentral) diff --git a/modules/geodesic/src/GeodesicEngineDGPC.cpp b/modules/geodesic/src/GeodesicEngineDGPC.cpp index 04dccfc4..c856a601 100644 --- a/modules/geodesic/src/GeodesicEngineDGPC.cpp +++ b/modules/geodesic/src/GeodesicEngineDGPC.cpp @@ -10,6 +10,8 @@ * governing permissions and limitations under the License. */ #include +#include +#include #include #include @@ -19,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -208,7 +211,7 @@ SingleSourceGeodesicResult GeodesicEngineDGPC::single_source_geod la_runtime_assert(total_angle > 0.0); if (!on_boundary) { for (Index i = 0; i < one_ring_size; i++) { - angles[i] = angles[i] / total_angle * 2 * M_PI; + angles[i] = angles[i] / total_angle * 2 * lagrange::internal::pi; } } @@ -221,10 +224,10 @@ SingleSourceGeodesicResult GeodesicEngineDGPC::single_source_geod angle_cumu -= angles[i]; } for (Index i = 0; i < one_ring_size; i++) { - if (angle_cumu > M_PI) { - angle_cumu -= 2 * M_PI; - } else if (angle_cumu < -M_PI) { - angle_cumu += 2 * M_PI; + if (angle_cumu > lagrange::internal::pi) { + angle_cumu -= 2 * lagrange::internal::pi; + } else if (angle_cumu < -lagrange::internal::pi) { + angle_cumu += 2 * lagrange::internal::pi; } geodesic_distance(one_ring_indices[i], 0) = (one_ring_vertices[i] - p).norm(); @@ -335,10 +338,12 @@ SingleSourceGeodesicResult GeodesicEngineDGPC::single_source_geod const HighResScalar& tj = theta(vj, 0); const HighResScalar& tk = theta(vk, 0); - if (std::abs(tk - tj) > M_PI) { + if (std::abs(tk - tj) > lagrange::internal::pi) { theta(vi, 0) = (1.0 - alpha) * tj + alpha * tk + - ((tj < tk) ? (1.0 - alpha) : alpha) * 2 * M_PI; - if (theta(vi, 0) > M_PI) theta(vi, 0) -= 2 * M_PI; + ((tj < tk) ? (1.0 - alpha) : alpha) * 2 * lagrange::internal::pi; + if (theta(vi, 0) > lagrange::internal::pi) { + theta(vi, 0) -= 2 * lagrange::internal::pi; + } } else { theta(vi, 0) = (1.0 - alpha) * tj + alpha * tk; } diff --git a/modules/geodesic/tests/CMakeLists.txt b/modules/geodesic/tests/CMakeLists.txt index b5d6de2e..684e58a1 100644 --- a/modules/geodesic/tests/CMakeLists.txt +++ b/modules/geodesic/tests/CMakeLists.txt @@ -9,7 +9,13 @@ # OF ANY KIND, either express or implied. See the License for the specific language # governing permissions and limitations under the License. # +lagrange_include_module(geodesic COMPONENTS geometrycentral) lagrange_add_test() lagrange_include_modules(primitive) -target_link_libraries(test_lagrange_geodesic PRIVATE lagrange::primitive) +target_link_libraries( + test_lagrange_geodesic + PRIVATE + lagrange::geodesic::geometrycentral + lagrange::primitive +) diff --git a/modules/image/include/lagrange/image/image_filters.h b/modules/image/include/lagrange/image/image_filters.h index 91873d75..c2609301 100644 --- a/modules/image/include/lagrange/image/image_filters.h +++ b/modules/image/include/lagrange/image/image_filters.h @@ -12,6 +12,7 @@ #pragma once #include +#include namespace lagrange { namespace image { @@ -22,56 +23,56 @@ namespace image { * @param[in] size The size of the kernel. * @param[out] kernel The resulting kernel. */ -void make_box_kernel(size_t size, image::ImageView& kernel); +LA_IMAGE_API void make_box_kernel(size_t size, image::ImageView& kernel); /** * Creates a 3 by 3 horizontal Sobel filter kernel. * * @param[out] kernel The resulting kernel. */ -void make_sobelh_kernel(image::ImageView& kernel); +LA_IMAGE_API void make_sobelh_kernel(image::ImageView& kernel); /** * Creates a 3 by 3 vertical Sobel filter kernel. * * @param[out] kernel The resulting kernel. */ -void make_sobelv_kernel(image::ImageView& kernel); +LA_IMAGE_API void make_sobelv_kernel(image::ImageView& kernel); /** * Creates a 3 by 3 approximation of a Gaussian filter kernel. * * @param[out] kernel The resulting kernel. */ -void make_gaussian_kernel(image::ImageView& kernel); +LA_IMAGE_API void make_gaussian_kernel(image::ImageView& kernel); /** * Creates a 3 by 1 horizontal kernel with weights (1, 2, 1) / 4. * * @param[out] kernel The resulting kernel. */ -void make_weighted_avg_xkernel(image::ImageView& kernel); +LA_IMAGE_API void make_weighted_avg_xkernel(image::ImageView& kernel); /** * Creates a 1 by 3 vertical kernel with weights (1, 2, 1) / 4. * * @param[out] kernel The resulting kernel. */ -void make_weighted_avg_ykernel(image::ImageView& kernel); +LA_IMAGE_API void make_weighted_avg_ykernel(image::ImageView& kernel); /** * Creates a 3 by 1 horizontal finite difference kernel with weights (-1, 0, 1). * * @param[out] kernel The resulting kernel. */ -void make_diff_xkernel(image::ImageView& kernel); +LA_IMAGE_API void make_diff_xkernel(image::ImageView& kernel); /** * Creates a 1 by 3 vertical finite difference kernel with weights (-1, 0, 1). * * @param[out] kernel The resulting kernel. */ -void make_diff_ykernel(image::ImageView& kernel); +LA_IMAGE_API void make_diff_ykernel(image::ImageView& kernel); /** * Convolves the given image with the specified kernel, using periodic boundary conditions. @@ -80,7 +81,7 @@ void make_diff_ykernel(image::ImageView& kernel); * @param[in] kernel The kernel. * @param[out] result The result of the convolution. Can be the same as \p image. */ -void convolve( +LA_IMAGE_API void convolve( const image::ImageView& image, const image::ImageView& kernel, image::ImageView& result); @@ -91,7 +92,7 @@ void convolve( * @param[in] image The input image. * @param[out] result The result of the convolution. Can be the same as \p image. */ -void sobel_x(const image::ImageView& image, image::ImageView& result); +LA_IMAGE_API void sobel_x(const image::ImageView& image, image::ImageView& result); /** * Convolves the given image with a vertical Sobel filter. @@ -99,7 +100,7 @@ void sobel_x(const image::ImageView& image, image::ImageView& resu * @param[in] image The input image. * @param[out] result The result of the convolution. Can be the same as \p image. */ -void sobel_y(const image::ImageView& image, image::ImageView& result); +LA_IMAGE_API void sobel_y(const image::ImageView& image, image::ImageView& result); /** * Convolves the given image twice with a horizontal Sobel filter. @@ -107,7 +108,7 @@ void sobel_y(const image::ImageView& image, image::ImageView& resu * @param[in] image The input image. * @param[out] result The result of the convolution. Can be the same as \p image. */ -void image_dxx(const image::ImageView& image, image::ImageView& result); +LA_IMAGE_API void image_dxx(const image::ImageView& image, image::ImageView& result); /** * Convolves the given image twice with a vertical Sobel filter. @@ -115,7 +116,7 @@ void image_dxx(const image::ImageView& image, image::ImageView& re * @param[in] image The input image. * @param[out] result The result of the convolution. Can be the same as \p image. */ -void image_dyy(const image::ImageView& image, image::ImageView& result); +LA_IMAGE_API void image_dyy(const image::ImageView& image, image::ImageView& result); } // namespace image } // namespace lagrange diff --git a/modules/image/include/lagrange/image/image_utils.h b/modules/image/include/lagrange/image/image_utils.h index 2711287b..58ad090d 100644 --- a/modules/image/include/lagrange/image/image_utils.h +++ b/modules/image/include/lagrange/image/image_utils.h @@ -13,11 +13,12 @@ #include #include +#include namespace lagrange { namespace image { -float image_standard_deviation(const image::ImageView& image); +LA_IMAGE_API float image_standard_deviation(const image::ImageView& image); /** * Converts a depth image to a disparity image. @@ -42,7 +43,7 @@ float image_standard_deviation(const image::ImageView& image); * @note If the depth at a pixel is zero or negative, the disparity at that pixel will be set to * zero. Depending on your application, you may want to handle this situation differently. */ -void depth_to_disparity( +LA_IMAGE_API void depth_to_disparity( const image::ImageView& image, float vfov, image::ImageView& result); @@ -55,7 +56,9 @@ void depth_to_disparity( * @param image input image * @param result output image */ -void normalize_max_image(const image::ImageView& image, image::ImageView& result); +LA_IMAGE_API void normalize_max_image( + const image::ImageView& image, + image::ImageView& result); /** * A struct for storing an image histogram. @@ -87,7 +90,8 @@ struct ImageHistogram * @return An ImageHistogram struct containing the histogram counts, bin boundaries, * and the overall minimum and maximum pixel values of the image. */ -ImageHistogram create_image_histogram(const image::ImageView& image, int num_bins); +LA_IMAGE_API ImageHistogram +create_image_histogram(const image::ImageView& image, int num_bins); /** * Computes the Otsu threshold value of a given histogram. @@ -118,7 +122,7 @@ ImageHistogram create_image_histogram(const image::ImageView& image, int * Otsu, N. (1979). A Threshold Selection Method from Gray-Level Histograms. * IEEE Transactions on Systems, Man, and Cybernetics, 9(1), 62–66. doi:10.1109/TSMC.1979.4310076 */ -float compute_otsu_threshold(const ImageHistogram& histogram, float upper_limit); +LA_IMAGE_API float compute_otsu_threshold(const ImageHistogram& histogram, float upper_limit); } // namespace image } // namespace lagrange diff --git a/modules/image_io/include/lagrange/image_io/exr.h b/modules/image_io/include/lagrange/image_io/exr.h index b1b1c36d..dba65f4e 100644 --- a/modules/image_io/include/lagrange/image_io/exr.h +++ b/modules/image_io/include/lagrange/image_io/exr.h @@ -13,13 +13,14 @@ #include #include +#include namespace lagrange { namespace image_io { enum class TinyexrPixelType : unsigned int { uint32, float32, unknown }; -bool load_image_exr( +LA_IMAGE_IO_API bool load_image_exr( const lagrange::fs::path& path, void** out_rgba, int* width, @@ -27,7 +28,7 @@ bool load_image_exr( int* components, TinyexrPixelType* pixeltype); -bool save_image_exr( +LA_IMAGE_IO_API bool save_image_exr( const lagrange::fs::path& path, const void* data, const int width, diff --git a/modules/io/include/lagrange/io/load_mesh.h b/modules/io/include/lagrange/io/load_mesh.h index 4751df43..eaebdb19 100644 --- a/modules/io/include/lagrange/io/load_mesh.h +++ b/modules/io/include/lagrange/io/load_mesh.h @@ -16,10 +16,12 @@ #include #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include namespace lagrange::io { @@ -39,7 +41,7 @@ namespace lagrange::io { template < typename MeshType, std::enable_if_t::value>* = nullptr> -MeshType load_mesh(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh(std::istream& input_stream, const LoadOptions& options = {}); /** * Load a mesh from a file. The loader will be chosen depending on the file extension. @@ -52,7 +54,7 @@ MeshType load_mesh(std::istream& input_stream, const LoadOptions& options = {}); template < typename MeshType, std::enable_if_t::value>* = nullptr> -MeshType load_mesh(const fs::path& filename, const LoadOptions& = {}); +LA_IO_API MeshType load_mesh(const fs::path& filename, const LoadOptions& = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh.impl.h b/modules/io/include/lagrange/io/load_mesh.impl.h index 867d7732..db2d22cd 100644 --- a/modules/io/include/lagrange/io/load_mesh.impl.h +++ b/modules/io/include/lagrange/io/load_mesh.impl.h @@ -13,6 +13,6 @@ // remove this file when we drop support for legacy mesh -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/io/include/lagrange/io/load_mesh_assimp.h b/modules/io/include/lagrange/io/load_mesh_assimp.h index e25eb2f4..0273259b 100644 --- a/modules/io/include/lagrange/io/load_mesh_assimp.h +++ b/modules/io/include/lagrange/io/load_mesh_assimp.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #ifdef LAGRANGE_WITH_ASSIMP #include @@ -18,7 +20,7 @@ #include #include - #ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif @@ -37,7 +39,7 @@ namespace lagrange::io { template < typename MeshType, std::enable_if_t::value>* = nullptr> -MeshType load_mesh_assimp(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_assimp(const fs::path& filename, const LoadOptions& options = {}); /** * Load a mesh using assimp. If the scene contains multiple meshes, they will be combined into one. @@ -50,7 +52,7 @@ MeshType load_mesh_assimp(const fs::path& filename, const LoadOptions& options = template < typename MeshType, std::enable_if_t::value>* = nullptr> -MeshType load_mesh_assimp(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_assimp(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_ext.h b/modules/io/include/lagrange/io/load_mesh_ext.h index 2b53dc39..cadf79c2 100644 --- a/modules/io/include/lagrange/io/load_mesh_ext.h +++ b/modules/io/include/lagrange/io/load_mesh_ext.h @@ -13,6 +13,6 @@ // remove this file when we drop support for legacy mesh -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/io/include/lagrange/io/load_mesh_fbx.h b/modules/io/include/lagrange/io/load_mesh_fbx.h index 503124bc..961f94c4 100644 --- a/modules/io/include/lagrange/io/load_mesh_fbx.h +++ b/modules/io/include/lagrange/io/load_mesh_fbx.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -31,7 +33,7 @@ namespace lagrange::io { * @return Loaded mesh. */ template -MeshType load_mesh_fbx(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_fbx(std::istream& input_stream, const LoadOptions& options = {}); /** * Loads a mesh from a file in fbx format. If the scene contains multiple meshes, they will @@ -45,6 +47,6 @@ MeshType load_mesh_fbx(std::istream& input_stream, const LoadOptions& options = * @return Loaded mesh. */ template -MeshType load_mesh_fbx(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_fbx(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_gltf.h b/modules/io/include/lagrange/io/load_mesh_gltf.h index 881a6306..bcbd266d 100644 --- a/modules/io/include/lagrange/io/load_mesh_gltf.h +++ b/modules/io/include/lagrange/io/load_mesh_gltf.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -31,7 +33,7 @@ namespace lagrange::io { * @return Loaded mesh. */ template -MeshType load_mesh_gltf(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_gltf(std::istream& input_stream, const LoadOptions& options = {}); /** * Loads a mesh from a file in glTF or GLB format. If the scene contains multiple meshes, they will @@ -45,6 +47,6 @@ MeshType load_mesh_gltf(std::istream& input_stream, const LoadOptions& options = * @return Loaded mesh. */ template -MeshType load_mesh_gltf(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_gltf(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_msh.h b/modules/io/include/lagrange/io/load_mesh_msh.h index 337a8090..d2481400 100644 --- a/modules/io/include/lagrange/io/load_mesh_msh.h +++ b/modules/io/include/lagrange/io/load_mesh_msh.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -30,7 +32,7 @@ namespace lagrange::io { * @return Loaded mesh. */ template -MeshType load_mesh_msh(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_msh(std::istream& input_stream, const LoadOptions& options = {}); /** * @overload @@ -47,6 +49,6 @@ MeshType load_mesh_msh(std::istream& input_stream, const LoadOptions& options = * @return Loaded mesh. */ template -MeshType load_mesh_msh(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_msh(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_obj.h b/modules/io/include/lagrange/io/load_mesh_obj.h index 99bc3b92..fd4f5395 100644 --- a/modules/io/include/lagrange/io/load_mesh_obj.h +++ b/modules/io/include/lagrange/io/load_mesh_obj.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -30,7 +32,7 @@ namespace lagrange::io { * @return Loaded mesh. */ template -MeshType load_mesh_obj(std::istream& input_stream_obj, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_obj(std::istream& input_stream_obj, const LoadOptions& options = {}); /** * Loads a mesh from a file in MSH format. @@ -43,6 +45,6 @@ MeshType load_mesh_obj(std::istream& input_stream_obj, const LoadOptions& option * @return Loaded mesh. */ template -MeshType load_mesh_obj(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_obj(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_ply.h b/modules/io/include/lagrange/io/load_mesh_ply.h index ff53f2a5..01f12850 100644 --- a/modules/io/include/lagrange/io/load_mesh_ply.h +++ b/modules/io/include/lagrange/io/load_mesh_ply.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -30,7 +32,7 @@ namespace lagrange::io { * @return Loaded mesh. */ template -MeshType load_mesh_ply(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_ply(std::istream& input_stream, const LoadOptions& options = {}); /** * @overload @@ -45,6 +47,6 @@ MeshType load_mesh_ply(std::istream& input_stream, const LoadOptions& options = * @return Loaded mesh. */ template -MeshType load_mesh_ply(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_ply(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_mesh_stl.h b/modules/io/include/lagrange/io/load_mesh_stl.h index 0e173c0b..bb3f1012 100644 --- a/modules/io/include/lagrange/io/load_mesh_stl.h +++ b/modules/io/include/lagrange/io/load_mesh_stl.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,7 @@ namespace lagrange::io { * @return Loaded mesh. */ template -MeshType load_mesh_stl(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_stl(std::istream& input_stream, const LoadOptions& options = {}); /** * @overload @@ -43,6 +45,6 @@ MeshType load_mesh_stl(std::istream& input_stream, const LoadOptions& options = * @return Loaded mesh. */ template -MeshType load_mesh_stl(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API MeshType load_mesh_stl(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_scene.h b/modules/io/include/lagrange/io/load_scene.h index 1110307f..5873afcb 100644 --- a/modules/io/include/lagrange/io/load_scene.h +++ b/modules/io/include/lagrange/io/load_scene.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include @@ -27,7 +29,7 @@ namespace lagrange::io { * @return A `Scene` object created from the input scene file. */ template -SceneType load_scene(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene(const fs::path& filename, const LoadOptions& options = {}); /** @@ -39,6 +41,6 @@ SceneType load_scene(const fs::path& filename, const LoadOptions& options = {}); * @return A `Scene` object loaded from the input stream. */ template -SceneType load_scene(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_scene_assimp.h b/modules/io/include/lagrange/io/load_scene_assimp.h index 11b0ec24..8bb1445d 100644 --- a/modules/io/include/lagrange/io/load_scene_assimp.h +++ b/modules/io/include/lagrange/io/load_scene_assimp.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #ifdef LAGRANGE_WITH_ASSIMP #include @@ -30,7 +32,7 @@ namespace lagrange::io { * @return loaded scene */ template -SceneType load_scene_assimp(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_assimp(const fs::path& filename, const LoadOptions& options = {}); /** * Load a scene using assimp. @@ -41,7 +43,7 @@ SceneType load_scene_assimp(const fs::path& filename, const LoadOptions& options * @return loaded scene */ template -SceneType load_scene_assimp(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_assimp(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_scene_fbx.h b/modules/io/include/lagrange/io/load_scene_fbx.h index 85f9f469..7fec157d 100644 --- a/modules/io/include/lagrange/io/load_scene_fbx.h +++ b/modules/io/include/lagrange/io/load_scene_fbx.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,7 @@ namespace lagrange::io { * @return loaded scene */ template -SceneType load_scene_fbx(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_fbx(const fs::path& filename, const LoadOptions& options = {}); /** * Load an fbx scene. @@ -39,6 +41,6 @@ SceneType load_scene_fbx(const fs::path& filename, const LoadOptions& options = * @return loaded scene */ template -SceneType load_scene_fbx(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_fbx(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_scene_gltf.h b/modules/io/include/lagrange/io/load_scene_gltf.h index 53bfb1c7..b1377e69 100644 --- a/modules/io/include/lagrange/io/load_scene_gltf.h +++ b/modules/io/include/lagrange/io/load_scene_gltf.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,7 @@ namespace lagrange::io { * @return loaded scene */ template -SceneType load_scene_gltf(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_gltf(const fs::path& filename, const LoadOptions& options = {}); /** * Load a scene using gltf. @@ -39,6 +41,6 @@ SceneType load_scene_gltf(const fs::path& filename, const LoadOptions& options = * @return loaded scene */ template -SceneType load_scene_gltf(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_gltf(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_scene_obj.h b/modules/io/include/lagrange/io/load_scene_obj.h index 6fe2cbb2..28673920 100644 --- a/modules/io/include/lagrange/io/load_scene_obj.h +++ b/modules/io/include/lagrange/io/load_scene_obj.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,7 @@ namespace lagrange::io { * @return loaded scene */ template -SceneType load_scene_obj(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType load_scene_obj(const fs::path& filename, const LoadOptions& options = {}); /** * Loadn obj into a scene. @@ -40,7 +42,7 @@ SceneType load_scene_obj(const fs::path& filename, const LoadOptions& options = * @return loaded scene */ template -SceneType load_scene_obj( +LA_IO_API SceneType load_scene_obj( std::istream& input_stream_obj, std::istream& input_stream_mtl, const LoadOptions& options = {}); diff --git a/modules/io/include/lagrange/io/load_simple_scene.h b/modules/io/include/lagrange/io/load_simple_scene.h index 48d87342..882763c2 100644 --- a/modules/io/include/lagrange/io/load_simple_scene.h +++ b/modules/io/include/lagrange/io/load_simple_scene.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -29,7 +31,7 @@ namespace lagrange::io { * @return A `SimpleScene` object created from the input scene file. */ template -SceneType load_simple_scene(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType load_simple_scene(const fs::path& filename, const LoadOptions& options = {}); /** * Load a simple scene from a stream. @@ -40,6 +42,6 @@ SceneType load_simple_scene(const fs::path& filename, const LoadOptions& options * @return A `SimpleScene` object loaded from the input stream. */ template -SceneType load_simple_scene(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType load_simple_scene(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_simple_scene_assimp.h b/modules/io/include/lagrange/io/load_simple_scene_assimp.h index a1d26fcb..8e840253 100644 --- a/modules/io/include/lagrange/io/load_simple_scene_assimp.h +++ b/modules/io/include/lagrange/io/load_simple_scene_assimp.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #ifdef LAGRANGE_WITH_ASSIMP #include @@ -30,7 +32,8 @@ namespace lagrange::io { * @return loaded scene */ template -SceneType load_simple_scene_assimp(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType +load_simple_scene_assimp(const fs::path& filename, const LoadOptions& options = {}); /** * Load a simple scene using assimp. @@ -41,7 +44,8 @@ SceneType load_simple_scene_assimp(const fs::path& filename, const LoadOptions& * @return loaded scene */ template -SceneType load_simple_scene_assimp(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType +load_simple_scene_assimp(std::istream& input_stream, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_simple_scene_fbx.h b/modules/io/include/lagrange/io/load_simple_scene_fbx.h index 1d853ef8..1e35e3b8 100644 --- a/modules/io/include/lagrange/io/load_simple_scene_fbx.h +++ b/modules/io/include/lagrange/io/load_simple_scene_fbx.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,8 @@ namespace lagrange::io { * @return scene */ template -SceneType load_simple_scene_fbx(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType +load_simple_scene_fbx(std::istream& input_stream, const LoadOptions& options = {}); /** * Load a simple scene from fbx. @@ -39,6 +42,7 @@ SceneType load_simple_scene_fbx(std::istream& input_stream, const LoadOptions& o * @return scene */ template -SceneType load_simple_scene_fbx(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType +load_simple_scene_fbx(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/load_simple_scene_gltf.h b/modules/io/include/lagrange/io/load_simple_scene_gltf.h index 8d824318..2981488e 100644 --- a/modules/io/include/lagrange/io/load_simple_scene_gltf.h +++ b/modules/io/include/lagrange/io/load_simple_scene_gltf.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,8 @@ namespace lagrange::io { * @return scene */ template -SceneType load_simple_scene_gltf(std::istream& input_stream, const LoadOptions& options = {}); +LA_IO_API SceneType +load_simple_scene_gltf(std::istream& input_stream, const LoadOptions& options = {}); /** * Load a simple scene with gltf. @@ -39,6 +42,7 @@ SceneType load_simple_scene_gltf(std::istream& input_stream, const LoadOptions& * @return scene */ template -SceneType load_simple_scene_gltf(const fs::path& filename, const LoadOptions& options = {}); +LA_IO_API SceneType +load_simple_scene_gltf(const fs::path& filename, const LoadOptions& options = {}); } // namespace lagrange::io diff --git a/modules/io/include/lagrange/io/save_mesh.h b/modules/io/include/lagrange/io/save_mesh.h index 764c3503..a8fd5f7c 100644 --- a/modules/io/include/lagrange/io/save_mesh.h +++ b/modules/io/include/lagrange/io/save_mesh.h @@ -15,10 +15,12 @@ #include #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include namespace lagrange::io { @@ -32,7 +34,7 @@ namespace lagrange::io { * @param[in] options Extra options related to saving. */ template -void save_mesh( +LA_IO_API void save_mesh( std::ostream& output_stream, const SurfaceMesh& mesh, FileFormat format, @@ -46,7 +48,7 @@ void save_mesh( * @param[in] options Extra options related to saving. */ template -void save_mesh( +LA_IO_API void save_mesh( const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_mesh_gltf.h b/modules/io/include/lagrange/io/save_mesh_gltf.h index 9406b03d..90d58fbc 100644 --- a/modules/io/include/lagrange/io/save_mesh_gltf.h +++ b/modules/io/include/lagrange/io/save_mesh_gltf.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -30,7 +32,7 @@ namespace lagrange::io { * @tparam Index Mesh index type. */ template -void save_mesh_gltf( +LA_IO_API void save_mesh_gltf( std::ostream& output_stream, const SurfaceMesh& mesh, const SaveOptions& options = {}); @@ -46,7 +48,7 @@ void save_mesh_gltf( * @tparam Index Mesh index type. */ template -void save_mesh_gltf( +LA_IO_API void save_mesh_gltf( const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_mesh_msh.h b/modules/io/include/lagrange/io/save_mesh_msh.h index 0047de52..3efdc094 100644 --- a/modules/io/include/lagrange/io/save_mesh_msh.h +++ b/modules/io/include/lagrange/io/save_mesh_msh.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -31,7 +33,7 @@ namespace lagrange::io { * @tparam Index Mesh index type. */ template -void save_mesh_msh( +LA_IO_API void save_mesh_msh( std::ostream& output_stream, const SurfaceMesh& mesh, const SaveOptions& options = {}); @@ -52,7 +54,7 @@ void save_mesh_msh( * @tparam Index Mesh index type. */ template -void save_mesh_msh( +LA_IO_API void save_mesh_msh( const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_mesh_obj.h b/modules/io/include/lagrange/io/save_mesh_obj.h index a317566d..490af576 100644 --- a/modules/io/include/lagrange/io/save_mesh_obj.h +++ b/modules/io/include/lagrange/io/save_mesh_obj.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -32,7 +34,7 @@ namespace io { /// @tparam Index Mesh index type. /// template -void save_mesh_obj( +LA_IO_API void save_mesh_obj( std::ostream& output_stream, const SurfaceMesh& mesh, const SaveOptions& options = {}); @@ -51,7 +53,7 @@ void save_mesh_obj( /// @tparam Index Mesh index type. /// template -void save_mesh_obj( +LA_IO_API void save_mesh_obj( const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_mesh_ply.h b/modules/io/include/lagrange/io/save_mesh_ply.h index 180126f5..cf6d0768 100644 --- a/modules/io/include/lagrange/io/save_mesh_ply.h +++ b/modules/io/include/lagrange/io/save_mesh_ply.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -30,7 +32,7 @@ namespace lagrange::io { /// @tparam Index Mesh index type. /// template -void save_mesh_ply( +LA_IO_API void save_mesh_ply( std::ostream& output_stream, const SurfaceMesh& mesh, const SaveOptions& options = {}); @@ -48,7 +50,7 @@ void save_mesh_ply( /// @tparam Index Mesh index type. /// template -void save_mesh_ply( +LA_IO_API void save_mesh_ply( const fs::path& filename, const SurfaceMesh& mesh, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_scene.h b/modules/io/include/lagrange/io/save_scene.h index 37c75d91..770b8175 100644 --- a/modules/io/include/lagrange/io/save_scene.h +++ b/modules/io/include/lagrange/io/save_scene.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -27,7 +29,7 @@ namespace lagrange::io { * @param[in] options SaveOptions, check the struct for more details. */ template -void save_scene( +LA_IO_API void save_scene( const fs::path& filename, const scene::Scene& scene, const SaveOptions& options = {}); @@ -41,7 +43,7 @@ void save_scene( * @param[in] options SaveOptions, check the struct for more details. */ template -void save_scene( +LA_IO_API void save_scene( std::ostream& output_stream, const scene::Scene& scene, FileFormat format, diff --git a/modules/io/include/lagrange/io/save_scene_gltf.h b/modules/io/include/lagrange/io/save_scene_gltf.h index 701a494e..5e9c7420 100644 --- a/modules/io/include/lagrange/io/save_scene_gltf.h +++ b/modules/io/include/lagrange/io/save_scene_gltf.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -27,7 +29,7 @@ namespace lagrange::io { * @param options SaveOptions, check the struct for more details. */ template -void save_scene_gltf( +LA_IO_API void save_scene_gltf( std::ostream& output_stream, const scene::Scene& scene, const SaveOptions& options = {}); @@ -40,7 +42,7 @@ void save_scene_gltf( * @param options SaveOptions, check the struct for more details. */ template -void save_scene_gltf( +LA_IO_API void save_scene_gltf( const fs::path& filename, const scene::Scene& scene, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_scene_obj.h b/modules/io/include/lagrange/io/save_scene_obj.h index 6466df17..2158bd2e 100644 --- a/modules/io/include/lagrange/io/save_scene_obj.h +++ b/modules/io/include/lagrange/io/save_scene_obj.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -39,7 +41,7 @@ namespace lagrange::io { * @param options SaveOptions, check the struct for more details. */ template -void save_scene_obj( +LA_IO_API void save_scene_obj( std::ostream& output_stream, const scene::Scene& scene, const SaveOptions& options = {}); @@ -65,7 +67,7 @@ void save_scene_obj( * @param options SaveOptions, check the struct for more details. */ template -void save_scene_obj( +LA_IO_API void save_scene_obj( const fs::path& filename, const scene::Scene& scene, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_simple_scene.h b/modules/io/include/lagrange/io/save_simple_scene.h index 1e415d35..296f3c4c 100644 --- a/modules/io/include/lagrange/io/save_simple_scene.h +++ b/modules/io/include/lagrange/io/save_simple_scene.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -27,7 +29,7 @@ namespace lagrange::io { * @param[in] options SaveOptions, check the struct for more details. */ template -void save_simple_scene( +LA_IO_API void save_simple_scene( const fs::path& filename, const scene::SimpleScene& scene, const SaveOptions& options = {}); @@ -41,7 +43,7 @@ void save_simple_scene( * @param[in] options SaveOptions, check the struct for more details. */ template -void save_simple_scene( +LA_IO_API void save_simple_scene( std::ostream& output_stream, const scene::SimpleScene& scene, FileFormat format, diff --git a/modules/io/include/lagrange/io/save_simple_scene_gltf.h b/modules/io/include/lagrange/io/save_simple_scene_gltf.h index c5113fb6..2fd76b0b 100644 --- a/modules/io/include/lagrange/io/save_simple_scene_gltf.h +++ b/modules/io/include/lagrange/io/save_simple_scene_gltf.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -27,7 +29,7 @@ namespace lagrange::io { * @param options SaveOptions, check the struct for more details. */ template -void save_simple_scene_gltf( +LA_IO_API void save_simple_scene_gltf( std::ostream& output_stream, const scene::SimpleScene& scene, const SaveOptions& options = {}); @@ -40,7 +42,7 @@ void save_simple_scene_gltf( * @param options SaveOptions, check the struct for more details. */ template -void save_simple_scene_gltf( +LA_IO_API void save_simple_scene_gltf( const fs::path& filename, const scene::SimpleScene& scene, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/save_simple_scene_obj.h b/modules/io/include/lagrange/io/save_simple_scene_obj.h index 975236d2..a03b3b1e 100644 --- a/modules/io/include/lagrange/io/save_simple_scene_obj.h +++ b/modules/io/include/lagrange/io/save_simple_scene_obj.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -28,7 +30,7 @@ namespace lagrange::io { * @param options SaveOptions, check the struct for more details. */ template -void save_simple_scene_obj( +LA_IO_API void save_simple_scene_obj( std::ostream& output_stream, const scene::SimpleScene& scene, const SaveOptions& options = {}); @@ -42,7 +44,7 @@ void save_simple_scene_obj( * @param options SaveOptions, check the struct for more details. */ template -void save_simple_scene_obj( +LA_IO_API void save_simple_scene_obj( const fs::path& filename, const scene::SimpleScene& scene, const SaveOptions& options = {}); diff --git a/modules/io/include/lagrange/io/types.h b/modules/io/include/lagrange/io/types.h index da27e13a..2bf55565 100644 --- a/modules/io/include/lagrange/io/types.h +++ b/modules/io/include/lagrange/io/types.h @@ -122,6 +122,10 @@ struct LoadOptions /// Load external images bool load_images = true; + /// Load line elements (e.g. OBJ polylines). When present, they are added as 2-vertex facets + /// and a `line_id` facet attribute is created. + bool load_lines = true; + /// Stitch duplicate boundary vertices together when loading file. When loading a glTF mesh with /// attributes, or when loading STL files, the loader will warn the users about unwelded /// vertices in the loaded mesh, unless silenced by setting `quiet` to true. diff --git a/modules/io/js/src/io.cpp b/modules/io/js/src/io.cpp index bbdad148..292b4e45 100644 --- a/modules/io/js/src/io.cpp +++ b/modules/io/js/src/io.cpp @@ -84,6 +84,7 @@ void parse_load_opts(const emscripten::val& opts, io::LoadOptions& o) apply_opt(opts, "loadVertexColors", o.load_vertex_colors); apply_opt(opts, "loadObjectIds", o.load_object_ids); apply_opt(opts, "loadImages", o.load_images); + apply_opt(opts, "loadLines", o.load_lines); apply_opt(opts, "stitchVertices", o.stitch_vertices); apply_opt(opts, "quiet", o.quiet); } diff --git a/modules/io/js/ts/io.ts b/modules/io/js/ts/io.ts index e5898148..3f6dc6cc 100644 --- a/modules/io/js/ts/io.ts +++ b/modules/io/js/ts/io.ts @@ -49,6 +49,8 @@ export interface LoadOptions { loadObjectIds?: boolean; /** Resolve and decode referenced image data. Default: `true`. */ loadImages?: boolean; + /** Import line elements (e.g. OBJ polylines) as 2-vertex facets with a `line_id` attribute. Default: `true`. */ + loadLines?: boolean; /** Merge coincident boundary vertices while loading. Default: `false`. */ stitchVertices?: boolean; /** Suppress warnings printed to the console. Default: `false`. */ diff --git a/modules/io/python/src/io.cpp b/modules/io/python/src/io.cpp index 8f8eb3bc..f6ec9640 100644 --- a/modules/io/python/src/io.cpp +++ b/modules/io/python/src/io.cpp @@ -77,6 +77,10 @@ void populate_io_module(nb::module_& m) "load_object_ids", &io::LoadOptions::load_object_ids, "Load object ids as facet attribute") + .def_rw( + "load_lines", + &io::LoadOptions::load_lines, + "Load line elements (e.g. OBJ polylines) as 2-vertex facets") .def_rw( "search_path", &io::LoadOptions::search_path, @@ -197,7 +201,8 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl bool load_images, bool stitch_vertices, bool quiet, - std::optional search_path) { + std::optional search_path, + bool load_lines) { io::LoadOptions opts; opts.triangulate = triangulate; opts.load_normals = load_normals; @@ -211,6 +216,7 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl opts.stitch_vertices = stitch_vertices; opts.quiet = quiet; if (search_path.has_value()) opts.search_path = search_path.value(); + opts.load_lines = load_lines; return io::load_mesh(filename, opts); }, "filename"_a, @@ -226,6 +232,7 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl "stitch_vertices"_a = io::LoadOptions().stitch_vertices, "quiet"_a = io::LoadOptions().quiet, "search_path"_a = nb::none(), + "load_lines"_a = io::LoadOptions().load_lines, R"(Load mesh from a file. :param filename: The input file name. @@ -241,6 +248,7 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl :param stitch_vertices: Whether to stitch boundary vertices based on position. Defaults to False. :param quiet: Whether to silence warnings during loading. Defaults to False. :param search_path: Search path for external references (e.g. .mtl, .bin, etc.). Defaults to "". +:param load_lines: Whether to load line elements (e.g. OBJ polylines) as 2-vertex facets with a `line_id` facet attribute. Defaults to True. :return SurfaceMesh: The mesh object loaded from the file.)"); @@ -258,7 +266,8 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl bool load_images, bool stitch_vertices, bool quiet, - std::optional search_path) { + std::optional search_path, + bool load_lines) { io::LoadOptions opts; opts.triangulate = triangulate; opts.load_normals = load_normals; @@ -272,6 +281,7 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl opts.stitch_vertices = stitch_vertices; opts.quiet = quiet; if (search_path.has_value()) opts.search_path = search_path.value(); + opts.load_lines = load_lines; return io::load_simple_scene(filename, opts); }, "filename"_a, @@ -287,6 +297,7 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl "stitch_vertices"_a = io::LoadOptions().stitch_vertices, "quiet"_a = io::LoadOptions().quiet, "search_path"_a = nb::none(), + "load_lines"_a = io::LoadOptions().load_lines, R"(Load a simple scene from file. :param filename: The input file name. @@ -302,6 +313,7 @@ Filename extension determines the file format. Supported formats are: `obj`, `pl :param stitch_vertices: Whether to stitch boundary vertices based on position. Defaults to False. :param quiet: Whether to silence warnings during loading. Defaults to False. :param search_path: Search path for external references (e.g. .mtl, .bin, etc.). Defaults to "". +:param load_lines: Whether to load line elements (e.g. OBJ polylines) as 2-vertex facets with a `line_id` facet attribute. Defaults to True. :return SimpleScene: The scene object loaded from the file.)"); @@ -424,7 +436,8 @@ The binary string should use one of the supported formats. Supported formats inc bool load_images, bool stitch_vertices, bool quiet, - std::optional search_path) { + std::optional search_path, + bool load_lines) { io::LoadOptions opts; opts.triangulate = triangulate; opts.load_normals = load_normals; @@ -438,6 +451,7 @@ The binary string should use one of the supported formats. Supported formats inc opts.stitch_vertices = stitch_vertices; opts.quiet = quiet; if (search_path.has_value()) opts.search_path = search_path.value(); + opts.load_lines = load_lines; return io::load_scene(filename, opts); }, "filename"_a, @@ -453,6 +467,7 @@ The binary string should use one of the supported formats. Supported formats inc "stitch_vertices"_a = io::LoadOptions().stitch_vertices, "quiet"_a = io::LoadOptions().quiet, "search_path"_a = nb::none(), + "load_lines"_a = io::LoadOptions().load_lines, R"(Load a scene. :param filename: The input file name. @@ -468,6 +483,7 @@ The binary string should use one of the supported formats. Supported formats inc :param stitch_vertices: Whether to stitch boundary vertices based on position. Defaults to False. :param quiet: Whether to silence warnings during loading. Defaults to False. :param search_path: Search path for external references (e.g. .mtl, .bin, etc.). Defaults to "". +:param load_lines: Whether to load line elements (e.g. OBJ polylines) as 2-vertex facets with a `line_id` facet attribute. Defaults to True. :return Scene: The loaded scene object.)"); diff --git a/modules/io/src/legacy_load_mesh.cpp b/modules/io/src/legacy_load_mesh.cpp index 37f0cf3d..8c618fa9 100644 --- a/modules/io/src/legacy_load_mesh.cpp +++ b/modules/io/src/legacy_load_mesh.cpp @@ -9,8 +9,10 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#include -#include + +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include namespace lagrange::io { LAGRANGE_LEGACY_INLINE @@ -33,3 +35,5 @@ template LA_IO_API std::unique_ptr> load_ } // namespace legacy } // namespace lagrange::io + +#endif diff --git a/modules/io/src/load_mesh_stl.cpp b/modules/io/src/load_mesh_stl.cpp index 4747ce2e..17b9fc31 100644 --- a/modules/io/src/load_mesh_stl.cpp +++ b/modules/io/src/load_mesh_stl.cpp @@ -20,6 +20,11 @@ #include "stitch_mesh.h" +#include +#include +#include +#include + namespace lagrange::io { namespace { diff --git a/modules/io/src/load_obj.cpp b/modules/io/src/load_obj.cpp index 7c94ae4f..076d9bf8 100644 --- a/modules/io/src/load_obj.cpp +++ b/modules/io/src/load_obj.cpp @@ -177,19 +177,24 @@ ObjReaderResult extract_mes shape.mesh.num_face_vertices.end()); num_facets_per_shape.push_back(static_cast(shape.mesh.num_face_vertices.size())); Index num_line_segments = 0; - for (auto nv : shape.lines.num_line_vertices) { - la_runtime_assert(nv >= 2, "Line element must have at least 2 vertices"); - num_line_segments += static_cast(nv - 1); + if (options.load_lines) { + for (auto nv : shape.lines.num_line_vertices) { + la_runtime_assert(nv >= 2, "Line element must have at least 2 vertices"); + num_line_segments += static_cast(nv - 1); + } } num_segments_per_shape.push_back(num_line_segments); - num_polylines_per_shape.push_back(static_cast(shape.lines.num_line_vertices.size())); + num_polylines_per_shape.push_back( + options.load_lines ? static_cast(shape.lines.num_line_vertices.size()) : 0); if (num_line_segments > 0) has_any_lines = true; result.names.push_back(shape.name); } // Second pass: append all line segment facet sizes (after all faces) - for (const auto& shape : shapes) { - for (auto nv : shape.lines.num_line_vertices) { - facet_sizes.insert(facet_sizes.end(), nv - 1, 2); + if (options.load_lines) { + for (const auto& shape : shapes) { + for (auto nv : shape.lines.num_line_vertices) { + facet_sizes.insert(facet_sizes.end(), nv - 1, 2); + } } } if (!facet_sizes.empty()) { @@ -303,7 +308,7 @@ ObjReaderResult extract_mes } // Copy line element indices as 2-vertex facets - if (!shape.lines.indices.empty()) { + if (options.load_lines && !shape.lines.indices.empty()) { const Index first_line_facet = total_facets + (i == 0 ? 0 : num_segments_per_shape[i - 1]); const Index first_polyline_id = (i == 0 ? 0 : num_polylines_per_shape[i - 1]); diff --git a/modules/io/tests/test_obj.cpp b/modules/io/tests/test_obj.cpp index 0f778a68..603726c5 100644 --- a/modules/io/tests/test_obj.cpp +++ b/modules/io/tests/test_obj.cpp @@ -531,6 +531,24 @@ l 8 9 REQUIRE(seg2[1] == 8); } + SECTION("load_lines=false skips line elements") + { + std::istringstream input(obj_data); + io::LoadOptions load_options; + load_options.load_lines = false; + auto mesh = io::load_mesh_obj(input, load_options); + testing::check_mesh(mesh); + + // Only the 2 face facets remain; the 3 line segments are skipped + REQUIRE(mesh.get_num_vertices() == 9); + REQUIRE(mesh.get_num_facets() == 2); + REQUIRE(mesh.get_facet_size(0) == 3); + REQUIRE(mesh.get_facet_size(1) == 3); + + // No line_id attribute is created when lines are not loaded + REQUIRE_FALSE(mesh.has_attribute(AttributeName::line_id)); + } + SECTION("roundtrip preserves polylines") { std::istringstream input(obj_data); diff --git a/modules/packing/include/lagrange/packing/compute_rectangle_packing.h b/modules/packing/include/lagrange/packing/compute_rectangle_packing.h index 608b100a..67eb619d 100644 --- a/modules/packing/include/lagrange/packing/compute_rectangle_packing.h +++ b/modules/packing/include/lagrange/packing/compute_rectangle_packing.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/packing/include/lagrange/packing/repack_uv_charts.h b/modules/packing/include/lagrange/packing/repack_uv_charts.h index 02ff0424..77b901e2 100644 --- a/modules/packing/include/lagrange/packing/repack_uv_charts.h +++ b/modules/packing/include/lagrange/packing/repack_uv_charts.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include @@ -53,6 +55,8 @@ struct RepackOptions /// @param[in] options The packing options. /// template -void repack_uv_charts(SurfaceMesh& mesh, const RepackOptions& options = {}); +LA_PACKING_API void repack_uv_charts( + SurfaceMesh& mesh, + const RepackOptions& options = {}); } // namespace lagrange::packing diff --git a/modules/poisson/include/lagrange/poisson/AttributeEvaluator.h b/modules/poisson/include/lagrange/poisson/AttributeEvaluator.h index 11891030..136ef69b 100644 --- a/modules/poisson/include/lagrange/poisson/AttributeEvaluator.h +++ b/modules/poisson/include/lagrange/poisson/AttributeEvaluator.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -51,14 +52,14 @@ class AttributeEvaluator /// @tparam Index Point cloud index type. /// template - AttributeEvaluator( + LA_POISSON_API AttributeEvaluator( const SurfaceMesh& points, const EvaluatorOptions& options = {}); /// /// Destroys the object. /// - ~AttributeEvaluator(); + LA_POISSON_API ~AttributeEvaluator(); /// /// Evaluate the extrapolated attribute at any point in 3D space. @@ -70,7 +71,7 @@ class AttributeEvaluator /// @tparam ValueType Attribute value type. /// template - void eval(span pos, span out) const; + LA_POISSON_API void eval(span pos, span out) const; private: struct Impl; diff --git a/modules/poisson/include/lagrange/poisson/mesh_from_oriented_points.h b/modules/poisson/include/lagrange/poisson/mesh_from_oriented_points.h index ead0731e..1cbe6268 100644 --- a/modules/poisson/include/lagrange/poisson/mesh_from_oriented_points.h +++ b/modules/poisson/include/lagrange/poisson/mesh_from_oriented_points.h @@ -13,6 +13,7 @@ #include #include +#include #include @@ -61,7 +62,7 @@ struct ReconstructionOptions : public CommonOptions /// @return Reconstructed triangle mesh. /// template -SurfaceMesh mesh_from_oriented_points( +LA_POISSON_API SurfaceMesh mesh_from_oriented_points( const SurfaceMesh& points, const ReconstructionOptions& options = {}); diff --git a/modules/poisson/src/AttributeEvaluator.cpp b/modules/poisson/src/AttributeEvaluator.cpp index 703d15f9..49cd615e 100644 --- a/modules/poisson/src/AttributeEvaluator.cpp +++ b/modules/poisson/src/AttributeEvaluator.cpp @@ -204,14 +204,16 @@ void AttributeEvaluator::eval(span pos, span out) const } } -#define LA_X_attribute_evaluator(_, Scalar, Index) \ - template AttributeEvaluator::AttributeEvaluator( \ - const SurfaceMesh& points, \ +#define LA_X_attribute_evaluator(_, Scalar, Index) \ + template LA_POISSON_API AttributeEvaluator::AttributeEvaluator( \ + const SurfaceMesh& points, \ const EvaluatorOptions& options); LA_SURFACE_MESH_X(attribute_evaluator, 0) -#define LA_X_eval_func(ValueType, Scalar) \ - template void AttributeEvaluator::eval(span pos, span out) const; +#define LA_X_eval_func(ValueType, Scalar) \ + template LA_POISSON_API void AttributeEvaluator::eval( \ + span pos, \ + span out) const; #define LA_X_eval_aux(_, ValueType) LA_SURFACE_MESH_SCALAR_X(eval_func, ValueType) LA_ATTRIBUTE_SCALAR_X(eval_aux, 0) diff --git a/modules/poisson/src/mesh_from_oriented_points.cpp b/modules/poisson/src/mesh_from_oriented_points.cpp index ed9c16c1..5f075fa8 100644 --- a/modules/poisson/src/mesh_from_oriented_points.cpp +++ b/modules/poisson/src/mesh_from_oriented_points.cpp @@ -621,9 +621,9 @@ SurfaceMesh mesh_from_oriented_points( } } -#define LA_X_mesh_reconstruction(_, Scalar, Index) \ - template SurfaceMesh mesh_from_oriented_points( \ - const SurfaceMesh& points, \ +#define LA_X_mesh_reconstruction(_, Scalar, Index) \ + template LA_POISSON_API SurfaceMesh mesh_from_oriented_points( \ + const SurfaceMesh& points, \ const ReconstructionOptions& options); LA_SURFACE_MESH_X(mesh_reconstruction, 0) diff --git a/modules/primitive/include/lagrange/primitive/SweepPath.h b/modules/primitive/include/lagrange/primitive/SweepPath.h index 6eed3a06..eac2c78a 100644 --- a/modules/primitive/include/lagrange/primitive/SweepPath.h +++ b/modules/primitive/include/lagrange/primitive/SweepPath.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/primitive/include/lagrange/primitive/generate_disc.h b/modules/primitive/include/lagrange/primitive/generate_disc.h index 836ad5e3..f00f0fbe 100644 --- a/modules/primitive/include/lagrange/primitive/generate_disc.h +++ b/modules/primitive/include/lagrange/primitive/generate_disc.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include #include #include @@ -62,7 +64,7 @@ struct DiscOptions : public PrimitiveOptions /// @return A `SurfaceMesh` object representing the generated disc. /// template -SurfaceMesh generate_disc(DiscOptions setting); +LA_PRIMITIVE_API SurfaceMesh generate_disc(DiscOptions setting); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_hexahedron.h b/modules/primitive/include/lagrange/primitive/generate_hexahedron.h index e64cec76..53f983a7 100644 --- a/modules/primitive/include/lagrange/primitive/generate_hexahedron.h +++ b/modules/primitive/include/lagrange/primitive/generate_hexahedron.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/primitive/include/lagrange/primitive/generate_icosahedron.h b/modules/primitive/include/lagrange/primitive/generate_icosahedron.h index 789a36f5..4805953d 100644 --- a/modules/primitive/include/lagrange/primitive/generate_icosahedron.h +++ b/modules/primitive/include/lagrange/primitive/generate_icosahedron.h @@ -11,6 +11,8 @@ */ #pragma once +#include + #include namespace lagrange::primitive { @@ -57,7 +59,7 @@ struct IcosahedronOptions : public PrimitiveOptions /// all parameters are within valid bounds before generation. /// template -SurfaceMesh generate_icosahedron(IcosahedronOptions settings); +LA_PRIMITIVE_API SurfaceMesh generate_icosahedron(IcosahedronOptions settings); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_octahedron.h b/modules/primitive/include/lagrange/primitive/generate_octahedron.h index 6831bc9e..3cab78ec 100644 --- a/modules/primitive/include/lagrange/primitive/generate_octahedron.h +++ b/modules/primitive/include/lagrange/primitive/generate_octahedron.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include namespace lagrange::primitive { @@ -61,7 +63,7 @@ struct OctahedronOptions : public PrimitiveOptions /// all parameters are within valid bounds before generation. /// template -SurfaceMesh generate_octahedron(OctahedronOptions settings); +LA_PRIMITIVE_API SurfaceMesh generate_octahedron(OctahedronOptions settings); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_rounded_cone.h b/modules/primitive/include/lagrange/primitive/generate_rounded_cone.h index 9b8196ab..a34f6394 100644 --- a/modules/primitive/include/lagrange/primitive/generate_rounded_cone.h +++ b/modules/primitive/include/lagrange/primitive/generate_rounded_cone.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include #include @@ -173,7 +175,7 @@ struct RoundedConeOptions : public PrimitiveOptions /// When radius_top is 0, the result is a traditional cone. /// template -SurfaceMesh generate_rounded_cone(RoundedConeOptions setting); +LA_PRIMITIVE_API SurfaceMesh generate_rounded_cone(RoundedConeOptions setting); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_rounded_cube.h b/modules/primitive/include/lagrange/primitive/generate_rounded_cube.h index f7882150..ed4cdcaa 100644 --- a/modules/primitive/include/lagrange/primitive/generate_rounded_cube.h +++ b/modules/primitive/include/lagrange/primitive/generate_rounded_cube.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include @@ -115,7 +117,7 @@ struct RoundedCubeOptions : public PrimitiveOptions * all parameters are within valid bounds before generation. */ template -SurfaceMesh generate_rounded_cube(RoundedCubeOptions setting); +LA_PRIMITIVE_API SurfaceMesh generate_rounded_cube(RoundedCubeOptions setting); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_rounded_cylinder.h b/modules/primitive/include/lagrange/primitive/generate_rounded_cylinder.h index ede4e8cd..b0dcb0df 100644 --- a/modules/primitive/include/lagrange/primitive/generate_rounded_cylinder.h +++ b/modules/primitive/include/lagrange/primitive/generate_rounded_cylinder.h @@ -11,7 +11,7 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/primitive/include/lagrange/primitive/generate_rounded_plane.h b/modules/primitive/include/lagrange/primitive/generate_rounded_plane.h index 2cca23d5..c8ddb9e3 100644 --- a/modules/primitive/include/lagrange/primitive/generate_rounded_plane.h +++ b/modules/primitive/include/lagrange/primitive/generate_rounded_plane.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include @@ -121,7 +123,7 @@ struct RoundedPlaneOptions : public PrimitiveOptions /// @see @ref RoundedPlaneOptions for detailed parameter descriptions. /// template -SurfaceMesh generate_rounded_plane(RoundedPlaneOptions settings); +LA_PRIMITIVE_API SurfaceMesh generate_rounded_plane(RoundedPlaneOptions settings); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_sphere.h b/modules/primitive/include/lagrange/primitive/generate_sphere.h index f42eaec5..e9d22e82 100644 --- a/modules/primitive/include/lagrange/primitive/generate_sphere.h +++ b/modules/primitive/include/lagrange/primitive/generate_sphere.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include #include @@ -72,7 +74,7 @@ struct SphereOptions : public PrimitiveOptions /// all parameters are within valid bounds before generation. /// template -SurfaceMesh generate_sphere(SphereOptions setting); +LA_PRIMITIVE_API SurfaceMesh generate_sphere(SphereOptions setting); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generate_subdivided_sphere.h b/modules/primitive/include/lagrange/primitive/generate_subdivided_sphere.h index b95bd154..47b6de96 100644 --- a/modules/primitive/include/lagrange/primitive/generate_subdivided_sphere.h +++ b/modules/primitive/include/lagrange/primitive/generate_subdivided_sphere.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include namespace lagrange::primitive { @@ -64,7 +66,7 @@ struct SubdividedSphereOptions : public PrimitiveOptions /// @return A surface mesh representing the subdivided sphere. /// template -SurfaceMesh generate_subdivided_sphere( +LA_PRIMITIVE_API SurfaceMesh generate_subdivided_sphere( const SurfaceMesh& base_shape, SubdividedSphereOptions setting); diff --git a/modules/primitive/include/lagrange/primitive/generate_swept_surface.h b/modules/primitive/include/lagrange/primitive/generate_swept_surface.h index b527b4e8..bcc72cba 100644 --- a/modules/primitive/include/lagrange/primitive/generate_swept_surface.h +++ b/modules/primitive/include/lagrange/primitive/generate_swept_surface.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include #include @@ -62,7 +64,7 @@ struct SweptSurfaceOptions : public PrimitiveOptions /// @return A SurfaceMesh representing the swept surface. /// template -SurfaceMesh generate_swept_surface( +LA_PRIMITIVE_API SurfaceMesh generate_swept_surface( span profile, const SweepOptions& sweep_setting, const SweptSurfaceOptions& options = {}); diff --git a/modules/primitive/include/lagrange/primitive/generate_torus.h b/modules/primitive/include/lagrange/primitive/generate_torus.h index c30ff36c..80770250 100644 --- a/modules/primitive/include/lagrange/primitive/generate_torus.h +++ b/modules/primitive/include/lagrange/primitive/generate_torus.h @@ -11,10 +11,12 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include #include @@ -96,7 +98,7 @@ struct TorusOptions : public PrimitiveOptions /// all parameters are within valid bounds before generation. /// template -SurfaceMesh generate_torus(TorusOptions setting); +LA_PRIMITIVE_API SurfaceMesh generate_torus(TorusOptions setting); /// @} diff --git a/modules/primitive/include/lagrange/primitive/generation_utils.h b/modules/primitive/include/lagrange/primitive/generation_utils.h index 563a66ee..86983ca4 100644 --- a/modules/primitive/include/lagrange/primitive/generation_utils.h +++ b/modules/primitive/include/lagrange/primitive/generation_utils.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/primitive/src/generate_swept_surface.cpp b/modules/primitive/src/generate_swept_surface.cpp index 563759c3..0ab2bbd7 100644 --- a/modules/primitive/src/generate_swept_surface.cpp +++ b/modules/primitive/src/generate_swept_surface.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -19,7 +20,10 @@ #include #include #include +#include +#include #include +#include #include #include diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt index 501bb9dc..6861c4d9 100644 --- a/modules/python/CMakeLists.txt +++ b/modules/python/CMakeLists.txt @@ -22,8 +22,7 @@ endif() # 1. define module include(nanobind) lagrange_find_package(TBB CONFIG REQUIRED) -nanobind_add_module(lagrange_python NB_STATIC) -set_target_properties(nanobind-static PROPERTIES FOLDER third_party) +nanobind_add_module(lagrange_python BACKEND_MODULE nanobind_backend) add_library(lagrange::python ALIAS lagrange_python) set_target_properties(lagrange_python PROPERTIES FOLDER "${LAGRANGE_IDE_PREFIX}Lagrange/Modules" diff --git a/modules/raycasting/include/lagrange/raycasting/ClosestPointResult.h b/modules/raycasting/include/lagrange/raycasting/ClosestPointResult.h index 64597987..6d6e54a5 100644 --- a/modules/raycasting/include/lagrange/raycasting/ClosestPointResult.h +++ b/modules/raycasting/include/lagrange/raycasting/ClosestPointResult.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/EmbreeHelper.h b/modules/raycasting/include/lagrange/raycasting/EmbreeHelper.h index fe405adf..61561b8e 100644 --- a/modules/raycasting/include/lagrange/raycasting/EmbreeHelper.h +++ b/modules/raycasting/include/lagrange/raycasting/EmbreeHelper.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h b/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h index 350a4e20..4a712729 100644 --- a/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h +++ b/modules/raycasting/include/lagrange/raycasting/EmbreeRayCaster.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/RayCasterMesh.h b/modules/raycasting/include/lagrange/raycasting/RayCasterMesh.h index 4e06d22c..a9f9850e 100644 --- a/modules/raycasting/include/lagrange/raycasting/RayCasterMesh.h +++ b/modules/raycasting/include/lagrange/raycasting/RayCasterMesh.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/create_ray_caster.h b/modules/raycasting/include/lagrange/raycasting/create_ray_caster.h index 7d65cdfa..1c33a9e5 100644 --- a/modules/raycasting/include/lagrange/raycasting/create_ray_caster.h +++ b/modules/raycasting/include/lagrange/raycasting/create_ray_caster.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h b/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h index 3b12c0af..d23d4049 100644 --- a/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h +++ b/modules/raycasting/include/lagrange/raycasting/embree_closest_point.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/project_attributes.h b/modules/raycasting/include/lagrange/raycasting/project_attributes.h index 7aad8cb1..671a5da5 100644 --- a/modules/raycasting/include/lagrange/raycasting/project_attributes.h +++ b/modules/raycasting/include/lagrange/raycasting/project_attributes.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/project_attributes_closest_point.h b/modules/raycasting/include/lagrange/raycasting/project_attributes_closest_point.h index 83c4c97f..7f2cd74e 100644 --- a/modules/raycasting/include/lagrange/raycasting/project_attributes_closest_point.h +++ b/modules/raycasting/include/lagrange/raycasting/project_attributes_closest_point.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/project_attributes_directional.h b/modules/raycasting/include/lagrange/raycasting/project_attributes_directional.h index edbaad88..a5eb0f19 100644 --- a/modules/raycasting/include/lagrange/raycasting/project_attributes_directional.h +++ b/modules/raycasting/include/lagrange/raycasting/project_attributes_directional.h @@ -11,6 +11,6 @@ */ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/project_options.h b/modules/raycasting/include/lagrange/raycasting/project_options.h index a23694ef..49a2864e 100644 --- a/modules/raycasting/include/lagrange/raycasting/project_options.h +++ b/modules/raycasting/include/lagrange/raycasting/project_options.h @@ -12,6 +12,6 @@ #pragma once // Deprecated: include instead. -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/include/lagrange/raycasting/project_particles_directional.h b/modules/raycasting/include/lagrange/raycasting/project_particles_directional.h index 727e9560..fb5edb5d 100644 --- a/modules/raycasting/include/lagrange/raycasting/project_particles_directional.h +++ b/modules/raycasting/include/lagrange/raycasting/project_particles_directional.h @@ -12,6 +12,6 @@ #pragma once // Deprecated. Use project_directional instead. -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/raycasting/src/compute_local_feature_size.cpp b/modules/raycasting/src/compute_local_feature_size.cpp index cf1b36d3..633e7f28 100644 --- a/modules/raycasting/src/compute_local_feature_size.cpp +++ b/modules/raycasting/src/compute_local_feature_size.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +#include #include #include "closest_vertex_from_barycentric.h" diff --git a/modules/raycasting/src/legacy/EmbreeHelper.cpp b/modules/raycasting/src/legacy/EmbreeHelper.cpp index 9ad36768..64def7cf 100644 --- a/modules/raycasting/src/legacy/EmbreeHelper.cpp +++ b/modules/raycasting/src/legacy/EmbreeHelper.cpp @@ -9,19 +9,21 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -#ifdef LAGRANGE_WITH_EMBREE_3 - #include -#else - #include -#endif -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #ifdef LAGRANGE_WITH_EMBREE_3 + #include + #else + #include + #endif + + #include + #include -#include -#include -#include -#include + #include + #include + #include + #include RTC_NAMESPACE_USE @@ -41,3 +43,5 @@ void lagrange::raycasting::legacy::EmbreeHelper::ensure_no_errors(const RTCDevic } throw std::runtime_error(err_msg.str()); } + +#endif diff --git a/modules/scene/python/src/bind_value.h b/modules/scene/python/src/bind_value.h index 900d30ad..455d1364 100644 --- a/modules/scene/python/src/bind_value.h +++ b/modules/scene/python/src/bind_value.h @@ -24,7 +24,7 @@ struct type_caster NB_TYPE_CASTER(lagrange::scene::Value, const_name("int | float | str | list | dict | bool")); template - bool try_cast(const handle& src, uint8_t flags, cleanup_list* cleanup) + bool try_cast(const handle& src, uint32_t flags, cleanup_list* cleanup) { using CasterT = make_caster; @@ -34,7 +34,7 @@ struct type_caster return true; } - bool from_python(handle src, uint8_t flags, cleanup_list* cleanup) noexcept + bool from_python(handle src, uint32_t flags, cleanup_list* cleanup) noexcept { if (PyNumber_Check(src.ptr())) { lagrange::logger().debug("Number!"); @@ -48,7 +48,7 @@ struct type_caster size_t n; PyObject* temp; /* Will initialize 'temp' (NULL in the case of a failure.) */ - PyObject** o = seq_get(src.ptr(), &n, &temp); + PyObject** o = NB_CALL(seq_get)(src.ptr(), &n, &temp); bool success = o != nullptr; diff --git a/modules/subdivision/include/lagrange/subdivision/compute_sharpness.h b/modules/subdivision/include/lagrange/subdivision/compute_sharpness.h index e6238239..23356b34 100644 --- a/modules/subdivision/include/lagrange/subdivision/compute_sharpness.h +++ b/modules/subdivision/include/lagrange/subdivision/compute_sharpness.h @@ -12,6 +12,8 @@ #pragma once +#include + #include #include @@ -78,9 +80,8 @@ struct SharpnessOptions /// @return Normal, edge and vertex sharpness attribute ids. /// template -SharpnessResults compute_sharpness( - SurfaceMesh& mesh, - const SharpnessOptions& options = {}); +LA_SUBDIVISION_API SharpnessResults +compute_sharpness(SurfaceMesh& mesh, const SharpnessOptions& options = {}); /// @} diff --git a/modules/subdivision/include/lagrange/subdivision/mesh_subdivision.h b/modules/subdivision/include/lagrange/subdivision/mesh_subdivision.h index 19988112..46c3e304 100644 --- a/modules/subdivision/include/lagrange/subdivision/mesh_subdivision.h +++ b/modules/subdivision/include/lagrange/subdivision/mesh_subdivision.h @@ -12,10 +12,12 @@ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include #include @@ -332,7 +334,7 @@ struct SubdivisionOptions /// @return Subdivided mesh. /// template -SurfaceMesh subdivide_mesh( +LA_SUBDIVISION_API SurfaceMesh subdivide_mesh( const SurfaceMesh& mesh, const SubdivisionOptions& options = {}); diff --git a/modules/subdivision/include/lagrange/subdivision/midpoint_subdivision.h b/modules/subdivision/include/lagrange/subdivision/midpoint_subdivision.h index 2ff31a4f..2d20b33b 100644 --- a/modules/subdivision/include/lagrange/subdivision/midpoint_subdivision.h +++ b/modules/subdivision/include/lagrange/subdivision/midpoint_subdivision.h @@ -12,10 +12,12 @@ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include namespace lagrange::subdivision { @@ -36,7 +38,8 @@ namespace lagrange::subdivision { /// @return Subdivided mesh. /// template -SurfaceMesh midpoint_subdivision(const SurfaceMesh& mesh); +LA_SUBDIVISION_API SurfaceMesh midpoint_subdivision( + const SurfaceMesh& mesh); /// @} diff --git a/modules/subdivision/include/lagrange/subdivision/sqrt_subdivision.h b/modules/subdivision/include/lagrange/subdivision/sqrt_subdivision.h index 1f2ea98b..1e4b7e17 100644 --- a/modules/subdivision/include/lagrange/subdivision/sqrt_subdivision.h +++ b/modules/subdivision/include/lagrange/subdivision/sqrt_subdivision.h @@ -12,10 +12,12 @@ #pragma once -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif +#include + #include namespace lagrange::subdivision { @@ -40,7 +42,8 @@ namespace lagrange::subdivision { /// @return Subdivided mesh. /// template -SurfaceMesh sqrt_subdivision(const SurfaceMesh& mesh); +LA_SUBDIVISION_API SurfaceMesh sqrt_subdivision( + const SurfaceMesh& mesh); /// @} diff --git a/modules/subdivision/src/compute_sharpness.cpp b/modules/subdivision/src/compute_sharpness.cpp index 575f3156..964644f7 100644 --- a/modules/subdivision/src/compute_sharpness.cpp +++ b/modules/subdivision/src/compute_sharpness.cpp @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +#include #include #include diff --git a/modules/subdivision/src/midpoint_subdivision.cpp b/modules/subdivision/src/midpoint_subdivision.cpp index 27264c4b..471e3adf 100644 --- a/modules/subdivision/src/midpoint_subdivision.cpp +++ b/modules/subdivision/src/midpoint_subdivision.cpp @@ -11,6 +11,7 @@ */ #include #include +#include #include #include diff --git a/modules/subdivision/src/subdivide_adaptive.cpp b/modules/subdivision/src/subdivide_adaptive.cpp index ee72c936..8042c2d2 100644 --- a/modules/subdivision/src/subdivide_adaptive.cpp +++ b/modules/subdivision/src/subdivide_adaptive.cpp @@ -9,8 +9,13 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +#include #include #include +#include +#include +#include +#include #include "MeshConverter.h" diff --git a/modules/subdivision/src/subdivide_uniform.cpp b/modules/subdivision/src/subdivide_uniform.cpp index 7e62eb77..6727bedc 100644 --- a/modules/subdivision/src/subdivide_uniform.cpp +++ b/modules/subdivision/src/subdivide_uniform.cpp @@ -9,8 +9,12 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +#include #include #include +#include +#include +#include #include #include diff --git a/modules/testing/include/lagrange/testing/common.h b/modules/testing/include/lagrange/testing/common.h index 04095d53..b91e9175 100644 --- a/modules/testing/include/lagrange/testing/common.h +++ b/modules/testing/include/lagrange/testing/common.h @@ -19,8 +19,10 @@ #include #include -#include #include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include +#endif #include #include @@ -137,6 +139,7 @@ LA_TESTING_API fs::path get_test_output_path(const fs::path& relative_path); /// /// @return A unique_ptr to the newly allocated mesh. /// +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS template std::unique_ptr load_mesh(const fs::path& relative_path) { @@ -146,6 +149,7 @@ std::unique_ptr load_mesh(const fs::path& relative_path) } extern template LA_TESTING_API std::unique_ptr load_mesh(const fs::path&); extern template LA_TESTING_API std::unique_ptr load_mesh(const fs::path&); +#endif /// /// Load a mesh from test data directory as a `SurfaceMesh`. diff --git a/modules/testing/include/lagrange/testing/require_approx.h b/modules/testing/include/lagrange/testing/require_approx.h index 7d5d6dd9..bca27555 100644 --- a/modules/testing/include/lagrange/testing/require_approx.h +++ b/modules/testing/include/lagrange/testing/require_approx.h @@ -13,7 +13,7 @@ #pragma once #include - +#include #include namespace lagrange::testing { diff --git a/modules/testing/src/common.cpp b/modules/testing/src/common.cpp index 6987811f..bed8bdfb 100644 --- a/modules/testing/src/common.cpp +++ b/modules/testing/src/common.cpp @@ -16,8 +16,10 @@ #include -#include -#include +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS + #include + #include +#endif #include @@ -138,8 +140,10 @@ fs::path get_test_output_path(const fs::path& relative_path) return absolute_path; } +#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS template std::unique_ptr load_mesh(const fs::path&); template std::unique_ptr load_mesh(const fs::path&); +#endif void setup_mkl_reproducibility() { diff --git a/modules/texproc/examples/CMakeLists.txt b/modules/texproc/examples/CMakeLists.txt index 2b9b1843..209111ea 100644 --- a/modules/texproc/examples/CMakeLists.txt +++ b/modules/texproc/examples/CMakeLists.txt @@ -22,7 +22,7 @@ lagrange_add_example(texture_filtering texture_filtering.cpp) target_link_libraries(texture_filtering lagrange::texproc CLI11::CLI11 lagrange::io lagrange::image_io) lagrange_add_example(texture_compositing texture_compositing.cpp) -target_link_libraries(texture_compositing lagrange::texproc CLI11::CLI11 lagrange::io lagrange::image_io) +target_link_libraries(texture_compositing lagrange::texproc CLI11::CLI11 lagrange::io lagrange::image_io lagrange::polyscope) lagrange_add_example(texture_rasterization texture_rasterization.cpp) target_link_libraries(texture_rasterization lagrange::texproc CLI11::CLI11 lagrange::io lagrange::image_io) diff --git a/modules/texproc/examples/texture_compositing.cpp b/modules/texproc/examples/texture_compositing.cpp index 5d2c0316..7db924dc 100644 --- a/modules/texproc/examples/texture_compositing.cpp +++ b/modules/texproc/examples/texture_compositing.cpp @@ -9,16 +9,52 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +/* + + ./examples/Release/texture_rasterization \ + --scene-in ../data/corp/texproc/prepared/pumpkin.glb \ + --renders-in ../data/corp/texproc/prepared/view_*.png \ + --width 1024 --height 1024 --base-confidence 0 + ./examples/Release/texture_compositing \ + --mesh-in ../data/corp/texproc/prepared/pumpkin.glb \ + --textures-in output_textures_*.exr \ + --weights-in output_weights_*.exr \ + --output composed.exr + +*/ #include "../shared/shared_utils.h" #include "io_helpers.h" +#include #include +#include +#include +#include +#include #include +#include #include +#include +#include #include +#include + +// clang-format off +#include +#include +#include +#include +// clang-format on + +#include +#include #include +#include +#include + namespace fs = lagrange::fs; template @@ -33,6 +69,260 @@ lagrange::image::experimental::View3D extract_channel( std::tuple{channel, 1}); } +// ============================================================================ +// GUI helpers +// ============================================================================ + +namespace { + +/// Convert a 3-channel Array3Df image to polyscope scan-line color data. +/// Layout: extent(0)=W, extent(1)=H, extent(2)>=3. +/// Polyscope expects data in row-major order: outer loop over H (extent(1)), +/// inner loop over W (extent(0)), matching addTextureColorQuantity(H, W, ...). +std::vector texture_to_colors(const Array3Df& tex) +{ + const size_t W = tex.extent(0); + const size_t H = tex.extent(1); + std::vector colors; + colors.reserve(W * H); + for (size_t jj = 0; jj < H; ++jj) { + for (size_t ii = 0; ii < W; ++ii) { + colors.emplace_back(tex(ii, jj, 0), tex(ii, jj, 1), tex(ii, jj, 2)); + } + } + return colors; +} + +/// Prepare a mesh for Polyscope display: +/// - Unify non-UV indexed attributes into a shared index buffer. +/// - Convert indexed UV attributes to per-corner attributes. +void prepare_mesh_for_display(lagrange::SurfaceMesh32f& mesh) +{ + lagrange::AttributeMatcher matcher; + matcher.element_types = lagrange::AttributeElement::Indexed; + + matcher.usages = ~lagrange::BitField(lagrange::AttributeUsage::UV); + auto ids = lagrange::find_matching_attributes(mesh, matcher); + if (!ids.empty()) { + mesh = lagrange::unify_index_buffer(mesh, ids); + } + + matcher.usages = lagrange::AttributeUsage::UV; + ids = lagrange::find_matching_attributes(mesh, matcher); + for (auto id : ids) { + lagrange::map_attribute_in_place(mesh, id, lagrange::AttributeElement::Corner); + } +} + +struct ImgSize +{ + size_t W, H; +}; + +struct DemoState +{ + lagrange::SurfaceMesh32f mesh; // display mesh (unified index buffer, corner UVs) + std::string mesh_name; + std::vector textures; // input texture images (3-channel float) + std::vector weights; // input weight images (1+ channel float) + Array3Df result; // composited output (3-channel float) + + // Pre-computed pixel buffers kept in memory for fast combo-box switching. + // Indices [0..N-1] = input textures, [N] = result. + std::vector> all_colors; + std::vector color_sizes; + // Indices [0..M-1] = weight maps. + std::vector> all_scalars; + std::vector scalar_sizes; + + // Current combo-box selection. + int color_idx = 0; + int scalar_idx = 0; + + // Polyscope handles (set during register_view, valid for the lifetime of the GUI). + polyscope::SurfaceMesh* ps_mesh = nullptr; + polyscope::SurfaceParameterizationQuantity* ps_uv = nullptr; +}; + +/// Push new color data into the single "color_view" texture quantity. +void update_color_quantity(DemoState& state) +{ + if (state.color_idx < 0) return; + const auto color_idx = static_cast(state.color_idx); + if (color_idx >= state.color_sizes.size()) return; + const auto [W, H] = state.color_sizes[color_idx]; + if (state.ps_mesh->getQuantity("color_view")) state.ps_mesh->removeQuantity("color_view"); + auto* q = state.ps_mesh->addTextureColorQuantity( + "color_view", + *state.ps_uv, + H, + W, + state.all_colors[color_idx], + polyscope::ImageOrigin::UpperLeft); + q->setFilterMode(polyscope::FilterMode::Nearest); + q->setEnabled(true); + if (auto* sq = state.ps_mesh->getQuantity("scalar_view")) sq->setEnabled(false); +} + +/// Push new scalar data into the single "scalar_view" texture quantity. +void update_scalar_quantity(DemoState& state) +{ + if (state.scalar_idx < 0) return; + const auto scalar_idx = static_cast(state.scalar_idx); + if (scalar_idx >= state.scalar_sizes.size()) return; + const auto [W, H] = state.scalar_sizes[scalar_idx]; + if (state.ps_mesh->getQuantity("scalar_view")) state.ps_mesh->removeQuantity("scalar_view"); + auto* q = state.ps_mesh->addTextureScalarQuantity( + "scalar_view", + *state.ps_uv, + H, + W, + state.all_scalars[scalar_idx], + polyscope::ImageOrigin::UpperLeft); + q->setColorMap("viridis"); + q->setEnabled(true); + if (auto* cq = state.ps_mesh->getQuantity("color_view")) cq->setEnabled(false); +} + +/// Register the mesh and exactly two texture quantities (one color, one scalar). +/// All pixel arrays are pre-computed here for instant combo-box switching later. +void register_view(DemoState& state) +{ + polyscope::removeAllStructures(); + + state.ps_mesh = lagrange::polyscope::register_mesh(state.mesh_name, state.mesh); + la_runtime_assert(state.ps_mesh != nullptr, "Failed to register mesh with Polyscope"); + + // Extract corner UV values for the parameterization quantity. + // After prepare_mesh_for_display, the UV attribute is a per-corner attribute. + auto uv_id = lagrange::find_matching_attribute(state.mesh, lagrange::AttributeUsage::UV); + la_runtime_assert(uv_id.has_value(), "No UV attribute found — mesh must have UVs for GUI"); + auto& uv_attr = state.mesh.get_attribute(*uv_id); + auto uv_mat = lagrange::matrix_view(uv_attr); + std::vector uv_vec; + uv_vec.reserve(state.mesh.get_num_corners()); + for (size_t c = 0; c < state.mesh.get_num_corners(); ++c) { + uv_vec.push_back({uv_mat(c, 0), uv_mat(c, 1)}); + } + state.ps_uv = state.ps_mesh->addParameterizationQuantity("uv", uv_vec); + la_runtime_assert(state.ps_uv != nullptr, "Failed to register UV parameterization"); + state.ps_uv->setEnabled(true); + + // Pre-compute color pixel buffers: input textures first, then result. + state.all_colors.clear(); + state.color_sizes.clear(); + for (const auto& tex : state.textures) { + la_runtime_assert(tex.extent(2) >= 3, "Input texture must have at least 3 channels"); + state.all_colors.push_back(texture_to_colors(tex)); + state.color_sizes.push_back({tex.extent(0), tex.extent(1)}); + } + la_runtime_assert(state.result.extent(2) >= 3, "Result must have at least 3 channels"); + state.all_colors.push_back(texture_to_colors(state.result)); + state.color_sizes.push_back({state.result.extent(0), state.result.extent(1)}); + + // Pre-compute scalar pixel buffers for each weight map. + state.all_scalars.clear(); + state.scalar_sizes.clear(); + for (const auto& wt : state.weights) { + std::vector scalars; + scalars.reserve(wt.extent(0) * wt.extent(1)); + for (size_t jj = 0; jj < wt.extent(1); ++jj) { + for (size_t ii = 0; ii < wt.extent(0); ++ii) { + scalars.push_back(wt(ii, jj, 0)); + } + } + state.all_scalars.push_back(std::move(scalars)); + state.scalar_sizes.push_back({wt.extent(0), wt.extent(1)}); + } + + // Default: show the composited result. Register the scalar quantity (disabled) so the combo + // can toggle to it without recreating the quantity, then enable the color view last so the + // colored result is visible on open. + state.color_idx = static_cast(state.textures.size()); // last entry = result + state.scalar_idx = 0; + + update_scalar_quantity(state); // registers scalar, then gets disabled below + update_color_quantity(state); // enables color, disables scalar +} + +void user_callback(DemoState& state) +{ + ImGui::Text( + "Mesh: %d vertices, %d facets", + static_cast(state.mesh.get_num_vertices()), + static_cast(state.mesh.get_num_facets())); + + const auto num_textures = state.textures.size(); + la_runtime_assert(num_textures == state.weights.size()); + const auto texture_width = num_textures > 0 ? state.textures.front().extent(0) : 0; + const auto texture_height = num_textures > 0 ? state.textures.front().extent(1) : 0; + ImGui::Text( + "Inputs: %d pairs (%dx%d)", + static_cast(num_textures), + static_cast(texture_width), + static_cast(texture_height)); + + ImGui::Separator(); + + for (const auto kk : lagrange::range(num_textures)) { + const auto& weight = state.weights[kk]; + + float weight_min = std::numeric_limits::max(); + float weight_max = std::numeric_limits::lowest(); + float weight_mean = 0.0f; + for (const auto ii : lagrange::range(weight.extent(0))) { + for (const auto jj : lagrange::range(weight.extent(1))) { + const float v = weight(ii, jj, 0); + weight_min = std::min(weight_min, v); + weight_max = std::max(weight_max, v); + weight_mean += v; + } + } + weight_mean /= static_cast(weight.extent(0) * weight.extent(1)); + + ImGui::PushID(static_cast(kk)); + + ImGui::Text( + "[%02zu]: min %.3f mean %.3f max %.3f", + kk, + weight_min, + weight_mean, + weight_max); + ImGui::SameLine(); + + if (ImGui::Button("texture")) { + lagrange::logger().info("Switching to texture {}", kk); + state.color_idx = static_cast(kk); + update_color_quantity(state); + } + ImGui::SameLine(); + + if (ImGui::Button("weight")) { + lagrange::logger().info("Switching to weight {}", kk); + state.scalar_idx = static_cast(kk); + update_scalar_quantity(state); + } + + ImGui::PopID(); + } + + ImGui::Separator(); + + ImGui::Text( + "Result: %dx%d", + static_cast(state.result.extent(0)), + static_cast(state.result.extent(1))); + ImGui::SameLine(); + + if (ImGui::Button("texture")) { + lagrange::logger().info("Switching to output texture"); + state.color_idx = static_cast(num_textures); + update_color_quantity(state); + } +} + +} // namespace + int main(int argc, char** argv) { struct @@ -40,11 +330,19 @@ int main(int argc, char** argv) fs::path input_mesh; std::vector input_textures; std::vector input_weights; - fs::path output_texture = "output.exr"; + fs::path output_path = "output.exr"; + bool gui = false; + bool sanity_check = false; int log_level = 2; } args; lagrange::texproc::CompositingOptions compositing_options; + const auto named_gradient_normalizations = + std::map{ + {"per-edge", lagrange::texproc::GradientNormalization::PerEdge}, + {"per-texel-sqrt", lagrange::texproc::GradientNormalization::PerTexelSqrt}, + }; + CLI::App app{argv[0]}; app.option_defaults()->always_capture_default(); app.add_option("--mesh-in", args.input_mesh, "Input mesh with UVs.") @@ -56,7 +354,7 @@ int main(int argc, char** argv) app.add_option("--weights-in", args.input_weights, "Input weights images.") ->required() ->check(CLI::ExistingFile); - app.add_option("--texture-out", args.output_texture, "Output texture."); + app.add_option("--output", args.output_path, "Output file (texture or .glb/.gltf scene)."); app.add_option( "--value-weight", compositing_options.value_weight, @@ -69,18 +367,49 @@ int main(int argc, char** argv) "--jitter-epsilon", compositing_options.jitter_epsilon, "Random jitter amount (0 if no jittering)."); + app.add_option( + "--gradient-normalization", + compositing_options.gradient_normalization, + "How to normalize per-view gradient contributions (per-edge | per-texel-sqrt).") + ->transform(CLI::CheckedTransformer(named_gradient_normalizations, CLI::ignore_case)); + app.add_flag( + "--direct-solver,!--no-direct-solver", + compositing_options.use_direct_solver, + "Use direct LDLT solver instead of multigrid v-cycle."); + app.add_option( + "--regularization", + compositing_options.stiffness_regularization_weight, + "Laplacian regularization weight for the stiffness matrix."); app.add_flag( "--smooth-low-weight-areas", compositing_options.smooth_low_weight_areas, - "Smooth pixels with low total weight (< 1)."); + "Smooth pixels with low total weight (< 1) when using PerTexelSqrt normalization."); app.add_option( "--clamp", compositing_options.clamp_to_range, "Clamp out-of-range texels to the given range."); + app.add_option( + "--num-multigrid-levels", + compositing_options.solver.num_multigrid_levels, + "Number of multigrid levels (ignored when using the direct solver)."); + app.add_option( + "--num-gauss-seidel-iterations", + compositing_options.solver.num_gauss_seidel_iterations, + "Number of Gauss-Seidel iterations per multigrid level, must be even (ignored when " + "using the direct solver)."); + app.add_option( + "--num-v-cycles", + compositing_options.solver.num_v_cycles, + "Number of V-cycles to perform (ignored when using the direct solver)."); + app.add_flag("--gui", args.gui, "Launch Polyscope GUI to visualize inputs and result."); + app.add_flag("--sanity-check", args.sanity_check, "Enable solver sanity checks."); app.add_option("-l,--level", args.log_level, "Log level (0 = most verbose, 6 = off)."); CLI11_PARSE(app, argc, argv) spdlog::set_level(static_cast(args.log_level)); + if (args.sanity_check) { + compositing_options.sanity_check = true; + } // Sort input textures and weights sort_paths(args.input_textures); @@ -95,25 +424,102 @@ int main(int argc, char** argv) std::vector textures; std::vector weights; std::vector> weighted_textures; - la_runtime_assert( - args.input_textures.size() == args.input_weights.size(), - "Number of textures and weights images must be the same"); - for (size_t i = 0; i < args.input_textures.size(); ++i) { - textures.emplace_back(load_image(args.input_textures[i])); - weights.emplace_back(load_image(args.input_weights[i])); + la_runtime_assert(args.input_textures.size() == args.input_weights.size()); + for (const auto ii : lagrange::range(args.input_textures.size())) { + textures.emplace_back(load_image(args.input_textures[ii])); + weights.emplace_back(load_image(args.input_weights[ii])); weighted_textures.push_back({ textures.back().to_mdspan(), extract_channel(weights.back().to_mdspan(), 0), }); } + lagrange::VerboseTimer timer("Texture compositing", nullptr, spdlog::level::level_enum::info); + + { + const auto gradient_normalization_name = [&]() { + for (const auto& [name, value] : named_gradient_normalizations) { + if (value == compositing_options.gradient_normalization) { + return name; + } + } + return std::string{"unknown"}; + }(); + + const auto clamp_to_range_name = [&]() { + if (compositing_options.clamp_to_range.has_value()) { + const auto [min, max] = *compositing_options.clamp_to_range; + return lagrange::format("[{:1.3f}, {:1.3f}]", min, max); + } else { + return std::string{"disabled"}; + } + }(); + + auto& logger = lagrange::logger(); + logger.debug("value_weight {:1.3e}", compositing_options.value_weight); + logger.debug("quadrature_samples {}", compositing_options.quadrature_samples); + logger.debug("jitter_epsilon {:1.3e}", compositing_options.jitter_epsilon); + logger.debug("clamp_to_range {}", clamp_to_range_name); + logger.debug("gradient_normalization {}", gradient_normalization_name); + logger.debug("use_direct_solver {}", compositing_options.use_direct_solver); + logger.debug( + "stiffness_regularization_weight {:1.3e}", + compositing_options.stiffness_regularization_weight); + logger.debug("smooth_low_weight_areas {}", compositing_options.smooth_low_weight_areas); + if (!compositing_options.use_direct_solver) { + logger.debug( + "num_multigrid_levels {}", + compositing_options.solver.num_multigrid_levels); + logger.debug( + "num_gauss_seidel_iterations {}", + compositing_options.solver.num_gauss_seidel_iterations); + logger.debug("num_v_cycles {}", compositing_options.solver.num_v_cycles); + } + } + timer.tick(); - auto image = + auto output_texture = lagrange::texproc::texture_compositing(mesh, weighted_textures, compositing_options); timer.tock(); - lagrange::logger().info("Saving result: {}", args.output_texture.string()); - save_image(args.output_texture, image.to_mdspan()); + if (!args.output_path.empty()) { + auto extension = args.output_path.extension().string(); + if (extension == ".glb" || extension == ".gltf") { + lagrange::logger().info("Saving scene: {}", args.output_path.string()); + auto scene = + lagrange::scene::internal::single_mesh_to_scene(mesh, output_texture.to_mdspan()); + lagrange::io::SaveOptions save_options; + save_options.encoding = lagrange::io::FileEncoding::Binary; + save_options.embed_images = true; + save_options.attribute_conversion_policy = + lagrange::io::SaveOptions::AttributeConversionPolicy::ConvertAsNeeded; + lagrange::io::save_scene(args.output_path, scene, save_options); + } else { + lagrange::logger().info("Saving texture: {}", args.output_path.string()); + save_image(args.output_path, output_texture.to_mdspan()); + } + } + + if (args.gui) { + polyscope::init(); + + DemoState state; + state.mesh = std::move(mesh); + state.mesh_name = args.input_mesh.stem().string(); + state.textures = std::move(textures); + state.weights = std::move(weights); + state.result = std::move(output_texture); + prepare_mesh_for_display(state.mesh); + register_view(state); + + polyscope::options::configureImGuiStyleCallback = []() { + ImGui::Spectrum::StyleColorsSpectrum(); + ImGui::Spectrum::LoadFont(); + }; + polyscope::state::userCallback = [&state]() { user_callback(state); }; + + polyscope::show(); + } return 0; } diff --git a/modules/texproc/include/lagrange/texproc/TextureRasterizer.h b/modules/texproc/include/lagrange/texproc/TextureRasterizer.h index 2d0d0c0f..720b4a63 100644 --- a/modules/texproc/include/lagrange/texproc/TextureRasterizer.h +++ b/modules/texproc/include/lagrange/texproc/TextureRasterizer.h @@ -36,6 +36,11 @@ struct TextureRasterizerOptions /// /// Ideally we'd use raytracing to determine if a texel is visible. But for simplicity we just /// compare the texel depth with depth map value at the screen location. + /// + /// For perspective cameras this is a relative tolerance, + /// a texel at depth z is considered visible when z < depth * (1 + depth_precision). + /// For orthographic cameras this is an absolute tolerance, + /// a texel is considered visible when z < depth + depth_precision. double depth_precision = 1e-3; /// Texture width. diff --git a/modules/texproc/include/lagrange/texproc/texture_compositing.h b/modules/texproc/include/lagrange/texproc/texture_compositing.h index 69b317b3..ecddc062 100644 --- a/modules/texproc/include/lagrange/texproc/texture_compositing.h +++ b/modules/texproc/include/lagrange/texproc/texture_compositing.h @@ -24,6 +24,23 @@ namespace lagrange::texproc { /// @addtogroup module-texproc /// @{ +/// +/// How per-view gradient contributions are normalized when assembling the edge-difference target. +/// +enum class GradientNormalization { + /// Per-edge weighted average of per-view gradients: + /// edge_target = sum_i(grad_i * w_i(n1) * w_i(n2)) / sum_i(w_i(n1) * w_i(n2)) + /// Matches the ShapeGradientDomain reference. Produces gradient targets bounded by the range + /// of per-view gradients (no overshoot from the gradient term alone). + PerEdge, + + /// Per-texel normalization with geometric-mean combination (legacy behavior): + /// edge_target = sum_i(grad_i * sqrt(w_i(n1)/Σw(n1) * w_i(n2)/Σw(n2))) + /// The gradient weights do not sum to 1 per edge at view boundaries, which attenuates + /// gradient targets in transition regions. + PerTexelSqrt, +}; + /// /// Options for texture compositing. /// @@ -41,22 +58,39 @@ struct CompositingOptions /// Clamp out-of-range texels to the given range (nullopt to disable). std::optional> clamp_to_range = std::nullopt; - /// Whether to smooth pixels with a low total weight (< 1). When enabled, this will not dampen - /// the gradient terms for pixels with a low total weight, resulting in a smoother texture in - /// low-confidence areas. + /// How per-view gradient contributions are normalized into the edge-difference target. + GradientNormalization gradient_normalization = GradientNormalization::PerTexelSqrt; + + /// Use a direct (LDLT) solver instead of the multigrid v-cycle. + /// The direct solver is exact, but uses more memory. + /// The multigrid solver is approximate, uses less memory and runs faster. + bool use_direct_solver = false; + + /// Weight for combinatorial Laplacian regularization added to the stiffness matrix. + /// Helps stabilize the system in poorly conditioned regions. + /// Set to 0 to disable. + double stiffness_regularization_weight = 1e-9; + + /// When using GradientNormalization::PerTexelSqrt, whether to smooth pixels with a low total + /// weight (< 1). When enabled, this will dampen the gradient terms for pixels with a low + /// total weight, resulting in a smoother texture in low-confidence areas. bool smooth_low_weight_areas = false; - /// Multigrid solver options. + /// Whether to run solver sanity checks. If unset, defaults to true in debug builds and false in + /// release builds. + std::optional sanity_check = std::nullopt; + + /// Multigrid solver options (ignored when use_direct_solver is true). struct SolverOptions { /// Number of multigrid levels. unsigned int num_multigrid_levels = 4; /// Number of Gauss-Seidel iterations per multigrid level. - unsigned int num_gauss_seidel_iterations = 3; + unsigned int num_gauss_seidel_iterations = 2; /// Number of V-cycles to perform. - unsigned int num_v_cycles = 5; + unsigned int num_v_cycles = 4; } solver; }; diff --git a/modules/texproc/include/lagrange/texproc/texture_filtering.h b/modules/texproc/include/lagrange/texproc/texture_filtering.h index d5255a41..39eaa35f 100644 --- a/modules/texproc/include/lagrange/texproc/texture_filtering.h +++ b/modules/texproc/include/lagrange/texproc/texture_filtering.h @@ -53,6 +53,10 @@ struct FilteringOptions /// Clamp out-of-range texels to the given range (nullopt to disable). std::optional> clamp_to_range = std::nullopt; + + /// Whether to run solver sanity checks. If unset, defaults to true in debug builds and false in + /// release builds. + std::optional sanity_check = std::nullopt; }; /// diff --git a/modules/texproc/include/lagrange/texproc/texture_stitching.h b/modules/texproc/include/lagrange/texproc/texture_stitching.h index fdd608d2..91ab2308 100644 --- a/modules/texproc/include/lagrange/texproc/texture_stitching.h +++ b/modules/texproc/include/lagrange/texproc/texture_stitching.h @@ -43,6 +43,10 @@ struct StitchingOptions /// Clamp out-of-range texels to the given range (nullopt to disable). std::optional> clamp_to_range = std::nullopt; + /// Whether to run solver sanity checks. If unset, defaults to true in debug builds and false in + /// release builds. + std::optional sanity_check = std::nullopt; + /// Initially the boundary texels to random values (for debugging purposes). bool __randomize = false; }; diff --git a/modules/texproc/python/examples/plot_convergence_benchmark.py b/modules/texproc/python/examples/plot_convergence_benchmark.py new file mode 100644 index 00000000..0e42db96 --- /dev/null +++ b/modules/texproc/python/examples/plot_convergence_benchmark.py @@ -0,0 +1,283 @@ +# +# Copyright 2026 Adobe. All rights reserved. +# This file is licensed to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may obtain a copy +# of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +# OF ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. +# +"""Benchmark and plot texture-compositing solver convergence. + +The script rasterizes compositing inputs from a mesh and multiview image, computes an exact +direct-solver reference, sweeps the number of Gauss-Seidel iterations used by the multigrid solver, +and plots error and runtime. + +Usage: + uv run --extra scripts modules/texproc/python/examples/plot_convergence_benchmark.py \ + --mesh data/corp/texproc/prepared/pumpkin.glb \ + --multiview data/corp/texproc/original/multiview.png \ + --output convergence.png + + uv run --extra scripts ./modules/texproc/python/examples/plot_convergence_benchmark.py \ + --mesh data/corp/texproc/mickey/mesh.glb \ + --multiview data/corp/texproc/mickey/multiview.png \ + --grid 3 3 --output convergence.png +""" + +import argparse +import json +import sys +import time +from pathlib import Path +from typing import Any + +import lagrange +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) +from texture_from_multiview import ( # noqa: E402 # ty: ignore[unresolved-import] + append_cameras, + split_multiview, +) + + +def rasterize_inputs( + mesh_path: Path, + cameras_path: Path | None, + multiview_path: Path, + grid: tuple[int, int], + width: int, + height: int, + base_confidence: float, +) -> tuple[lagrange.SurfaceMesh, list[np.ndarray], list[np.ndarray]]: + """Rasterize compositing inputs from a mesh and multiview image.""" + with Image.open(multiview_path) as multiview: + views = split_multiview(multiview, grid) + + scene = lagrange.io.load_scene(mesh_path, stitch_vertices=True) + if cameras_path is not None: + cameras = json.loads(cameras_path.read_text(encoding="utf-8")) + append_cameras(scene, cameras) + textures, weights = lagrange.texproc.rasterize_textures_from_renders( + scene, + views, + width=width, + height=height, + base_confidence=base_confidence, + ) + return lagrange.scene.scene_to_mesh(scene), textures, weights + + +def compute_image_distance(approx: np.ndarray, reference: np.ndarray) -> dict[str, float]: + """Compute per-channel distance metrics between identically shaped images.""" + if approx.shape != reference.shape: + raise ValueError(f"Image shape mismatch: {approx.shape} != {reference.shape}") + difference = approx.astype(np.float64) - reference.astype(np.float64) + abs_difference = np.abs(difference) + return { + "rmse": float(np.sqrt(np.mean(np.square(difference)))), + "mae": float(np.mean(abs_difference)), + "max_abs": float(np.max(abs_difference)), + } + + +def compositing_options(args: argparse.Namespace) -> dict[str, Any]: + gradient_normalizations = { + "per-edge": lagrange.texproc.GradientNormalization.PerEdge, + "per-texel-sqrt": lagrange.texproc.GradientNormalization.PerTexelSqrt, + } + return { + "value_weight": args.value_weight, + "quadrature_samples": args.quadrature, + "jitter_epsilon": args.jitter_epsilon, + "clamp_to_range": tuple(args.clamp) if args.clamp is not None else None, + "gradient_normalization": gradient_normalizations[args.gradient_normalization], + "stiffness_regularization_weight": args.regularization, + "smooth_low_weight_areas": args.smooth_low_weight_areas, + "sanity_check": args.sanity_check, + "num_multigrid_levels": args.num_multigrid_levels, + "num_v_cycles": args.num_v_cycles, + } + + +def run_benchmark( + mesh: lagrange.SurfaceMesh, + textures: list[np.ndarray], + weights: list[np.ndarray], + args: argparse.Namespace, +) -> dict[str, Any]: + """Run the direct reference and multigrid convergence sweep.""" + options = compositing_options(args) + + print("Computing direct-solver reference...") + start = time.perf_counter() + reference = lagrange.texproc.texture_compositing( + mesh, textures, weights, use_direct_solver=True, **options + ) + direct_seconds = time.perf_counter() - start + print(f"direct: {direct_seconds:.3f} s") + + iterations = [] + for count in range(2, args.max_gauss_seidel_iterations + 1, 1): + start = time.perf_counter() + approx = lagrange.texproc.texture_compositing( + mesh, + textures, + weights, + use_direct_solver=False, + num_gauss_seidel_iterations=count, + **options, + ) + solve_seconds = time.perf_counter() - start + result = { + "num_gauss_seidel_iterations": count, + "num_v_cycles": args.num_v_cycles, + **compute_image_distance(approx, reference), + "solve_seconds": solve_seconds, + } + iterations.append(result) + print( + f"iterations={count:2d} rmse={result['rmse']:.3e} " + f"mae={result['mae']:.3e} max={result['max_abs']:.3e} " + f"time={solve_seconds:.3f} s" + ) + + return { + "reference_solver": "direct", + "direct_solve_seconds": direct_seconds, + "max_gauss_seidel_iterations": args.max_gauss_seidel_iterations, + "options": { + "value_weight": args.value_weight, + "quadrature_samples": args.quadrature, + "jitter_epsilon": args.jitter_epsilon, + "clamp_to_range": args.clamp, + "gradient_normalization": args.gradient_normalization, + "stiffness_regularization_weight": args.regularization, + "smooth_low_weight_areas": args.smooth_low_weight_areas, + "sanity_check": args.sanity_check, + "num_multigrid_levels": args.num_multigrid_levels, + "num_v_cycles": args.num_v_cycles, + }, + "iterations": iterations, + } + + +def plot_results(data: dict[str, Any], output: Path | None) -> None: + """Plot convergence errors and runtimes.""" + iterations = data["iterations"] + gs_iterations = [entry["num_gauss_seidel_iterations"] for entry in iterations] + rmse = [entry["rmse"] for entry in iterations] + mae = [entry["mae"] for entry in iterations] + max_abs = [entry["max_abs"] for entry in iterations] + solve_seconds = [entry["solve_seconds"] for entry in iterations] + direct_seconds = data["direct_solve_seconds"] + + fig, (ax_err, ax_time) = plt.subplots(1, 2, figsize=(12, 5)) + + ax_err.semilogy(gs_iterations, rmse, "o-", label="RMSE") + ax_err.semilogy(gs_iterations, mae, "s-", label="MAE") + ax_err.semilogy(gs_iterations, max_abs, "^-", label="max abs") + ax_err.set_xlabel("Number of Gauss-Seidel iterations") + ax_err.set_ylabel("Distance to direct solution") + ax_err.set_title("Iterative solver convergence") + ax_err.grid(True, which="both", ls=":", alpha=0.5) + ax_err.legend() + + ax_time.plot(gs_iterations, solve_seconds, "o-", color="tab:green", label="iterative solve") + ax_time.axhline( + direct_seconds, + color="tab:red", + ls="--", + label=f"direct solve ({direct_seconds:.3f} s)", + ) + ax_time.set_xlabel("Number of Gauss-Seidel iterations") + ax_time.set_ylabel("Solve time (s)") + ax_time.set_title("Solve time vs. iterations") + ax_time.grid(True, ls=":", alpha=0.5) + ax_time.legend() + + subtitle = ", ".join(f"{key}={value}" for key, value in data["options"].items()) + fig.suptitle(subtitle, fontsize=9) + fig.tight_layout() + + if output is not None: + fig.savefig(output, dpi=150, bbox_inches="tight") + print(f"Saved figure to {output}") + else: + plt.show() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mesh", type=Path, required=True, help="Input mesh or scene with UVs.") + parser.add_argument( + "--cameras", + type=Path, + default=None, + help="Optional camera JSON when the input scene has no cameras.", + ) + parser.add_argument("--multiview", type=Path, required=True, help="Input multiview image.") + parser.add_argument("--grid", type=int, nargs=2, default=(4, 4), metavar=("ROWS", "COLS")) + parser.add_argument("--width", type=int, default=1024, help="Rasterized texture width.") + parser.add_argument("--height", type=int, default=1024, help="Rasterized texture height.") + parser.add_argument( + "--base-confidence", type=float, default=0.0, help="Confidence in the mesh base texture." + ) + parser.add_argument( + "--output", type=Path, default=None, help="Save the figure instead of showing it." + ) + parser.add_argument( + "--json-output", type=Path, default=None, help="Optionally save raw benchmark results." + ) + parser.add_argument("--value-weight", type=float, default=1e3) + parser.add_argument("--quadrature", type=int, choices=(1, 3, 6, 12, 24, 32), default=6) + parser.add_argument("--jitter-epsilon", type=float, default=1e-4) + parser.add_argument( + "--gradient-normalization", + choices=("per-edge", "per-texel-sqrt"), + default="per-texel-sqrt", + ) + parser.add_argument("--regularization", type=float, default=1e-9) + parser.add_argument("--smooth-low-weight-areas", action="store_true") + parser.add_argument( + "--sanity-check", + action=argparse.BooleanOptionalAction, + default=None, + help="Override solver sanity checks (defaults depend on build type).", + ) + parser.add_argument("--clamp", type=float, nargs=2, default=None, metavar=("MIN", "MAX")) + parser.add_argument("--num-multigrid-levels", type=int, default=4) + parser.add_argument("--num-v-cycles", type=int, default=4) + parser.add_argument("--max-gauss-seidel-iterations", type=int, default=30) + args = parser.parse_args() + if args.max_gauss_seidel_iterations < 2: + parser.error("--max-gauss-seidel-iterations must be at least 2") + return args + + +def main() -> None: + args = parse_args() + mesh, textures, weights = rasterize_inputs( + args.mesh, + args.cameras, + args.multiview, + (args.grid[0], args.grid[1]), + args.width, + args.height, + args.base_confidence, + ) + results = run_benchmark(mesh, textures, weights, args) + + if args.json_output is not None: + args.json_output.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + print(f"Saved benchmark results to {args.json_output}") + plot_results(results, args.output) + + +if __name__ == "__main__": + main() diff --git a/modules/texproc/python/scripts/texture_from_multiview.py b/modules/texproc/python/scripts/texture_from_multiview.py index 4d7c5aef..b902bb01 100644 --- a/modules/texproc/python/scripts/texture_from_multiview.py +++ b/modules/texproc/python/scripts/texture_from_multiview.py @@ -17,7 +17,10 @@ To try it out on sample data, run the following commands: uv run scripts/artifactory.py pull - uv run --extra scripts texture_from_multiview.py --mesh data/corp/texproc/original/pumpkin.glb --cameras data/corp/texproc/original/demo-16-views.json --multiview data/corp/texproc/original/multiview.png out.png + + uv run --extra scripts texture_from_multiview.py --mesh data/corp/texproc/original/pumpkin.glb --cameras data/corp/texproc/original/demo-16-views.json --multiview data/corp/texproc/original/multiview.png --output out.png + + uv run --extra scripts texture_from_multiview.py --grid 3 3 --base-confidence 0 --mesh data/corp/texproc/mickey/mesh.glb --multiview data/corp/texproc/mickey/multiview.png --height 2048 --width 2048 """ from pathlib import Path @@ -111,10 +114,69 @@ def split_multiview(multiview: Image.Image, grid_shape: Tuple[int, int]) -> List return renders +def add_texture( + scene: lagrange.scene.Scene, + *, + name: str, + texture: npt.NDArray[np.uint8], +) -> lagrange.scene.TextureInfo: + if texture.ndim != 3: + raise ValueError(f"Expected texture shape (H, W, C), got {texture.shape}.") + image = lagrange.scene.Image() + image.name = f"{name}_image" + image_buffer = lagrange.scene.ImageBuffer() + image_buffer.data = texture + image.image = image_buffer + image_id = scene.add(image) + + scene_texture = lagrange.scene.Texture() + scene_texture.name = f"{name}_texture" + scene_texture.image = image_id + scene_texture.mag_filter = lagrange.scene.Texture.TextureFilter.Linear + scene_texture.min_filter = lagrange.scene.Texture.TextureFilter.LinearMipmapLinear + texture_id = scene.add(scene_texture) + + texture_info = lagrange.scene.TextureInfo() + texture_info.index = texture_id + return texture_info + + +def create_textured_scene( + mesh: lagrange.SurfaceMesh, + base_color: npt.NDArray[np.uint8], +) -> lagrange.scene.Scene: + scene = lagrange.scene.Scene() + mesh_id = scene.add(mesh) + material = lagrange.scene.Material() + material.name = "base_color_material" + material.double_sided = True + material.base_color_value = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32) + material.alpha_mode = lagrange.scene.Material.AlphaMode.Opaque + material.base_color_texture = add_texture(scene, name="base_color", texture=base_color) + material_id = scene.add(material) + + instance = lagrange.scene.SceneMeshInstance() + instance.mesh = mesh_id + instance.materials.append(material_id) + + node = lagrange.scene.Node() + node.name = "mesh_node" + node.meshes.append(instance) + node_id = scene.add(node) + scene.root_nodes.append(node_id) + return scene + + def parse_args(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--cameras", type=Path, required=True, help="Input multiview camera json") + parser.add_argument("--cameras", type=Path, required=False, help="Input multiview camera json") parser.add_argument("--mesh", type=Path, required=True, help="Input mesh file") + parser.add_argument( + "--base-confidence", + type=float, + default=0.3, + help="Confidence in base mesh texture if available. Set to 0 to ignore base mesh texture.", + ) parser.add_argument( "--multiview", type=Path, @@ -122,31 +184,46 @@ def parse_args(): nargs="+", help="Input renders as either a single 4x4 grid image or multiple individual render images", ) + parser.add_argument( + "--grid", + type=int, + nargs=2, + default=(4, 4), + help="Grid size when a single image is provided", + ) + # TODO: Use base texture width/height as default parser.add_argument("--width", "-W", type=int, default=1024, help="Target texture width") parser.add_argument("--height", "-H", type=int, default=1024, help="Target texture height") parser.add_argument( - "--output", + "--output-texture", nargs="?", type=Path, default="output.exr", help="Output texture file", ) + parser.add_argument( + "--output-mesh", type=Path, default="output.glb", help="Output textured mesh" + ) return parser.parse_args() def main(): args = parse_args() if len(args.multiview) == 1: - views = split_multiview(Image.open(args.multiview[0]), grid_shape=(4, 4)) + views = split_multiview( + Image.open(args.multiview[0]), grid_shape=(args.grid[0], args.grid[1]) + ) else: views = [np.asarray(Image.open(p)).astype(np.float32) / 255.0 for p in args.multiview] scene = lagrange.io.load_scene(args.mesh, stitch_vertices=True) - append_cameras(scene, json.loads(args.cameras.read_text())) + if args.cameras: + append_cameras(scene, json.loads(args.cameras.read_text())) colors, weights = lagrange.texproc.rasterize_textures_from_renders( scene, views, width=args.width, height=args.height, + base_confidence=args.base_confidence, ) mesh = lagrange.scene.scene_to_mesh(scene) image = lagrange.texproc.texture_compositing( @@ -154,8 +231,17 @@ def main(): colors, weights, ) - lagrange.logger.info(f"Saving result to: '{args.output.with_suffix('.exr')}'") - pyexr.write(args.output.with_suffix(".exr"), image) + if args.output_texture: + lagrange.logger.info(f"Saving texture to: '{args.output_texture.with_suffix('.exr')}'") + pyexr.write(args.output_texture.with_suffix(".exr"), image) + if args.output_mesh: + lagrange.logger.info(f"Saving mesh to '{args.output_mesh}'") + image = np.clip(image, 0.0, 1.0) + image_uint8 = np.round(image * 255.0).astype(np.uint8) + textured_scene = create_textured_scene(lagrange.unify_index_buffer(mesh), image_uint8) + save_options = lagrange.io.SaveOptions() + save_options.embed_images = True + lagrange.io.save_scene(str(args.output_mesh), textured_scene, save_options) if __name__ == "__main__": diff --git a/modules/texproc/python/src/texproc.cpp b/modules/texproc/python/src/texproc.cpp index 21a5eb5d..6796ceee 100644 --- a/modules/texproc/python/src/texproc.cpp +++ b/modules/texproc/python/src/texproc.cpp @@ -56,7 +56,8 @@ void populate_texproc_module(nb::module_& m) unsigned int quadrature_samples, double jitter_epsilon, double stiffness_regularization_weight, - std::optional> clamp_to_range) { + std::optional> clamp_to_range, + std::optional sanity_check) { auto image = image::experimental::create_image( image_.shape(0), image_.shape(1), @@ -71,6 +72,7 @@ void populate_texproc_module(nb::module_& m) options.jitter_epsilon = jitter_epsilon; options.stiffness_regularization_weight = stiffness_regularization_weight; options.clamp_to_range = clamp_to_range; + options.sanity_check = sanity_check; tp::texture_filtering(mesh, image.to_mdspan(), options); @@ -86,6 +88,7 @@ void populate_texproc_module(nb::module_& m) "stiffness_regularization_weight"_a = tp::FilteringOptions().stiffness_regularization_weight, "clamp_to_range"_a = tp::FilteringOptions().clamp_to_range, + "sanity_check"_a = tp::FilteringOptions().sanity_check, R"("Smooth or sharpen a texture image associated with a mesh. :param mesh: Input mesh with UV attributes. @@ -97,6 +100,7 @@ void populate_texproc_module(nb::module_& m) :param jitter_epsilon: Jitter amount per texel (0 to deactivate). :param stiffness_regularization_weight: Regularize the stiffness matrix using a combinatorial Laplacian energy. :param clamp_to_range: Clamp out-of-range texels to the given range (disabled by default). +:param sanity_check: Whether to run solver sanity checks. If unset, defaults to true in debug builds and false in release builds. :return: The filtered texture image.)"); @@ -108,7 +112,8 @@ void populate_texproc_module(nb::module_& m) unsigned int quadrature_samples, double jitter_epsilon, double stiffness_regularization_weight, - std::optional> clamp_to_range) { + std::optional> clamp_to_range, + std::optional sanity_check) { auto image = image::experimental::create_image( image_.shape(0), image_.shape(1), @@ -121,6 +126,7 @@ void populate_texproc_module(nb::module_& m) options.jitter_epsilon = jitter_epsilon; options.stiffness_regularization_weight = stiffness_regularization_weight; options.clamp_to_range = clamp_to_range; + options.sanity_check = sanity_check; tp::texture_stitching(mesh, image.to_mdspan(), options); @@ -134,6 +140,7 @@ void populate_texproc_module(nb::module_& m) "stiffness_regularization_weight"_a = tp::StitchingOptions().stiffness_regularization_weight, "clamp_to_range"_a = tp::StitchingOptions().clamp_to_range, + "sanity_check"_a = tp::StitchingOptions().sanity_check, R"(Smooth or sharpen a texture image associated with a mesh. :param mesh: Input mesh with UV attributes. @@ -143,6 +150,7 @@ void populate_texproc_module(nb::module_& m) :param jitter_epsilon: Jitter amount per texel (0 to deactivate). :param stiffness_regularization_weight: Regularize the stiffness matrix using a combinatorial Laplacian energy. :param clamp_to_range: Clamp out-of-range texels to the given range (disabled by default). +:param sanity_check: Whether to run solver sanity checks. If unset, defaults to true in debug builds and false in release builds. :return: The stitched texture image.)"); @@ -208,6 +216,24 @@ void populate_texproc_module(nb::module_& m) :return: The dilated position map.)"); + nb::enum_( + m, + "GradientNormalization", + "How per-view gradient contributions are normalized " + "when assembling the edge-difference target.") + .value( + "PerEdge", + tp::GradientNormalization::PerEdge, + "Per-edge weighted average of per-view gradients, matching the ShapeGradientDomain " + "reference. Produces gradient targets bounded by the range of per-view gradients.") + .value( + "PerTexelSqrt", + tp::GradientNormalization::PerTexelSqrt, + "Per-texel normalization with geometric-mean combination (legacy behavior). Gradient " + "weights do not sum to 1 per edge at view boundaries, attenuating gradient targets " + "in transition regions.") + .export_values(); + m.def( "texture_compositing", [](const SurfaceMesh& mesh, @@ -217,10 +243,14 @@ void populate_texproc_module(nb::module_& m) unsigned int quadrature_samples, double jitter_epsilon, std::optional> clamp_to_range, + tp::GradientNormalization gradient_normalization, + bool use_direct_solver, + double stiffness_regularization_weight, bool smooth_low_weight_areas, unsigned int num_multigrid_levels, unsigned int num_gauss_seidel_iterations, - unsigned int num_v_cycles) { + unsigned int num_v_cycles, + std::optional sanity_check) { la_runtime_assert( textures.size() == weights.size(), "Number of colors and weights images must be the same."); @@ -241,10 +271,14 @@ void populate_texproc_module(nb::module_& m) options.quadrature_samples = quadrature_samples; options.jitter_epsilon = jitter_epsilon; options.clamp_to_range = clamp_to_range; + options.gradient_normalization = gradient_normalization; + options.use_direct_solver = use_direct_solver; + options.stiffness_regularization_weight = stiffness_regularization_weight; options.smooth_low_weight_areas = smooth_low_weight_areas; options.solver.num_multigrid_levels = num_multigrid_levels; options.solver.num_gauss_seidel_iterations = num_gauss_seidel_iterations; options.solver.num_v_cycles = num_v_cycles; + options.sanity_check = sanity_check; auto image = tp::texture_compositing(mesh, weighted_textures, options); @@ -253,15 +287,21 @@ void populate_texproc_module(nb::module_& m) "mesh"_a, "colors"_a, "weights"_a, + nb::kw_only(), "value_weight"_a = tp::CompositingOptions().value_weight, "quadrature_samples"_a = tp::CompositingOptions().quadrature_samples, "jitter_epsilon"_a = tp::CompositingOptions().jitter_epsilon, "clamp_to_range"_a = tp::CompositingOptions().clamp_to_range, + "gradient_normalization"_a = tp::CompositingOptions().gradient_normalization, + "use_direct_solver"_a = tp::CompositingOptions().use_direct_solver, + "stiffness_regularization_weight"_a = + tp::CompositingOptions().stiffness_regularization_weight, "smooth_low_weight_areas"_a = tp::CompositingOptions().smooth_low_weight_areas, "num_multigrid_levels"_a = tp::CompositingOptions().solver.num_multigrid_levels, "num_gauss_seidel_iterations"_a = tp::CompositingOptions().solver.num_gauss_seidel_iterations, "num_v_cycles"_a = tp::CompositingOptions().solver.num_v_cycles, + "sanity_check"_a = tp::CompositingOptions().sanity_check, R"(Composite multiple (color, weight) into a single texture given a unwrapped mesh. :param mesh: Input mesh with UV attributes. @@ -271,10 +311,14 @@ void populate_texproc_module(nb::module_& m) :param quadrature_samples: The number of quadrature samples to use for integration (in {1, 3, 6, 12, 24, 32}). :param jitter_epsilon: Jitter amount per texel (0 to deactivate). :param clamp_to_range: Clamp out-of-range texels to the given range (disabled by default). -:param smooth_low_weight_areas: Whether to smooth pixels with a low total weight (< 1). When enabled, this will not dampen the gradient terms for pixels with a low total weight, resulting in a smoother texture in low-confidence areas. -:param num_multigrid_levels: Number of multigrid levels. -:param num_gauss_seidel_iterations: Number of Gauss-Seidel iterations per multigrid level. -:param num_v_cycles: Number of V-cycles to perform. +:param gradient_normalization: How per-view gradient contributions are normalized into the edge-difference target. +:param use_direct_solver: Use a direct (LDLT) solver instead of the multigrid v-cycle. Exact and supports Laplacian regularization for better conditioning, but uses more memory. +:param stiffness_regularization_weight: Weight for combinatorial Laplacian regularization added to the stiffness matrix, to help stabilize the system in poorly conditioned regions. Set to 0 to disable. +:param smooth_low_weight_areas: When gradient_normalization is PerTexelSqrt, whether to smooth pixels with a low total weight (< 1). When enabled, this will dampen the gradient terms for pixels with a low total weight, resulting in a smoother texture in low-confidence areas. +:param num_multigrid_levels: Number of multigrid levels (ignored when use_direct_solver is true). +:param num_gauss_seidel_iterations: Number of Gauss-Seidel iterations per multigrid level. Must be even (ignored when use_direct_solver is true). +:param num_v_cycles: Number of V-cycles to perform (ignored when use_direct_solver is true). +:param sanity_check: Whether to run solver sanity checks. If unset, defaults to true in debug builds and false in release builds. :return: The composited texture image.)"); diff --git a/modules/texproc/python/tests/test_texproc.py b/modules/texproc/python/tests/test_texproc.py index e44e4cba..5f983a90 100644 --- a/modules/texproc/python/tests/test_texproc.py +++ b/modules/texproc/python/tests/test_texproc.py @@ -25,6 +25,7 @@ def test_filtering(self, quad_mesh, quad_tex): quad_mesh, quad_tex, stiffness_regularization_weight=1e-5, + sanity_check=True, ) assert quad_tex_.shape == quad_tex.shape @@ -35,7 +36,11 @@ def test_stitching_quad(self, quad_mesh, quad_tex): assert quad_mesh.num_facets == 2 assert quad_tex.dtype == np.float32 - quad_tex_ = lagrange.texproc.texture_stitching(quad_mesh, quad_tex) + quad_tex_ = lagrange.texproc.texture_stitching( + quad_mesh, + quad_tex, + sanity_check=True, + ) assert quad_tex_.shape == quad_tex.shape @@ -105,6 +110,7 @@ def test_rasterize_and_compose(self, quad_scene, quad_tex): colors, weights, clamp_to_range=(0.0, 1.0), + sanity_check=True, ) assert final_color.shape == (128, 128, 4) diff --git a/modules/texproc/src/TextureRasterizer.cpp b/modules/texproc/src/TextureRasterizer.cpp index a55c926e..d7d72b94 100644 --- a/modules/texproc/src/TextureRasterizer.cpp +++ b/modules/texproc/src/TextureRasterizer.cpp @@ -594,14 +594,18 @@ struct TextureAndConfidenceFromRender : Eigen::Vector3d(p_c[0], p_c[1], p_c[2]).stableNormalized(); // The normal confidence based on the alignment of the normal with the - // view direction + // view direction. [[maybe_unused]] double normal_confidence = fabs(n_view.dot(dir_view)); - // If the texel is visible, set the confidence to the product of the depth and - // normal confidences - if (d < std::numeric_limits::infinity() && - z < d * (1. + depth_precision)) { - confidence[i] = normal_confidence * depth_confidence(q); + // Compute confidence based on depth comparison. Perspective camera has relative + // tolerance, orthographic camera has absolute tolerance. + if (d < std::numeric_limits::infinity()) { + const double depth_tol = + camera_params.is_orthographic ? depth_precision : d * depth_precision; + if (z < d + depth_tol) { + confidence[i] = 0; + confidence[i] = normal_confidence * depth_confidence(q); + } } } }); diff --git a/modules/texproc/src/geodesic_dilation.cpp b/modules/texproc/src/geodesic_dilation.cpp index f66511f8..042023ab 100644 --- a/modules/texproc/src/geodesic_dilation.cpp +++ b/modules/texproc/src/geodesic_dilation.cpp @@ -60,7 +60,8 @@ void position_dilation( unsigned int res[] = { static_cast(texture.extent(0)), - static_cast(texture.extent(1))}; + static_cast(texture.extent(1)), + }; Padding padding = create_padding(wrapper, res[0], res[1]); res[0] += padding.width(); diff --git a/modules/texproc/src/mesh_utils.h b/modules/texproc/src/mesh_utils.h index 11ab6492..3e1ed6fa 100644 --- a/modules/texproc/src/mesh_utils.h +++ b/modules/texproc/src/mesh_utils.h @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -50,23 +49,20 @@ using namespace lagrange::texproc::threadpool; namespace lagrange::texproc { -namespace { - -using namespace MishaK; - // Using `Point` directly leads to ambiguity with Apple Accelerate types. template using Vector = MishaK::Point; +namespace { + +using namespace MishaK; + // The dimension of the embedding space static const unsigned int Dim = 3; // The dimension of the manifold static const unsigned int K = 2; -// The linear solver -using Solver = lagrange::solver::SolverLDLT>; - } // namespace enum class RequiresIndexedTexcoords { Yes, No }; diff --git a/modules/texproc/src/texture_compositing.cpp b/modules/texproc/src/texture_compositing.cpp index 4b8903ad..d60874d6 100644 --- a/modules/texproc/src/texture_compositing.cpp +++ b/modules/texproc/src/texture_compositing.cpp @@ -14,7 +14,9 @@ #include "mesh_utils.h" +#include #include +#include #include // clang-format off @@ -32,6 +34,7 @@ namespace lagrange::texproc { namespace { using namespace MishaK::TSP; +using Solver = lagrange::solver::SolverLDLT>; template bool is_exactly_zero(Scalar x) @@ -39,35 +42,38 @@ bool is_exactly_zero(Scalar x) return std::abs(x) < std::numeric_limits::denorm_min(); } -template -image::experimental::Array3D texture_compositing( - const SurfaceMesh& mesh, - std::vector> textures, - const CompositingOptions& options) +template +struct InputData { - struct InputData - { - RegularGrid> texture; - RegularGrid> weights; - }; - - VerboseTimer timer("[compositing] "); + RegularGrid> texture; + RegularGrid> weights; +}; - timer.tick(); - auto wrapper = - mesh_utils::create_mesh_wrapper(mesh, RequiresIndexedTexcoords::Yes, CheckFlippedUV::Yes); - std::vector in(textures.size()); - RegularGrid> out; +// Load input textures/weights into RegularGrids and apply jitter and padding. +template +std::tuple>, Padding, unsigned int, unsigned int> +copy_jitter_and_pad( + const std::vector>& textures, + mesh_utils::MeshWrapper& wrapper, + double jitter_epsilon) +{ + la_runtime_assert(!textures.empty()); + std::vector> in(textures.size()); + auto width = static_cast(textures[0].texture.extent(0)); + auto height = static_cast(textures[0].texture.extent(1)); for (size_t i = 0; i < textures.size(); i++) { + la_debug_assert(textures[i].texture.extent(0) == width); + la_debug_assert(textures[i].texture.extent(1) == height); + la_debug_assert(textures[i].texture.extent(2) == NumChannels); + la_debug_assert(textures[i].weights.extent(0) == width); + la_debug_assert(textures[i].weights.extent(1) == height); + la_debug_assert(textures[i].weights.extent(2) == 1); mesh_utils::set_grid(textures[i].texture, in[i].texture); mesh_utils::set_grid(textures[i].weights, in[i].weights); } - unsigned int width = in[0].texture.res(0); - unsigned int height = in[0].texture.res(1); - - mesh_utils::jitter_texture(wrapper.texcoords, width, height, options.jitter_epsilon); + mesh_utils::jitter_texture(wrapper.texcoords, width, height, jitter_epsilon); Padding padding = mesh_utils::create_padding(wrapper, width, height); tbb::parallel_for(size_t(0), textures.size(), [&](size_t i) { @@ -77,205 +83,369 @@ image::experimental::Array3D texture_compositing( width += padding.width(); height += padding.height(); - out.resize(width, height); - timer.tock("preprocessing"); - - // Construct the hierarchical gradient domain object - // TODO: Compute number of levels based on texture size - const bool normalize = true; -#if LAGRANGE_TARGET_BUILD_TYPE(DEBUG) - const bool sanity_check = true; -#else - const bool sanity_check = false; -#endif - timer.tick(); - HierarchicalGradientDomain> hgd( - options.quadrature_samples, - wrapper.num_simplices(), - wrapper.num_vertices(), - wrapper.num_texcoords(), - [&](size_t t, unsigned int k) { return wrapper.vertex_index(t, k); }, - [&](size_t v) { return wrapper.vertex(v); }, - [&](size_t t, unsigned int k) { return wrapper.texture_index(t, k); }, - [&](size_t v) { return wrapper.texcoord(v); }, // solver internally flips the v coordinate + return { + std::move(in), + padding, width, height, - options.solver.num_multigrid_levels, - normalize, - sanity_check); - timer.tock("hierarchy construction"); + }; +} - logger().debug( - "[compositing] mesh: {} triangles, {} vertices, {} texcoords", - wrapper.num_simplices(), - wrapper.num_vertices(), - wrapper.num_texcoords()); - logger().debug( - "[compositing] texture: {}x{} ({} channels), {} views", - width, - height, - NumChannels, - textures.size()); - logger().debug( - "[compositing] solver: {} nodes, {} edges, {} multigrid levels, {} v-cycles", - hgd.numNodes(), - hgd.numEdges(), - options.solver.num_multigrid_levels, - options.solver.num_v_cycles); - - // Get the pointers to the solver constraints and solution - span> x{hgd.x(), hgd.numNodes()}; - span> b{hgd.b(), hgd.numNodes()}; - - // Compute the sum of weights for texel n - auto compute_weight_sum = [&](unsigned int row, unsigned int col) { +// Compute the weighted-average value target at each node, +// fill unobserved texels with the average observed color. +// Returns (weight_colors, weight_sums) where weight_sums[n] = Σ_i weights_i(n). +template +std::pair>, std::vector> compute_value_target( + const GradientDomain& gd, + const std::vector>& in) +{ + // Compute weighted texel values. + std::vector weight_sums(gd.numNodes()); + std::vector> weight_colors(gd.numNodes()); + for (const auto nn : range(gd.numNodes())) { + auto [row, col] = gd.node(nn); double weight_sum = 0; - for (unsigned int i = 0; i < in.size(); i++) { - weight_sum += in[i].weights(row, col)[0]; + for (const auto& ii : in) { + weight_sum += ii.weights(row, col)[0]; } - return weight_sum; - }; + weight_sums[nn] = weight_sum; + const double scale = weight_sum > 0 ? 1. / weight_sum : 1.; + for (const auto& ii : in) { + weight_colors[nn] += ii.texture(row, col) * ii.weights(row, col)[0] * scale; + } + } - // Normalization factor for texel n - auto normalization_weight = [&](unsigned int row, unsigned int col, bool is_grad) -> double { - const double weight_sum = compute_weight_sum(row, col); - if (is_grad && options.smooth_low_weight_areas && weight_sum < 1.) { - // Do not normalize gradients in low-confidence areas. This reduces the importance of - // the gradient terms and smooths the resulting texture in those areas. - return 1.; - } else { - return weight_sum > 0 ? 1. / weight_sum : 1.; + // Compute the average observed color. + Vector mean_observed_color; + size_t num_observed_nodes = 0; + for (const auto nn : range(gd.numNodes())) { + if (!is_exactly_zero(weight_sums[nn])) { + mean_observed_color += weight_colors[nn]; + num_observed_nodes++; } + } + if (num_observed_nodes > 0) { + mean_observed_color /= static_cast(num_observed_nodes); + } + la_debug_assert(num_observed_nodes != 0 || mean_observed_color.squareNorm() == 0.0); + + // Set unobserved texels to the average observed color. + if (num_observed_nodes < gd.numNodes()) { + logger().warn( + "Found {} unobserved texels. " + "Setting target values to the average observed color.", + gd.numNodes() - num_observed_nodes); + for (const auto nn : range(gd.numNodes())) { + if (is_exactly_zero(weight_sums[nn])) { + weight_colors[nn] = mean_observed_color; + } + } + } + + return { + std::move(weight_colors), + std::move(weight_sums), }; +} - // Compute the weighted sum of texture values - timer.tick(); - for (size_t n = 0; n < hgd.numNodes(); n++) { - auto [row, col] = hgd.node(n); +// Compute the gradient constraints using the selected normalization mode. +template +std::vector> compute_edge_differences( + const GradientDomain& gd, + const std::vector>& in, + const std::vector& weight_sums, + const GradientNormalization mode, + const bool smooth_low_weight_areas) +{ + std::vector> edge_differences(gd.numEdges()); + std::vector edge_weight_sums(gd.numEdges(), 0.0); - const double scale = normalization_weight(row, col, false); - for (unsigned int i = 0; i < in.size(); i++) { - x[n] += in[i].texture(row, col) * in[i].weights(row, col)[0] * scale; + auto compute_node_scale = [&](size_t n) { + if (smooth_low_weight_areas && weight_sums[n] < 1.0) { + return 1.0; } - } + return weight_sums[n] > 0 ? 1.0 / weight_sums[n] : 1.0; + }; + + auto compute_edge_weight = [&](size_t n1, size_t n2, double w1, double w2) -> double { + switch (mode) { + case GradientNormalization::PerEdge: return w1 * w2; break; + case GradientNormalization::PerTexelSqrt: + return std::sqrt(w1 * compute_node_scale(n1) * w2 * compute_node_scale(n2)); + break; + default: + la_debug_assert(false); + return 0.0; + break; + } + }; - timer.tock("weighted sum"); - - // Set unobserved texels to the average observed color - { - Vector avg_observed_color; - size_t num_observed_texels = 0; - size_t num_unobserved_texels = 0; - for (size_t n = 0; n < hgd.numNodes(); n++) { - auto [row, col] = hgd.node(n); - if (!is_exactly_zero(compute_weight_sum(row, col))) { - avg_observed_color += x[n]; - num_observed_texels++; - } else { - num_unobserved_texels++; + for (const auto ee : range(gd.numEdges())) { + const std::pair end_points = gd.edge(ee); + const auto [row1, col1] = gd.node(end_points.first); + const auto [row2, col2] = gd.node(end_points.second); + for (const auto ii : range(in.size())) { + const double w1 = in[ii].weights(row1, col1)[0]; + const double w2 = in[ii].weights(row2, col2)[0]; + const double edge_weight = + compute_edge_weight(end_points.first, end_points.second, w1, w2); + if (edge_weight > 0) { + edge_differences[ee] += + (in[ii].texture(row2, col2) - in[ii].texture(row1, col1)) * edge_weight; + if (mode == GradientNormalization::PerEdge) { + edge_weight_sums[ee] += edge_weight; + } } } - if (num_unobserved_texels > 0) { - logger().warn( - "Found {} unobserved texels. Setting target values to the average observed color.", - num_unobserved_texels); + } + + if (mode == GradientNormalization::PerEdge) { + for (const auto ee : range(gd.numEdges())) { + if (edge_weight_sums[ee] > 0) edge_differences[ee] /= edge_weight_sums[ee]; } - if (num_observed_texels > 0) { - avg_observed_color /= static_cast(num_observed_texels); + } + + return edge_differences; +} + +// Direct LDLT solve with Laplacian regularization of the stiffness matrix. +// Modifies xx in place. +template +void solve_direct( + const GradientDomain& gd, + const std::vector>& edge_differences, + const double value_weight, + const double gradient_weight, + const double regularization_weight, + std::vector>& xx) +{ + la_runtime_assert(gd.numNodes() > 0); + la_runtime_assert(edge_differences.size() == gd.numEdges()); + + const Eigen::SparseMatrix M = gd.mass(); + const Eigen::SparseMatrix S_reg = + mesh_utils::laplacian_regularization(gd.stiffness(), regularization_weight); + const Eigen::SparseMatrix A = value_weight * M + gradient_weight * S_reg; + + Solver solver(A); + la_runtime_assert(solver.info() == Eigen::Success, "Failed to factor system matrix"); + + std::vector> gradient_b(gd.numNodes()); + gd.divergence(edge_differences.data(), gradient_b.data()); + + Eigen::VectorXd x_c(gd.numNodes()); + Eigen::VectorXd gradient_b_c(gd.numNodes()); + for (const auto cc : range(NumChannels)) { + for (const auto nn : range(gd.numNodes())) { + x_c[nn] = xx[nn][cc]; + gradient_b_c[nn] = gradient_b[nn][cc]; } - for (size_t n = 0; n < hgd.numNodes(); n++) { - auto [row, col] = hgd.node(n); - if (is_exactly_zero(compute_weight_sum(row, col))) { - x[n] = avg_observed_color; - } + Eigen::VectorXd b_c = value_weight * (M * x_c) + gradient_weight * gradient_b_c; + Eigen::VectorXd sol = solver.solve(b_c); + for (const auto nn : range(gd.numNodes())) { + xx[nn][cc] = sol[nn]; } } +} - // Construct the constraints - timer.tick(); - { - std::vector> value_b(hgd.numNodes()), - gradient_b(hgd.numNodes()); - - // Get the constraints from the values - hgd.mass(&x[0], &value_b[0]); - - // Get the constraints from the gradients - { - // Compute the edge differences - std::vector> edge_differences(hgd.numEdges()); - for (size_t e = 0; e < hgd.numEdges(); e++) { - std::pair end_points = hgd.edge(e); - - auto [row1, col1] = hgd.node(end_points.first); - auto [row2, col2] = hgd.node(end_points.second); - - const double scale1 = normalization_weight(row1, col1, true); - const double scale2 = normalization_weight(row2, col2, true); - for (unsigned int i = 0; i < in.size(); i++) { - double weight = in[i].weights(row1, col1)[0] * scale1 * - in[i].weights(row2, col2)[0] * scale2; - if (weight > 0) { - weight = sqrt(weight); - edge_differences[e] += - (in[i].texture(row2, col2) - in[i].texture(row1, col1)) * weight; - } - } - } +// Multigrid v-cycle solve. +// Modifies xx in place. +template +void solve_multigrid( + HierarchicalGradientDomain>& hgd, + const std::vector>& edge_differences, + const double value_weight, + const double gradient_weight, + const CompositingOptions::SolverOptions& options, + std::vector>& xx) +{ + la_runtime_assert(options.num_gauss_seidel_iterations > 0); - // Compute the associated divergence - hgd.divergence(&edge_differences[0], &gradient_b[0]); - } + la_runtime_assert(hgd.numNodes() > 0); + la_runtime_assert(edge_differences.size() == hgd.numEdges()); - // Combine the constraints - for (size_t n = 0; n < hgd.numNodes(); n++) { - b[n] = value_b[n] * options.value_weight + gradient_b[n]; - } + span> hx{hgd.x(), hgd.numNodes()}; + span> hb{hgd.b(), hgd.numNodes()}; + for (const auto nn : range(hgd.numNodes())) { + hx[nn] = xx[nn]; } - timer.tock("constraint assembly"); + std::vector> value_b(hgd.numNodes()); + std::vector> gradient_b(hgd.numNodes()); - // Compute the system matrix - timer.tick(); - const double gradient_weight = 1.0; - hgd.updateSystem(options.value_weight, gradient_weight); - timer.tock("system update"); + hgd.mass(hx.data(), value_b.data()); + hgd.divergence(edge_differences.data(), gradient_b.data()); + for (const auto nn : range(hgd.numNodes())) { + hb[nn] = value_weight * value_b[nn] + gradient_weight * gradient_b[nn]; + } - // Relax the solution - timer.tick(); - for (unsigned int v = 0; v < options.solver.num_v_cycles; ++v) { - hgd.vCycle(options.solver.num_gauss_seidel_iterations); + hgd.updateSystem(value_weight, gradient_weight); + for ([[maybe_unused]] const auto vv : range(options.num_v_cycles)) { + // The upstream code alternates between boundary and interior updates, so it needs an even + // number of iterations to converge: + // https://github.com/mkazhdan/TextureSignalProcessing/blob/dd7ab66cc0e75bec1e6eb6b704d240e8780f46e7/include/Src/IterativeSolvers.inl#L447 + hgd.vCycle(options.num_gauss_seidel_iterations * 2); } - timer.tock("v-cycles"); - if (options.clamp_to_range.has_value()) { - mesh_utils::clamp_out_of_range(x, hgd, options.clamp_to_range.value()); + for (const auto nn : range(hgd.numNodes())) { + xx[nn] = hx[nn]; } +} + +// Write the solved texel values back into an Array3D output, undoing padding. +template +image::experimental::Array3D unpad_and_copy( + const GradientDomain& gd, + const std::vector>& x, + const Padding padding, + const unsigned int width, + const unsigned int height) +{ + RegularGrid> grid; + grid.resize(width, height); + for (const auto nn : range(gd.numNodes())) { + const auto [row, col] = gd.node(nn); + grid(row, col) = x[nn]; + } + padding.unpad(grid); + + const unsigned int width_ = width - padding.width(); + const unsigned int height_ = height - padding.height(); + la_debug_assert(grid.res(0) == width_); + la_debug_assert(grid.res(1) == height_); + auto composite = image::experimental::create_image(width_, height_, NumChannels); + // TODO: The two grids are transposed in memory. Measure and check if tiling the copy over 32x32 + // blocks is faster. + for (const auto ii : range(width_)) { + for (const auto jj : range(height_)) { + for (const auto cc : range(NumChannels)) { + composite(ii, jj, cc) = grid(ii, jj)[cc]; + } + } + } + + return composite; +} + +template +image::experimental::Array3D texture_compositing( + const SurfaceMesh& mesh, + std::vector> textures, + const CompositingOptions& options) +{ + VerboseTimer timer("[compositing] "); - // Put the texel values back into the texture timer.tick(); - for (size_t n = 0; n < hgd.numNodes(); n++) { - std::pair coords = hgd.node(n); - out(coords.first, coords.second) = x[n]; + auto wrapper = + mesh_utils::create_mesh_wrapper(mesh, RequiresIndexedTexcoords::Yes, CheckFlippedUV::Yes); + auto [in, padding, width, height] = + copy_jitter_and_pad(textures, wrapper, options.jitter_epsilon); + timer.tock("preprocessing"); + + // HierarchicalGradientDomain inherits from GradientDomain — construct only one to avoid + // duplicating the expensive operator initialization. + const bool normalize = true; +#if LAGRANGE_TARGET_BUILD_TYPE(DEBUG) + const bool sanity_check = options.sanity_check.value_or(true); +#else + const bool sanity_check = options.sanity_check.value_or(false); +#endif + auto surface_corner = [&](size_t t, unsigned int k) { return wrapper.vertex_index(t, k); }; + auto surface_vertex = [&](size_t v) { return wrapper.vertex(v); }; + auto texture_corner = [&](size_t t, unsigned int k) { return wrapper.texture_index(t, k); }; + auto texture_vertex = [&](size_t v) { return wrapper.texcoord(v); }; + + timer.tick(); + using GradDomain = GradientDomain; + using HierGradDomain = HierarchicalGradientDomain>; + std::optional gd_storage; + std::optional hgd_storage; + if (options.use_direct_solver) { + gd_storage.emplace(GradDomain( + options.quadrature_samples, + wrapper.num_simplices(), + wrapper.num_vertices(), + wrapper.num_texcoords(), + surface_corner, + surface_vertex, + texture_corner, + texture_vertex, + width, + height, + normalize, + sanity_check)); + } else { + hgd_storage.emplace(HierGradDomain( + options.quadrature_samples, + wrapper.num_simplices(), + wrapper.num_vertices(), + wrapper.num_texcoords(), + surface_corner, + surface_vertex, + texture_corner, + texture_vertex, + width, + height, + options.solver.num_multigrid_levels, + normalize, + sanity_check)); } + GradDomain& gd = options.use_direct_solver ? static_cast(*gd_storage) + : static_cast(*hgd_storage); + timer.tock("solver construction"); + + timer.tick(); + logger().debug( + "[compositing] mesh: {} triangles, {} vertices, {} texcoords", + wrapper.num_simplices(), + wrapper.num_vertices(), + wrapper.num_texcoords()); + logger().debug( + "[compositing] texture: {}x{} ({} channels), {} views, direct {}", + width, + height, + NumChannels, + textures.size(), + options.use_direct_solver); + auto [x, weight_sums] = compute_value_target(gd, in); + timer.tock("value target"); - // Undo padding - padding.unpad(out); + timer.tick(); + auto edge_differences = compute_edge_differences( + gd, + in, + weight_sums, + options.gradient_normalization, + options.smooth_low_weight_areas); + timer.tock("edge differences"); - image::experimental::Array3D composite = - image::experimental::create_image( - textures[0].texture.extent(0), - textures[0].texture.extent(1), - textures[0].texture.extent(2)); + timer.tick(); + const double gradient_weight = 1.0; + if (options.use_direct_solver) { + solve_direct( + gd, + edge_differences, + options.value_weight, + gradient_weight, + options.stiffness_regularization_weight, + x); + } else { + hgd_storage->applyLaplacianRegularization(options.stiffness_regularization_weight); + solve_multigrid( + *hgd_storage, + edge_differences, + options.value_weight, + gradient_weight, + options.solver, + x); + } + timer.tock("solve"); - // Copy the texture grid data back into the texture - for (unsigned int j = 0; j < out.res(1); j++) { - for (unsigned int i = 0; i < out.res(0); i++) { - for (unsigned int c = 0; c < NumChannels; c++) { - composite(i, j, c) = out(i, j)[c]; - } - } + timer.tick(); + if (options.clamp_to_range.has_value()) { + mesh_utils::clamp_out_of_range(x, gd, options.clamp_to_range.value()); } + auto composite = unpad_and_copy(gd, x, padding, width, height); timer.tock("postprocessing"); return composite; @@ -316,9 +486,6 @@ image::experimental::Array3D texture_compositing( throw std::runtime_error("Weights must have the same dimensions as the texture"); } } - if (!textures.size()) throw std::runtime_error("Expected at least one texture"); - - unsigned int num_channels = static_cast(textures[0].texture.extent(2)); image::experimental::Array3D output; diff --git a/modules/texproc/src/texture_filtering.cpp b/modules/texproc/src/texture_filtering.cpp index d3416d6c..19b60076 100644 --- a/modules/texproc/src/texture_filtering.cpp +++ b/modules/texproc/src/texture_filtering.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include "mesh_utils.h" @@ -30,6 +31,7 @@ namespace lagrange::texproc { namespace { using namespace MishaK::TSP; +using Solver = lagrange::solver::SolverLDLT>; template void texture_gradient_modulation( @@ -52,9 +54,9 @@ void texture_gradient_modulation( // TODO: Switch to multi-grid solver const bool normalize = true; #if LAGRANGE_TARGET_BUILD_TYPE(DEBUG) - const bool sanity_check = true; + const bool sanity_check = options.sanity_check.value_or(true); #else - const bool sanity_check = false; + const bool sanity_check = options.sanity_check.value_or(false); #endif GradientDomain gd( options.quadrature_samples, diff --git a/modules/texproc/src/texture_stitching.cpp b/modules/texproc/src/texture_stitching.cpp index 6955bdc5..48f098e8 100644 --- a/modules/texproc/src/texture_stitching.cpp +++ b/modules/texproc/src/texture_stitching.cpp @@ -14,6 +14,7 @@ #include "mesh_utils.h" +#include #include // clang-format off @@ -32,6 +33,7 @@ namespace lagrange::texproc { namespace { using namespace MishaK::TSP; +using Solver = lagrange::solver::SolverLDLT>; template void texture_stitching( @@ -53,9 +55,9 @@ void texture_stitching( // Construct the gradient-domain object const bool normalize = true; #if LAGRANGE_TARGET_BUILD_TYPE(DEBUG) - const bool sanity_check = true; + const bool sanity_check = options.sanity_check.value_or(true); #else - const bool sanity_check = false; + const bool sanity_check = options.sanity_check.value_or(false); #endif GradientDomain gd( options.quadrature_samples, diff --git a/modules/texproc/tests/CMakeLists.txt b/modules/texproc/tests/CMakeLists.txt index b4ced457..fa95bf50 100644 --- a/modules/texproc/tests/CMakeLists.txt +++ b/modules/texproc/tests/CMakeLists.txt @@ -11,10 +11,11 @@ # lagrange_add_test() -lagrange_include_modules(image_io scene) +lagrange_include_modules(image_io scene solver) target_link_libraries(test_lagrange_texproc PRIVATE texture_signal_processing::texture_signal_processing lagrange::image_io lagrange::scene + lagrange::solver ) diff --git a/modules/texproc/tests/test_texture_compositing_regularization.cpp b/modules/texproc/tests/test_texture_compositing_regularization.cpp new file mode 100644 index 00000000..20b1c060 --- /dev/null +++ b/modules/texproc/tests/test_texture_compositing_regularization.cpp @@ -0,0 +1,363 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +#include "../examples/io_helpers.h" +#include "../shared/shared_utils.h" +#include "../src/mesh_utils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// + +namespace { + +using Scalar = float; +using Index = uint32_t; + +std::tuple, unsigned int, unsigned int> +make_pumpkin_wrapper(const size_t width, const size_t height) +{ + auto scene_options = lagrange::io::LoadOptions(); + scene_options.stitch_vertices = true; + const auto scene = lagrange::io::load_scene( + lagrange::testing::get_data_path("corp/texproc/prepared/pumpkin.glb"), + scene_options); + + auto [mesh, _] = lagrange::scene::internal::single_mesh_from_scene(scene); + + auto wrapper = lagrange::texproc::mesh_utils::create_mesh_wrapper( + mesh, + lagrange::texproc::RequiresIndexedTexcoords::Yes, + lagrange::texproc::CheckFlippedUV::Yes); + + unsigned int padded_width = static_cast(width); + unsigned int padded_height = static_cast(height); + lagrange::texproc::mesh_utils::jitter_texture( + wrapper.texcoords, + padded_width, + padded_height, + 1e-4); + auto padding = + lagrange::texproc::mesh_utils::create_padding(wrapper, padded_width, padded_height); + padded_width += padding.width(); + padded_height += padding.height(); + + return { + std::move(wrapper), + padded_width, + padded_height, + }; +} + +std::tuple, std::vector>> +rasterize_pumpkin_views(const size_t width, const size_t height) +{ + auto scene_options = lagrange::io::LoadOptions(); + scene_options.stitch_vertices = true; + const auto scene = lagrange::io::load_scene( + lagrange::testing::get_data_path("corp/texproc/prepared/pumpkin.glb"), + scene_options); + + auto [mesh, _] = lagrange::scene::internal::single_mesh_from_scene(scene); + const auto cameras = lagrange::scene::internal::camera_transforms_from_scene(scene); + REQUIRE(cameras.size() == 16); + + std::vector views; + for (const auto kk : lagrange::range(cameras.size())) { + const auto path = lagrange::testing::get_data_path( + lagrange::format("corp/texproc/prepared/view_{:02d}.png", kk)); + const auto view = load_image(path); + if (!views.empty()) { + REQUIRE(view.extent(0) == views.front().extent(0)); + REQUIRE(view.extent(1) == views.front().extent(1)); + } + views.emplace_back(std::move(view)); + } + REQUIRE(cameras.size() == views.size()); + + auto rasterizer_options = lagrange::texproc::TextureRasterizerOptions(); + rasterizer_options.width = width; + rasterizer_options.height = height; + const auto rasterizer = lagrange::texproc::TextureRasterizer(mesh, rasterizer_options); + + std::vector> colors_and_weights(cameras.size()); + tbb::parallel_for(size_t(0), cameras.size(), [&](size_t i) { + colors_and_weights[i] = rasterizer.weighted_texture_from_render(views[i], cameras[i]); + }); + + return { + std::move(mesh), + std::move(colors_and_weights), + }; +} + +lagrange::image::experimental::Array3D composite_with_options( + const lagrange::SurfaceMesh& mesh, + const std::vector>& colors_and_weights, + const lagrange::texproc::CompositingOptions& options) +{ + using WeightedTexture = lagrange::texproc::ConstWeightedTextureView; + std::vector weighted_textures; + weighted_textures.reserve(colors_and_weights.size()); + for (const auto& [color, weight] : colors_and_weights) { + weighted_textures.emplace_back( + WeightedTexture{ + color.to_mdspan(), + weight.to_mdspan(), + }); + } + return lagrange::texproc::texture_compositing(mesh, weighted_textures, options); +} + +void require_solver_approx( + lagrange::span aa, + lagrange::span bb, + float eps_rel, + float eps_abs) +{ + REQUIRE(aa.size() == bb.size()); + double squared_error = 0; + double squared_reference = 0; + float max_absolute_error = 0; + float max_relative_error = 0; + constexpr float relative_scale_floor = 5e-2f; + for (size_t i = 0; i < aa.size(); ++i) { + const float absolute_error = std::abs(aa[i] - bb[i]); + const float scale = std::max({std::abs(aa[i]), std::abs(bb[i]), relative_scale_floor}); + max_absolute_error = std::max(max_absolute_error, absolute_error); + max_relative_error = std::max(max_relative_error, absolute_error / scale); + squared_error += static_cast(absolute_error) * absolute_error; + squared_reference += static_cast(bb[i]) * bb[i]; + } + const double root_mean_squared_error = std::sqrt(squared_error / aa.size()); + const double relative_l2_error = std::sqrt(squared_error / squared_reference); + CAPTURE(root_mean_squared_error, relative_l2_error, max_absolute_error, max_relative_error); + REQUIRE(root_mean_squared_error < eps_abs); + REQUIRE(relative_l2_error < eps_rel); + REQUIRE(max_absolute_error < 7e-1f); + REQUIRE(max_relative_error < 9e-1f); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +TEST_CASE( + "Check GradientDomain operators", + "[texproc][regularization]" LA_SLOW_DEBUG_FLAG LA_CORP_FLAG) +{ + using namespace MishaK::TSP; + + const auto pumpkin = make_pumpkin_wrapper(128, 128); + const auto& wrapper = std::get<0>(pumpkin); + const auto width = std::get<1>(pumpkin); + const auto height = std::get<2>(pumpkin); + + auto surface_corner = [&](size_t t, unsigned int k) { return wrapper.vertex_index(t, k); }; + auto surface_vertex = [&](size_t v) { return wrapper.vertex(v); }; + auto texture_corner = [&](size_t t, unsigned int k) { return wrapper.texture_index(t, k); }; + auto texture_vertex = [&](size_t v) { return wrapper.texcoord(v); }; + + GradientDomain gd( + /*quadrature=*/6, + wrapper.num_simplices(), + wrapper.num_vertices(), + wrapper.num_texcoords(), + surface_corner, + surface_vertex, + texture_corner, + texture_vertex, + width, + height, + /*normalize=*/true, + /*sanityCheck=*/false, + /*regularizationWeight=*/0.0); + + const double reg_weight = 1e-3; + + Eigen::SparseMatrix S0 = gd.stiffness(); + gd.applyLaplacianRegularization(reg_weight); + Eigen::SparseMatrix Sw = gd.stiffness(); + Eigen::SparseMatrix Sref = + lagrange::texproc::mesh_utils::laplacian_regularization(S0, reg_weight); + + REQUIRE(Sw.rows() == Sref.rows()); + REQUIRE(Sw.cols() == Sref.cols()); + + const double error = (Sw - Sref).norm(); + CAPTURE(error); + REQUIRE(error < 1e-7); +} + +TEST_CASE( + "Check HierarchicalGradientDomain operators", + "[texproc][regularization]" LA_SLOW_DEBUG_FLAG LA_CORP_FLAG) +{ + using namespace MishaK::TSP; + using Data3 = lagrange::texproc::Vector; + using Solver = lagrange::solver::SolverLDLT>; + + const auto pumpkin = make_pumpkin_wrapper(128, 128); + const auto& wrapper = std::get<0>(pumpkin); + const auto width = std::get<1>(pumpkin); + const auto height = std::get<2>(pumpkin); + + auto surface_corner = [&](size_t t, unsigned int k) { return wrapper.vertex_index(t, k); }; + auto surface_vertex = [&](size_t v) { return wrapper.vertex(v); }; + auto texture_corner = [&](size_t t, unsigned int k) { return wrapper.texture_index(t, k); }; + auto texture_vertex = [&](size_t v) { return wrapper.texcoord(v); }; + + HierarchicalGradientDomain hgd( + /*quadrature=*/6, + wrapper.num_simplices(), + wrapper.num_vertices(), + wrapper.num_texcoords(), + surface_corner, + surface_vertex, + texture_corner, + texture_vertex, + width, + height, + /*levels=*/2u, + /*normalize=*/true, + /*sanityCheck=*/false, + /*regularizationWeight=*/0.0); + + const double reg_weight = 1e-3; + + Eigen::SparseMatrix S0 = hgd.stiffness(); + hgd.applyLaplacianRegularization(reg_weight); + Eigen::SparseMatrix Sw = hgd.stiffness(); + Eigen::SparseMatrix Sref = + lagrange::texproc::mesh_utils::laplacian_regularization(S0, reg_weight); + + REQUIRE(Sw.rows() == S0.rows()); + REQUIRE(Sw.cols() == S0.cols()); + REQUIRE(Sref.rows() == S0.rows()); + REQUIRE(Sref.cols() == S0.cols()); + + const double error = (Sw - Sref).norm(); + CAPTURE(error); + REQUIRE(error < 1e-7); +} + +TEST_CASE( + "Benchmark texture compositing solvers", + "[texproc][regularization][!benchmark]" LA_CORP_FLAG) +{ + lagrange::ScopedLogLevel _(spdlog::level::critical); + + for (const size_t texture_size : {128, 256, 512}) { + const auto pumpkin_views = rasterize_pumpkin_views(texture_size, texture_size); + const auto& mesh = std::get<0>(pumpkin_views); + const auto& colors_and_weights = std::get<1>(pumpkin_views); + + const lagrange::texproc::CompositingOptions default_options; + auto direct_options = default_options; + direct_options.use_direct_solver = true; + + BENCHMARK(lagrange::format("multigrid solver ({}x{})", texture_size, texture_size)) + { + return composite_with_options(mesh, colors_and_weights, default_options); + }; + + BENCHMARK(lagrange::format("direct solver ({}x{})", texture_size, texture_size)) + { + return composite_with_options(mesh, colors_and_weights, direct_options); + }; + } +} + +TEST_CASE( + "Test multigrid solver options", + "[texproc][regularization]" LA_SLOW_DEBUG_FLAG LA_CORP_FLAG) +{ + const auto& [mesh, colors_and_weights] = rasterize_pumpkin_views(128, 128); + + lagrange::texproc::CompositingOptions options_direct; + options_direct.stiffness_regularization_weight = 1e-3; + options_direct.use_direct_solver = true; + + lagrange::texproc::CompositingOptions options_multigrid = options_direct; + REQUIRE(options_multigrid.stiffness_regularization_weight == 1e-3); + options_multigrid.use_direct_solver = false; + + constexpr float eps_rel = 2e-2f; + constexpr float eps_abs = 1e-2f; + + auto out_direct = composite_with_options(mesh, colors_and_weights, options_direct); + REQUIRE(out_direct.extent(0) == 128); + REQUIRE(out_direct.extent(1) == 128); + + SECTION("Low V-cycles") + { + options_multigrid.solver.num_v_cycles = 2; + + auto out_multigrid = composite_with_options(mesh, colors_and_weights, options_multigrid); + REQUIRE(out_multigrid.extent(0) == 128); + REQUIRE(out_multigrid.extent(1) == 128); + + require_solver_approx( + lagrange::span(out_direct.data(), out_direct.size()), + lagrange::span(out_multigrid.data(), out_multigrid.size()), + eps_rel, + eps_abs); + } + + SECTION("Defaults") + { + REQUIRE(options_multigrid.solver.num_v_cycles == 4); + + auto out_multigrid = composite_with_options(mesh, colors_and_weights, options_multigrid); + REQUIRE(out_multigrid.extent(0) == 128); + REQUIRE(out_multigrid.extent(1) == 128); + + require_solver_approx( + lagrange::span(out_direct.data(), out_direct.size()), + lagrange::span(out_multigrid.data(), out_multigrid.size()), + eps_rel, + eps_abs); + } + + SECTION("High V-cycles") + { + options_multigrid.solver.num_v_cycles = 6; + + auto out_multigrid = composite_with_options(mesh, colors_and_weights, options_multigrid); + REQUIRE(out_multigrid.extent(0) == 128); + REQUIRE(out_multigrid.extent(1) == 128); + + require_solver_approx( + lagrange::span(out_direct.data(), out_direct.size()), + lagrange::span(out_multigrid.data(), out_multigrid.size()), + eps_rel, + eps_abs); + } +} diff --git a/modules/volume/examples/CMakeLists.txt b/modules/volume/examples/CMakeLists.txt index eda4f235..81943a94 100644 --- a/modules/volume/examples/CMakeLists.txt +++ b/modules/volume/examples/CMakeLists.txt @@ -32,6 +32,10 @@ target_link_libraries(voxelize_gui lagrange::io CLI11::CLI11 ) +if(TARGET OpenVDB::nanovdb) + target_link_libraries(voxelize_gui OpenVDB::nanovdb) + target_compile_definitions(voxelize_gui PRIVATE NANOVDB_ENABLED) +endif() lagrange_add_example(fill_with_spheres fill_with_spheres.cpp) target_link_libraries(fill_with_spheres lagrange::volume lagrange::io CLI11::CLI11) diff --git a/modules/volume/examples/grid_viewer.cpp b/modules/volume/examples/grid_viewer.cpp index 13f58c54..e226d44d 100644 --- a/modules/volume/examples/grid_viewer.cpp +++ b/modules/volume/examples/grid_viewer.cpp @@ -38,19 +38,26 @@ int main(int argc, char** argv) struct { std::vector inputs; + bool show_isocontour = true; + bool apply_zup = true; } args; CLI::App app{argv[0]}; app.option_defaults()->always_capture_default(); app.add_option("input", args.inputs, "Input grids.")->required()->check(CLI::ExistingFile); + app.add_flag( + "--isocontour,!--no-isocontour", + args.show_isocontour, + "Enable isosurface extraction at startup."); + app.add_flag("--z-up,!--no-z-up", args.apply_zup, "Use Z-up orientation instead of Y-up."); CLI11_PARSE(app, argc, argv) spdlog::set_level(spdlog::level::debug); + openvdb::initialize(); // TODO: Handle both float and double auto load_grid = [&](fs::path input) -> ConstFloatGridPtr { if (input.extension() == ".vdb") { - openvdb::initialize(); openvdb::io::File file(input.string()); file.open(); auto grids_ptr = file.getGrids(); @@ -76,11 +83,14 @@ int main(int argc, char** argv) ImGui::Spectrum::StyleColorsSpectrum(); ImGui::Spectrum::LoadFont(); }; + if (args.apply_zup) { + polyscope::view::setUpDir(polyscope::UpDir::ZUp); + } polyscope::init(); for (const auto& input : args.inputs) { auto grid = load_grid(input); - register_grid(input.stem().string(), *grid); + register_grid(input.stem().string(), *grid, args.show_isocontour); } polyscope::show(); diff --git a/modules/volume/examples/register_grid.h b/modules/volume/examples/register_grid.h index 22dfdb8c..7f6faed6 100644 --- a/modules/volume/examples/register_grid.h +++ b/modules/volume/examples/register_grid.h @@ -30,11 +30,12 @@ using FloatGrid = lagrange::volume::Grid; -void register_grid(std::string_view name, const FloatGrid& grid) +void register_grid(std::string_view name, const FloatGrid& grid, bool isocontour = false) { - auto bbox_index = grid.evalActiveVoxelBoundingBox(); + const auto bbox_index = grid.evalActiveVoxelBoundingBox(); la_runtime_assert(!bbox_index.empty(), "Grid has no active voxels."); - auto bbox_world = grid.transform().indexToWorld(bbox_index); + const auto bbox_world = grid.transform().indexToWorld(bbox_index); + const auto voxel_size = grid.transform().voxelSize(); uint32_t dimX = bbox_index.dim().x(); uint32_t dimY = bbox_index.dim().y(); @@ -78,14 +79,16 @@ void register_grid(std::string_view name, const FloatGrid& grid) } } }); - lagrange::logger().info( - "Registered {} voxels. Min corner: {}, {}, {}", - num_voxels, - bbox_min.x, - bbox_min.y, - bbox_min.z); + lagrange::logger().debug("num_voxels {}", num_voxels); + lagrange::logger().debug("bbox_low {} {} {}", bbox_min.x, bbox_min.y, bbox_min.z); + lagrange::logger().debug("bbox_high {} {} {}", bbox_max.x, bbox_max.y, bbox_max.z); + lagrange::logger().debug("voxel_size {} {} {}", voxel_size.x(), voxel_size.y(), voxel_size.z()); polyscope::VolumeGridNodeScalarQuantity* ps_scalars = ps_grid->addNodeScalarQuantity("values", std::make_tuple(values.data(), num_voxels)); ps_scalars->setEnabled(true); + if (isocontour) { + ps_scalars->setIsosurfaceVizEnabled(true); + ps_scalars->setGridcubeVizEnabled(false); + } } diff --git a/modules/volume/examples/voxelize_cli.cpp b/modules/volume/examples/voxelize_cli.cpp index 29f4af9b..cf1eed94 100644 --- a/modules/volume/examples/voxelize_cli.cpp +++ b/modules/volume/examples/voxelize_cli.cpp @@ -21,6 +21,7 @@ // clang-format off #include #include +#include #include #include // clang-format on @@ -69,12 +70,12 @@ int main(int argc, char** argv) CLI11_PARSE(app, argc, argv) spdlog::set_level(spdlog::level::debug); + openvdb::initialize(); auto grid = [&]() -> FloatGrid::Ptr { const auto& path = args.input; if (path.extension() == ".vdb") { lagrange::logger().info("Loading volume from OpenVDB grid: {}", path.string()); - openvdb::initialize(); openvdb::io::File file(path.string()); file.open(); auto grids_ptr = file.getGrids(); @@ -92,6 +93,14 @@ int main(int argc, char** argv) const std::string& name = grid_ptr->getName(); lagrange::logger().info("Using grid: {}", name); return openvdb::gridPtrCast(grid_ptr); + } else if (path.extension() == ".nvdb") { +#ifdef NANOVDB_ENABLED + auto handle = nanovdb::io::readGrid(path.string()); + auto grid_ptr = nanovdb::tools::nanoToOpenVDB(handle); + return openvdb::gridPtrCast(grid_ptr); +#else + throw lagrange::Error("NanoVDB support is not enabled in this build."); +#endif } else { lagrange::logger().info("Loading input mesh: {}", path.string()); auto mesh = lagrange::io::load_mesh(path); diff --git a/modules/volume/examples/voxelize_gui.cpp b/modules/volume/examples/voxelize_gui.cpp index 4830b999..fb402bc1 100644 --- a/modules/volume/examples/voxelize_gui.cpp +++ b/modules/volume/examples/voxelize_gui.cpp @@ -27,6 +27,15 @@ #include // clang-format on +#ifdef NANOVDB_ENABLED +// clang-format off +#include +#include +#include +#include +// clang-format on +#endif + #include #include @@ -77,12 +86,12 @@ int main(int argc, char** argv) CLI11_PARSE(app, argc, argv) spdlog::set_level(spdlog::level::debug); + openvdb::initialize(); auto grid = [&]() -> FloatGrid::Ptr { const auto& path = args.input; if (path.extension() == ".vdb") { lagrange::logger().info("Loading volume from OpenVDB grid: {}", path.string()); - openvdb::initialize(); openvdb::io::File file(path.string()); file.open(); auto grids_ptr = file.getGrids(); @@ -100,6 +109,14 @@ int main(int argc, char** argv) const std::string& name = grid_ptr->getName(); lagrange::logger().info("Using grid: {}", name); return openvdb::gridPtrCast(grid_ptr); + } else if (path.extension() == ".nvdb") { +#ifdef NANOVDB_ENABLED + auto handle = nanovdb::io::readGrid(path.string()); + auto grid_ptr = nanovdb::tools::nanoToOpenVDB(handle); + return openvdb::gridPtrCast(grid_ptr); +#else + throw lagrange::Error("NanoVDB support is not enabled in this build."); +#endif } else { lagrange::logger().info("Loading input mesh: {}", path.string()); auto mesh = lagrange::io::load_mesh(path); diff --git a/modules/volume/include/lagrange/volume/SmoothedMeshGradientField.h b/modules/volume/include/lagrange/volume/SmoothedMeshGradientField.h new file mode 100644 index 00000000..0931e230 --- /dev/null +++ b/modules/volume/include/lagrange/volume/SmoothedMeshGradientField.h @@ -0,0 +1,82 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#pragma once + +#include +#include + +#include + +// clang-format off +#include +#include +#include +#include +// clang-format on + +namespace lagrange::volume { + +/// +/// A world-space direction field obtained by Gaussian-filtering a winding-number signed distance +/// field and differentiating the filtered scalar field. +/// +class LA_VOLUME_API SmoothedMeshGradientField +{ +public: + using Grid = openvdb::Vec3SGrid; + using Accessor = Grid::ConstAccessor; + + /// + /// Construct a smoothed gradient field for a triangle surface mesh. + /// + /// @param[in] mesh Input mesh. Its winding-number sign defines the SDF interior. + /// @param[in] voxel_size Grid voxel size in world units; must be positive. + /// @param[in] epsilon Gaussian smoothing scale in world units; must be positive. + /// @param[in] query_reach Maximum expected query distance from the surface in world units. + /// + /// @tparam Scalar Mesh scalar type. + /// @tparam Index Mesh index type. + /// + template + SmoothedMeshGradientField( + const SurfaceMesh& mesh, + double voxel_size, + double epsilon, + double query_reach); + + /// Return a read-only shared handle to the underlying OpenVDB grid. + Grid::ConstPtr grid() const { return m_grid; } + + /// Return a read-only accessor to the underlying OpenVDB grid. + Accessor accessor() const { return m_grid->getConstAccessor(); } + + /// + /// Sample and normalize the gradient at a world-space point. + /// + /// @param[in] position Query position. + /// @param[out] direction_out Outward unit direction when sampling succeeds. + /// @param[in] accessor Optional grid accessor to cache repeated queries (one per thread). + /// + /// @return True for a finite, non-degenerate in-band gradient; false when the caller should + /// use an analytic fallback. + /// + bool sample_direction( + const Eigen::Vector3d& position, + Eigen::Vector3d& direction_out, + Accessor* accessor = nullptr) const; + +private: + // The grid is never mutated after construction. + Grid::Ptr m_grid; +}; + +} // namespace lagrange::volume diff --git a/modules/volume/include/lagrange/volume/mesh_to_volume.h b/modules/volume/include/lagrange/volume/mesh_to_volume.h index f46cf19c..281421eb 100644 --- a/modules/volume/include/lagrange/volume/mesh_to_volume.h +++ b/modules/volume/include/lagrange/volume/mesh_to_volume.h @@ -14,7 +14,7 @@ #include #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif @@ -41,6 +41,12 @@ struct MeshToVolumeOptions /// Method used to compute the sign of the distance field that determines interior voxels. Sign signing_method = Sign::FloodFill; + + /// Half-band width in voxels outside the surface. + float exterior_bandwidth = 3.0f; + + /// Half-band width in voxels inside the surface. + float interior_bandwidth = 3.0f; }; /// diff --git a/modules/volume/include/lagrange/volume/volume_to_mesh.h b/modules/volume/include/lagrange/volume/volume_to_mesh.h index 5be99be4..b6d8d35c 100644 --- a/modules/volume/include/lagrange/volume/volume_to_mesh.h +++ b/modules/volume/include/lagrange/volume/volume_to_mesh.h @@ -14,7 +14,7 @@ #include #include -#ifdef LAGRANGE_ENABLE_LEGACY_FUNCTIONS +#ifdef LAGRANGE_ENABLE_LEGACY_HEADERS #include #endif diff --git a/modules/volume/python/src/volume.cpp b/modules/volume/python/src/volume.cpp index 7bb04132..0af80586 100644 --- a/modules/volume/python/src/volume.cpp +++ b/modules/volume/python/src/volume.cpp @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +#include #include "GridWrapper.h" #include @@ -887,10 +888,14 @@ void populate_volume_module(nb::module_& m) [](const SurfaceMesh& mesh, double voxel_size, Sign signing_method, + float exterior_bandwidth, + float interior_bandwidth, nb::type_object dtype) { lagrange::volume::MeshToVolumeOptions options; options.voxel_size = voxel_size; options.signing_method = signing_method; + options.exterior_bandwidth = exterior_bandwidth; + options.interior_bandwidth = interior_bandwidth; auto run = [&](auto&& grid_scalar) -> GridWrapper { using GridScalar = std::decay_t; @@ -908,14 +913,19 @@ void populate_volume_module(nb::module_& m) } }, "mesh"_a, + nb::kw_only(), "voxel_size"_a = MeshToVolumeOptions().voxel_size, "signing_method"_a = MeshToVolumeOptions().signing_method, + "exterior_bandwidth"_a = MeshToVolumeOptions().exterior_bandwidth, + "interior_bandwidth"_a = MeshToVolumeOptions().interior_bandwidth, "dtype"_a = float_type, R"(Convert a triangle mesh to a sparse voxel grid, writing the result to a file. :param mesh: Input mesh. Must be a triangle mesh, a quad-mesh, or a quad-dominant mesh. :param voxel_size: Voxel size. Negative means relative to bbox diagonal (`vs -> -vs * bbox_diag`). :param signing_method: Method used to compute the sign of the distance field. +:param exterior_bandwidth: Exterior bandwidth of the narrow band. +:param interior_bandwidth: Interior bandwidth of the narrow band. :param dtype: Scalar type of the output grid (float32 or float64). :returns: Generated sparse voxel grid.)"); diff --git a/modules/volume/src/SmoothedMeshGradientField.cpp b/modules/volume/src/SmoothedMeshGradientField.cpp new file mode 100644 index 00000000..b758fdb1 --- /dev/null +++ b/modules/volume/src/SmoothedMeshGradientField.cpp @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include + +#include +#include +#include + +#include +#include +#include + +// clang-format off +#include +#include +#include +#include +#include +// clang-format on + +namespace lagrange::volume { + +template +SmoothedMeshGradientField::SmoothedMeshGradientField( + const SurfaceMesh& mesh, + double voxel_size, + double epsilon, + double query_reach) +{ + la_runtime_assert(voxel_size > 0, "voxel_size must be positive"); + la_runtime_assert(epsilon > 0, "epsilon must be positive"); + la_runtime_assert(query_reach >= 0, "query_reach must be nonnegative"); + + openvdb::initialize(); + const int filter_width = std::max(1, static_cast(std::round(epsilon / voxel_size))); + + // - Gaussian filtering performs four separable box-filters (4*w). + // - Central-difference gradient stencil requires one extra voxel on one axis. + // - Trilinear interpolation requires one extra voxel along each axis. + // + // Therefore, a conservative bandwidth estimate would be B_ext = B + (4*w + 2)*√3 + // + // A tighter bound could use ‖(4w+2, 4w+1, 4w+1)‖, since the gradient stencil is only applied + // along one axis. For simplicity, and because axis-aligned correctness is enough here, we drop + // the *√3 factor and just add 4w+2. + const float band = std::ceil(query_reach / voxel_size) + 4.0f * filter_width + 2.0f; + + MeshToVolumeOptions options; + options.voxel_size = voxel_size; + options.signing_method = MeshToVolumeOptions::Sign::WindingNumber; + options.exterior_bandwidth = band; + options.interior_bandwidth = band; + auto sdf = mesh_to_volume(mesh, options); + + openvdb::tools::Filter filter(*sdf); + filter.gaussian(filter_width, 1); + m_grid = openvdb::tools::gradient(*sdf); + + // TODO: Consider masking & pruning the grid back to the desired narrow-band if storage is a + // concern. +} + +bool SmoothedMeshGradientField::sample_direction( + const Eigen::Vector3d& position, + Eigen::Vector3d& direction_out, + Accessor* accessor_ptr) const +{ + using Sampler = openvdb::tools::GridSampler; + + const Accessor& accessor = accessor_ptr ? *accessor_ptr : m_grid->getConstAccessor(); + const openvdb::Vec3d world_position(position.x(), position.y(), position.z()); + const openvdb::Coord base = openvdb::Coord::floor(m_grid->worldToIndex(world_position)); + for (int x = 0; x < 2; ++x) { + for (int y = 0; y < 2; ++y) { + for (int z = 0; z < 2; ++z) { + const openvdb::Coord corner(base.x() + x, base.y() + y, base.z() + z); + if (!accessor.isValueOn(corner)) return false; + } + } + } + + const Sampler sampler(accessor, m_grid->transform()); + const openvdb::Vec3s gradient = sampler.wsSample(world_position); + const Eigen::Vector3d direction(gradient.x(), gradient.y(), gradient.z()); + const double norm = direction.norm(); + // Vec3SGrid stores floats; values at float numerical zero cannot define a direction. + if (!std::isfinite(norm) || norm <= std::numeric_limits::epsilon()) { + return false; + } + direction_out = direction / norm; + return direction_out.allFinite(); +} + +#define LA_X_construct(_, Scalar, Index) \ + template SmoothedMeshGradientField::SmoothedMeshGradientField( \ + const SurfaceMesh&, \ + double, \ + double, \ + double); +LA_SURFACE_MESH_X(construct, 0) + +} // namespace lagrange::volume diff --git a/modules/volume/src/mesh_to_volume.cpp b/modules/volume/src/mesh_to_volume.cpp index 5fb79c7a..c0467cee 100644 --- a/modules/volume/src/mesh_to_volume.cpp +++ b/modules/volume/src/mesh_to_volume.cpp @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +#include #include #include @@ -180,15 +181,13 @@ auto mesh_to_volume(const SurfaceMesh& mesh_, const MeshToVolumeO // Use winding number to sign the distance field logger().debug("Computing distance field with winding number signing"); - const float exterior_bandwidth = 3.0f; - const float interior_bandwidth = 3.0f; openvdb::util::NullInterrupter null_interrupter; grid = openvdb::tools::meshToVolume, MeshAdapterType>( null_interrupter, adapter, *transform, - exterior_bandwidth, - interior_bandwidth, + options.exterior_bandwidth, + options.interior_bandwidth, 0 /* flags */, nullptr /* polygonIndexGrid */, [transform, &engine](openvdb::Coord ijk) { @@ -202,18 +201,18 @@ auto mesh_to_volume(const SurfaceMesh& mesh_, const MeshToVolumeO openvdb::tools::EVAL_EVERY_VOXEL); } else if (options.signing_method == MeshToVolumeOptions::Sign::Unsigned) { // Compute unsigned distance field - const float exterior_bandwidth = 3.0f; - const float interior_bandwidth = 3.0f; grid = openvdb::tools::meshToVolume, MeshAdapterType>( adapter, *transform, - exterior_bandwidth, - interior_bandwidth, + options.exterior_bandwidth, + options.interior_bandwidth, openvdb::tools::UNSIGNED_DISTANCE_FIELD); } else { grid = openvdb::tools::meshToVolume, MeshAdapterType>( adapter, - *transform); + *transform, + options.exterior_bandwidth, + options.interior_bandwidth); } } catch (const openvdb::ArithmeticError&) { logger().error("Voxel size too small: {}", voxel_size); diff --git a/modules/volume/tests/CMakeLists.txt b/modules/volume/tests/CMakeLists.txt index fcd7df1e..826ed8c3 100644 --- a/modules/volume/tests/CMakeLists.txt +++ b/modules/volume/tests/CMakeLists.txt @@ -10,5 +10,5 @@ # governing permissions and limitations under the License. # lagrange_add_test() -lagrange_include_modules(scene) -target_link_libraries(test_lagrange_volume PRIVATE lagrange::scene) +lagrange_include_modules(scene primitive) +target_link_libraries(test_lagrange_volume PRIVATE lagrange::scene lagrange::primitive) diff --git a/modules/volume/tests/test_smoothed_mesh_gradient_field.cpp b/modules/volume/tests/test_smoothed_mesh_gradient_field.cpp new file mode 100644 index 00000000..c518f9ab --- /dev/null +++ b/modules/volume/tests/test_smoothed_mesh_gradient_field.cpp @@ -0,0 +1,254 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +lagrange::SurfaceMesh make_sheet() +{ + lagrange::SurfaceMesh mesh; + mesh.add_vertex({-1, -1, 0}); + mesh.add_vertex({1, -1, 0}); + mesh.add_vertex({1, 1, 0}); + mesh.add_vertex({-1, 1, 0}); + mesh.add_triangle(0, 1, 2); + mesh.add_triangle(0, 2, 3); + return mesh; +} + +lagrange::SurfaceMesh make_box() +{ + lagrange::SurfaceMesh mesh; + for (const auto& p : std::vector{ + {-1, -1, -1}, + {1, -1, -1}, + {1, 1, -1}, + {-1, 1, -1}, + {-1, -1, 1}, + {1, -1, 1}, + {1, 1, 1}, + {-1, 1, 1}}) { + mesh.add_vertex({p.x(), p.y(), p.z()}); + } + for (const auto& f : std::vector{ + {0, 2, 1}, + {0, 3, 2}, + {4, 5, 6}, + {4, 6, 7}, + {0, 1, 5}, + {0, 5, 4}, + {3, 7, 6}, + {3, 6, 2}, + {0, 4, 7}, + {0, 7, 3}, + {1, 2, 6}, + {1, 6, 5}}) { + mesh.add_triangle(f.x(), f.y(), f.z()); + } + return mesh; +} + +lagrange::SurfaceMesh make_oriented_box( + const Eigen::Vector3d& normal, + const Eigen::Vector3d& translation, + double half_extent) +{ + auto mesh = make_box(); + const Eigen::Vector3d x = normal.normalized(); + const Eigen::Vector3d y = x.cross(Eigen::Vector3d::UnitZ()).normalized(); + const Eigen::Vector3d z = x.cross(y); + Eigen::Matrix3d rotation; + rotation.col(0) = x; + rotation.col(1) = y; + rotation.col(2) = z; + for (uint32_t v = 0; v < mesh.get_num_vertices(); ++v) { + const auto p = mesh.get_position(v); + const Eigen::Vector3d local(p[0], p[1], p[2]); + const Eigen::Vector3d world = translation + half_extent * rotation * local; + auto output = mesh.ref_position(v); + std::copy(world.data(), world.data() + 3, output.begin()); + } + return mesh; +} + +} // namespace + +TEST_CASE("smoothed mesh gradient: accurate at query reach", "[volume][smoothed_gradient]") +{ + auto sheet = make_sheet(); + constexpr double query_reach = 0.2; + lagrange::volume::SmoothedMeshGradientField field(sheet, 0.05, 0.2, query_reach); + Eigen::Vector3d direction; + REQUIRE(field.grid()); + REQUIRE(field.sample_direction({0, 0, query_reach}, direction)); + REQUIRE(direction.dot(Eigen::Vector3d::UnitZ()) > 0.99); +} + +TEST_CASE( + "smoothed mesh gradient: accurate directions at query reach", + "[volume][smoothed_gradient]") +{ + constexpr double voxel_size = 0.1; + constexpr double epsilon = 0.2; + constexpr double query_reach = 0.2; + constexpr double half_extent = 2.0; + constexpr int filter_width = 2; + const double reference_reach = + query_reach + std::sqrt(3.0) * (4.0 * filter_width + 2.0) * voxel_size; + + const std::array normals = { + Eigen::Vector3d(1, 2, 3).normalized(), + Eigen::Vector3d(-2, 3, 1).normalized()}; + const std::array translations = { + Eigen::Vector3d(0.137, -0.219, 0.083), + Eigen::Vector3d(-0.173, 0.091, 0.227)}; + + for (size_t i = 0; i < normals.size(); ++i) { + const Eigen::Vector3d& normal = normals[i]; + CAPTURE(i, normal.x(), normal.y(), normal.z()); + REQUIRE(normal.cwiseAbs().minCoeff() > 0.25); + auto box = make_oriented_box(normal, translations[i], half_extent); + lagrange::volume::SmoothedMeshGradientField subject(box, voxel_size, epsilon, query_reach); + lagrange::volume::SmoothedMeshGradientField reference( + box, + voxel_size, + epsilon, + reference_reach); + const Eigen::Vector3d face_center = translations[i] + half_extent * normal; + + for (double side : {-1.0, 1.0}) { + const Eigen::Vector3d position = face_center + side * query_reach * normal; + Eigen::Vector3d subject_direction; + Eigen::Vector3d reference_direction; + CAPTURE(side, position.x(), position.y(), position.z()); + REQUIRE(subject.sample_direction(position, subject_direction)); + REQUIRE(reference.sample_direction(position, reference_direction)); + REQUIRE(std::abs(subject_direction.norm() - 1.0) < 1e-6); + REQUIRE(std::abs(reference_direction.norm() - 1.0) < 1e-6); + REQUIRE(subject_direction.dot(reference_direction) > 0.99999); + REQUIRE(subject_direction.dot(normal) > 0.995); + } + } +} + +TEST_CASE("smoothed mesh gradient: concurrent sampling", "[volume][smoothed_gradient]") +{ + auto box = make_box(); + lagrange::volume::SmoothedMeshGradientField field(box, 0.05, 0.2, 0.2); + Eigen::Vector3d expected; + REQUIRE(field.sample_direction({0, 0, 1.05}, expected)); + + constexpr size_t num_threads = 8; + std::array passed{}; + std::array threads; + for (size_t i = 0; i < num_threads; ++i) { + threads[i] = std::thread([&, i] { + Eigen::Vector3d actual; + passed[i] = + field.sample_direction({0, 0, 1.05}, actual) && (actual - expected).norm() < 1e-12; + }); + } + for (auto& thread : threads) thread.join(); + for (bool result : passed) REQUIRE(result); +} + +TEST_CASE("smoothed mesh gradient: flat and sphere orientation", "[volume][smoothed_gradient]") +{ + auto box = make_box(); + lagrange::volume::SmoothedMeshGradientField box_field(box, 0.05, 0.2, 0.2); + Eigen::Vector3d direction; + REQUIRE(box_field.sample_direction({0, 0, 1.05}, direction)); + REQUIRE(direction.dot(Eigen::Vector3d::UnitZ()) > 0.99); + + lagrange::primitive::SphereOptions options; + options.radius = 1; + options.num_longitude_sections = 48; + options.num_latitude_sections = 48; + options.triangulate = true; + auto sphere = lagrange::primitive::generate_sphere(options); + lagrange::volume::SmoothedMeshGradientField sphere_field(sphere, 0.04, 0.12, 0.2); + for (const Eigen::Vector3d p : + {Eigen::Vector3d(1.05, 0, 0), Eigen::Vector3d(0, 1.05, 0), Eigen::Vector3d(0, 0, 1.05)}) { + CAPTURE(p.x(), p.y(), p.z()); + REQUIRE(sphere_field.sample_direction(p, direction)); + REQUIRE(direction.dot(p.normalized()) > 0.98); + } + + const Eigen::Vector3d interior_point(0.95, 0, 0); + REQUIRE(sphere_field.sample_direction(interior_point, direction)); + REQUIRE(direction.dot(Eigen::Vector3d::UnitX()) > 0.98); +} + +TEST_CASE("smoothed mesh gradient: feature rounding follows epsilon", "[volume][smoothed_gradient]") +{ + auto box = make_box(); + lagrange::volume::SmoothedMeshGradientField narrow(box, 0.03, 0.09, 0.2); + lagrange::volume::SmoothedMeshGradientField wide(box, 0.03, 0.3, 0.2); + const Eigen::Vector3d p(1.06, 0, 0.82); + Eigen::Vector3d narrow_direction; + Eigen::Vector3d wide_direction; + REQUIRE(narrow.sample_direction(p, narrow_direction)); + REQUIRE(wide.sample_direction(p, wide_direction)); + REQUIRE(wide_direction.z() > narrow_direction.z() + 0.08); +} + +TEST_CASE( + "smoothed mesh gradient: sharp feature continuity and band", + "[volume][smoothed_gradient]") +{ + auto box = make_box(); + lagrange::volume::SmoothedMeshGradientField field(box, 0.04, 0.24, 0.2); + + std::vector directions; + for (int i = 0; i <= 20; ++i) { + const double t = static_cast(i) / 20.0; + const Eigen::Vector3d p(1.08 - 0.18 * t, 0, 0.9 + 0.18 * t); + Eigen::Vector3d direction; + CAPTURE(i, p.x(), p.z()); + REQUIRE(field.sample_direction(p, direction)); + directions.push_back(direction); + } + REQUIRE(directions.front().dot(Eigen::Vector3d::UnitX()) > 0.85); + REQUIRE(directions.back().dot(Eigen::Vector3d::UnitZ()) > 0.85); + for (size_t i = 1; i < directions.size(); ++i) { + REQUIRE(directions[i - 1].dot(directions[i]) > 0.97); + } + + Eigen::Vector3d direction; + REQUIRE_FALSE(field.sample_direction({4, 0, 0}, direction)); + + openvdb::Coord edge; + bool found_edge = false; + for (auto iter = field.grid()->cbeginValueOn(); iter; ++iter) { + if (iter.getValue().lengthSqr() <= 1e-8f) continue; + if (!found_edge || iter.getCoord().x() > edge.x()) { + edge = iter.getCoord(); + found_edge = true; + } + } + REQUIRE(found_edge); + const openvdb::Vec3d edge_index(edge.x() + 0.75, edge.y(), edge.z()); + const openvdb::Vec3d edge_world = field.grid()->indexToWorld(edge_index); + REQUIRE_FALSE( + field.sample_direction({edge_world.x(), edge_world.y(), edge_world.z()}, direction)); +} diff --git a/modules/volume/tests/test_voxelization.cpp b/modules/volume/tests/test_voxelization.cpp index 34bfbf1d..50c5caef 100644 --- a/modules/volume/tests/test_voxelization.cpp +++ b/modules/volume/tests/test_voxelization.cpp @@ -70,6 +70,41 @@ TEST_CASE("voxelization: winding number", "[volume]") REQUIRE(mesh3.get_num_facets() > mesh2.get_num_facets()); } +TEST_CASE("mesh_to_volume: configurable narrow band", "[volume]") +{ + auto mesh = lagrange::to_surface_mesh_copy(*lagrange::create_cube()); + + lagrange::volume::MeshToVolumeOptions options; + options.voxel_size = 0.1; + const auto narrow = lagrange::volume::mesh_to_volume(mesh, options); + + SECTION("wide exterior") + { + options.exterior_bandwidth = 8.0f; + const auto wide = lagrange::volume::mesh_to_volume(mesh, options); + + const auto narrow_bbox = narrow->evalActiveVoxelBoundingBox(); + const auto wide_bbox = wide->evalActiveVoxelBoundingBox(); + REQUIRE(wide->activeVoxelCount() > narrow->activeVoxelCount()); + REQUIRE( + wide_bbox.max().z() - wide_bbox.min().z() > + narrow_bbox.max().z() - narrow_bbox.min().z()); + } + + SECTION("wide interior") + { + options.interior_bandwidth = 8.0f; + const auto wide = lagrange::volume::mesh_to_volume(mesh, options); + + REQUIRE(wide->activeVoxelCount() > narrow->activeVoxelCount()); + const openvdb::Vec3d world_position(0.5, 0, 0); + const auto coord = openvdb::Coord::round(wide->worldToIndex(world_position)); + REQUIRE_FALSE(narrow->getConstAccessor().isValueOn(coord)); + REQUIRE(wide->getConstAccessor().isValueOn(coord)); + REQUIRE(wide->getConstAccessor().getValue(coord) < 0); + } +} + TEST_CASE("mesh_to_volume: edge facets", "[volume]") { using Scalar = float; diff --git a/pyproject.toml b/pyproject.toml index 47e33f43..e0e2773e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] build-backend = "scikit_build_core.build" requires = [ + "nanobind-backend>=1.0", # needed at build time for stub generation "numpy>=1.25", # needed at build time for default dtype args "scikit-build-core==0.11.6", "typing-extensions~=4.1", @@ -30,6 +31,7 @@ dynamic = [ "version" ] dependencies = [ "colorama>=0.4.4; platform_system!='Windows'", "colorama>=0.4.6; platform_system=='Windows'", + "nanobind-backend>=1.0", "numpy>=1.25", "scipy>=1.13.0", ] @@ -70,6 +72,7 @@ ninja.version = ">=1.11.1" ninja.make-fallback = false install.components = [ "Lagrange_Python_Runtime" ] wheel.packages = [ "modules/python/lagrange" ] +wheel.py-api = "cp310" build-dir = "build-python" editable.rebuild = false metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"