From 574c520211bb1cdfe5d4302e3ccaae93997d54b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Tue, 14 Jul 2026 21:07:40 -0700 Subject: [PATCH 1/7] Bind bvh UVOverlapResult via nanobind_namedtuple Replace the hand-rolled typing.NamedTuple construction (and its LA_STUB_HINT/StubType stub workaround) in the bvh Python bindings with an NB_NAMED_TUPLE registration from the header-only nanobind_namedtuple library, pinned via a new CPM recipe. compute_uv_overlap now returns the C++ UVOverlapResult struct directly; the type caster converts it to a collections.namedtuple subclass on the Python side. --- .../external/nanobind_namedtuple.cmake | 38 ++++++++++ modules/bvh/python/CMakeLists.txt | 3 + modules/bvh/python/src/bvh.cpp | 69 ++++++++----------- 3 files changed, 68 insertions(+), 42 deletions(-) create mode 100644 cmake/recipes/external/nanobind_namedtuple.cmake diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake new file mode 100644 index 00000000..c7c02ab1 --- /dev/null +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -0,0 +1,38 @@ +# +# 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'") + +include(CPM) +CPMAddPackage( + NAME nanobind_namedtuple + GITHUB_REPOSITORY jdumas/nanobind_namedtuple + GIT_TAG 38daf55e8100c903e511d422566362befd495104 + DOWNLOAD_ONLY ON +) + +# The upstream CMakeLists.txt runs its own find_package(Python)/find_package(nanobind), which is +# redundant within Lagrange's build (nanobind is already fetched via CPM). Since the library is +# header-only, we declare the interface target manually instead. +add_library(nanobind_namedtuple INTERFACE) +add_library(nanobind_namedtuple::nanobind_namedtuple ALIAS nanobind_namedtuple) + +target_include_directories(nanobind_namedtuple INTERFACE + $ +) + +target_compile_features(nanobind_namedtuple INTERFACE cxx_std_17) + +set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party) diff --git a/modules/bvh/python/CMakeLists.txt b/modules/bvh/python/CMakeLists.txt index 897e9639..9fefe02d 100644 --- a/modules/bvh/python/CMakeLists.txt +++ b/modules/bvh/python/CMakeLists.txt @@ -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) diff --git a/modules/bvh/python/src/bvh.cpp b/modules/bvh/python/src/bvh.cpp index b4eb1fdf..1b434ca2 100644 --- a/modules/bvh/python/src/bvh.cpp +++ b/modules/bvh/python/src/bvh.cpp @@ -20,19 +20,31 @@ #include #include #include -#include + +#include #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; + +// clang-format off +#include +NB_NAMED_TUPLE( + UVOverlapResult, + "UVOverlapResult", + NB_NT_FIELD(has_overlap), + NB_NT_FIELD(overlap_area), + NB_NT_FIELD(overlapping_pairs), + NB_NT_FIELD(overlap_coloring_id)) +#include +// clang-format on -// 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; +namespace lagrange::python { void populate_bvh_module(nb::module_& m) { @@ -491,30 +503,18 @@ 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(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; @@ -522,22 +522,7 @@ Both meshes must have the same spatial dimension and must be triangle meshes. 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, From ec808a72d2a0d39cc905b6194cf15d9bf11548d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Wed, 15 Jul 2026 07:04:15 -0700 Subject: [PATCH 2/7] Bump nanobind_namedtuple pin and drop the -Wshadow workaround Update the CPM pin to 5eea9b5, which fixes the -Wshadow warning in NB_NAMED_TUPLE and sanitizes field annotations so that typing.get_type_hints() works on the generated namedtuple. Remove the warnoff.h/warnon.h wrapper that is no longer needed. --- cmake/recipes/external/nanobind_namedtuple.cmake | 2 +- modules/bvh/python/src/bvh.cpp | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake index c7c02ab1..7d607ea9 100644 --- a/cmake/recipes/external/nanobind_namedtuple.cmake +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -19,7 +19,7 @@ include(CPM) CPMAddPackage( NAME nanobind_namedtuple GITHUB_REPOSITORY jdumas/nanobind_namedtuple - GIT_TAG 38daf55e8100c903e511d422566362befd495104 + GIT_TAG 5eea9b5497c031ef556a250b04d47b68e54395c1 DOWNLOAD_ONLY ON ) diff --git a/modules/bvh/python/src/bvh.cpp b/modules/bvh/python/src/bvh.cpp index 1b434ca2..29dbfd2f 100644 --- a/modules/bvh/python/src/bvh.cpp +++ b/modules/bvh/python/src/bvh.cpp @@ -32,8 +32,6 @@ using namespace nb::literals; // single type name token, hence the alias for the template instantiation. using UVOverlapResult = lagrange::bvh::UVOverlapResult; -// clang-format off -#include NB_NAMED_TUPLE( UVOverlapResult, "UVOverlapResult", @@ -41,8 +39,6 @@ NB_NAMED_TUPLE( NB_NT_FIELD(overlap_area), NB_NT_FIELD(overlapping_pairs), NB_NT_FIELD(overlap_coloring_id)) -#include -// clang-format on namespace lagrange::python { From 3c086ac7516ee8b698df37edc35c6679c0603908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Wed, 15 Jul 2026 07:34:46 -0700 Subject: [PATCH 3/7] Reuse nanobind_namedtuple's own CMake in the recipe --- .../external/nanobind_namedtuple.cmake | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake index 7d607ea9..d1361f1e 100644 --- a/cmake/recipes/external/nanobind_namedtuple.cmake +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -15,24 +15,17 @@ 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 5eea9b5497c031ef556a250b04d47b68e54395c1 - DOWNLOAD_ONLY ON ) -# The upstream CMakeLists.txt runs its own find_package(Python)/find_package(nanobind), which is -# redundant within Lagrange's build (nanobind is already fetched via CPM). Since the library is -# header-only, we declare the interface target manually instead. -add_library(nanobind_namedtuple INTERFACE) -add_library(nanobind_namedtuple::nanobind_namedtuple ALIAS nanobind_namedtuple) - -target_include_directories(nanobind_namedtuple INTERFACE - $ -) - -target_compile_features(nanobind_namedtuple INTERFACE cxx_std_17) - set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party) From 312ca2a515dd26f0e6f4c6b7514f98754ace6c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Wed, 15 Jul 2026 08:36:52 -0700 Subject: [PATCH 4/7] Update to the new bare-name NB_NAMED_TUPLE macro API --- cmake/recipes/external/nanobind_namedtuple.cmake | 2 +- modules/bvh/python/src/bvh.cpp | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake index d1361f1e..5f7b4581 100644 --- a/cmake/recipes/external/nanobind_namedtuple.cmake +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -25,7 +25,7 @@ include(CPM) CPMAddPackage( NAME nanobind_namedtuple GITHUB_REPOSITORY jdumas/nanobind_namedtuple - GIT_TAG 5eea9b5497c031ef556a250b04d47b68e54395c1 + GIT_TAG bfb9d8a58f4e4cf64a0751efaa947746af01573b ) set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party) diff --git a/modules/bvh/python/src/bvh.cpp b/modules/bvh/python/src/bvh.cpp index 29dbfd2f..4f5374a0 100644 --- a/modules/bvh/python/src/bvh.cpp +++ b/modules/bvh/python/src/bvh.cpp @@ -34,11 +34,10 @@ using UVOverlapResult = lagrange::bvh::UVOverlapResult; NB_NAMED_TUPLE( UVOverlapResult, - "UVOverlapResult", - NB_NT_FIELD(has_overlap), - NB_NT_FIELD(overlap_area), - NB_NT_FIELD(overlapping_pairs), - NB_NT_FIELD(overlap_coloring_id)) + has_overlap, + overlap_area, + overlapping_pairs, + overlap_coloring_id) namespace lagrange::python { From ab58ca062b21c3dc516e3b7a9ddc1a13b6b5f4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Wed, 15 Jul 2026 14:35:00 -0700 Subject: [PATCH 5/7] Bump nanobind_namedtuple pin and feed a generated pattern file to stubgen --- .../external/nanobind_namedtuple.cmake | 2 +- modules/python/CMakeLists.txt | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake index 5f7b4581..a3945653 100644 --- a/cmake/recipes/external/nanobind_namedtuple.cmake +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -25,7 +25,7 @@ include(CPM) CPMAddPackage( NAME nanobind_namedtuple GITHUB_REPOSITORY jdumas/nanobind_namedtuple - GIT_TAG bfb9d8a58f4e4cf64a0751efaa947746af01573b + GIT_TAG 906b5bd02968e92d0f08d6259250dd395e8ef3d7 ) set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party) diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt index 501bb9dc..44afc331 100644 --- a/modules/python/CMakeLists.txt +++ b/modules/python/CMakeLists.txt @@ -224,6 +224,37 @@ 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) + get_target_property(nbnt_source_dir nanobind_namedtuple SOURCE_DIR) + set(pattern_file ${CMAKE_CURRENT_BINARY_DIR}/nanobind_namedtuple.pat) + set(pattern_module_flags "") + 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_module_flags -m lagrange.${python_name}) + endforeach() + list(JOIN pattern_module_flags " " pattern_module_flags) + install(CODE " + message(STATUS \"Generating nanobind_namedtuple pattern file: ${pattern_file}\") + execute_process( + COMMAND \"${CMAKE_COMMAND}\" -E env \"PYTHONPATH=${nbnt_source_dir}/stubgen\" + \"${Python_EXECUTABLE}\" -m nanobind_namedtuple_stubgen + -i \"${SKBUILD_PLATLIB_DIR}/lagrange/\" ${pattern_module_flags} -o \"${pattern_file}\" + COMMAND_ERROR_IS_FATAL ANY + )" + COMPONENT Lagrange_Python_Runtime + ) + 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) @@ -239,6 +270,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() From 7b74fc8720fcd58e0d9278f92ddce8a5f34c6be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Wed, 15 Jul 2026 15:52:44 -0700 Subject: [PATCH 6/7] Bump nanobind_namedtuple pin and use its stub pattern CMake helper --- .../external/nanobind_namedtuple.cmake | 2 +- modules/python/CMakeLists.txt | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake index a3945653..492b74fe 100644 --- a/cmake/recipes/external/nanobind_namedtuple.cmake +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -25,7 +25,7 @@ include(CPM) CPMAddPackage( NAME nanobind_namedtuple GITHUB_REPOSITORY jdumas/nanobind_namedtuple - GIT_TAG 906b5bd02968e92d0f08d6259250dd395e8ef3d7 + GIT_TAG ec5f32323e309072fe5a47306d34d4f7fa6ddbea ) set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party) diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt index 44afc331..4f1742ce 100644 --- a/modules/python/CMakeLists.txt +++ b/modules/python/CMakeLists.txt @@ -231,26 +231,21 @@ function(lagrange_generate_python_binding_module) # single shared file is safe to pass to every module's stubgen invocation. set(pattern_file_args "") if(TARGET nanobind_namedtuple) - get_target_property(nbnt_source_dir nanobind_namedtuple SOURCE_DIR) set(pattern_file ${CMAKE_CURRENT_BINARY_DIR}/nanobind_namedtuple.pat) - set(pattern_module_flags "") + 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_module_flags -m lagrange.${python_name}) + list(APPEND pattern_modules lagrange.${python_name}) endforeach() - list(JOIN pattern_module_flags " " pattern_module_flags) - install(CODE " - message(STATUS \"Generating nanobind_namedtuple pattern file: ${pattern_file}\") - execute_process( - COMMAND \"${CMAKE_COMMAND}\" -E env \"PYTHONPATH=${nbnt_source_dir}/stubgen\" - \"${Python_EXECUTABLE}\" -m nanobind_namedtuple_stubgen - -i \"${SKBUILD_PLATLIB_DIR}/lagrange/\" ${pattern_module_flags} -o \"${pattern_file}\" - COMMAND_ERROR_IS_FATAL ANY - )" + 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() From b7420b2a12c138d6e9f85158cfad8dab3191b34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Je=CC=81re=CC=81mie=20Dumas?= Date: Wed, 15 Jul 2026 16:38:18 -0700 Subject: [PATCH 7/7] Bump nanobind_namedtuple pin to pick up __new__ annotations parity --- cmake/recipes/external/nanobind_namedtuple.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake index 492b74fe..384c7bb0 100644 --- a/cmake/recipes/external/nanobind_namedtuple.cmake +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -25,7 +25,7 @@ include(CPM) CPMAddPackage( NAME nanobind_namedtuple GITHUB_REPOSITORY jdumas/nanobind_namedtuple - GIT_TAG ec5f32323e309072fe5a47306d34d4f7fa6ddbea + GIT_TAG 84243680d678a6a8b54f88c7df5c713d9bec46d1 ) set_target_properties(nanobind_namedtuple PROPERTIES FOLDER third_party)