diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index ee8c9b6a0..4dd16ad15 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -52,10 +52,11 @@ else() nanobind GIT_REPOSITORY https://github.com/wjakob/nanobind.git GIT_TAG - 9641bb7151f04120013b812789b3ebdfa7e7324f # v2.1.0 + 784efa2a0358a4dc5432c74f5685ee026e20f2b6 # v2.2.0 GIT_PROGRESS TRUE ) FetchContent_MakeAvailable(nanobind) + set(NB_VERSION 2.2.0) endif() if(NB_VERSION VERSION_LESS 2.1.0) @@ -96,6 +97,8 @@ set( ${CMAKE_SOURCE_DIR}/src/cross_section/cross_section.cpp ${CMAKE_SOURCE_DIR}/src/polygon.cpp ${CMAKE_SOURCE_DIR}/include/manifold/common.h + ${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py + ${CMAKE_CURRENT_SOURCE_DIR}/docstring_override.txt ) add_custom_command( OUTPUT autogen_docstrings.inl diff --git a/bindings/python/docstring_override.txt b/bindings/python/docstring_override.txt new file mode 100644 index 000000000..9bdbf58e9 --- /dev/null +++ b/bindings/python/docstring_override.txt @@ -0,0 +1,51 @@ +cross_section__warp__warp_func: + Move the vertices of this CrossSection (creating a new one) according to + any arbitrary input function, followed by a union operation (with a + Positive fill rule) that ensures any introduced intersections are not + included in the result. + :param warp_func: A function that takes the original vertex position and + return the new position. +cross_section__warp_batch__warp_func: + Same as CrossSection::warp but calls warpFunc with + an ndarray[n, 2] instead of processing only one vertex at a time. + :param warp_func: A function that takes multiple vertex positions as an + ndarray[n, 2] and returns the new vertex positions. +manifold__warp__warp_func: + This function does not change the topology, but allows the vertices to be + moved according to any arbitrary input function. It is easy to create a + function that warps a geometrically valid object into one which overlaps, but + that is not checked here, so it is up to the user to choose their function + with discretion. + :param warp_func: A function that takes the original vertex position and + return the new position. +manifold__warp_batch__warp_func: + Same as Manifold::warp but calls warpFunc with with + an ndarray[n, 3] instead of processing only one vertex at a time. + :param warp_func: A function that takes multiple vertex positions as an + ndarray[n, 3] and returns the new vertex positions. The result should have the + same shape as the input. +manifold__smooth__mesh_gl__sharpened_edges: + Constructs a smooth version of the input mesh by creating tangents; this + method will throw if you have supplied tangents with your mesh already. The + actual triangle resolution is unchanged; use the Refine() method to + interpolate to a higher-resolution curve. + By default, every edge is calculated for maximum smoothness (very much + approximately), attempting to minimize the maximum mean Curvature magnitude. + No higher-order derivatives are considered, as the interpolation is + independent per triangle, only sharing constraints on their boundaries. + :param mesh: input Mesh. + :param sharpened_edges: If desired, you can supply a vector of sharpened + halfedges, which should in general be a small subset of all halfedges. Order + of entries doesn't matter, as each one specifies the desired smoothness + (between zero and one, with one the default for all unspecified halfedges) + and the halfedge index (3 * triangle index + [0,1,2] where 0 is the edge + between triVert 0 and 1, etc). + :param edge_smoothness: Smoothness values associated to each halfedge defined + in sharpened_edges. At a smoothness value of zero, a sharp crease is made. The + smoothness is interpolated along each edge, so the specified value should be + thought of as an average. Where exactly two sharpened edges meet at a vertex, + their tangents are rotated to be colinear so that the sharpened edge can be + continuous. Vertices with only one sharpened edge are completely smooth, + allowing sharpened edges to smoothly vanish at termination. A single vertex + can be sharpened by sharping all edges that are incident on it, allowing cones + to be formed. diff --git a/bindings/python/examples/all_apis.py b/bindings/python/examples/all_apis.py index a8a36f1b3..027b4acf2 100644 --- a/bindings/python/examples/all_apis.py +++ b/bindings/python/examples/all_apis.py @@ -112,7 +112,7 @@ def all_manifold(): m = m.translate((0, 0, 0)) m = m.trim_by_plane((0, 0, 1), 0) m = m.warp(lambda p: (p[0] + 1, p[1] / 2, p[2] * 2)) - m = m.warp_batch(lambda ps: ps * [1, 0.5, 2] + [1, 0, 0]) + m = m.warp_batch(lambda ps: ps * 2 + [1, 0, 0]) m = Manifold.cube() m2 = Manifold.cube().translate([2, 0, 0]) d = m.min_gap(m2, 2) diff --git a/bindings/python/examples/test_torus_knot.py b/bindings/python/examples/test_torus_knot.py index 20e19addb..65086ca28 100644 --- a/bindings/python/examples/test_torus_knot.py +++ b/bindings/python/examples/test_torus_knot.py @@ -73,12 +73,11 @@ def func(pts): m3 = ax_rotate(2, psi) v = v[:, None, :] @ m1 @ m2 @ m3 - pts[:] = v[:, 0, :3] + return v[:, 0, :3] def func_single(v): pts = np.array([v]) - func(pts) - return pts[0] + return func(pts)[0] if warp_single: return Manifold.revolve(circle, int(m)).warp(func_single) diff --git a/bindings/python/gen_docs.py b/bindings/python/gen_docs.py index 0c2239fea..0b5677d30 100644 --- a/bindings/python/gen_docs.py +++ b/bindings/python/gen_docs.py @@ -98,6 +98,18 @@ def select_functions(s): comments = dict(sorted(comments.items())) +with open(f"{base}/bindings/python/docstring_override.txt") as f: + key = "" + for l in f: + if l.startswith(" "): + comments[key] += l[2:] + else: + key = l[:-2] + if key not in comments.keys(): + print(f"Error, unknown docstring override key {key}") + exit(-1) + comments[key] = "" + gen_h = "autogen_docstrings.inl" with open(gen_h, "w") as f: f.write("#pragma once\n\n") diff --git a/bindings/python/manifold3d.cpp b/bindings/python/manifold3d.cpp index 9aaab4815..1405ca0cb 100644 --- a/bindings/python/manifold3d.cpp +++ b/bindings/python/manifold3d.cpp @@ -105,17 +105,16 @@ struct nb::detail::type_caster> { } static handle from_cpp(la_type mat, rv_policy policy, cleanup_list *cleanup) noexcept { - T *buffer = new T[R * C]; - nb::capsule mem_mgr(buffer, [](void *p) noexcept { delete[] (T *)p; }); + std::array buffer; for (int i = 0; i < R; i++) { for (int j = 0; j < C; j++) { // py is (Rows, Cols), la is (Cols, Rows) buffer[i * C + j] = mat[j][i]; } } - numpy_type arr{buffer, {R, C}, std::move(mem_mgr)}; - return ndarray_wrap(arr.handle(), int(ndarray_framework::numpy), policy, - cleanup); + numpy_type arr{buffer, {R, C}, nb::handle()}; + // we must copy the underlying data + return make_caster::from_cpp(arr, rv_policy::copy, cleanup); } }; @@ -160,12 +159,12 @@ struct nb::detail::type_caster>> { } } numpy_type arr{buffer, {num_vec, N}, std::move(mem_mgr)}; - return ndarray_wrap(arr.handle(), ndarray_framework::numpy, policy, - cleanup); + // we can just do a move because we already did the copying + return make_caster::from_cpp(arr, rv_policy::move, cleanup); } }; -// handle VecView +// handle VecView template struct nb::detail::type_caster>> { using la_type = la::vec; @@ -173,28 +172,17 @@ struct nb::detail::type_caster>> { NB_TYPE_CASTER(manifold::VecView, const_name(la_name::multi_name)); - bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { - make_caster arr_cast; - if (!arr_cast.from_python(src, flags, cleanup)) return false; - // TODO try 2d iterators if numpy cast fails - size_t num_vec = arr_cast.value.shape(0); - if (num_vec != value.size()) return false; - for (size_t i = 0; i < num_vec; i++) { - for (int j = 0; j < N; j++) { - value[i][j] = arr_cast.value(i, j); - } - } - return true; - } static handle from_cpp(Value vec, rv_policy policy, cleanup_list *cleanup) noexcept { - // do we have ownership issue here? size_t num_vec = vec.size(); - static_assert(sizeof(vec[0]) == (N * sizeof(T)), + // assume packed struct + static_assert(alignof(la::vec) <= (N * sizeof(T)), "VecView -> numpy requires packed structs"); - numpy_type arr{&vec[0], {num_vec, N}, nb::handle()}; - return ndarray_wrap(arr.handle(), ndarray_framework::numpy, policy, - cleanup); + static_assert(sizeof(la::vec) == (N * sizeof(T)), + "VecView -> numpy requires packed structs"); + numpy_type arr{vec.data(), {num_vec, N}, nb::handle()}; + // we must copy the underlying data + return make_caster::from_cpp(arr, rv_policy::copy, cleanup); } }; @@ -274,8 +262,22 @@ NB_MODULE(manifold3d, m) { return self.Warp([&warp_func](vec3 &v) { v = warp_func(v); }); }, nb::arg("warp_func"), manifold__warp__warp_func) - .def("warp_batch", &Manifold::WarpBatch, nb::arg("warp_func"), - manifold__warp_batch__warp_func) + .def( + "warp_batch", + [](const Manifold &self, + std::function)> warp_func) { + // need a wrapper because python cant modify a reference in-place + return self.WarpBatch([&warp_func](VecView v) { + auto tmp = warp_func(v); + nb::ndarray, nanobind::c_contig> tmpnd; + if (!nb::try_cast(tmp, tmpnd) || tmpnd.ndim() != 2) + throw std::runtime_error( + "Invalid vector shape, expected (:, 3)"); + std::copy(tmpnd.data(), tmpnd.data() + v.size() * 3, + &v.data()->x); + }); + }, + nb::arg("warp_func"), manifold__warp_batch__warp_func) .def( "set_properties", [](const Manifold &self, int newNumProp, @@ -400,7 +402,6 @@ NB_MODULE(manifold3d, m) { }, nb::arg("mesh"), nb::arg("sharpened_edges") = nb::list(), nb::arg("edge_smoothness") = nb::list(), - // TODO: params slightly diff manifold__smooth__mesh_gl__sharpened_edges) .def_static("batch_boolean", &Manifold::BatchBoolean, nb::arg("manifolds"), nb::arg("op"), @@ -669,6 +670,23 @@ NB_MODULE(manifold3d, m) { nb::arg("warp_func"), cross_section__warp__warp_func) .def("warp_batch", &CrossSection::WarpBatch, nb::arg("warp_func"), cross_section__warp_batch__warp_func) + + .def( + "warp_batch", + [](const CrossSection &self, + std::function)> warp_func) { + // need a wrapper because python cant modify a reference in-place + return self.WarpBatch([&warp_func](VecView v) { + auto tmp = warp_func(v); + nb::ndarray, nanobind::c_contig> tmpnd; + if (!nb::try_cast(tmp, tmpnd) || tmpnd.ndim() != 2) + throw std::runtime_error( + "Invalid vector shape, expected (:, 2)"); + std::copy(tmpnd.data(), tmpnd.data() + v.size() * 2, + &v.data()->x); + }); + }, + nb::arg("warp_func"), cross_section__warp_batch__warp_func) .def("simplify", &CrossSection::Simplify, nb::arg("epsilon") = 1e-6, cross_section__simplify__epsilon) .def( diff --git a/pyproject.toml b/pyproject.toml index 166753335..cc38ed26b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ [build-system] requires = [ - "nanobind>=1.8.0", + "nanobind>=1.8.0,<=2.2.0", "scikit-build-core", ] build-backend = "scikit_build_core.build"