Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func_*.json
/.idea
/.clangd
/.cache
/.intent
/.scout
/3rdparty
/cmake-build-*
/build*
Expand Down
2 changes: 1 addition & 1 deletion cmake/lagrange/lagrangeMklModules.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
anorigami;baking;cad_io;contouring;decal;deformers;filtering;meshproc;polyddg;quadrangulation;solver;texproc
anorigami;baking;cad_io;contouring;decal;deformers;filtering;meshproc;polyddg;quadgen;quadrangulation;solver;texproc
28 changes: 27 additions & 1 deletion cmake/lagrange/lagrange_add_test.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ function(lagrange_add_test)
include(FetchContent)
target_code_coverage(${test_target} AUTO ALL EXCLUDE "${FETCHCONTENT_BASE_DIR}/*")

# TSan suppression file to be passed to catch_discover_tests
# Sanitizer suppression files to be passed to catch_discover_tests
set(LAGRANGE_TESTS_ENVIRONMENT
"TSAN_OPTIONS=suppressions=${PROJECT_SOURCE_DIR}/.github/tsan.suppressions.ini"
"LSAN_OPTIONS=suppressions=${PROJECT_SOURCE_DIR}/.github/lsan.suppressions.ini"
"ASAN_SAVE_DUMPS=${module_name}.dmp"
)

Expand All @@ -67,6 +68,31 @@ function(lagrange_add_test)
set(_discovery_mode POST_BUILD)
endif()

# On Linux, ThreadSanitizer can fail with "FATAL: ThreadSanitizer: unexpected memory mapping" on
# kernels with high ASLR entropy (e.g. kernel 6.x with vm.mmap_rnd_bits > 28). LLVM 18+ / GCC 15+
# include an auto-retry that re-executes the process with ASLR disabled
# (https://github.com/llvm/llvm-project/pull/78351). For older compilers, we work around this by
# wrapping test discovery and execution with `setarch --addr-no-randomize`.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND USE_SANITIZER MATCHES "([Tt]hread)")
set(_tsan_needs_aslr_workaround FALSE)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "15")
set(_tsan_needs_aslr_workaround TRUE)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "18")
set(_tsan_needs_aslr_workaround TRUE)
endif()
if(_tsan_needs_aslr_workaround)
find_program(_setarch setarch)
if(_setarch)
set_target_properties(${test_target} PROPERTIES
CROSSCOMPILING_EMULATOR "${_setarch};${CMAKE_HOST_SYSTEM_PROCESSOR};--addr-no-randomize"
)
set(_discovery_mode PRE_TEST)
else()
message(WARNING "setarch not found — TSan tests may fail with 'unexpected memory mapping' on high-ASLR kernels")
endif()
endif()
endif()

