Skip to content
Open
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
31 changes: 31 additions & 0 deletions cmake/recipes/external/nanobind_namedtuple.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#
# 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.
#
if(TARGET nanobind_namedtuple::nanobind_namedtuple)
return()
endif()

message(STATUS "Third-party (external): creating target 'nanobind_namedtuple::nanobind_namedtuple'")

# Provide Python and nanobind before adding the package: the upstream CMakeLists.txt skips its own
# find_package(Python) when Python::Module exists, and skips nanobind discovery (which probes the
# interpreter via `python -m nanobind`) when nanobind_add_module is available.
include(python)
include(nanobind)

include(CPM)
CPMAddPackage(
NAME nanobind_namedtuple
GITHUB_REPOSITORY jdumas/nanobind_namedtuple
GIT_TAG 84243680d678a6a8b54f88c7df5c713d9bec46d1
)

set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party)
3 changes: 3 additions & 0 deletions modules/bvh/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
# governing permissions and limitations under the License.
#
lagrange_add_python_binding()

lagrange_find_package(nanobind_namedtuple REQUIRED)
target_link_libraries(lagrange_python PRIVATE nanobind_namedtuple::nanobind_namedtuple)
64 changes: 22 additions & 42 deletions modules/bvh/python/src/bvh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,26 @@
#include <lagrange/python/binding.h>
#include <lagrange/python/bvh.h>
#include <lagrange/python/eigen_utils.h>
#include <lagrange/python/utils/StubType.h>

#include <nanobind_namedtuple/named_tuple.h>

#include "PyEdgeAABBTree.h"

namespace nb = nanobind;
using namespace nb::literals;

namespace lagrange::python {
// Expose lagrange::bvh::UVOverlapResult to Python as a collections.namedtuple. The macro takes a
// single type name token, hence the alias for the template instantiation.
using UVOverlapResult = lagrange::bvh::UVOverlapResult<double, uint32_t>;

// Renders the dynamically-built `UVOverlapResult` NamedTuple (a runtime nb::object)
// with the right stub type. TODO: retire once nanobind supports NamedTuple directly.
LA_STUB_HINT(UVOverlapResultHint, "UVOverlapResult");
using UVOverlapResultObject = StubType<nb::object, UVOverlapResultHint>;
NB_NAMED_TUPLE(
UVOverlapResult,
has_overlap,
overlap_area,
overlapping_pairs,
overlap_coloring_id)

namespace lagrange::python {

void populate_bvh_module(nb::module_& m)
{
Expand Down Expand Up @@ -491,53 +498,26 @@ Both meshes must have the same spatial dimension and must be triangle meshes.
"Zomorodian-Edelsbrunner HYBRID algorithm (recursive divide-and-conquer).");

// UV overlap result (Python NamedTuple)
// Note: No direct nanobind support for NamedTuple yet, see:
// https://github.com/wjakob/nanobind/discussions/1279
nb::object typing = nb::module_::import_("typing");
nb::object builtins = nb::module_::import_("builtins");
nb::object UVOverlapResult = typing.attr("NamedTuple")(
"UVOverlapResult",
nb::make_tuple(
nb::make_tuple("has_overlap", builtins.attr("bool")),
nb::make_tuple("overlap_area", typing.attr("Optional")[builtins.attr("float")]),
nb::make_tuple("overlapping_pairs", builtins.attr("list")),
nb::make_tuple("overlap_coloring_id", builtins.attr("int"))));
m.attr("UVOverlapResult") = UVOverlapResult;
nbnt::bind_namedtuple<UVOverlapResult>(m);

// compute_uv_overlap function
m.def(
"compute_uv_overlap",
[UVOverlapResult](
MeshType& mesh,
std::string uv_attribute_name,
bool compute_overlap_area,
bool compute_overlap_coloring,
std::string overlap_coloring_attribute_name,
bool compute_overlapping_pairs,
bvh::UVOverlapMethod method) -> UVOverlapResultObject {
[](MeshType& mesh,
std::string uv_attribute_name,
bool compute_overlap_area,
bool compute_overlap_coloring,
std::string overlap_coloring_attribute_name,
bool compute_overlapping_pairs,
bvh::UVOverlapMethod method) {
bvh::UVOverlapOptions opts;
opts.uv_attribute_name = std::move(uv_attribute_name);
opts.compute_overlap_area = compute_overlap_area;
opts.compute_overlap_coloring = compute_overlap_coloring;
opts.overlap_coloring_attribute_name = std::move(overlap_coloring_attribute_name);
opts.compute_overlapping_pairs = compute_overlapping_pairs;
opts.method = method;
auto result = bvh::compute_uv_overlap(mesh, opts);

nb::object area = result.overlap_area.has_value()
? nb::cast(result.overlap_area.value())
: nb::none();

nb::list pairs;
for (auto& [i, j] : result.overlapping_pairs) {
pairs.append(nb::make_tuple(i, j));
}

return UVOverlapResultObject{UVOverlapResult(
nb::cast(result.has_overlap),
area,
pairs,
nb::cast(result.overlap_coloring_id))};
return bvh::compute_uv_overlap(mesh, opts);
},
"mesh"_a,
"uv_attribute_name"_a = bvh::UVOverlapOptions{}.uv_attribute_name,
Expand Down
27 changes: 27 additions & 0 deletions modules/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,32 @@ function(lagrange_generate_python_binding_module)

# Generate stubs for python binding within the install location.
get_target_property(active_modules lagrange_python LAGRANGE_ACTIVE_MODULES)

# When nanobind_namedtuple is in use, generate a stubgen pattern file at install
# time so that classes registered through nbnt::bind_namedtuple are emitted as
# canonical typing.NamedTuple blocks. Pattern queries are fully qualified, so a
# single shared file is safe to pass to every module's stubgen invocation.
set(pattern_file_args "")
if(TARGET nanobind_namedtuple)
set(pattern_file ${CMAKE_CURRENT_BINARY_DIR}/nanobind_namedtuple.pat)
set(pattern_modules "")
foreach(module_name IN ITEMS ${active_modules})
get_target_property(python_name lagrange_python LAGRANGE_PYTHON_NAME_${module_name})
if(NOT python_name)
set(python_name ${module_name})
endif()
list(APPEND pattern_modules lagrange.${python_name})
endforeach()
nanobind_namedtuple_stub_pattern(
OUTPUT ${pattern_file}
MODULE ${pattern_modules}
INSTALL_TIME
COMPONENT Lagrange_Python_Runtime
PYTHON_PATH ${SKBUILD_PLATLIB_DIR}/lagrange/
)
set(pattern_file_args PATTERN_FILE ${pattern_file})
endif()

foreach(module_name IN ITEMS ${active_modules})
get_target_property(python_name lagrange_python LAGRANGE_PYTHON_NAME_${module_name})
if(NOT python_name)
Expand All @@ -239,6 +265,7 @@ function(lagrange_generate_python_binding_module)
DEPENDS lagrange_python
COMPONENT Lagrange_Python_Runtime
PYTHON_PATH ${SKBUILD_PLATLIB_DIR}/lagrange/
${pattern_file_args}
VERBOSE
)
endforeach()
Expand Down
Loading