diff --git a/cmake/recipes/external/nanobind_namedtuple.cmake b/cmake/recipes/external/nanobind_namedtuple.cmake new file mode 100644 index 00000000..384c7bb0 --- /dev/null +++ b/cmake/recipes/external/nanobind_namedtuple.cmake @@ -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) 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..4f5374a0 100644 --- a/modules/bvh/python/src/bvh.cpp +++ b/modules/bvh/python/src/bvh.cpp @@ -20,19 +20,26 @@ #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; -// 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_NAMED_TUPLE( + UVOverlapResult, + has_overlap, + overlap_area, + overlapping_pairs, + overlap_coloring_id) + +namespace lagrange::python { void populate_bvh_module(nb::module_& m) { @@ -491,30 +498,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 +517,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, diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt index 501bb9dc..4f1742ce 100644 --- a/modules/python/CMakeLists.txt +++ b/modules/python/CMakeLists.txt @@ -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) @@ -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()