if(LAGRANGE_TOPLEVEL_PROJECT AND NOT USE_SANITIZER MATCHES "([Tt]hread)")
catch_discover_tests(${test_target}
REPORTER junit
Expand Down
2 changes: 1 addition & 1 deletion cmake/lagrange/lagrange_download_data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function(lagrange_download_data)
PREFIX "${FETCHCONTENT_BASE_DIR}/lagrange-test-data"
SOURCE_DIR ${LAGRANGE_DATA_FOLDER}
GIT_REPOSITORY https://github.com/adobe/lagrange-test-data.git
GIT_TAG a8331c8a1fd9c114abdcc3d5ed8ea5f7f8058c99
GIT_TAG 009e99371d7495f7ad81d42d32080ba8afd4d4cd
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
Expand Down
8 changes: 8 additions & 0 deletions cmake/recipes/external/blosc.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ block()
set(BUILD_BENCHMARKS OFF)
set(PREFER_EXTERNAL_ZLIB ON)
set(ZLIB_FOUND ON)
set(PREFER_EXTERNAL_ZSTD ON)
set(ZSTD_FOUND ON)

ignore_package(ZLIB)
include(miniz)
Expand All @@ -73,6 +75,11 @@ block()
set(ZLIB_INCLUDE_DIR "")
set(ZLIB_LIBRARY ZLIB::ZLIB)

ignore_package(Zstd)
include(zstd)
set(ZSTD_INCLUDE_DIR "")
set(ZSTD_LIBRARY zstd::libzstd)

# Copy miniz.h as zlib.h to have blosc use miniz symbols (which are aliased through #define in miniz.h)
FetchContent_GetProperties(miniz)
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include")
Expand All @@ -94,6 +101,7 @@ block()
endforeach()

unignore_package(ZLIB)
unignore_package(Zstd)
endblock()

set_target_properties(blosc_static PROPERTIES POSITION_INDEPENDENT_CODE ON)
Expand Down
8 changes: 5 additions & 3 deletions cmake/recipes/external/zstd.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ if(NOT TARGET zstd::libzstd)
endif()
endif()

if(TARGET libzstd_static)
set_target_properties(libzstd_static PROPERTIES FOLDER "third_party")
endif()
foreach(name IN ITEMS libzstd_static libzstd_shared uninstall clean-all)
if(TARGET ${name})
set_target_properties(${name} PROPERTIES FOLDER "third_party")
endif()
endforeach()
80 changes: 79 additions & 1 deletion modules/bvh/examples/uv_overlap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
#include <lagrange/bvh/compute_uv_overlap.h>
#include <lagrange/cast_attribute.h>
#include <lagrange/compute_uv_charts.h>
#include <lagrange/compute_uv_orientation.h>
#include <lagrange/disconnect_uv_charts.h>
#include <lagrange/find_matching_attributes.h>
#include <lagrange/internal/compact_chart_ids.h>
#include <lagrange/io/load_mesh.h>
#include <lagrange/io/save_mesh.h>
#include <lagrange/map_attribute.h>
#include <lagrange/packing/repack_uv_charts.h>
#include <lagrange/polyscope/register_mesh.h>
#include <lagrange/triangulate_polygonal_facets.h>
#include <lagrange/unflip_uv_charts.h>
#include <lagrange/unify_index_buffer.h>
#include <lagrange/utils/fmt/join.h>
#include <lagrange/utils/timing.h>
Expand Down Expand Up @@ -108,12 +111,17 @@ void repack_overlapping_charts(
for (uint32_t f = 0; f < num_facets; ++f) {
split_ids[f] = static_cast<uint32_t>(chart_ids[f] * num_colors + color_ids[f]);
}

// Compact per-facet ids so the downstream packer doesn't allocate empty slots for unused
// combinations.
auto compacted = lagrange::internal::compact_chart_ids<uint32_t>(
lagrange::span<const uint32_t>(split_ids.data(), split_ids.size()));
mesh.template create_attribute<uint32_t>(
"@split_chart_id",
lagrange::AttributeElement::Facet,
lagrange::AttributeUsage::Scalar,
1,
split_ids);
compacted.first);

// 3. Split UV indices so that facets in different split charts don't share UV vertices
lagrange::DisconnectUVChartsOptions disconnect_options;
Expand All @@ -127,6 +135,44 @@ void repack_overlapping_charts(
lagrange::packing::repack_uv_charts(mesh, repack_options);
}

size_t apply_unflip(SurfaceMesh& mesh, const std::string& uv_attribute_name)
{
// Resolve the target UV attribute before any mapping so the selection is stable.
// map_attribute_in_place() can delete/recreate attributes and alter iteration order,
// which would cause the default "first UV" selection to pick a different attribute.
std::string resolved_name = uv_attribute_name;
if (resolved_name.empty()) {
lagrange::AttributeMatcher matcher;
matcher.usages = lagrange::AttributeUsage::UV;
auto uv_id = lagrange::find_matching_attribute(mesh, matcher);
if (uv_id.has_value()) {
resolved_name = std::string(mesh.get_attribute_name(uv_id.value()));
}
}

// unflip_uv_charts requires an indexed UV attribute; map only the target attribute if needed.
if (!resolved_name.empty() && mesh.has_attribute(resolved_name)) {
auto id = mesh.get_attribute_id(resolved_name);
if (mesh.get_attribute_base(id).get_element_type() != lagrange::AttributeElement::Indexed) {
lagrange::logger().info(
"Mapping non-indexed UV attribute '{}' to indexed for unflipping.",
resolved_name);
map_attribute_in_place(mesh, id, lagrange::AttributeElement::Indexed);
}
}

lagrange::UnflipUVChartsOptions unflip_options;
unflip_options.uv_attribute_name = resolved_name;
size_t n = lagrange::unflip_uv_charts(mesh, unflip_options);
lagrange::UVOrientationOptions flip_options;
flip_options.uv_attribute_name = resolved_name;
size_t still_flipped = lagrange::compute_uv_orientation(mesh, flip_options).negative;
lagrange::logger().info(
"Number of flipped triangles after unflipping charts: {}.",
still_flipped);
return n;
}

// ============================================================================
// GUI state and callbacks
// ============================================================================
Expand Down Expand Up @@ -198,6 +244,10 @@ void register_uv_mesh(
auto& ca = mesh.template get_attribute<uint32_t>(coloring_id);
lagrange::polyscope::register_attribute(*ps_mesh, "uv_overlap_color", ca);
}
if (mesh.has_attribute("@uv_orientation")) {
auto& fa = mesh.template get_attribute<int8_t>(mesh.get_attribute_id("@uv_orientation"));
lagrange::polyscope::register_attribute(*ps_mesh, "uv_orientation", fa);
}
}

/// Remove all polyscope structures and re-register them for the current view mode.
Expand Down Expand Up @@ -290,6 +340,17 @@ void user_callback(DemoState& state)
toggle_uv_view(state);
}

if (ImGui::Button("Unflip UV Charts")) {
size_t n = apply_unflip(state.mesh_original, state.uv_attribute_name);
lagrange::logger().info("Unflipped {} chart(s).", n);
state.mesh_display = state.mesh_original;
prepare_mesh_for_display(state.mesh_display);

auto camera_json = polyscope::view::getViewAsJson();
register_view(state);
polyscope::view::setViewFromJson(camera_json, false);
}

if (state.has_coloring()) {
ImGui::BeginDisabled(state.repacked);
if (ImGui::Button("Repack UV Charts")) {
Expand Down Expand Up @@ -332,6 +393,7 @@ int main(int argc, char** argv)
bool gui = false;
bool uv_view = false;
bool repack = false;
bool unflip = false;
int log_level = 2;
} args;

Expand All @@ -345,6 +407,10 @@ int main(int argc, char** argv)
app.add_flag("--gui", args.gui, "Launch the Polyscope GUI to visualize results.");
app.add_flag("--uv-view", args.uv_view, "Start in the 2D UV layout view (implies --gui).");
app.add_flag("--repack", args.repack, "Repack UV charts per overlap color layer.");
app.add_flag(
"--unflip",
args.unflip,
"Reverse winding of any UV chart with negative signed area.");
app.add_option("-l,--level", args.log_level, "Log level (0 = most verbose, 6 = off).");
CLI11_PARSE(app, argc, argv)

Expand Down Expand Up @@ -394,6 +460,18 @@ int main(int argc, char** argv)
lagrange::logger().info("No UV overlap detected.");
}

if (args.gui || args.unflip) {
lagrange::UVOrientationOptions orient_options;
orient_options.uv_attribute_name = args.uv_attribute_name;
size_t num_flipped = lagrange::compute_uv_orientation(mesh, orient_options).negative;
lagrange::logger().info("Flipped UV triangles: {}", num_flipped);

if (args.unflip && num_flipped > 0) {
size_t num_unflipped = apply_unflip(mesh, args.uv_attribute_name);
lagrange::logger().info("Unflipped {} chart(s).", num_unflipped);
}
}

// GUI or CLI output
if (args.gui) {
polyscope::options::configureImGuiStyleCallback = []() {
Expand Down
3 changes: 3 additions & 0 deletions modules/core/examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ target_link_libraries(mesh_cleanup lagrange::core lagrange::io CLI11::CLI11)

lagrange_add_example(fix_nonmanifold fix_nonmanifold.cpp)
target_link_libraries(fix_nonmanifold lagrange::core lagrange::io CLI11::CLI11)

lagrange_add_example(orient_outward orient_outward.cpp)
target_link_libraries(orient_outward lagrange::core lagrange::io CLI11::CLI11)
42 changes: 20 additions & 22 deletions modules/core/examples/mesh_cleanup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,23 @@
#include <lagrange/common.h>
#include <lagrange/io/load_mesh.h>
#include <lagrange/io/save_mesh.h>
#include <lagrange/mesh_bbox.h>
#include <lagrange/mesh_cleanup/close_small_holes.h>
#include <lagrange/mesh_cleanup/remove_degenerate_triangles.h>
#include <lagrange/mesh_cleanup/remove_degenerate_facets.h>
#include <lagrange/mesh_cleanup/remove_duplicate_vertices.h>
#include <lagrange/mesh_cleanup/split_long_edges.h>
#include <lagrange/triangulate_polygonal_facets.h>

#include <CLI/CLI.hpp>
#include <Eigen/Core>

Eigen::AlignedBox3d mesh_bbox(const lagrange::TriangleMesh3D& mesh)
{
using Index = typename lagrange::TriangleMesh3D::Index;
Eigen::AlignedBox3d bbox;
la_runtime_assert(mesh.get_vertices().cols() == 3);
for (Index v = 0; v < mesh.get_num_vertices(); ++v) {
bbox.extend(mesh.get_vertices().row(v).transpose());
}
return bbox;
}

int main(int argc, char** argv)
{
struct
{
std::string input;
std::string output = "output.obj";
double tol = 0.001;
float tol = 0.01f;
size_t max_holes = 0;
bool holes_only = false;
bool relative = true;
Expand All @@ -59,10 +50,10 @@ int main(int argc, char** argv)
lagrange::logger().set_level(spdlog::level::trace);

lagrange::logger().info("Loading input mesh: {}", args.input);
auto mesh = lagrange::io::load_mesh<lagrange::TriangleMesh3D>(args.input);
auto mesh = lagrange::io::load_mesh<lagrange::SurfaceMesh32d>(args.input);

if (args.relative) {
double diag = mesh_bbox(*mesh).diagonal().norm();
if (args.relative && mesh.get_num_vertices() > 0) {
float diag = static_cast<float>(lagrange::mesh_bbox<3>(mesh).diagonal().norm());
lagrange::logger().info(
"Using a relative tolerance of {:.3f} x {:.3f} = {:.3f}",
args.tol,
Expand All @@ -73,20 +64,27 @@ int main(int argc, char** argv)

if (args.max_holes) {
lagrange::logger().info("Closing small holes");
mesh = lagrange::close_small_holes(*mesh, args.max_holes);
lagrange::CloseSmallHolesOptions holes_options;
holes_options.max_hole_size = args.max_holes;
lagrange::close_small_holes(mesh, holes_options);
}

if (!args.holes_only) {
lagrange::logger().info("Removing degenerate triangles");
mesh = lagrange::remove_degenerate_triangles(*mesh);
lagrange::logger().info("Triangulating polygonal facets");
lagrange::triangulate_polygonal_facets(mesh);
lagrange::logger().info("Removing duplicate vertices");
mesh = lagrange::remove_duplicate_vertices(*mesh);
lagrange::remove_duplicate_vertices(mesh);
lagrange::logger().info("Removing degenerate facets");
lagrange::remove_degenerate_facets(mesh);
lagrange::logger().info("Splitting long edges");
mesh = lagrange::split_long_edges(*mesh, args.tol * args.tol, true);
lagrange::SplitLongEdgesOptions split_options;
split_options.max_edge_length = args.tol;
split_options.recursive = true;
lagrange::split_long_edges(mesh, split_options);
}

lagrange::logger().info("Saving result: {}", args.output);
lagrange::io::save_mesh(args.output, *mesh);
lagrange::io::save_mesh(args.output, mesh);

return 0;
}
Loading
Loading