From 8060376e1252be35e03be40ed97a15cf0bf24090 Mon Sep 17 00:00:00 2001 From: pca006132 Date: Sat, 16 Dec 2023 18:15:53 +0800 Subject: [PATCH 01/15] add formatting script (#660) * update formatting * update formatter --- .github/workflows/check_format.yml | 2 +- README.md | 8 ++++++++ format.sh | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100755 format.sh diff --git a/.github/workflows/check_format.yml b/.github/workflows/check_format.yml index ae92f9493..482153fc4 100644 --- a/.github/workflows/check_format.yml +++ b/.github/workflows/check_format.yml @@ -17,7 +17,7 @@ jobs: source: '.' exclude: '*/third_party' extensions: 'h,cpp,js,ts,html' - clangFormatVersion: 12 + clangFormatVersion: 11 - uses: psf/black@stable with: options: "--check --verbose" diff --git a/README.md b/README.md index 95f652511..322d50980 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,14 @@ For more detailed documentation, please refer to the C++ API. Contributions are welcome! A lower barrier contribution is to simply make a PR that adds a test, especially if it repros an issue you've found. Simply name it prepended with DISABLED_, so that it passes the CI. That will be a very strong signal to me to fix your issue. However, if you know how to fix it yourself, then including the fix in your PR would be much appreciated! +### Formatting + +There is a formatting script `format.sh` that automatically formats everything. +It requires clang-format 11 and black formatter for python. + +If you have clang-format installed but without clang-11, you can specify the +clang-format executable by setting the `CLANG_FORMAT` environment variable. + ### Profiling There is now basic support for the [Tracy profiler](https://github.com/wolfpld/tracy) for our tests. diff --git a/format.sh b/format.sh new file mode 100755 index 000000000..8a23db65f --- /dev/null +++ b/format.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +shopt -s extglob +if [ -z "$CLANG_FORMAT" ]; then +CLANG_FORMAT=$(dirname $(which clang-11))/clang-format +fi + +$CLANG_FORMAT -i extras/*.cpp +$CLANG_FORMAT -i meshIO/**/*.{h,cpp} +$CLANG_FORMAT -i samples/**/*.{h,cpp} +$CLANG_FORMAT -i test/*.{h,cpp} +$CLANG_FORMAT -i bindings/*/*.cpp +$CLANG_FORMAT -i bindings/c/include/*.h +$CLANG_FORMAT -i bindings/wasm/**/*.{js,ts,html} +$CLANG_FORMAT -i src/!(third_party)/*/*.{h,cpp} +black bindings/python/examples/*.py From d52decc4f1c071a5f4036b89d42ee4d5e3e6280b Mon Sep 17 00:00:00 2001 From: pca006132 Date: Sat, 16 Dec 2023 18:25:45 +0800 Subject: [PATCH 02/15] example url (#643) * example url * update * format * address comments * fix formatting --- bindings/wasm/examples/editor.js | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/bindings/wasm/examples/editor.js b/bindings/wasm/examples/editor.js index ef3ed8402..c6954331d 100644 --- a/bindings/wasm/examples/editor.js +++ b/bindings/wasm/examples/editor.js @@ -90,9 +90,9 @@ function switchTo(scriptName) { switching = true; currentFileElement.textContent = scriptName; setScript('currentName', scriptName); - const code = - exampleFunctions.get(scriptName) ?? getScript(scriptName) ?? ''; isExample = exampleFunctions.get(scriptName) != null; + const code = isExample ? exampleFunctions.get(scriptName).substring(1) : + getScript(scriptName) ?? ''; editor.setValue(code); } } @@ -110,6 +110,8 @@ function createDropdownItem(name) { button.onclick = function() { saveCurrent(); + window.location.hash = `#${label.textContent}`; + window.location.search = ''; switchTo(label.textContent); }; // Stop text input spaces from triggering the button @@ -196,6 +198,7 @@ function addEdit(button) { removeScript(label.textContent); if (currentFileElement.textContent == label.textContent) { switchTo('Intro'); + window.location.hash = '#Intro'; } const container = button.parentElement; container.parentElement.removeChild(container); @@ -204,16 +207,16 @@ function addEdit(button) { } const newButton = document.querySelector('#new'); -function newItem(code) { - const name = uniqueName('New Script'); +function newItem(code, scriptName = undefined) { + const name = uniqueName(scriptName ?? 'New Script'); setScript(name, code); const nextButton = createDropdownItem(name); newButton.insertAdjacentElement('afterend', nextButton.parentElement); addEdit(nextButton); - nextButton.click(); + return {button: nextButton, name}; }; newButton.onclick = function() { - newItem(''); + newItem('').button.click(); }; const runButton = document.querySelector('#compile'); @@ -299,7 +302,20 @@ require(['vs/editor/editor.main'], async function() { addEdit(button); } } - switchTo(currentName); + const params = new URLSearchParams(window.location.search); + if (window.location.hash.length > 0) { + const name = unescape(window.location.hash.substring(1)); + switchTo(name); + } else if (params.get('script') != null) { + console.log(`Fetching ${params.get('script')}`); + autoExecute = false; + const response = await fetch(params.get('script')); + const code = await response.text(); + switchTo(newItem(code, params.get('name')).name); + } else { + switchTo(currentName); + window.location.hash = `#${currentName}`; + } if (manifoldInitialized) { initializeRun(); @@ -313,7 +329,7 @@ require(['vs/editor/editor.main'], async function() { } if (isExample) { const cursor = editor.getPosition(); - newItem(editor.getValue()); + newItem(editor.getValue()).button.click(); editor.setPosition(cursor); } }); From bc0bded294cb638174b4d97b281fe7ca663e1e46 Mon Sep 17 00:00:00 2001 From: Kyle Finn Date: Sun, 17 Dec 2023 04:15:28 -0500 Subject: [PATCH 03/15] add WarpBatch - array process warp with single callback (#651) Add WarpBatch - array process warp with single callback --- bindings/python/examples/torus_knot.py | 88 +++++++++++++++++ bindings/python/manifold3d.cpp | 88 +++++++++++++---- src/cross_section/include/cross_section.h | 3 + src/cross_section/src/cross_section.cpp | 45 ++++++--- src/manifold/include/manifold.h | 2 + src/manifold/src/impl.cpp | 9 +- src/manifold/src/impl.h | 1 + src/manifold/src/manifold.cpp | 14 +++ src/utilities/include/vec.h | 95 +----------------- src/utilities/include/vec_view.h | 112 ++++++++++++++++++++++ test/manifold_test.cpp | 17 ++++ 11 files changed, 349 insertions(+), 125 deletions(-) create mode 100644 bindings/python/examples/torus_knot.py create mode 100644 src/utilities/include/vec_view.h diff --git a/bindings/python/examples/torus_knot.py b/bindings/python/examples/torus_knot.py new file mode 100644 index 000000000..34d69bf4d --- /dev/null +++ b/bindings/python/examples/torus_knot.py @@ -0,0 +1,88 @@ +# %% +import numpy as np +from manifold3d import * + +# Creates a classic torus knot, defined as a string wrapping periodically +# around the surface of an imaginary donut. If p and q have a common +# factor then you will get multiple separate, interwoven knots. This is +# an example of using the warp() method, thus avoiding any direct +# handling of triangles. + + +def run(): + # The number of times the thread passes through the donut hole. + p = 1 + # The number of times the thread circles the donut. + q = 3 + # Radius of the interior of the imaginary donut. + majorRadius = 25 + # Radius of the small cross-section of the imaginary donut. + minorRadius = 10 + # Radius of the small cross-section of the actual object. + threadRadius = 3.75 + # Number of linear segments making up the threadRadius circle. Default is + # getCircularSegments(threadRadius). + circularSegments = -1 + # Number of segments along the length of the knot. Default makes roughly + # square facets. + linearSegments = -1 + + # These default values recreate Matlab Knot by Emmett Lalish: + # https://www.thingiverse.com/thing:7080 + + kLoops = np.gcd(p, q) + pk = p / kLoops + qk = q / kLoops + n = ( + circularSegments + if circularSegments > 2 + else get_circular_segments(threadRadius) + ) + m = linearSegments if linearSegments > 2 else n * qk * majorRadius / threadRadius + + offset = 2 + circle = CrossSection.circle(1, n).translate([offset, 0]) + + def ax_rotate(x, theta): + a, b = (x + 1) % 3, (x + 2) % 3 + s, c = np.sin(theta), np.cos(theta) + m = np.zeros((len(theta), 4, 4), dtype=np.float32) + m[:, a, a], m[:, a, b] = c, s + m[:, b, a], m[:, b, b] = -s, c + m[:, x, x], m[:, 3, 3] = 1, 1 + return m + + def func(pts): + npts = pts.shape[0] + x, y, z = pts[:, 0], pts[:, 1], pts[:, 2] + psi = qk * np.arctan2(x, y) + theta = psi * pk / qk + x1 = np.sqrt(x * x + y * y) + phi = np.arctan2(x1 - offset, z) + + v = np.zeros((npts, 4), dtype=np.float32) + v[:, 0] = threadRadius * np.cos(phi) + v[:, 2] = threadRadius * np.sin(phi) + v[:, 3] = 1 + r = majorRadius + minorRadius * np.cos(theta) + + m1 = ax_rotate(0, -np.arctan2(pk * minorRadius, qk * r)) + m1[:, 3, 0] = minorRadius + m2 = ax_rotate(1, theta) + m2[:, 3, 0] = majorRadius + m3 = ax_rotate(2, psi) + + v = v[:, None, :] @ (m1 @ m2 @ m3) + return v[:, 0, :3] + + def func_single(v): + pts = np.array(v)[None, :] + pts = func(pts) + return tuple(pts[0]) + + return circle.revolve(int(m)).warp_batch(func) + # return circle.revolve(int(m)).warp(func_single) + + +if __name__ == "__main__": + run() diff --git a/bindings/python/manifold3d.cpp b/bindings/python/manifold3d.cpp index 00f7d89ee..9c2fd7daf 100644 --- a/bindings/python/manifold3d.cpp +++ b/bindings/python/manifold3d.cpp @@ -70,31 +70,49 @@ namespace nb = nanobind; // helper to convert std::vector to numpy template nb::ndarray> to_numpy( - std::vector> const &vecvec) { + glm::vec const *begin, + glm::vec const *end) { // transfer ownership to PyObject - T *buffer = new T[vecvec.size() * N]; + size_t nvert = end - begin; + T *buffer = new T[nvert * N]; nb::capsule mem_mgr(buffer, [](void *p) noexcept { delete[](T *) p; }); - for (int i = 0; i < vecvec.size(); i++) { + for (int i = 0; i < nvert; i++) { for (int j = 0; j < N; j++) { - buffer[i * N + j] = vecvec[i][j]; + buffer[i * N + j] = begin[i][j]; } } - return {buffer, {vecvec.size(), N}, mem_mgr}; + return {buffer, {nvert, N}, mem_mgr}; +} + +// helper to convert std::vector to numpy +template +nb::ndarray> to_numpy( + std::vector> const &vec) { + return to_numpy(vec.data(), vec.data() + vec.size()); } // helper to convert numpy to std::vector template -std::vector> to_glm_vector( - nb::ndarray> const &arr) { - std::vector> out; - out.reserve(arr.shape(0)); +void to_glm_range(nb::ndarray> const &arr, + glm::vec *begin, + glm::vec *end) { + if (arr.shape(0) != end - begin) { + throw std::runtime_error( + "received numpy.shape[0]: " + std::to_string(arr.shape(0)) + + " expected: " + std::to_string(int(end - begin))); + } for (int i = 0; i < arr.shape(0); i++) { - out.emplace_back(); for (int j = 0; j < N; j++) { - out.back()[j] = arr(i, j); + begin[i][j] = arr(i, j); } } - return out; +} +// helper to convert numpy to std::vector +template +void to_glm_vector(nb::ndarray> const &arr, + std::vector> &out) { + out.resize(arr.shape(0)); + to_glm_range(arr, out.data(), out.data() + out.size()); } using namespace manifold; @@ -102,6 +120,10 @@ using namespace manifold; typedef std::tuple Float2; typedef std::tuple Float3; +using NumpyFloatNx2 = nb::ndarray>; +using NumpyFloatNx3 = nb::ndarray>; +using NumpyUintNx3 = nb::ndarray>; + template std::vector toVector(const T *arr, size_t size) { return std::vector(arr, arr + size); @@ -150,12 +172,10 @@ NB_MODULE(manifold3d, m) { "triangulate", [](std::vector>> polys) { - std::vector> polys_vec; - polys_vec.reserve(polys.size()); - for (auto &numpy : polys) { - polys_vec.push_back(to_glm_vector(numpy)); + std::vector> polys_vec(polys.size()); + for (int i = 0; i < polys.size(); i++) { + to_glm_vector(polys[i], polys_vec[i]); } - return to_numpy(Triangulate(polys_vec)); }, "Given a list polygons (each polygon shape=(N,2) dtype=float), " @@ -309,7 +329,23 @@ NB_MODULE(manifold3d, m) { "one which overlaps, but that is not checked here, so it is up to " "the user to choose their function with discretion." "\n\n" - ":param warpFunc: A function that modifies a given vertex position.") + ":param f: A function that modifies a given vertex position.") + .def( + "warp_batch", + [](Manifold &self, + const std::function &f) { + return self.WarpBatch([&f](VecView vecs) { + NumpyFloatNx3 arr = f(to_numpy(vecs.begin(), vecs.end())); + to_glm_range(arr, vecs.begin(), vecs.end()); + }); + }, + nb::arg("f"), + "Same as Manifold.warp but calls `f` with a " + "ndarray(shape=(N,3), dtype=float) and expects an ndarray " + "of the same shape and type in return. The input array can be " + "modified and returned if desired. " + "\n\n" + ":param f: A function that modifies multiple vertex positions.") .def( "set_properties", [](Manifold &self, int newNumProp, @@ -979,6 +1015,22 @@ NB_MODULE(manifold3d, m) { "intersections are not included in the result." "\n\n" ":param warpFunc: A function that modifies a given vertex position.") + .def( + "warp_batch", + [](CrossSection &self, + const std::function &f) { + return self.WarpBatch([&f](VecView vecs) { + NumpyFloatNx2 arr = f(to_numpy(vecs.begin(), vecs.end())); + to_glm_range(arr, vecs.begin(), vecs.end()); + }); + }, + nb::arg("f"), + "Same as CrossSection.warp but calls `f` with a " + "ndarray(shape=(N,2), dtype=float) and expects an ndarray " + "of the same shape and type in return. The input array can be " + "modified and returned if desired. " + "\n\n" + ":param f: A function that modifies multiple vertex positions.") .def("simplify", &CrossSection::Simplify, nb::arg("epsilon") = 1e-6, "Remove vertices from the contours in this CrossSection that are " "less than the specified distance epsilon from an imaginary line " diff --git a/src/cross_section/include/cross_section.h b/src/cross_section/include/cross_section.h index abe32bce5..9e15bb529 100644 --- a/src/cross_section/include/cross_section.h +++ b/src/cross_section/include/cross_section.h @@ -21,6 +21,7 @@ #include "glm/ext/matrix_float3x2.hpp" #include "glm/ext/vector_float2.hpp" #include "public.h" +#include "vec_view.h" namespace manifold { @@ -97,6 +98,8 @@ class CrossSection { CrossSection Mirror(const glm::vec2 ax) const; CrossSection Transform(const glm::mat3x2& m) const; CrossSection Warp(std::function warpFunc) const; + CrossSection WarpBatch( + std::function)> warpFunc) const; CrossSection Simplify(double epsilon = 1e-6) const; // Adapted from Clipper2 docs: diff --git a/src/cross_section/src/cross_section.cpp b/src/cross_section/src/cross_section.cpp index 9e3065942..658587255 100644 --- a/src/cross_section/src/cross_section.cpp +++ b/src/cross_section/src/cross_section.cpp @@ -567,21 +567,42 @@ CrossSection CrossSection::Transform(const glm::mat3x2& m) const { */ CrossSection CrossSection::Warp( std::function warpFunc) const { - auto paths = GetPaths(); - auto warped = C2::PathsD(); - warped.reserve(paths->paths_.size()); - for (auto path : paths->paths_) { - auto sz = path.size(); - auto s = C2::PathD(sz); - for (int i = 0; i < sz; ++i) { - auto v = v2_of_pd(path[i]); - warpFunc(v); - s[i] = v2_to_pd(v); + return WarpBatch([&warpFunc](VecView vecs) { + for (glm::vec2& p : vecs) { + warpFunc(p); + } + }); +} + +/** + * Same as CrossSection::Warp but calls warpFunc with + * a VecView which is roughly equivalent to std::span + * pointing to all vec2 elements to be modified in-place + * + * @param warpFunc A function that modifies multiple vertex positions. + */ +CrossSection CrossSection::WarpBatch( + std::function)> warpFunc) const { + std::vector tmp_verts; + C2::PathsD paths = GetPaths()->paths_; // deep copy + for (C2::PathD const& path : paths) { + for (C2::PointD const& p : path) { + tmp_verts.push_back(v2_of_pd(p)); + } + } + + warpFunc(VecView(tmp_verts.data(), tmp_verts.size())); + + auto cursor = tmp_verts.begin(); + for (C2::PathD& path : paths) { + for (C2::PointD& p : path) { + p = v2_to_pd(*cursor); + ++cursor; } - warped.push_back(s); } + return CrossSection( - shared_paths(C2::Union(warped, C2::FillRule::Positive, precision_))); + shared_paths(C2::Union(paths, C2::FillRule::Positive, precision_))); } /** diff --git a/src/manifold/include/manifold.h b/src/manifold/include/manifold.h index 283b9346e..11366d5c8 100644 --- a/src/manifold/include/manifold.h +++ b/src/manifold/include/manifold.h @@ -18,6 +18,7 @@ #include "cross_section.h" #include "public.h" +#include "vec_view.h" namespace manifold { @@ -206,6 +207,7 @@ class Manifold { Manifold Transform(const glm::mat4x3&) const; Manifold Mirror(glm::vec3) const; Manifold Warp(std::function) const; + Manifold WarpBatch(std::function)>) const; Manifold SetProperties( int, std::function) const; Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const; diff --git a/src/manifold/src/impl.cpp b/src/manifold/src/impl.cpp index 5b71f4999..bf405ea9b 100644 --- a/src/manifold/src/impl.cpp +++ b/src/manifold/src/impl.cpp @@ -719,7 +719,14 @@ void Manifold::Impl::MarkFailure(Error status) { } void Manifold::Impl::Warp(std::function warpFunc) { - thrust::for_each_n(thrust::host, vertPos_.begin(), NumVert(), warpFunc); + WarpBatch([&warpFunc](VecView vecs) { + thrust::for_each(thrust::host, vecs.begin(), vecs.end(), warpFunc); + }); +} + +void Manifold::Impl::WarpBatch( + std::function)> warpFunc) { + warpFunc(vertPos_.view()); CalculateBBox(); if (!IsFinite()) { MarkFailure(Error::NonFiniteVertex); diff --git a/src/manifold/src/impl.h b/src/manifold/src/impl.h index 4d12b637c..36e37fcc0 100644 --- a/src/manifold/src/impl.h +++ b/src/manifold/src/impl.h @@ -76,6 +76,7 @@ struct Manifold::Impl { void Update(); void MarkFailure(Error status); void Warp(std::function warpFunc); + void WarpBatch(std::function)> warpFunc); Impl Transform(const glm::mat4x3& transform) const; SparseIndices EdgeCollisions(const Impl& B, bool inverted = false) const; SparseIndices VertexCollisionsZ(VecView vertsIn, diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 124a67f86..841339aac 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -569,6 +569,20 @@ Manifold Manifold::Warp(std::function warpFunc) const { return Manifold(std::make_shared(pImpl)); } +/** + * Same as Manifold::Warp but calls warpFunc with with + * a VecView which is roughly equivalent to std::span + * pointing to all vec3 elements to be modified in-place + * + * @param warpFunc A function that modifies multiple vertex positions. + */ +Manifold Manifold::WarpBatch( + std::function)> warpFunc) const { + auto pImpl = std::make_shared(*GetCsgLeafNode().GetImpl()); + pImpl->WarpBatch(warpFunc); + return Manifold(std::make_shared(pImpl)); +} + /** * Create a new copy of this manifold with updated vertex properties by * supplying a function that takes the existing position and properties as diff --git a/src/utilities/include/vec.h b/src/utilities/include/vec.h index de84724f2..f5170b054 100644 --- a/src/utilities/include/vec.h +++ b/src/utilities/include/vec.h @@ -24,6 +24,7 @@ // #include "optional_assert.h" #include "par.h" #include "public.h" +#include "vec_view.h" namespace manifold { @@ -33,100 +34,6 @@ namespace manifold { template class Vec; -/** - * View for Vec, can perform offset operation. - * This will be invalidated when the original vector is dropped or changes - * length. - */ -template -class VecView { - public: - using Iter = T *; - using IterC = const T *; - - VecView(T *ptr_, int size_) : ptr_(ptr_), size_(size_) {} - - VecView(const VecView &other) { - ptr_ = other.ptr_; - size_ = other.size_; - } - - VecView &operator=(const VecView &other) { - ptr_ = other.ptr_; - size_ = other.size_; - return *this; - } - - // allows conversion to a const VecView - operator VecView() const { return {ptr_, size_}; } - - inline const T &operator[](int i) const { - if (i < 0 || i >= size_) throw std::out_of_range("Vec out of range"); - return ptr_[i]; - } - - inline T &operator[](int i) { - if (i < 0 || i >= size_) throw std::out_of_range("Vec out of range"); - return ptr_[i]; - } - - IterC cbegin() const { return ptr_; } - IterC cend() const { return ptr_ + size_; } - - IterC begin() const { return cbegin(); } - IterC end() const { return cend(); } - - Iter begin() { return ptr_; } - Iter end() { return ptr_ + size_; } - - const T &front() const { - if (size_ == 0) - throw std::out_of_range("attempt to take the front of an empty vector"); - return ptr_[0]; - } - - const T &back() const { - if (size_ == 0) - throw std::out_of_range("attempt to take the back of an empty vector"); - return ptr_[size_ - 1]; - } - - T &front() { - if (size_ == 0) - throw std::out_of_range("attempt to take the front of an empty vector"); - return ptr_[0]; - } - - T &back() { - if (size_ == 0) - throw std::out_of_range("attempt to take the back of an empty vector"); - return ptr_[size_ - 1]; - } - - int size() const { return size_; } - - bool empty() const { return size_ == 0; } - -#ifdef MANIFOLD_DEBUG - void Dump() { - std::cout << "Vec = " << std::endl; - for (int i = 0; i < size(); ++i) { - std::cout << i << ", " << ptr_[i] << ", " << std::endl; - } - std::cout << std::endl; - } -#endif - - protected: - T *ptr_ = nullptr; - int size_ = 0; - - VecView() = default; - friend class Vec; - friend class Vec::type>; - friend class VecView::type>; -}; - /* * Specialized vector implementation with multithreaded fill and uninitialized * memory optimizations. diff --git a/src/utilities/include/vec_view.h b/src/utilities/include/vec_view.h new file mode 100644 index 000000000..d4b4b649c --- /dev/null +++ b/src/utilities/include/vec_view.h @@ -0,0 +1,112 @@ +// Copyright 2023 The Manifold Authors. +// +// Licensed 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 CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace manifold { + +/** + * View for Vec, can perform offset operation. + * This will be invalidated when the original vector is dropped or changes + * length. Roughly equivalent to std::span from c++20 + */ +template +class VecView { + public: + using Iter = T *; + using IterC = const T *; + + VecView(T *ptr_, int size_) : ptr_(ptr_), size_(size_) {} + + VecView(const VecView &other) { + ptr_ = other.ptr_; + size_ = other.size_; + } + + VecView &operator=(const VecView &other) { + ptr_ = other.ptr_; + size_ = other.size_; + return *this; + } + + // allows conversion to a const VecView + operator VecView() const { return {ptr_, size_}; } + + inline const T &operator[](int i) const { + if (i < 0 || i >= size_) throw std::out_of_range("Vec out of range"); + return ptr_[i]; + } + + inline T &operator[](int i) { + if (i < 0 || i >= size_) throw std::out_of_range("Vec out of range"); + return ptr_[i]; + } + + IterC cbegin() const { return ptr_; } + IterC cend() const { return ptr_ + size_; } + + IterC begin() const { return cbegin(); } + IterC end() const { return cend(); } + + Iter begin() { return ptr_; } + Iter end() { return ptr_ + size_; } + + const T &front() const { + if (size_ == 0) + throw std::out_of_range("attempt to take the front of an empty vector"); + return ptr_[0]; + } + + const T &back() const { + if (size_ == 0) + throw std::out_of_range("attempt to take the back of an empty vector"); + return ptr_[size_ - 1]; + } + + T &front() { + if (size_ == 0) + throw std::out_of_range("attempt to take the front of an empty vector"); + return ptr_[0]; + } + + T &back() { + if (size_ == 0) + throw std::out_of_range("attempt to take the back of an empty vector"); + return ptr_[size_ - 1]; + } + + int size() const { return size_; } + + bool empty() const { return size_ == 0; } + +#ifdef MANIFOLD_DEBUG + void Dump() { + std::cout << "Vec = " << std::endl; + for (int i = 0; i < size(); ++i) { + std::cout << i << ", " << ptr_[i] << ", " << std::endl; + } + std::cout << std::endl; + } +#endif + + protected: + T *ptr_ = nullptr; + int size_ = 0; + + VecView() = default; +}; + +} // namespace manifold \ No newline at end of file diff --git a/test/manifold_test.cpp b/test/manifold_test.cpp index 036f5182d..14884af8a 100644 --- a/test/manifold_test.cpp +++ b/test/manifold_test.cpp @@ -303,6 +303,23 @@ TEST(Manifold, Warp2) { EXPECT_NEAR(propBefore.volume, 321, 1); } +TEST(Manifold, WarpBatch) { + Manifold shape1 = + Manifold::Cube({2, 3, 4}).Warp([](glm::vec3& v) { v.x += v.z * v.z; }); + auto prop1 = shape1.GetProperties(); + + Manifold shape2 = + Manifold::Cube({2, 3, 4}).WarpBatch([](VecView vecs) { + for (glm::vec3& v : vecs) { + v.x += v.z * v.z; + } + }); + auto prop2 = shape2.GetProperties(); + + EXPECT_EQ(prop1.volume, prop2.volume); + EXPECT_EQ(prop1.surfaceArea, prop2.surfaceArea); +} + TEST(Manifold, Smooth) { Manifold tet = Manifold::Tetrahedron(); Manifold smooth = Manifold::Smooth(tet.GetMesh()); From 7d564cfbc19ee606523cbabb1c34d96fb748b9c6 Mon Sep 17 00:00:00 2001 From: Kyle Finn Date: Mon, 18 Dec 2023 19:34:40 -0500 Subject: [PATCH 04/15] Python more numpy (#656) * [python] numpy updates and cleanup Also add CrossSection::Bounds() * cleanup - const and reference args in py bind * fix unnamed args * all_apis.py more coverage * fixup - more param renaming, docstring fixes * fixup - run black * fixup - move const to left side * fixup extrude.py get_volume * fix python examples for api update --- bindings/python/examples/all_apis.py | 101 ++ bindings/python/examples/bricks.py | 42 +- bindings/python/examples/cube_with_dents.py | 8 +- bindings/python/examples/extrude.py | 6 +- bindings/python/examples/gyroid_module.py | 14 +- bindings/python/examples/maze.py | 2 +- bindings/python/examples/scallop.py | 3 +- bindings/python/examples/split_cube.py | 4 +- bindings/python/examples/sponge.py | 4 +- bindings/python/examples/union_failure.py | 2 +- bindings/python/manifold3d.cpp | 998 +++++++++----------- 11 files changed, 572 insertions(+), 612 deletions(-) create mode 100644 bindings/python/examples/all_apis.py diff --git a/bindings/python/examples/all_apis.py b/bindings/python/examples/all_apis.py new file mode 100644 index 000000000..9ea55b5e8 --- /dev/null +++ b/bindings/python/examples/all_apis.py @@ -0,0 +1,101 @@ +from manifold3d import * +import numpy as np + + +def all_root_level(): + set_min_circular_angle(10) + set_min_circular_edge_length(1) + set_circular_segments(22) + n = get_circular_segments(1) + assert n == 22 + poly = [[0, 0], [1, 0], [1, 1]] + tris = triangulate([poly]) + tris = triangulate([np.array(poly)]) + + +def all_cross_section(): + poly = [[0, 0], [1, 0], [1, 1]] + c = CrossSection([np.array(poly)]) + c = CrossSection([poly]) + c = CrossSection() + c + a = c.area() + c = CrossSection.batch_hull([c, c.translate((1, 0))]) + b = c.bounds() + c = CrossSection.circle(1) + cs = c.decompose() + m = c.extrude(1) + c = c.hull() + c = CrossSection.hull_points(poly) + c = CrossSection.hull_points(np.array(poly)) + e = c.is_empty() + c = c.mirror((0, 1)) + n = c.num_contour() + n = c.num_vert() + c = c.offset(1, JoinType.Round) + m = c.revolve() + c = c.rotate(90) + c = c.scale((2, 2)) + c = c.simplify() + c = CrossSection.square((1, 1)) + p = c.to_polygons() + c = c.transform([[1, 0, 0], [0, 1, 0]]) + c = c.translate((1, 1)) + c = c.warp(lambda p: (p[0] + 1, p[1] / 2)) + c = c.warp_batch(lambda ps: ps * [1, 0.5] + [1, 0]) + + +def all_manifold(): + mesh = Manifold.sphere(1).to_mesh() + m = Manifold(mesh) + m = Manifold() + m + m = m.as_original() + m = Manifold.batch_hull([m, m.translate((0, 0, 1))]) + b = m.bounding_box() + m = m.calculate_curvature(4, 5) + m = Manifold.compose([m, m.translate((5, 0, 0))]) + m = Manifold.cube((1, 1, 1)) + m = Manifold.cylinder(1, 1) + ms = m.decompose() + g = m.genus() + a = m.surface_area() + v = m.volume() + m = m.hull() + m = m.hull_points(mesh.vert_properties) + e = m.is_empty() + m = m.mirror((0, 0, 1)) + n = m.num_edge() + n = m.num_prop() + n = m.num_prop_vert() + n = m.num_tri() + n = m.num_vert() + i = m.original_id() + p = m.precision() + c = m.project() + m = m.refine(2) + i = Manifold.reserve_ids(1) + m = m.scale((1, 2, 3)) + m = m.set_properties(3, lambda pos, prop: pos) + c = m.slice(0.5) + m = Manifold.smooth(mesh, [0], [0.5]) + m = Manifold.sphere(1) + m, n = m.split(m.translate((1, 0, 0))) + m, n = m.split_by_plane((0, 0, 1), 0) + e = m.status() + m = Manifold.tetrahedron() + mesh = m.to_mesh() + m = m.transform([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]]) + 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]) + + +def run(): + all_root_level() + all_cross_section() + all_manifold() + return Manifold() + + +if __name__ == "__main__": + run() diff --git a/bindings/python/examples/bricks.py b/bindings/python/examples/bricks.py index e2b3cf5ab..a0d04aa1a 100644 --- a/bindings/python/examples/bricks.py +++ b/bindings/python/examples/bricks.py @@ -28,44 +28,50 @@ def brick(): - return Manifold.cube(brick_length, brick_depth, brick_height) + return Manifold.cube([brick_length, brick_depth, brick_height]) def halfbrick(): - return Manifold.cube((brick_length - mortar_gap) / 2, brick_depth, brick_height) + return Manifold.cube([(brick_length - mortar_gap) / 2, brick_depth, brick_height]) def row(length): bricks = [ - brick().translate((brick_length + mortar_gap) * x, 0, 0) for x in range(length) + brick().translate([(brick_length + mortar_gap) * x, 0, 0]) + for x in range(length) ] - return Manifold(bricks) + return sum(bricks, Manifold()) def wall(length, height, alternate=0): bricks = [ row(length).translate( - ((z + alternate) % 2) * (brick_depth + mortar_gap), - 0, - (brick_height + mortar_gap) * z, + [ + ((z + alternate) % 2) * (brick_depth + mortar_gap), + 0, + (brick_height + mortar_gap) * z, + ] ) for z in range(height) ] - return Manifold(bricks) + return sum(bricks, Manifold()) def walls(length, width, height): - return Manifold( + return sum( [ wall(length, height), - wall(width, height, 1).rotate(0, 0, 90).translate(brick_depth, 0, 0), + wall(width, height, 1).rotate([0, 0, 90]).translate([brick_depth, 0, 0]), wall(length, height, 1).translate( - 0, (width) * (brick_length + mortar_gap), 0 + [0, (width) * (brick_length + mortar_gap), 0] ), wall(width, height) - .rotate(0, 0, 90) - .translate((length + 0.5) * (brick_length + mortar_gap) - mortar_gap, 0, 0), - ] + .rotate([0, 0, 90]) + .translate( + [(length + 0.5) * (brick_length + mortar_gap) - mortar_gap, 0, 0] + ), + ], + Manifold(), ) @@ -74,7 +80,7 @@ def floor(length, width): if length > 1 and width > 1: results.append( floor(length - 1, width - 1).translate( - brick_depth + mortar_gap, brick_depth + mortar_gap, 0 + [brick_depth + mortar_gap, brick_depth + mortar_gap, 0] ) ) if length == 1 and width > 1: @@ -82,13 +88,13 @@ def floor(length, width): if width == 1 and length > 1: results.append( row(length - 1).translate( - 2 * (brick_depth + mortar_gap), brick_depth + mortar_gap, 0 + [2 * (brick_depth + mortar_gap), brick_depth + mortar_gap, 0] ) ) results.append( - halfbrick().translate(brick_depth + mortar_gap, brick_depth + mortar_gap, 0) + halfbrick().translate([brick_depth + mortar_gap, brick_depth + mortar_gap, 0]) ) - return Manifold(results) + return sum(results, Manifold()) def run(width=10, length=10, height=10): diff --git a/bindings/python/examples/cube_with_dents.py b/bindings/python/examples/cube_with_dents.py index d9eed7151..d7e854a20 100644 --- a/bindings/python/examples/cube_with_dents.py +++ b/bindings/python/examples/cube_with_dents.py @@ -21,13 +21,13 @@ def run(n=5, overlap=True): - a = Manifold.cube(n, n, 0.5).translate(-0.5, -0.5, -0.5) + a = Manifold.cube([n, n, 0.5]).translate([-0.5, -0.5, -0.5]) spheres = [ - Manifold.sphere(0.45 if overlap else 0.55, 50).translate(i, j, 0) + Manifold.sphere(0.45 if overlap else 0.55, 50).translate([i, j, 0]) for i in range(n) for j in range(n) ] - spheres = reduce(lambda a, b: a + b, spheres) + # spheres = reduce(lambda a, b: a + b, spheres) - return a - spheres + return a - sum(spheres, Manifold()) diff --git a/bindings/python/examples/extrude.py b/bindings/python/examples/extrude.py index 747423c8a..4139e5fe0 100644 --- a/bindings/python/examples/extrude.py +++ b/bindings/python/examples/extrude.py @@ -18,13 +18,13 @@ def run(): # extrude a polygon to create a manifold extruded_polygon = cross_section.extrude(10.0) eps = 0.001 - observed_volume = extruded_polygon.get_volume() + observed_volume = extruded_polygon.volume() expected_volume = 10.0 if abs(observed_volume - expected_volume) > eps: raise Exception( f"observed_volume={observed_volume} differs from expected_volume={expected_volume}" ) - observed_surface_area = extruded_polygon.get_surface_area() + observed_surface_area = extruded_polygon.surface_area() expected_surface_area = 42.0 if abs(observed_surface_area - expected_surface_area) > eps: raise Exception( @@ -32,7 +32,7 @@ def run(): ) # get bounding box from manifold - observed_bbox = extruded_polygon.bounding_box + observed_bbox = extruded_polygon.bounding_box() expected_bbox = (0.0, 0.0, 0.0, 1.0, 1.0, 10.0) if observed_bbox != expected_bbox: raise Exception( diff --git a/bindings/python/examples/gyroid_module.py b/bindings/python/examples/gyroid_module.py index e48cb7287..e57040531 100644 --- a/bindings/python/examples/gyroid_module.py +++ b/bindings/python/examples/gyroid_module.py @@ -15,7 +15,11 @@ """ import math +import sys import numpy as np + +sys.path.append("/Users/k/projects/python/badcad/wip/manifold/build/bindings/python") + from manifold3d import Mesh, Manifold @@ -31,20 +35,20 @@ def gyroid(x, y, z): def gyroid_levelset(level, period, size, n): - return Manifold.from_mesh( + return Manifold( Mesh.level_set( gyroid, [-period, -period, -period, period, period, period], period / n, level, ) - ).scale(size / period) + ).scale([size / period] * 3) def rhombic_dodecahedron(size): box = Manifold.cube(size * math.sqrt(2.0) * np.array([1, 1, 2]), True) - result = box.rotate(90, 45) ^ box.rotate(90, 45, 90) - return result ^ box.rotate(0, 0, 45) + result = box.rotate([90, 45, 0]) ^ box.rotate([90, 45, 90]) + return result ^ box.rotate([0, 0, 45]) def gyroid_module(size=20, n=15): @@ -52,7 +56,7 @@ def gyroid_module(size=20, n=15): result = ( gyroid_levelset(-0.4, period, size, n) ^ rhombic_dodecahedron(size) ) - gyroid_levelset(0.4, period, size, n) - return result.rotate(-45, 0, 90).translate(0, 0, size / math.sqrt(2.0)) + return result.rotate([-45, 0, 90]).translate([0, 0, size / math.sqrt(2.0)]) def run(size=20, n=15): diff --git a/bindings/python/examples/maze.py b/bindings/python/examples/maze.py index 095933a13..f86c734e3 100644 --- a/bindings/python/examples/maze.py +++ b/bindings/python/examples/maze.py @@ -4039,4 +4039,4 @@ def ball(*args): ball([10, 10, 10], 0.4) - return Manifold.cube((n + 1) * np.array([1, 1, 1])) - Manifold(cavity) + return Manifold.cube([n + 1] * 3) - sum(cavity, Manifold()) diff --git a/bindings/python/examples/scallop.py b/bindings/python/examples/scallop.py index 5679cd9aa..717c877f5 100644 --- a/bindings/python/examples/scallop.py +++ b/bindings/python/examples/scallop.py @@ -72,8 +72,9 @@ def colorCurvature(_pos, oldProp): blue = [0, 0, 1] return [(1 - b) * blue[i] + b * red[i] for i in range(3)] + edges, smoothing = zip(*sharpenedEdges) return ( - Manifold.smooth(scallop, sharpenedEdges) + Manifold.smooth(scallop, edges, smoothing) .refine(n) .calculate_curvature(-1, 0) .set_properties(3, colorCurvature) diff --git a/bindings/python/examples/split_cube.py b/bindings/python/examples/split_cube.py index 70c8ac5b1..4dc449a93 100644 --- a/bindings/python/examples/split_cube.py +++ b/bindings/python/examples/split_cube.py @@ -18,6 +18,6 @@ def run(): - a = Manifold.cube(1.0, 1.0, 1.0) - b = Manifold.cube(1.0, 1.0, 1.0).rotate(45.0, 45.0, 45.0) + a = Manifold.cube([1.0, 1.0, 1.0]) + b = Manifold.cube([1.0, 1.0, 1.0]).rotate([45.0, 45.0, 45.0]) return a.split(b)[0] diff --git a/bindings/python/examples/sponge.py b/bindings/python/examples/sponge.py index 56adf0959..d0bdd7f69 100644 --- a/bindings/python/examples/sponge.py +++ b/bindings/python/examples/sponge.py @@ -38,4 +38,6 @@ def run(n=1): result -= hole.rotate([90, 0, 0]) result -= hole.rotate([0, 90, 0]) - return result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale(100) + return ( + result.trim_by_plane([1, 1, 1], 0).set_properties(4, posColors).scale([100] * 3) + ) diff --git a/bindings/python/examples/union_failure.py b/bindings/python/examples/union_failure.py index 30dbba2c2..3b6ba72eb 100644 --- a/bindings/python/examples/union_failure.py +++ b/bindings/python/examples/union_failure.py @@ -4,5 +4,5 @@ def run(): # for some reason this causes collider error obj = Manifold.cube() - obj += Manifold([Manifold.cube()]).rotate(0, 0, 45 + 180) + obj += Manifold.cube().rotate([0, 0, 45 + 180]) return obj diff --git a/bindings/python/manifold3d.cpp b/bindings/python/manifold3d.cpp index 9c2fd7daf..6e0bd1357 100644 --- a/bindings/python/manifold3d.cpp +++ b/bindings/python/manifold3d.cpp @@ -22,107 +22,180 @@ #include "nanobind/operators.h" #include "nanobind/stl/function.h" #include "nanobind/stl/optional.h" +#include "nanobind/stl/pair.h" #include "nanobind/stl/tuple.h" #include "nanobind/stl/vector.h" #include "polygon.h" #include "sdf.h" +namespace nb = nanobind; +using namespace manifold; + +template +struct glm_name {}; +template <> +struct glm_name { + static constexpr char const name[] = "Floatx3"; + static constexpr char const multi_name[] = "FloatNx3"; +}; +template <> +struct glm_name { + static constexpr char const name[] = "Floatx2"; + static constexpr char const multi_name[] = "FloatNx2"; +}; +template <> +struct glm_name { + static constexpr char const name[] = "Intx3"; + static constexpr char const multi_name[] = "IntNx3"; +}; template <> -struct nanobind::detail::type_caster { - NB_TYPE_CASTER(glm::vec3, const_name("Vec3")); +struct glm_name { + static constexpr char const name[] = "Float3x4"; +}; +template <> +struct glm_name { + static constexpr char const name[] = "Float2x3"; +}; - using Caster = make_caster; +// handle glm::vecN +template +struct nb::detail::type_caster> { + using glm_type = glm::vec; + NB_TYPE_CASTER(glm_type, const_name(glm_name::name)); bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { - size_t size; - PyObject *temp; - /* Will initialize 'size' and 'temp'. All return values and - return parameters are zero/NULL in the case of a failure. */ - PyObject **o = seq_get(src.ptr(), &size, &temp); - if (size != 3) return false; - Caster caster; - if (o == nullptr) { - Py_XDECREF(temp); - return false; + int size = PyObject_Size(src.ptr()); // negative on failure + if (size != N) return false; + make_caster t_cast; + for (size_t i = 0; i < size; i++) { + if (!t_cast.from_python(src[i], flags, cleanup)) return false; + value[i] = t_cast.value; } - - bool success = true; - for (size_t i = 0; i < size; ++i) { - if (!caster.from_python(o[i], flags, cleanup)) { - success = false; - break; - } - value[i] = caster.value; - } - Py_XDECREF(temp); - return success; + return true; } - - static handle from_cpp(glm::vec3 vec, rv_policy policy, - cleanup_list *ls) noexcept { - std::vector v{vec.x, vec.y, vec.z}; - return make_caster>().from_cpp(&v, policy, ls); + static handle from_cpp(glm_type vec, rv_policy policy, + cleanup_list *cleanup) noexcept { + nb::list out; + for (int i = 0; i < N; i++) out.append(vec[i]); + return out.release(); } }; -namespace nb = nanobind; +// handle glm::matMxN +template +struct nb::detail::type_caster> { + using glm_type = glm::mat; + using numpy_type = nb::ndarray>; + NB_TYPE_CASTER(glm_type, const_name(glm_name::name)); -// helper to convert std::vector to numpy -template -nb::ndarray> to_numpy( - glm::vec const *begin, - glm::vec const *end) { - // transfer ownership to PyObject - size_t nvert = end - begin; - T *buffer = new T[nvert * N]; - nb::capsule mem_mgr(buffer, [](void *p) noexcept { delete[](T *) p; }); - for (int i = 0; i < nvert; i++) { - for (int j = 0; j < N; j++) { - buffer[i * N + j] = begin[i][j]; + bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { + int rows = PyObject_Size(src.ptr()); // negative on failure + if (rows != R) return false; + for (size_t i = 0; i < R; i++) { + const nb::object &slice = src[i]; + int cols = PyObject_Size(slice.ptr()); // negative on failure + if (cols != C) return false; + for (size_t j = 0; j < C; j++) { + make_caster t_cast; + if (!t_cast.from_python(slice[j], flags, cleanup)) return false; + value[j][i] = t_cast.value; + } } + return true; } - return {buffer, {nvert, N}, mem_mgr}; -} + static handle from_cpp(glm_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; }); + for (int i = 0; i < R; i++) { + for (int j = 0; j < C; j++) { + // py is (Rows, Cols), glm 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); + } +}; -// helper to convert std::vector to numpy -template -nb::ndarray> to_numpy( - std::vector> const &vec) { - return to_numpy(vec.data(), vec.data() + vec.size()); -} +// handle std::vector +template +struct nb::detail::type_caster>> { + using glm_type = glm::vec; + using numpy_type = nb::ndarray>; + NB_TYPE_CASTER(std::vector, + const_name(glm_name::multi_name)); -// helper to convert numpy to std::vector -template -void to_glm_range(nb::ndarray> const &arr, - glm::vec *begin, - glm::vec *end) { - if (arr.shape(0) != end - begin) { - throw std::runtime_error( - "received numpy.shape[0]: " + std::to_string(arr.shape(0)) + - " expected: " + std::to_string(int(end - begin))); + bool from_python(handle src, uint8_t flags, cleanup_list *cleanup) noexcept { + make_caster arr_cast; + if (arr_cast.from_python(src, flags, cleanup)) { + int num_vec = arr_cast.value.shape(0); + value.resize(num_vec); + for (int i = 0; i < num_vec; i++) { + for (int j = 0; j < N; j++) { + value[i][j] = arr_cast.value(i, j); + } + } + } else { + int num_vec = PyObject_Size(src.ptr()); // negative on failure + if (num_vec < 0) return false; + value.resize(num_vec); + for (int i = 0; i < num_vec; i++) { + make_caster vec_cast; + if (!vec_cast.from_python(src[i], flags, cleanup)) return false; + value[i] = vec_cast.value; + } + } + return true; } - for (int i = 0; i < arr.shape(0); i++) { - for (int j = 0; j < N; j++) { - begin[i][j] = arr(i, j); + static handle from_cpp(Value vec, rv_policy policy, + cleanup_list *cleanup) noexcept { + size_t num_vec = vec.size(); + T *buffer = new T[num_vec * N]; + nb::capsule mem_mgr(buffer, [](void *p) noexcept { delete[](T *) p; }); + for (int i = 0; i < num_vec; i++) { + for (int j = 0; j < N; j++) { + buffer[i * N + j] = vec[i][j]; + } } + numpy_type arr{buffer, {num_vec, N}, std::move(mem_mgr)}; + return ndarray_wrap(arr.handle(), int(ndarray_framework::numpy), policy, + cleanup); } -} -// helper to convert numpy to std::vector -template -void to_glm_vector(nb::ndarray> const &arr, - std::vector> &out) { - out.resize(arr.shape(0)); - to_glm_range(arr, out.data(), out.data() + out.size()); -} - -using namespace manifold; +}; -typedef std::tuple Float2; -typedef std::tuple Float3; +// handle VecView +template +struct nb::detail::type_caster>> { + using glm_type = glm::vec; + using numpy_type = nb::ndarray>; + NB_TYPE_CASTER(manifold::VecView, + const_name(glm_name::multi_name)); -using NumpyFloatNx2 = nb::ndarray>; -using NumpyFloatNx3 = nb::ndarray>; -using NumpyUintNx3 = nb::ndarray>; + 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 + int num_vec = arr_cast.value.shape(0); + if (num_vec != value.size()) return false; + for (int 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 { + size_t num_vec = vec.size(); + static_assert(sizeof(vec[0]) == (N * sizeof(T)), + "VecView -> numpy requires packed structs"); + numpy_type arr{&vec[0], {num_vec, N}}; + return ndarray_wrap(arr.handle(), int(ndarray_framework::numpy), policy, + cleanup); + } +}; template std::vector toVector(const T *arr, size_t size) { @@ -133,6 +206,7 @@ NB_MODULE(manifold3d, m) { m.doc() = "Python binding for the Manifold library."; m.def("set_min_circular_angle", Quality::SetMinCircularAngle, + nb::arg("angle"), "Sets an angle constraint the default number of circular segments for " "the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), " "and Manifold::Revolve() constructors. The number of segments will be " @@ -144,6 +218,7 @@ NB_MODULE(manifold3d, m) { "Default is 10 degrees."); m.def("set_min_circular_edge_length", Quality::SetMinCircularEdgeLength, + nb::arg("length"), "Sets a length constraint the default number of circular segments for " "the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), " "and Manifold::Revolve() constructors. The number of segments will be " @@ -153,6 +228,7 @@ NB_MODULE(manifold3d, m) { "increase if the the segments hit the minimum angle. Default is 1.0."); m.def("set_circular_segments", Quality::SetCircularSegments, + nb::arg("number"), "Sets the default number of circular segments for the " "CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and " "Manifold::Revolve() constructors. Overrides the edge length and angle " @@ -162,117 +238,85 @@ NB_MODULE(manifold3d, m) { "constraint is applied."); m.def("get_circular_segments", Quality::GetCircularSegments, + nb::arg("radius"), "Determine the result of the SetMinCircularAngle(), " "SetMinCircularEdgeLength(), and SetCircularSegments() defaults." "\n\n" ":param radius: For a given radius of circle, determine how many " "default"); - m.def( - "triangulate", - [](std::vector>> - polys) { - std::vector> polys_vec(polys.size()); - for (int i = 0; i < polys.size(); i++) { - to_glm_vector(polys[i], polys_vec[i]); - } - return to_numpy(Triangulate(polys_vec)); - }, - "Given a list polygons (each polygon shape=(N,2) dtype=float), " - "returns the indices of the triangle vertices as a " - "numpy.ndarray(shape=(N, 3), dtype=np.uint32)."); + m.def("triangulate", &Triangulate, nb::arg("polygons"), + nb::arg("precision") = -1, // TODO document + "Given a list polygons (each polygon shape=(N,2) dtype=float), " + "returns the indices of the triangle vertices as a " + "numpy.ndarray(shape=(N,3), dtype=np.uint32)."); nb::class_(m, "Manifold") - .def(nb::init<>()) - .def( - "__init__", - [](Manifold *self, std::vector &manifolds) { - if (manifolds.size() >= 1) { - // for some reason using Manifold() as the initial object - // will cause failure for python specifically - // unable to reproduce with c++ directly - Manifold first = manifolds[0]; - for (int i = 1; i < manifolds.size(); i++) first += manifolds[i]; - new (self) Manifold(first); - } else { - new (self) Manifold(); - } - }, - "Construct manifold as the union of a set of manifolds.") + .def(nb::init<>(), "Construct empty Manifold object") + .def(nb::init &>(), + nb::arg("mesh"), nb::arg("property_tolerance") = nb::list(), + "Convert a MeshGL into a Manifold, retaining its properties and " + "merging onlythe positions according to the merge vectors. Will " + "return an empty Manifoldand set an Error Status if the result is " + "not an oriented 2-manifold. Willcollapse degenerate triangles and " + "unnecessary vertices.\n\n" + "All fields are read, making this structure suitable for a lossless " + "round-tripof data from GetMeshGL. For multi-material input, use " + "ReserveIDs to set aunique originalID for each material, and sort " + "the materials into triangleruns.\n\n" + ":param meshGL: The input MeshGL.\n" + ":param propertyTolerance: A vector of precision values for each " + "property beyond position. If specified, the propertyTolerance " + "vector must have size = numProp - 3. This is the amount of " + "interpolation error allowed before two neighboring triangles are " + "considered to be on a property boundary edge. Property boundary " + "edges will be retained across operations even if thetriangles are " + "coplanar. Defaults to 1e-5, which works well for most properties " + "in the [-1, 1] range.") .def(nb::self + nb::self, "Boolean union.") .def(nb::self - nb::self, "Boolean difference.") .def(nb::self ^ nb::self, "Boolean intersection.") .def( - "hull", [](Manifold &self) { return self.Hull(); }, + "hull", [](const Manifold &self) { return self.Hull(); }, "Compute the convex hull of all points in this manifold.") .def_static( "batch_hull", - [](std::vector &ms) { return Manifold::Hull(ms); }, + [](std::vector ms) { return Manifold::Hull(ms); }, + nb::arg("manifolds"), "Compute the convex hull enveloping a set of manifolds.") .def_static( "hull_points", - [](std::vector &pts) { - std::vector vec(pts.size()); - for (int i = 0; i < pts.size(); i++) { - vec[i] = {std::get<0>(pts[i]), std::get<1>(pts[i]), - std::get<2>(pts[i])}; - } - return Manifold::Hull(vec); - }, + [](std::vector pts) { return Manifold::Hull(pts); }, + nb::arg("pts"), "Compute the convex hull enveloping a set of 3d points.") .def( - "transform", - [](Manifold &self, nb::ndarray> &mat) { - if (mat.ndim() != 2 || mat.shape(0) != 3 || mat.shape(1) != 4) - throw std::runtime_error("Invalid matrix shape, expected (3, 4)"); - glm::mat4x3 mat_glm; - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 4; j++) { - mat_glm[j][i] = mat(i, j); - } - } - return self.Transform(mat_glm); - }, - nb::arg("m"), + "transform", &Manifold::Transform, nb::arg("m"), "Transform this Manifold in space. The first three columns form a " "3x3 matrix transform and the last is a translation vector. This " "operation can be chained. Transforms are combined and applied " "lazily.\n" "\n\n" ":param m: The affine transform matrix to apply to all the vertices.") - .def( - "translate", - [](Manifold &self, float x = 0.0f, float y = 0.0f, float z = 0.0f) { - return self.Translate(glm::vec3(x, y, z)); - }, - nb::arg("x") = 0.0f, nb::arg("y") = 0.0f, nb::arg("z") = 0.0f, - "Move this Manifold in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param x: X axis translation. (default 0.0).\n" - ":param y: Y axis translation. (default 0.0).\n" - ":param z: Z axis translation. (default 0.0).") .def("translate", &Manifold::Translate, nb::arg("t"), "Move this Manifold in space. This operation can be chained. " "Transforms are combined and applied lazily." "\n\n" - ":param v: The vector to add to every vertex.") + ":param t: The vector to add to every vertex.") + .def("scale", &Manifold::Scale, nb::arg("v"), + "Scale this Manifold in space. This operation can be chained. " + "Transforms are combined and applied lazily." + "\n\n" + ":param v: The vector to multiply every vertex by component.") .def( "scale", - [](Manifold &self, float scale) { - return self.Scale(glm::vec3(scale)); + [](const Manifold &m, float s) { + m.Scale({s, s, s}); }, - nb::arg("scale"), + nb::arg("s"), "Scale this Manifold in space. This operation can be chained. " "Transforms are combined and applied lazily." "\n\n" - ":param scale: The scalar multiplier for each component of every " - "vertices.") - .def("scale", &Manifold::Scale, nb::arg("v"), - "Scale this Manifold in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param v: The vector to multiply every vertex by component.") + ":param s: The scalar to multiply every vertex by component.") .def("mirror", &Manifold::Mirror, nb::arg("v"), "Mirror this Manifold in space. This operation can be chained. " "Transforms are combined and applied lazily." @@ -280,8 +324,8 @@ NB_MODULE(manifold3d, m) { ":param mirror: The vector defining the axis of mirroring.") .def( "rotate", - [](Manifold &self, glm::vec3 v) { - return self.Rotate(v[0], v[1], v[2]); + [](const Manifold &self, glm::vec3 v) { + return self.Rotate(v.x, v.y, v.z); }, nb::arg("v"), "Applies an Euler angle rotation to the manifold, first about the X " @@ -294,35 +338,7 @@ NB_MODULE(manifold3d, m) { "\n\n" ":param v: [X, Y, Z] rotation in degrees.") .def( - "rotate", - [](Manifold &self, float xDegrees = 0.0f, float yDegrees = 0.0f, - float zDegrees = 0.0f) { - return self.Rotate(xDegrees, yDegrees, zDegrees); - }, - nb::arg("x_degrees") = 0.0f, nb::arg("y_degrees") = 0.0f, - nb::arg("z_degrees") = 0.0f, - "Applies an Euler angle rotation to the manifold, first about the X " - "axis, then Y, then Z, in degrees. We use degrees so that we can " - "minimize rounding error, and eliminate it completely for any " - "multiples of 90 degrees. Additionally, more efficient code paths " - "are used to update the manifold when the transforms only rotate by " - "multiples of 90 degrees. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param x: X rotation in degrees. (default 0.0).\n" - ":param y: Y rotation in degrees. (default 0.0).\n" - ":param z: Z rotation in degrees. (default 0.0).") - .def( - "warp", - [](Manifold &self, const std::function &f) { - return self.Warp([&f](glm::vec3 &v) { - Float3 fv = f(std::make_tuple(v.x, v.y, v.z)); - v.x = std::get<0>(fv); - v.y = std::get<1>(fv); - v.z = std::get<2>(fv); - }); - }, - nb::arg("f"), + "warp", &Manifold::Warp, nb::arg("f"), "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 " @@ -330,37 +346,27 @@ NB_MODULE(manifold3d, m) { "the user to choose their function with discretion." "\n\n" ":param f: A function that modifies a given vertex position.") + .def("warp_batch", &Manifold::WarpBatch, nb::arg("f"), + "Same as Manifold.warp but calls `f` with a " + "ndarray(shape=(N,3), dtype=float) and expects an ndarray " + "of the same shape and type in return. The input array can be " + "modified and returned if desired. " + "\n\n" + ":param f: A function that modifies multiple vertex positions.") .def( - "warp_batch", - [](Manifold &self, - const std::function &f) { - return self.WarpBatch([&f](VecView vecs) { - NumpyFloatNx3 arr = f(to_numpy(vecs.begin(), vecs.end())); - to_glm_range(arr, vecs.begin(), vecs.end()); - }); - }, - nb::arg("f"), - "Same as Manifold.warp but calls `f` with a " - "ndarray(shape=(N,3), dtype=float) and expects an ndarray " - "of the same shape and type in return. The input array can be " - "modified and returned if desired. " - "\n\n" - ":param f: A function that modifies multiple vertex positions.") - .def( - "set_properties", - [](Manifold &self, int newNumProp, + "set_properties", // TODO this needs a batch version! + [](const Manifold &self, int newNumProp, const std::function - &)> &f) { + glm::vec3, const nb::ndarray &)> &f) { const int oldNumProp = self.NumProp(); return self.SetProperties(newNumProp, [newNumProp, oldNumProp, &f]( float *newProps, glm::vec3 v, const float *oldProps) { auto result = - f(std::make_tuple(v.x, v.y, v.z), - nb::ndarray( - oldProps, {static_cast(oldNumProp)})); + f(v, nb::ndarray( + oldProps, {static_cast(oldNumProp)})); nb::ndarray> array; std::vector vec; if (nb::try_cast(result, array)) { @@ -384,14 +390,11 @@ NB_MODULE(manifold3d, m) { "undefined behavior will result if you read past the number of input " "properties or write past the number of output properties." "\n\n" - ":param numProp: The new number of properties per vertex." - ":param propFunc: A function that modifies the properties of a given " + ":param new_num_prop: The new number of properties per vertex." + ":param f: A function that modifies the properties of a given " "vertex.") .def( - "calculate_curvature", - [](Manifold &self, int gaussianIdx, int meanIdx) { - return self.CalculateCurvature(gaussianIdx, meanIdx); - }, + "calculate_curvature", &Manifold::CalculateCurvature, nb::arg("gaussian_idx"), nb::arg("mean_idx"), "Curvature is the inverse of the radius of curvature, and signed " "such that positive is convex and negative is concave. There are two " @@ -404,51 +407,37 @@ NB_MODULE(manifold3d, m) { ":param gaussianIdx: The property channel index in which to store " "the Gaussian curvature. An index < 0 will be ignored (stores " "nothing). The property set will be automatically expanded to " - "include the channel index specified." + "include the channel index specified.\n" ":param meanIdx: The property channel index in which to store the " "mean curvature. An index < 0 will be ignored (stores nothing). The " "property set will be automatically expanded to include the channel " "index specified.") - .def( - "refine", [](Manifold &self, int n) { return self.Refine(n); }, - nb::arg("n"), - "Increase the density of the mesh by splitting every edge into n " - "pieces. For instance, with n = 2, each triangle will be split into " - "4 triangles. These will all be coplanar (and will not be " - "immediately collapsed) unless the Mesh/Manifold has " - "halfedgeTangents specified (e.g. from the Smooth() constructor), " - "in which case the new vertices will be moved to the interpolated " - "surface according to their barycentric coordinates." - "\n\n" - ":param n: The number of pieces to split every edge into. Must be > " - "1.") - .def( - "to_mesh", - [](Manifold &self, - std::optional>> &normalIdx) { - glm::ivec3 v(0); - if (normalIdx.has_value()) { - if (normalIdx->ndim() != 1 || normalIdx->shape(0) != 3) - throw std::runtime_error("Invalid vector shape, expected (3)"); - auto value = *normalIdx; - v = glm::ivec3(value(0), value(1), value(2)); - } - return self.GetMeshGL(v); - }, - "The most complete output of this library, returning a MeshGL that " - "is designed to easily push into a renderer, including all " - "interleaved vertex properties that may have been input. It also " - "includes relations to all the input meshes that form a part of " - "this result and the transforms applied to each." - "\n\n" - ":param normalIdx: If the original MeshGL inputs that formed this " - "manifold had properties corresponding to normal vectors, you can " - "specify which property channels these are (x, y, z), which will " - "cause this output MeshGL to automatically update these normals " - "according to the applied transforms and front/back side. Each " - "channel must be >= 3 and < numProp, and all original MeshGLs must " - "use the same channels for their normals.", - nb::arg("normalIdx") = nb::none()) + .def("refine", &Manifold::Refine, nb::arg("n"), + "Increase the density of the mesh by splitting every edge into n " + "pieces. For instance, with n = 2, each triangle will be split into " + "4 triangles. These will all be coplanar (and will not be " + "immediately collapsed) unless the Mesh/Manifold has " + "halfedgeTangents specified (e.g. from the Smooth() constructor), " + "in which case the new vertices will be moved to the interpolated " + "surface according to their barycentric coordinates." + "\n\n" + ":param n: The number of pieces to split every edge into. Must be > " + "1.") + .def("to_mesh", &Manifold::GetMeshGL, + nb::arg("normal_idx") = glm::ivec3(0), + "The most complete output of this library, returning a MeshGL that " + "is designed to easily push into a renderer, including all " + "interleaved vertex properties that may have been input. It also " + "includes relations to all the input meshes that form a part of " + "this result and the transforms applied to each." + "\n\n" + ":param normal_idx: If the original MeshGL inputs that formed this " + "manifold had properties corresponding to normal vectors, you can " + "specify which property channels these are (x, y, z), which will " + "cause this output MeshGL to automatically update these normals " + "according to the applied transforms and front/back side. Each " + "channel must be >= 3 and < numProp, and all original MeshGLs must " + "use the same channels for their normals.") .def("num_vert", &Manifold::NumVert, "The number of vertices in the Manifold.") .def("num_edge", &Manifold::NumEdge, @@ -476,13 +465,13 @@ NB_MODULE(manifold3d, m) { "meaningful for a single mesh, so it is best to call Decompose() " "first.") .def( - "get_volume", - [](Manifold &self) { return self.GetProperties().volume; }, + "volume", + [](const Manifold &self) { return self.GetProperties().volume; }, "Get the volume of the manifold\n This is clamped to zero for a " "given face if they are within the Precision().") .def( - "get_surface_area", - [](Manifold &self) { return self.GetProperties().surfaceArea; }, + "surface_area", + [](const Manifold &self) { return self.GetProperties().surfaceArea; }, "Get the surface area of the manifold\n This is clamped to zero for " "a given face if they are within the Precision().") .def("original_id", &Manifold::OriginalID, @@ -498,65 +487,44 @@ NB_MODULE(manifold3d, m) { .def("is_empty", &Manifold::IsEmpty, "Does the Manifold have any triangles?") .def( - "decompose", [](Manifold &self) { return self.Decompose(); }, + "decompose", &Manifold::Decompose, "This operation returns a vector of Manifolds that are " "topologically disconnected. If everything is connected, the vector " "is length one, containing a copy of the original. It is the inverse " "operation of Compose().") - .def( - "split", - [](Manifold &self, Manifold &cutter) { - auto p = self.Split(cutter); - return nb::make_tuple(p.first, p.second); - }, - nb::arg("cutter"), - "Split cuts this manifold in two using the cutter manifold. The " - "first result is the intersection, second is the difference. This " - "is more efficient than doing them separately." - "\n\n" - ":param cutter: This is the manifold to cut by.\n") - .def( - "split_by_plane", - [](Manifold &self, Float3 normal, float originOffset) { - auto p = self.SplitByPlane( - {std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)}, - originOffset); - return nb::make_tuple(p.first, p.second); - }, - nb::arg("normal"), nb::arg("origin_offset"), - "Convenient version of Split() for a half-space." - "\n\n" - ":param normal: This vector is normal to the cutting plane and its " - "length does not matter. The first result is in the direction of " - "this vector, the second result is on the opposite side.\n" - ":param originOffset: The distance of the plane from the origin in " - "the direction of the normal vector.") - .def( - "trim_by_plane", - [](Manifold &self, Float3 normal, float originOffset) { - return self.TrimByPlane( - {std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)}, - originOffset); - }, - nb::arg("normal"), nb::arg("origin_offset"), + .def("split", &Manifold::Split, nb::arg("cutter"), + "Split cuts this manifold in two using the cutter manifold. The " + "first result is the intersection, second is the difference. This " + "is more efficient than doing them separately." + "\n\n" + ":param cutter: This is the manifold to cut by.\n") + .def("split_by_plane", &Manifold::SplitByPlane, nb::arg("normal"), + nb::arg("origin_offset"), + "Convenient version of Split() for a half-space." + "\n\n" + ":param normal: This vector is normal to the cutting plane and its " + "length does not matter. The first result is in the direction of " + "this vector, the second result is on the opposite side.\n" + ":param origin_offset: The distance of the plane from the origin in " + "the direction of the normal vector.") + .def( + "trim_by_plane", &Manifold::TrimByPlane, nb::arg("normal"), + nb::arg("origin_offset"), "Identical to SplitByPlane(), but calculating and returning only the " "first result." "\n\n" ":param normal: This vector is normal to the cutting plane and its " "length does not matter. The result is in the direction of this " "vector from the plane.\n" - ":param originOffset: The distance of the plane from the origin in " + ":param origin_offset: The distance of the plane from the origin in " "the direction of the normal vector.") - .def( - "slice", - [](Manifold &self, float height) { return self.Slice(height); }, - nb::arg("height"), - "Returns the cross section of this object parallel to the X-Y plane " - "at the specified height. Using a height equal to the bottom of the " - "bounding box will return the bottom faces, while using a height " - "equal to the top of the bounding box will return empty." - "\n\n" - ":param height: The Z-level of the slice, defaulting to zero.") + .def("slice", &Manifold::Slice, nb::arg("height"), + "Returns the cross section of this object parallel to the X-Y plane " + "at the specified height. Using a height equal to the bottom of the " + "bounding box will return the bottom faces, while using a height " + "equal to the top of the bounding box will return empty." + "\n\n" + ":param height: The Z-level of the slice, defaulting to zero.") .def("project", &Manifold::Project, "Returns a cross section representing the projected outline of this " "object onto the X-Y plane.") @@ -568,28 +536,31 @@ NB_MODULE(manifold3d, m) { "as empty. Likewise, empty meshes may still show NoError, for " "instance if they are small enough relative to their precision to " "be collapsed to nothing.") - .def_prop_ro( + .def( "bounding_box", - [](Manifold &self) { - auto b = self.BoundingBox(); - nb::tuple box = nb::make_tuple(b.min[0], b.min[1], b.min[2], - b.max[0], b.max[1], b.max[2]); - return box; + [](const Manifold &self) { + Box b = self.BoundingBox(); + return nb::make_tuple(b.min[0], b.min[1], b.min[2], b.max[0], + b.max[1], b.max[2]); }, "Gets the manifold bounding box as a tuple " "(xmin, ymin, zmin, xmax, ymax, zmax).") .def_static( "smooth", - [](const MeshGL &mesh, - const std::vector> &sharpenedEdges = {}) { - std::vector vec(sharpenedEdges.size()); - for (int i = 0; i < sharpenedEdges.size(); i++) { - vec[i] = {std::get<0>(sharpenedEdges[i]), - std::get<1>(sharpenedEdges[i])}; + [](const MeshGL &mesh, std::vector sharpened_edges, + std::vector edge_smoothness) { + if (sharpened_edges.size() != edge_smoothness.size()) { + throw std::runtime_error( + "sharpened_edges.size() != edge_smoothness.size()"); + } + std::vector vec(sharpened_edges.size()); + for (int i = 0; i < vec.size(); i++) { + vec[i] = {sharpened_edges[i], edge_smoothness[i]}; } return Manifold::Smooth(mesh, vec); }, - nb::arg("mesh"), nb::arg("sharpened_edges"), + nb::arg("mesh"), nb::arg("sharpened_edges") = nb::list(), + nb::arg("edge_smoothness") = nb::list(), "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 " @@ -602,13 +573,14 @@ NB_MODULE(manifold3d, m) { "constraints on their boundaries." "\n\n" ":param mesh: input Mesh.\n" - ":param sharpenedEdges: If desired, you can supply a vector of " + ":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 " + "all halfedges. The halfedge index is " "(3 * triangle index + [0,1,2] where 0 is the edge between triVert 0 " "and 1, etc)." + ":param edge_smoothness: must be same length as shapened_edges. " + "Each entry specifies the desired smoothness (between zero and one, " + "with one being the default for all unspecified halfedges)" "\n\n" "At a smoothness value of zero, a sharp crease is made. The " "smoothness is interpolated along each edge, so the specified value " @@ -619,29 +591,15 @@ NB_MODULE(manifold3d, m) { "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.") + .def_static("compose", &Manifold::Compose, nb::arg("manifolds"), + "combine several manifolds into one without checking for " + "intersections.") .def_static( - "from_mesh", [](const MeshGL &mesh) { return Manifold(mesh); }, - nb::arg("mesh")) - .def_static( - "compose", - [](const std::vector &list) { - return Manifold::Compose(list); - }, - "combine several manifolds into one without checking for " - "intersections.") - .def_static( - "tetrahedron", []() { return Manifold::Tetrahedron(); }, + "tetrahedron", &Manifold::Tetrahedron, "Constructs a tetrahedron centered at the origin with one vertex at " "(1,1,1) and the rest at similarly symmetric points.") .def_static( - "cube", - [](Float3 size, bool center = false) { - return Manifold::Cube( - glm::vec3(std::get<0>(size), std::get<1>(size), - std::get<2>(size)), - center); - }, - nb::arg("size") = std::make_tuple(1.0f, 1.0f, 1.0f), + "cube", &Manifold::Cube, nb::arg("size") = glm::vec3{1, 1, 1}, nb::arg("center") = false, "Constructs a unit cube (edge lengths all one), by default in the " "first octant, touching the origin." @@ -649,56 +607,27 @@ NB_MODULE(manifold3d, m) { ":param size: The X, Y, and Z dimensions of the box.\n" ":param center: Set to true to shift the center to the origin.") .def_static( - "cube", &Manifold::Cube, nb::arg("size"), nb::arg("center") = false, - "Constructs a unit cube (edge lengths all one), by default in the " - "first octant, touching the origin." - "\n\n" - ":param size: The X, Y, and Z dimensions of the box.\n" - ":param center: Set to true to shift the center to the origin.") - .def_static( - "cube", - [](float x, float y, float z, bool center = false) { - return Manifold::Cube(glm::vec3(x, y, z), center); - }, - nb::arg("x"), nb::arg("y"), nb::arg("z"), nb::arg("center") = false, - "Constructs a unit cube (edge lengths all one), by default in the " - "first octant, touching the origin." - "\n\n" - ":param x: The X dimensions of the box.\n" - ":param y: The Y dimensions of the box.\n" - ":param z: The Z dimensions of the box.\n" - ":param center: Set to true to shift the center to the origin.") - .def_static( - "cylinder", - [](float height, float radiusLow, float radiusHigh = -1.0f, - int circularSegments = 0, bool center = false) { - return Manifold::Cylinder(height, radiusLow, radiusHigh, - circularSegments, center); - }, - nb::arg("height"), nb::arg("radius_low"), - nb::arg("radius_high") = -1.0f, nb::arg("circular_segments") = 0, - nb::arg("center") = false, + "cylinder", &Manifold::Cylinder, nb::arg("height"), + nb::arg("radius_low"), nb::arg("radius_high") = -1.0f, + nb::arg("circular_segments") = 0, nb::arg("center") = false, "A convenience constructor for the common case of extruding a " "circle. Can also form cones if both radii are specified." "\n\n" ":param height: Z-extent\n" - ":param radiusLow: Radius of bottom circle. Must be positive.\n" - ":param radiusHigh: Radius of top circle. Can equal zero. Default " - "(-1) is equal to radiusLow.\n" - ":param circularSegments: How many line segments to use around the " + ":param radius_low: Radius of bottom circle. Must be positive.\n" + ":param radius_high: Radius of top circle. Can equal zero. Default " + "(-1) is equal to radius_low.\n" + ":param circular_segments: How many line segments to use around the " "circle. Default (-1) is calculated by the static Defaults.\n" ":param center: Set to true to shift the center to the origin. " "Default is origin at the bottom.") .def_static( - "sphere", - [](float radius, int circularSegments = 0) { - return Manifold::Sphere(radius, circularSegments); - }, - nb::arg("radius"), nb::arg("circular_segments") = 0, + "sphere", &Manifold::Sphere, nb::arg("radius"), + nb::arg("circular_segments") = 0, "Constructs a geodesic sphere of a given radius.\n" "\n" ":param radius: Radius of the sphere. Must be positive.\n" - ":param circularSegments: Number of segments along its diameter. " + ":param circular_segments: Number of segments along its diameter. " "This number will always be rounded up to the nearest factor of " "four, as this sphere is constructed by refining an octahedron. This " "means there are a circle of vertices on all three of the axis " @@ -733,56 +662,40 @@ NB_MODULE(manifold3d, m) { const std::optional, nb::c_contig>> &halfedgeTangent, float precision) { - new (self) MeshGL(); - MeshGL &out = *self; - out.numProp = vertProp.shape(1); - out.vertProperties = - toVector(vertProp.data(), vertProp.size()); + new (self) MeshGL(); + MeshGL &out = *self; + out.numProp = vertProp.shape(1); + out.vertProperties = toVector(vertProp.data(), vertProp.size()); - if (triVerts.ndim() != 2 || triVerts.shape(1) != 3) - throw std::runtime_error( - "Invalid tri_verts shape, expected (-1, 3)"); - out.triVerts = toVector(triVerts.data(), triVerts.size()); + out.triVerts = toVector(triVerts.data(), triVerts.size()); - if (mergeFromVert.has_value()) - out.mergeFromVert = toVector(mergeFromVert->data(), - mergeFromVert->size()); + if (mergeFromVert.has_value()) + out.mergeFromVert = + toVector(mergeFromVert->data(), mergeFromVert->size()); - if (mergeToVert.has_value()) - out.mergeToVert = - toVector(mergeToVert->data(), mergeToVert->size()); + if (mergeToVert.has_value()) + out.mergeToVert = + toVector(mergeToVert->data(), mergeToVert->size()); - if (runIndex.has_value()) - out.runIndex = - toVector(runIndex->data(), runIndex->size()); + if (runIndex.has_value()) + out.runIndex = toVector(runIndex->data(), runIndex->size()); - if (runOriginalID.has_value()) - out.runOriginalID = toVector(runOriginalID->data(), - runOriginalID->size()); + if (runOriginalID.has_value()) + out.runOriginalID = + toVector(runOriginalID->data(), runOriginalID->size()); - if (runTransform.has_value()) { - auto runTransform1 = *runTransform; - if (runTransform1.ndim() != 3 || runTransform1.shape(1) != 4 || - runTransform1.shape(2) != 3) - throw std::runtime_error( - "Invalid run_transform shape, expected (-1, 4, 3)"); - out.runTransform = - toVector(runTransform1.data(), runTransform1.size()); - } + if (runTransform.has_value()) { + out.runTransform = + toVector(runTransform->data(), runTransform->size()); + } - if (faceID.has_value()) - out.faceID = toVector(faceID->data(), faceID->size()); + if (faceID.has_value()) + out.faceID = toVector(faceID->data(), faceID->size()); - if (halfedgeTangent.has_value()) { - auto halfedgeTangent1 = *halfedgeTangent; - if (halfedgeTangent1.ndim() != 3 || - halfedgeTangent1.shape(1) != 3 || - halfedgeTangent1.shape(2) != 4) - throw std::runtime_error( - "Invalid halfedge_tangent shape, expected (-1, 3, 4)"); - out.halfedgeTangent = toVector(halfedgeTangent1.data(), - halfedgeTangent1.size()); - } + if (halfedgeTangent.has_value()) { + out.halfedgeTangent = + toVector(halfedgeTangent->data(), halfedgeTangent->size()); + } }, nb::arg("vert_properties"), nb::arg("tri_verts"), nb::arg("merge_from_vert") = nb::none(), @@ -794,25 +707,24 @@ NB_MODULE(manifold3d, m) { nb::arg("halfedge_tangent") = nb::none(), nb::arg("precision") = 0) .def_prop_ro("vert_properties", [](const MeshGL &self) { - return nb::ndarray( - self.vertProperties.data(), - {self.vertProperties.size() / self.numProp, - self.numProp}); + return nb::ndarray( + self.vertProperties.data(), + {self.vertProperties.size() / self.numProp, self.numProp}); }, nb::rv_policy::reference_internal) .def_prop_ro("tri_verts", [](const MeshGL &self) { - return nb::ndarray( - self.triVerts.data(), {self.triVerts.size() / 3, 3}); + return nb::ndarray( + self.triVerts.data(), {self.triVerts.size() / 3, 3}); }, nb::rv_policy::reference_internal) .def_prop_ro("run_transform", [](const MeshGL &self) { - return nb::ndarray( - self.runTransform.data(), {self.runTransform.size() / 12, 4, 3}); + return nb::ndarray( + self.runTransform.data(), {self.runTransform.size() / 12, 4, 3}); }, nb::rv_policy::reference_internal) .def_prop_ro("halfedge_tangent", [](const MeshGL &self) { - return nb::ndarray( - self.halfedgeTangent.data(), {self.halfedgeTangent.size() / 12, 3, 4}); + return nb::ndarray( + self.halfedgeTangent.data(), {self.halfedgeTangent.size() / 12, 3, 4}); }, nb::rv_policy::reference_internal) .def_ro("merge_from_vert", &MeshGL::mergeFromVert) .def_ro("merge_to_vert", &MeshGL::mergeToVert) @@ -820,19 +732,18 @@ NB_MODULE(manifold3d, m) { .def_ro("run_original_id", &MeshGL::runOriginalID) .def_ro("face_id", &MeshGL::faceID) .def_static( - "level_set", + "level_set", [](const std::function &f, std::vector bounds, float edgeLength, float level = 0.0) { - // Same format as Manifold.bounding_box - Box bound = {glm::vec3(bounds[0], bounds[1], bounds[2]), - glm::vec3(bounds[3], bounds[4], bounds[5])}; + // Same format as Manifold.bounding_box + Box bound = {glm::vec3(bounds[0], bounds[1], bounds[2]), + glm::vec3(bounds[3], bounds[4], bounds[5])}; - std::function cppToPython = - [&f](glm::vec3 v) { return f(v.x, v.y, v.z); }; - Mesh result = - LevelSet(cppToPython, bound, - edgeLength, level, false); - return MeshGL(result); + std::function cppToPython = [&f](glm::vec3 v) { + return f(v.x, v.y, v.z); + }; + Mesh result = LevelSet(cppToPython, bound, edgeLength, level, false); + return MeshGL(result); }, nb::arg("f"), nb::arg("bounds"), nb::arg("edgeLength"), nb::arg("level") = 0.0, @@ -909,32 +820,20 @@ NB_MODULE(manifold3d, m) { "onwards). This class makes use of the " "[Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library " "for polygon clipping (boolean) and offsetting operations.") - .def(nb::init<>()) - .def( - "__init__", - [](CrossSection *self, std::vector> &polygons, - CrossSection::FillRule fillrule) { - std::vector simplePolygons(polygons.size()); - for (int i = 0; i < polygons.size(); i++) { - simplePolygons[i] = std::vector(polygons[i].size()); - for (int j = 0; j < polygons[i].size(); j++) { - simplePolygons[i][j] = {std::get<0>(polygons[i][j]), - std::get<1>(polygons[i][j])}; - } - } - new (self) CrossSection(simplePolygons, fillrule); - }, - nb::arg("polygons"), - nb::arg("fillrule") = CrossSection::FillRule::Positive, - "Create a 2d cross-section from a set of contours (complex " - "polygons). A boolean union operation (with Positive filling rule " - "by default) performed to combine overlapping polygons and ensure " - "the resulting CrossSection is free of intersections." - "\n\n" - ":param contours: A set of closed paths describing zero or more " - "complex polygons.\n" - ":param fillrule: The filling rule used to interpret polygon " - "sub-regions in contours.") + .def(nb::init<>(), "Construct empty CrossSection object") + .def(nb::init>, + CrossSection::FillRule>(), + nb::arg("polygons"), + nb::arg("fillrule") = CrossSection::FillRule::Positive, + "Create a 2d cross-section from a set of contours (complex " + "polygons). A boolean union operation (with Positive filling rule " + "by default) performed to combine overlapping polygons and ensure " + "the resulting CrossSection is free of intersections." + "\n\n" + ":param contours: A set of closed paths describing zero or more " + "complex polygons.\n" + ":param fillrule: The filling rule used to interpret polygon " + "sub-regions in contours.") .def("area", &CrossSection::Area, "Return the total area covered by complex polygons making up the " "CrossSection.") @@ -946,34 +845,41 @@ NB_MODULE(manifold3d, m) { .def("is_empty", &CrossSection::IsEmpty, "Does the CrossSection contain any contours?") .def( - "translate", - [](CrossSection &self, Float2 v) { - return self.Translate({std::get<0>(v), std::get<1>(v)}); + "bounds", + [](const CrossSection &self) { + Rect r = self.Bounds(); + return nb::make_tuple(r.min[0], r.min[1], r.max[0], r.max[1]); }, - "Move this CrossSection in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param v: The vector to add to every vertex.") - .def("rotate", &CrossSection::Rotate, + "Return bounding box of CrossSection as tuple(" + "min_x, min_y, max_x, max_y)") + .def("translate", &CrossSection::Translate, nb::arg("v"), + "Move this CrossSection in space. This operation can be chained. " + "Transforms are combined and applied lazily." + "\n\n" + ":param v: The vector to add to every vertex.") + .def("rotate", &CrossSection::Rotate, nb::arg("angle"), "Applies a (Z-axis) rotation to the CrossSection, in degrees. This " "operation can be chained. Transforms are combined and applied " "lazily." "\n\n" ":param degrees: degrees about the Z-axis to rotate.") + .def("scale", &CrossSection::Scale, nb::arg("v"), + "Scale this CrossSection in space. This operation can be chained. " + "Transforms are combined and applied lazily." + "\n\n" + ":param v: The vector to multiply every vertex by per component.") .def( "scale", - [](CrossSection &self, Float2 s) { - return self.Scale({std::get<0>(s), std::get<1>(s)}); + [](const CrossSection &self, float s) { + self.Scale({s, s}); }, + nb::arg("s"), "Scale this CrossSection in space. This operation can be chained. " "Transforms are combined and applied lazily." "\n\n" - ":param v: The vector to multiply every vertex by per component.") + ":param s: The scalar to multiply every vertex by per component.") .def( - "mirror", - [](CrossSection &self, Float2 ax) { - return self.Mirror({std::get<0>(ax), std::get<1>(ax)}); - }, + "mirror", &CrossSection::Mirror, nb::arg("ax"), "Mirror this CrossSection over the arbitrary axis described by the " "unit form of the given vector. If the length of the vector is zero, " "an empty CrossSection is returned. This operation can be chained. " @@ -981,56 +887,27 @@ NB_MODULE(manifold3d, m) { "\n\n" ":param ax: the axis to be mirrored over") .def( - "transform", - [](CrossSection &self, nb::ndarray> &mat) { - if (mat.ndim() != 2 || mat.shape(0) != 2 || mat.shape(1) != 3) - throw std::runtime_error("Invalid matrix shape, expected (2, 3)"); - glm::mat3x2 mat_glm; - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 3; j++) { - mat_glm[j][i] = mat(i, j); - } - } - return self.Transform(mat_glm); - }, + "transform", &CrossSection::Transform, nb::arg("m"), "Transform this CrossSection in space. The first two columns form a " "2x2 matrix transform and the last is a translation vector. This " "operation can be chained. Transforms are combined and applied " "lazily." "\n\n" ":param m: The affine transform matrix to apply to all the vertices.") - .def( - "warp", - [](CrossSection &self, const std::function &f) { - return self.Warp([&f](glm::vec2 &v) { - Float2 fv = f(std::make_tuple(v.x, v.y)); - v.x = std::get<0>(fv); - v.y = std::get<1>(fv); - }); - }, - nb::arg("f"), - "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." - "\n\n" - ":param warpFunc: A function that modifies a given vertex position.") - .def( - "warp_batch", - [](CrossSection &self, - const std::function &f) { - return self.WarpBatch([&f](VecView vecs) { - NumpyFloatNx2 arr = f(to_numpy(vecs.begin(), vecs.end())); - to_glm_range(arr, vecs.begin(), vecs.end()); - }); - }, - nb::arg("f"), - "Same as CrossSection.warp but calls `f` with a " - "ndarray(shape=(N,2), dtype=float) and expects an ndarray " - "of the same shape and type in return. The input array can be " - "modified and returned if desired. " - "\n\n" - ":param f: A function that modifies multiple vertex positions.") + .def("warp", &CrossSection::Warp, nb::arg("f"), + "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." + "\n\n" + ":param warpFunc: A function that modifies a given vertex position.") + .def("warp_batch", &CrossSection::WarpBatch, nb::arg("f"), + "Same as CrossSection.warp but calls `f` with a " + "ndarray(shape=(N,2), dtype=float) and expects an ndarray " + "of the same shape and type in return. The input array can be " + "modified and returned if desired. " + "\n\n" + ":param f: A function that modifies multiple vertex positions.") .def("simplify", &CrossSection::Simplify, nb::arg("epsilon") = 1e-6, "Remove vertices from the contours in this CrossSection that are " "less than the specified distance epsilon from an imaginary line " @@ -1046,7 +923,7 @@ NB_MODULE(manifold3d, m) { "which would compound the issue.") .def("offset", &CrossSection::Offset, nb::arg("delta"), nb::arg("join_type"), nb::arg("miter_limit") = 2.0, - nb::arg("circular_segments") = 0.0, + nb::arg("circular_segments") = 0, "Inflate the contours in CrossSection by the specified delta, " "handling corners according to the given JoinType." "\n\n" @@ -1070,48 +947,28 @@ NB_MODULE(manifold3d, m) { .def(nb::self - nb::self, "Boolean difference.") .def(nb::self ^ nb::self, "Boolean intersection.") .def( - "hull", [](CrossSection &self) { return self.Hull(); }, + "hull", [](const CrossSection &self) { return self.Hull(); }, "Compute the convex hull of this cross-section.") .def_static( "batch_hull", - [](std::vector &cs) { return CrossSection::Hull(cs); }, + [](std::vector cs) { return CrossSection::Hull(cs); }, + nb::arg("cross_sections"), "Compute the convex hull enveloping a set of cross-sections.") .def_static( "hull_points", - [](std::vector &pts) { - std::vector poly(pts.size()); - for (int i = 0; i < pts.size(); i++) { - poly[i] = {std::get<0>(pts[i]), std::get<1>(pts[i])}; - } - return CrossSection::Hull(poly); - }, + [](std::vector pts) { return CrossSection::Hull(pts); }, + nb::arg("pts"), "Compute the convex hull enveloping a set of 2d points.") .def("decompose", &CrossSection::Decompose, "This operation returns a vector of CrossSections that are " "topologically disconnected, each containing one outline contour " "with zero or more holes.") + .def("to_polygons", &CrossSection::ToPolygons, + "Returns the vertices of the cross-section's polygons " + "as a List[ndarray(shape=(*,2), dtype=float)].") .def( - "to_polygons", - [](CrossSection &self) { - nb::list polygon_list; - for (auto &poly : self.ToPolygons()) { - polygon_list.append(to_numpy(poly)); - } - return polygon_list; - }, - "Returns the vertices of the cross-section's polygons " - "as a List[ndarray(shape=(*,2), dtype=float)].") - .def( - "extrude", - [](CrossSection &self, float height, int nDivisions = 0, - float twistDegrees = 0.0f, - Float2 scaleTop = std::make_tuple(1.0f, 1.0f)) { - glm::vec2 scaleTopVec(std::get<0>(scaleTop), std::get<1>(scaleTop)); - return Manifold::Extrude(self, height, nDivisions, twistDegrees, - scaleTopVec); - }, - nb::arg("height"), nb::arg("n_divisions") = 0, - nb::arg("twist_degrees") = 0.0f, + "extrude", &Manifold::Extrude, nb::arg("height"), + nb::arg("n_divisions") = 0, nb::arg("twist_degrees") = 0.0f, nb::arg("scale_top") = std::make_tuple(1.0f, 1.0f), "Constructs a manifold from the set of polygons by extruding them " "along the Z-axis.\n" @@ -1127,12 +984,8 @@ NB_MODULE(manifold3d, m) { "Y). If the scale is (0, 0), a pure cone is formed with only a " "single vertex at the top. Default (1, 1).") .def( - "revolve", - [](CrossSection &self, int circularSegments = 0, - float revolveDegrees = 360.0f) { - return Manifold::Revolve(self, circularSegments, revolveDegrees); - }, - nb::arg("circular_segments") = 0, nb::arg("revolve_degrees") = 360.0, + "revolve", &Manifold::Revolve, nb::arg("circular_segments") = 0, + nb::arg("revolve_degrees") = 360.0, "Constructs a manifold from the set of polygons by revolving this " "cross-section around its Y-axis and then setting this as the Z-axis " "of the resulting manifold. If the polygons cross the Y-axis, only " @@ -1143,12 +996,8 @@ NB_MODULE(manifold3d, m) { "Default is calculated by the static Defaults.\n" ":param revolve_degrees: rotation angle for the sweep.") .def_static( - "square", - [](Float2 dims, bool center) { - return CrossSection::Square({std::get<0>(dims), std::get<1>(dims)}, - center); - }, - nb::arg("dims"), nb::arg("center") = false, + "square", &CrossSection::Square, nb::arg("size"), + nb::arg("center") = false, "Constructs a square with the given XY dimensions. By default it is " "positioned in the first quadrant, touching the origin. If any " "dimensions in size are negative, or if all are zero, an empty " @@ -1157,15 +1006,12 @@ NB_MODULE(manifold3d, m) { ":param size: The X, and Y dimensions of the square.\n" ":param center: Set to true to shift the center to the origin.") .def_static( - "circle", - [](float radius, int circularSegments) { - return CrossSection::Circle(radius, circularSegments); - }, - nb::arg("radius"), nb::arg("circularSegments") = 0, + "circle", &CrossSection::Circle, nb::arg("radius"), + nb::arg("circular_segments") = 0, "Constructs a circle of a given radius." "\n\n" ":param radius: Radius of the circle. Must be positive.\n" - ":param circularSegments: Number of segments along its diameter. " + ":param circular_segments: Number of segments along its diameter. " "Default is calculated by the static Quality defaults according to " "the radius."); } From 41b6ed569cd7922e689f0c23da6369bf61f5e390 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 12:39:04 -0800 Subject: [PATCH 05/15] Add Hull Method Comparison --- src/manifold/include/manifold.h | 2 +- src/manifold/src/manifold.cpp | 143 ++++++++++++++++++-------------- test/boolean_test.cpp | 19 +++++ 3 files changed, 103 insertions(+), 61 deletions(-) diff --git a/src/manifold/include/manifold.h b/src/manifold/include/manifold.h index 2ad2e7213..071cbedf5 100644 --- a/src/manifold/include/manifold.h +++ b/src/manifold/include/manifold.h @@ -206,7 +206,7 @@ class Manifold { Manifold Transform(const glm::mat4x3&) const; Manifold Mirror(glm::vec3) const; Manifold Warp(std::function) const; - Manifold Offset(float delta, int circularSegments = 0) const; + Manifold Offset(float delta, int circularSegments = 0, bool useHullMethod = false) const; Manifold SetProperties( int, std::function) const; Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const; diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 2ad34c63d..f9f73a295 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -604,7 +604,7 @@ Manifold Manifold::Warp(std::function warpFunc) const { * vertices. Default is calculated by the static Quality defaults according to * the radius, which is delta. */ -Manifold Manifold::Offset(float delta, int circularSegments) const { +Manifold Manifold::Offset(float delta, int circularSegments, bool useHullMethod) const { auto pImpl = std::make_shared(*GetCsgLeafNode().GetImpl()); if (delta == 0) { @@ -616,68 +616,91 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { const int n = circularSegments > 0 ? (circularSegments + 3) / 4 : Quality::GetCircularSegments(delta) / 4; const Manifold sphere = Manifold::Sphere(radius, circularSegments); - const Manifold cylinder = Manifold::Cylinder(1, radius, radius, 4 * n); - const SimplePolygon triangle = {{-1, -1}, {1, 0}, {0, 1}}; - const Manifold block = Manifold::Extrude(triangle, 1); - - Vec convexEdges(NumEdge()); - Vec vertConvex(NumVert(), false); - convexEdges.resize( - copy_if(countAt(0), countAt(pImpl->halfedge_.size()), convexEdges.begin(), - ConvexEdge({vertConvex, pImpl->halfedge_, pImpl->vertPos_, - pImpl->faceNormal_, inset})) - - convexEdges.begin()); - - Vec convexVerts(NumVert()); - convexVerts.resize(copy_if(countAt(0), countAt(NumVert()), vertConvex.begin(), - convexVerts.begin(), thrust::identity()) - - convexVerts.begin()); - - const int edgeOffset = 1 + NumTri(); - const int vertOffset = edgeOffset + convexEdges.size(); - std::vector batch(vertOffset + convexVerts.size()); - batch[0] = *this; - - for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { - glm::mat3 triPos; - for (const int i : {0, 1, 2}) { - triPos[i] = pImpl->vertPos_[pImpl->halfedge_[3 * tri + i].startVert]; - } - const glm::vec3 normal = radius * pImpl->faceNormal_[tri]; - batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { - const float dir = pos.z > 0 ? 1.0f : -1.0f; - if (pos.x < 0) { - pos = triPos[0]; - } else if (pos.x > 0) { - pos = triPos[1]; - } else { - pos = triPos[2]; + std::vector batch; + batch.push_back(*this); + + if (!useHullMethod) { + const Manifold cylinder = Manifold::Cylinder(1, radius, radius, 4 * n); + const SimplePolygon triangle = {{-1, -1}, {1, 0}, {0, 1}}; + const Manifold block = Manifold::Extrude(triangle, 1); + + Vec convexEdges(NumEdge()); + Vec vertConvex(NumVert(), false); + convexEdges.resize( + copy_if(countAt(0), countAt(pImpl->halfedge_.size()), + convexEdges.begin(), + ConvexEdge({vertConvex, pImpl->halfedge_, pImpl->vertPos_, + pImpl->faceNormal_, inset})) - + convexEdges.begin()); + + Vec convexVerts(NumVert()); + convexVerts.resize(copy_if(countAt(0), countAt(NumVert()), + vertConvex.begin(), convexVerts.begin(), + thrust::identity()) - + convexVerts.begin()); + + const int edgeOffset = 1 + NumTri(); + const int vertOffset = edgeOffset + convexEdges.size(); + batch.resize(vertOffset + convexVerts.size()); + + for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { + glm::mat3 triPos; + for (const int i : {0, 1, 2}) { + triPos[i] = pImpl->vertPos_[pImpl->halfedge_[3 * tri + i].startVert]; } - pos += dir * normal; + const glm::vec3 normal = radius * pImpl->faceNormal_[tri]; + batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { + const float dir = pos.z > 0 ? 1.0f : -1.0f; + if (pos.x < 0) { + pos = triPos[0]; + } else if (pos.x > 0) { + pos = triPos[1]; + } else { + pos = triPos[2]; + } + pos += dir * normal; + }); }); - }); - - for_each_n(countAt(0), convexEdges.size(), - [&batch, &cylinder, &pImpl, &convexEdges, edgeOffset](int idx) { - const Halfedge halfedge = pImpl->halfedge_[convexEdges[idx]]; - glm::vec3 edge = pImpl->vertPos_[halfedge.endVert] - - pImpl->vertPos_[halfedge.startVert]; - const float length = glm::length(edge); - // Reverse RotateUp - edge.x *= -1; - edge.y *= -1; - batch[edgeOffset + idx] = - cylinder.Scale({1, 1, length}) - .Transform(RotateUp(edge)) - .Translate(pImpl->vertPos_[halfedge.startVert]); - }); - - for_each_n(countAt(0), convexVerts.size(), - [&batch, &sphere, &pImpl, &convexVerts, vertOffset](int idx) { - batch[vertOffset + idx] = - sphere.Translate(pImpl->vertPos_[convexVerts[idx]]); - }); + for_each_n(countAt(0), convexEdges.size(), + [&batch, &cylinder, &pImpl, &convexEdges, edgeOffset](int idx) { + const Halfedge halfedge = pImpl->halfedge_[convexEdges[idx]]; + glm::vec3 edge = pImpl->vertPos_[halfedge.endVert] - + pImpl->vertPos_[halfedge.startVert]; + const float length = glm::length(edge); + // Reverse RotateUp + edge.x *= -1; + edge.y *= -1; + batch[edgeOffset + idx] = + cylinder.Scale({1, 1, length}) + .Transform(RotateUp(edge)) + .Translate(pImpl->vertPos_[halfedge.startVert]); + }); + + for_each_n(countAt(0), convexVerts.size(), + [&batch, &sphere, &pImpl, &convexVerts, vertOffset](int idx) { + batch[vertOffset + idx] = + sphere.Translate(pImpl->vertPos_[convexVerts[idx]]); + }); + } else { + manifold::Mesh aMesh = this->GetMesh(); + std::vector> composedParts; + for (glm::ivec3 vertexIndices : aMesh.triVerts) { + composedParts.push_back({sphere.Translate(aMesh.vertPos[vertexIndices.x]), + sphere.Translate(aMesh.vertPos[vertexIndices.y]), + sphere.Translate(aMesh.vertPos[vertexIndices.z])}); + } + std::vector newHulls; + newHulls.reserve(composedParts.size()); + newHulls.resize(composedParts.size()); + thrust::for_each_n( + thrust::host, zip(composedParts.begin(), newHulls.begin()), + composedParts.size(), + [](thrust::tuple, Manifold&> inOut) { + thrust::get<1>(inOut) = Manifold::Hull(thrust::get<0>(inOut)); + }); + batch.insert(batch.end(), newHulls.begin(), newHulls.end()); + } return BatchBoolean(batch, inset ? OpType::Subtract : OpType::Add); } diff --git a/test/boolean_test.cpp b/test/boolean_test.cpp index 7ed46a9ad..0d7509785 100644 --- a/test/boolean_test.cpp +++ b/test/boolean_test.cpp @@ -611,6 +611,25 @@ TEST(Boolean, Offset) { PolygonParams().processOverlaps = false; } +TEST(Boolean, OffsetHull) { + PolygonParams().processOverlaps = true; + + Manifold cutout = Manifold::Cube(glm::vec3(100), true) - Manifold::Sphere(60); + Manifold fat = cutout.Offset(5, 0, true); + Manifold thin = cutout.Offset(-5, 0, true); + EXPECT_EQ(fat.Genus(), cutout.Genus()); + EXPECT_EQ(thin.Genus(), -7); + +#ifdef MANIFOLD_EXPORT + if (options.exportModels) { + ExportMesh("fatHulled.glb", fat.GetMesh(), {}); + ExportMesh("thinHulled.glb", thin.GetMesh(), {}); + } +#endif + + PolygonParams().processOverlaps = false; +} + TEST(Boolean, Close) { PolygonParams().processOverlaps = true; From d1a5871deb3e88c94bdceb6254647f3d5932bc6b Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 15:18:23 -0800 Subject: [PATCH 06/15] Ensure this is here... --- src/manifold/src/manifold.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index f9f73a295..de84147d1 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -642,6 +642,7 @@ Manifold Manifold::Offset(float delta, int circularSegments, bool useHullMethod) const int edgeOffset = 1 + NumTri(); const int vertOffset = edgeOffset + convexEdges.size(); batch.resize(vertOffset + convexVerts.size()); + batch[0] = (*this); for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { glm::mat3 triPos; From 53cdc11ece1792cde34f9e572712a020e1cd30f2 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 16:39:49 -0800 Subject: [PATCH 07/15] Get Elegant Positive Offset Working Easier than I expected! --- bindings/python/manifold3d.cpp | 8 ++ src/manifold/src/manifold.cpp | 134 ++++++++++++++++----------------- 2 files changed, 74 insertions(+), 68 deletions(-) diff --git a/bindings/python/manifold3d.cpp b/bindings/python/manifold3d.cpp index 00f7d89ee..338427710 100644 --- a/bindings/python/manifold3d.cpp +++ b/bindings/python/manifold3d.cpp @@ -253,6 +253,14 @@ NB_MODULE(manifold3d, m) { "Transforms are combined and applied lazily." "\n\n" ":param v: The vector to multiply every vertex by component.") + .def("offset", &Manifold::Offset, nb::arg("delta"), nb::arg("circularSegments"), + nb::arg("useHullMethod"), + " * Inflate the Manifold by the specified delta, rounding convex vertices." + "\n\n" + ":param delta: Positive deltas will add volume to all surfaces of the Manifold," + "dilating it. Negative deltas will have the opposite effect, eroding it." + ":param circularSegments Denotes the resolution of the sphere used at convex" + "vertices. Default is calculated by the static Quality defaults according to the radius, which is delta.") .def("mirror", &Manifold::Mirror, nb::arg("v"), "Mirror this Manifold in space. This operation can be chained. " "Transforms are combined and applied lazily." diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index de84147d1..a98a3abcb 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "QuickHull.hpp" #include "boolean3.h" @@ -41,32 +42,6 @@ struct MakeTri { } }; -struct ConvexEdge { - VecView vertConvex; - VecView halfedge; - VecView vertPos; - VecView faceNormal; - const bool inset; - - bool operator()(int idx) { - const Halfedge edge = halfedge[idx]; - if (!edge.IsForward()) return false; - - const glm::vec3 normal0 = faceNormal[edge.face]; - const glm::vec3 normal1 = faceNormal[halfedge[edge.pairedHalfedge].face]; - if (glm::all(glm::equal(normal0, normal1))) return false; - - const glm::vec3 edgeVec = vertPos[edge.endVert] - vertPos[edge.startVert]; - const bool convex = ((inset ? -1 : 1) * - glm::dot(edgeVec, glm::cross(normal0, normal1))) > 0; - if (convex) { - vertConvex[edge.startVert] = true; - vertConvex[edge.endVert] = true; - } - return convex; - } -}; - struct UpdateProperties { float* properties; const int numProp; @@ -620,30 +595,10 @@ Manifold Manifold::Offset(float delta, int circularSegments, bool useHullMethod) batch.push_back(*this); if (!useHullMethod) { - const Manifold cylinder = Manifold::Cylinder(1, radius, radius, 4 * n); + // Extrude Triangles + batch.resize(NumTri() + 1); const SimplePolygon triangle = {{-1, -1}, {1, 0}, {0, 1}}; const Manifold block = Manifold::Extrude(triangle, 1); - - Vec convexEdges(NumEdge()); - Vec vertConvex(NumVert(), false); - convexEdges.resize( - copy_if(countAt(0), countAt(pImpl->halfedge_.size()), - convexEdges.begin(), - ConvexEdge({vertConvex, pImpl->halfedge_, pImpl->vertPos_, - pImpl->faceNormal_, inset})) - - convexEdges.begin()); - - Vec convexVerts(NumVert()); - convexVerts.resize(copy_if(countAt(0), countAt(NumVert()), - vertConvex.begin(), convexVerts.begin(), - thrust::identity()) - - convexVerts.begin()); - - const int edgeOffset = 1 + NumTri(); - const int vertOffset = edgeOffset + convexEdges.size(); - batch.resize(vertOffset + convexVerts.size()); - batch[0] = (*this); - for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { glm::mat3 triPos; for (const int i : {0, 1, 2}) { @@ -663,26 +618,69 @@ Manifold Manifold::Offset(float delta, int circularSegments, bool useHullMethod) }); }); - for_each_n(countAt(0), convexEdges.size(), - [&batch, &cylinder, &pImpl, &convexEdges, edgeOffset](int idx) { - const Halfedge halfedge = pImpl->halfedge_[convexEdges[idx]]; - glm::vec3 edge = pImpl->vertPos_[halfedge.endVert] - - pImpl->vertPos_[halfedge.startVert]; - const float length = glm::length(edge); - // Reverse RotateUp - edge.x *= -1; - edge.y *= -1; - batch[edgeOffset + idx] = - cylinder.Scale({1, 1, length}) - .Transform(RotateUp(edge)) - .Translate(pImpl->vertPos_[halfedge.startVert]); - }); - - for_each_n(countAt(0), convexVerts.size(), - [&batch, &sphere, &pImpl, &convexVerts, vertOffset](int idx) { - batch[vertOffset + idx] = - sphere.Translate(pImpl->vertPos_[convexVerts[idx]]); - }); + // Iterate over all the edges to find the convex edges and vertices + std::unordered_map < int, + std::vector> verticesToCenteredWedgePoints; + + for (int idx = 0; idx < pImpl->halfedge_.size(); idx++) { + // Determine if the Current HalfEdge is a Forward-facing Convex Edge + const Halfedge edge = pImpl->halfedge_[idx]; + if (!edge.IsForward()) continue; + const glm::vec3 normal0 = pImpl->faceNormal_[edge.face]; + const glm::vec3 normal1 = pImpl->faceNormal_[pImpl->halfedge_[edge.pairedHalfedge].face]; + if (glm::all(glm::equal(normal0, normal1))) continue; + const glm::vec3 edgeVec = pImpl->vertPos_[edge.endVert] - pImpl->vertPos_[edge.startVert]; + const bool convex = ((inset ? -1 : 1) * + glm::dot(edgeVec, glm::cross(normal0, normal1))) > 0; + + // If so, construct a wedge around this edge + if (convex) { + // Compute wedge points by rotating around the edgeVec from normal0 to normal1 + float normal0Tonormal1Angle = fmodf(atan2( + glm::dot(glm::normalize(edgeVec), glm::cross(normal0, normal1)), + glm::dot(normal0, normal1)), glm::two_pi()); + + std::vector wedgePointsStart; + std::vector wedgePointsEnd; + for (int seg = 0; seg <= circularSegments; seg++) { + float wdgAlpha = (float)seg / circularSegments; + glm::qua rotation = glm::angleAxis(wdgAlpha * normal0Tonormal1Angle, + glm::normalize(edgeVec)); + glm::vec3 wedgePt = rotation * (normal0 * delta); + wedgePointsStart.push_back(pImpl->vertPos_[edge.startVert] + wedgePt); + wedgePointsEnd.push_back(pImpl->vertPos_[edge.endVert] + wedgePt); + } + + // Add the WedgePoints to each Convex Vertex for use later + verticesToCenteredWedgePoints[edge.startVert].insert( + verticesToCenteredWedgePoints[edge.startVert].end(), + wedgePointsStart.begin(), wedgePointsStart.end()); + verticesToCenteredWedgePoints[edge.endVert].insert( + verticesToCenteredWedgePoints[edge.endVert].end(), + wedgePointsEnd.begin(), wedgePointsEnd.end()); + + // Also construct the wedge shape while we're here + std::vector fullWedgePoints; // 2 * (wedgePoints.size())); + fullWedgePoints.push_back(pImpl->vertPos_[edge.startVert]); + fullWedgePoints.push_back(pImpl->vertPos_[edge.endVert]); + fullWedgePoints.insert(fullWedgePoints.end(), wedgePointsStart.begin(), + wedgePointsStart.end()); + fullWedgePoints.insert(fullWedgePoints.end(), wedgePointsEnd.begin(), + wedgePointsEnd.end()); + batch.push_back(Hull(fullWedgePoints)); + } + } + + // Hull the wedge points for each convex vertex with a sphere + for (auto pair : verticesToCenteredWedgePoints) { + auto sphereVerts = sphere.Translate(pImpl->vertPos_[pair.first]) + .GetCsgLeafNode() + .GetImpl() + ->vertPos_; + pair.second.insert(pair.second.end(), sphereVerts.begin(), + sphereVerts.end()); + batch.push_back(Hull(pair.second)); + } } else { manifold::Mesh aMesh = this->GetMesh(); std::vector> composedParts; From ab4736eb4c0b6e3a59bbae4ad4302a615d9aca45 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 16:52:27 -0800 Subject: [PATCH 08/15] Remove the old minkowski implementation --- src/manifold/src/manifold.cpp | 179 +++++++++++++++------------------- 1 file changed, 77 insertions(+), 102 deletions(-) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index a98a3abcb..68b615887 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -579,7 +579,8 @@ Manifold Manifold::Warp(std::function warpFunc) const { * vertices. Default is calculated by the static Quality defaults according to * the radius, which is delta. */ -Manifold Manifold::Offset(float delta, int circularSegments, bool useHullMethod) const { +Manifold Manifold::Offset(float delta, int circularSegments, + bool useHullMethod) const { auto pImpl = std::make_shared(*GetCsgLeafNode().GetImpl()); if (delta == 0) { @@ -594,112 +595,86 @@ Manifold Manifold::Offset(float delta, int circularSegments, bool useHullMethod) std::vector batch; batch.push_back(*this); - if (!useHullMethod) { - // Extrude Triangles - batch.resize(NumTri() + 1); - const SimplePolygon triangle = {{-1, -1}, {1, 0}, {0, 1}}; - const Manifold block = Manifold::Extrude(triangle, 1); - for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { - glm::mat3 triPos; - for (const int i : {0, 1, 2}) { - triPos[i] = pImpl->vertPos_[pImpl->halfedge_[3 * tri + i].startVert]; + // Extrude Triangles + batch.resize(NumTri() + 1); + for_each_n(countAt(0), NumTri(), [&batch, &pImpl, delta](int tri) { + const glm::vec3 off = delta * pImpl->faceNormal_[tri]; + batch[1 + tri] = + Hull({pImpl->vertPos_[pImpl->halfedge_[3 * tri + 0].startVert], + pImpl->vertPos_[pImpl->halfedge_[3 * tri + 1].startVert], + pImpl->vertPos_[pImpl->halfedge_[3 * tri + 2].startVert], + pImpl->vertPos_[pImpl->halfedge_[3 * tri + 0].startVert] + off, + pImpl->vertPos_[pImpl->halfedge_[3 * tri + 1].startVert] + off, + pImpl->vertPos_[pImpl->halfedge_[3 * tri + 2].startVert] + off}); + }); + + // Iterate over all the edges to find the convex edges and vertices + std::unordered_map> verticesToCenteredWedgePoints; + + for (int idx = 0; idx < pImpl->halfedge_.size(); idx++) { + // Determine if the Current HalfEdge is a Forward-facing Convex Edge + const Halfedge edge = pImpl->halfedge_[idx]; + if (!edge.IsForward()) continue; + const glm::vec3 normal0 = pImpl->faceNormal_[edge.face]; + const glm::vec3 normal1 = + pImpl->faceNormal_[pImpl->halfedge_[edge.pairedHalfedge].face]; + if (glm::all(glm::equal(normal0, normal1))) continue; + const glm::vec3 edgeVec = + pImpl->vertPos_[edge.endVert] - pImpl->vertPos_[edge.startVert]; + const bool convex = ((inset ? -1 : 1) * + glm::dot(edgeVec, glm::cross(normal0, normal1))) > 0; + + // If so, construct a wedge around this edge + if (convex) { + // Compute wedge points by rotating around the edgeVec from normal0 to + // normal1 + float normal0Tonormal1Angle = fmodf( + atan2(glm::dot(glm::normalize(edgeVec), glm::cross(normal0, normal1)), + glm::dot(normal0, normal1)), + glm::two_pi()); + + std::vector wedgePointsStart; + std::vector wedgePointsEnd; + for (int seg = 0; seg <= circularSegments; seg++) { + float wdgAlpha = (float)seg / circularSegments; + glm::qua rotation = glm::angleAxis(wdgAlpha * normal0Tonormal1Angle, + glm::normalize(edgeVec)); + glm::vec3 wedgePt = rotation * (normal0 * delta); + wedgePointsStart.push_back(pImpl->vertPos_[edge.startVert] + wedgePt); + wedgePointsEnd.push_back(pImpl->vertPos_[edge.endVert] + wedgePt); } - const glm::vec3 normal = radius * pImpl->faceNormal_[tri]; - batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { - const float dir = pos.z > 0 ? 1.0f : -1.0f; - if (pos.x < 0) { - pos = triPos[0]; - } else if (pos.x > 0) { - pos = triPos[1]; - } else { - pos = triPos[2]; - } - pos += dir * normal; - }); - }); - // Iterate over all the edges to find the convex edges and vertices - std::unordered_map < int, - std::vector> verticesToCenteredWedgePoints; - - for (int idx = 0; idx < pImpl->halfedge_.size(); idx++) { - // Determine if the Current HalfEdge is a Forward-facing Convex Edge - const Halfedge edge = pImpl->halfedge_[idx]; - if (!edge.IsForward()) continue; - const glm::vec3 normal0 = pImpl->faceNormal_[edge.face]; - const glm::vec3 normal1 = pImpl->faceNormal_[pImpl->halfedge_[edge.pairedHalfedge].face]; - if (glm::all(glm::equal(normal0, normal1))) continue; - const glm::vec3 edgeVec = pImpl->vertPos_[edge.endVert] - pImpl->vertPos_[edge.startVert]; - const bool convex = ((inset ? -1 : 1) * - glm::dot(edgeVec, glm::cross(normal0, normal1))) > 0; - - // If so, construct a wedge around this edge - if (convex) { - // Compute wedge points by rotating around the edgeVec from normal0 to normal1 - float normal0Tonormal1Angle = fmodf(atan2( - glm::dot(glm::normalize(edgeVec), glm::cross(normal0, normal1)), - glm::dot(normal0, normal1)), glm::two_pi()); - - std::vector wedgePointsStart; - std::vector wedgePointsEnd; - for (int seg = 0; seg <= circularSegments; seg++) { - float wdgAlpha = (float)seg / circularSegments; - glm::qua rotation = glm::angleAxis(wdgAlpha * normal0Tonormal1Angle, - glm::normalize(edgeVec)); - glm::vec3 wedgePt = rotation * (normal0 * delta); - wedgePointsStart.push_back(pImpl->vertPos_[edge.startVert] + wedgePt); - wedgePointsEnd.push_back(pImpl->vertPos_[edge.endVert] + wedgePt); - } - - // Add the WedgePoints to each Convex Vertex for use later - verticesToCenteredWedgePoints[edge.startVert].insert( - verticesToCenteredWedgePoints[edge.startVert].end(), - wedgePointsStart.begin(), wedgePointsStart.end()); - verticesToCenteredWedgePoints[edge.endVert].insert( - verticesToCenteredWedgePoints[edge.endVert].end(), - wedgePointsEnd.begin(), wedgePointsEnd.end()); - - // Also construct the wedge shape while we're here - std::vector fullWedgePoints; // 2 * (wedgePoints.size())); - fullWedgePoints.push_back(pImpl->vertPos_[edge.startVert]); - fullWedgePoints.push_back(pImpl->vertPos_[edge.endVert]); - fullWedgePoints.insert(fullWedgePoints.end(), wedgePointsStart.begin(), - wedgePointsStart.end()); - fullWedgePoints.insert(fullWedgePoints.end(), wedgePointsEnd.begin(), - wedgePointsEnd.end()); - batch.push_back(Hull(fullWedgePoints)); - } + // Add the WedgePoints to each Convex Vertex for use later + verticesToCenteredWedgePoints[edge.startVert].insert( + verticesToCenteredWedgePoints[edge.startVert].end(), + wedgePointsStart.begin(), wedgePointsStart.end()); + verticesToCenteredWedgePoints[edge.endVert].insert( + verticesToCenteredWedgePoints[edge.endVert].end(), + wedgePointsEnd.begin(), wedgePointsEnd.end()); + + // Also construct the wedge shape while we're here + std::vector fullWedgePoints; // 2 * (wedgePoints.size())); + fullWedgePoints.push_back(pImpl->vertPos_[edge.startVert]); + fullWedgePoints.push_back(pImpl->vertPos_[edge.endVert]); + fullWedgePoints.insert(fullWedgePoints.end(), wedgePointsStart.begin(), + wedgePointsStart.end()); + fullWedgePoints.insert(fullWedgePoints.end(), wedgePointsEnd.begin(), + wedgePointsEnd.end()); + batch.push_back(Hull(fullWedgePoints)); } + } - // Hull the wedge points for each convex vertex with a sphere - for (auto pair : verticesToCenteredWedgePoints) { - auto sphereVerts = sphere.Translate(pImpl->vertPos_[pair.first]) - .GetCsgLeafNode() - .GetImpl() - ->vertPos_; - pair.second.insert(pair.second.end(), sphereVerts.begin(), - sphereVerts.end()); - batch.push_back(Hull(pair.second)); - } - } else { - manifold::Mesh aMesh = this->GetMesh(); - std::vector> composedParts; - for (glm::ivec3 vertexIndices : aMesh.triVerts) { - composedParts.push_back({sphere.Translate(aMesh.vertPos[vertexIndices.x]), - sphere.Translate(aMesh.vertPos[vertexIndices.y]), - sphere.Translate(aMesh.vertPos[vertexIndices.z])}); - } - std::vector newHulls; - newHulls.reserve(composedParts.size()); - newHulls.resize(composedParts.size()); - thrust::for_each_n( - thrust::host, zip(composedParts.begin(), newHulls.begin()), - composedParts.size(), - [](thrust::tuple, Manifold&> inOut) { - thrust::get<1>(inOut) = Manifold::Hull(thrust::get<0>(inOut)); - }); - batch.insert(batch.end(), newHulls.begin(), newHulls.end()); + // Hull the wedge points for each convex vertex with a sphere + for (auto pair : verticesToCenteredWedgePoints) { + auto sphereVerts = sphere.Translate(pImpl->vertPos_[pair.first]) + .GetCsgLeafNode() + .GetImpl() + ->vertPos_; + pair.second.insert(pair.second.end(), sphereVerts.begin(), + sphereVerts.end()); + batch.push_back(Hull(pair.second)); } + return BatchBoolean(batch, inset ? OpType::Subtract : OpType::Add); } From 1354bfb05ffebb2472ef8b88133900c71a7bd90d Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 17:16:24 -0800 Subject: [PATCH 09/15] Remove unused arg and obsolete test --- bindings/python/manifold3d.cpp | 1 - src/manifold/include/manifold.h | 2 +- src/manifold/src/manifold.cpp | 5 +++-- test/boolean_test.cpp | 19 ------------------- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/bindings/python/manifold3d.cpp b/bindings/python/manifold3d.cpp index 338427710..2bc8fd3e8 100644 --- a/bindings/python/manifold3d.cpp +++ b/bindings/python/manifold3d.cpp @@ -254,7 +254,6 @@ NB_MODULE(manifold3d, m) { "\n\n" ":param v: The vector to multiply every vertex by component.") .def("offset", &Manifold::Offset, nb::arg("delta"), nb::arg("circularSegments"), - nb::arg("useHullMethod"), " * Inflate the Manifold by the specified delta, rounding convex vertices." "\n\n" ":param delta: Positive deltas will add volume to all surfaces of the Manifold," diff --git a/src/manifold/include/manifold.h b/src/manifold/include/manifold.h index 071cbedf5..2ad2e7213 100644 --- a/src/manifold/include/manifold.h +++ b/src/manifold/include/manifold.h @@ -206,7 +206,7 @@ class Manifold { Manifold Transform(const glm::mat4x3&) const; Manifold Mirror(glm::vec3) const; Manifold Warp(std::function) const; - Manifold Offset(float delta, int circularSegments = 0, bool useHullMethod = false) const; + Manifold Offset(float delta, int circularSegments = 0) const; Manifold SetProperties( int, std::function) const; Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const; diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 68b615887..2e6afbab4 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -579,8 +579,7 @@ Manifold Manifold::Warp(std::function warpFunc) const { * vertices. Default is calculated by the static Quality defaults according to * the radius, which is delta. */ -Manifold Manifold::Offset(float delta, int circularSegments, - bool useHullMethod) const { +Manifold Manifold::Offset(float delta, int circularSegments) const { auto pImpl = std::make_shared(*GetCsgLeafNode().GetImpl()); if (delta == 0) { @@ -589,6 +588,8 @@ Manifold Manifold::Offset(float delta, int circularSegments, const bool inset = delta < 0; const float radius = glm::abs(delta); + // This is unused in the original implementation, + // and I can't find a value that looks good in all cases const int n = circularSegments > 0 ? (circularSegments + 3) / 4 : Quality::GetCircularSegments(delta) / 4; const Manifold sphere = Manifold::Sphere(radius, circularSegments); diff --git a/test/boolean_test.cpp b/test/boolean_test.cpp index 0d7509785..7ed46a9ad 100644 --- a/test/boolean_test.cpp +++ b/test/boolean_test.cpp @@ -611,25 +611,6 @@ TEST(Boolean, Offset) { PolygonParams().processOverlaps = false; } -TEST(Boolean, OffsetHull) { - PolygonParams().processOverlaps = true; - - Manifold cutout = Manifold::Cube(glm::vec3(100), true) - Manifold::Sphere(60); - Manifold fat = cutout.Offset(5, 0, true); - Manifold thin = cutout.Offset(-5, 0, true); - EXPECT_EQ(fat.Genus(), cutout.Genus()); - EXPECT_EQ(thin.Genus(), -7); - -#ifdef MANIFOLD_EXPORT - if (options.exportModels) { - ExportMesh("fatHulled.glb", fat.GetMesh(), {}); - ExportMesh("thinHulled.glb", thin.GetMesh(), {}); - } -#endif - - PolygonParams().processOverlaps = false; -} - TEST(Boolean, Close) { PolygonParams().processOverlaps = true; From 4a49765e6b80688ddfa3b9479f2e5904dc8b48c8 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 17:40:42 -0800 Subject: [PATCH 10/15] Use n properly --- src/manifold/src/manifold.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 2e6afbab4..ab4d3121d 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -588,8 +588,6 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { const bool inset = delta < 0; const float radius = glm::abs(delta); - // This is unused in the original implementation, - // and I can't find a value that looks good in all cases const int n = circularSegments > 0 ? (circularSegments + 3) / 4 : Quality::GetCircularSegments(delta) / 4; const Manifold sphere = Manifold::Sphere(radius, circularSegments); @@ -636,8 +634,8 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { std::vector wedgePointsStart; std::vector wedgePointsEnd; - for (int seg = 0; seg <= circularSegments; seg++) { - float wdgAlpha = (float)seg / circularSegments; + for (int seg = 0; seg <= (4*n); seg++) { + float wdgAlpha = (float)seg / (4*n); glm::qua rotation = glm::angleAxis(wdgAlpha * normal0Tonormal1Angle, glm::normalize(edgeVec)); glm::vec3 wedgePt = rotation * (normal0 * delta); From 7a23d17d4fdfcd29e32ca1064c699754bc7cf9d9 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 18:16:52 -0800 Subject: [PATCH 11/15] Switch back to triangle warping for precision --- src/manifold/src/manifold.cpp | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index ab4d3121d..7641364bb 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -591,20 +591,31 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { const int n = circularSegments > 0 ? (circularSegments + 3) / 4 : Quality::GetCircularSegments(delta) / 4; const Manifold sphere = Manifold::Sphere(radius, circularSegments); + const SimplePolygon triangle = {{-1, -1}, {1, 0}, {0, 1}}; + const Manifold block = Manifold::Extrude(triangle, 1); + std::vector batch; batch.push_back(*this); // Extrude Triangles - batch.resize(NumTri() + 1); - for_each_n(countAt(0), NumTri(), [&batch, &pImpl, delta](int tri) { - const glm::vec3 off = delta * pImpl->faceNormal_[tri]; - batch[1 + tri] = - Hull({pImpl->vertPos_[pImpl->halfedge_[3 * tri + 0].startVert], - pImpl->vertPos_[pImpl->halfedge_[3 * tri + 1].startVert], - pImpl->vertPos_[pImpl->halfedge_[3 * tri + 2].startVert], - pImpl->vertPos_[pImpl->halfedge_[3 * tri + 0].startVert] + off, - pImpl->vertPos_[pImpl->halfedge_[3 * tri + 1].startVert] + off, - pImpl->vertPos_[pImpl->halfedge_[3 * tri + 2].startVert] + off}); + batch.resize(NumTri() + batch.size()); + for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { + glm::mat3 triPos; + for (const int i : {0, 1, 2}) { + triPos[i] = pImpl->vertPos_[pImpl->halfedge_[3 * tri + i].startVert]; + } + const glm::vec3 normal = radius * pImpl->faceNormal_[tri]; + batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { + const float dir = pos.z > 0 ? 1.0f : -1.0f; + if (pos.x < 0) { + pos = triPos[0]; + } else if (pos.x > 0) { + pos = triPos[1]; + } else { + pos = triPos[2]; + } + pos += dir * normal; + }); }); // Iterate over all the edges to find the convex edges and vertices From d615ecf507a3392d517843b939a662024fff99f3 Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Wed, 20 Dec 2023 23:58:07 -0800 Subject: [PATCH 12/15] Make circularSegments match the behavior of the original PR --- src/manifold/src/manifold.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 7641364bb..d54b9f968 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -643,10 +643,12 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { glm::dot(normal0, normal1)), glm::two_pi()); + int numSegments = abs((int)((normal0Tonormal1Angle / glm::two_pi()) * (n * 4))); + std::vector wedgePointsStart; std::vector wedgePointsEnd; - for (int seg = 0; seg <= (4*n); seg++) { - float wdgAlpha = (float)seg / (4*n); + for (int seg = 0; seg <= numSegments; seg++) { + float wdgAlpha = (float)seg / numSegments; glm::qua rotation = glm::angleAxis(wdgAlpha * normal0Tonormal1Angle, glm::normalize(edgeVec)); glm::vec3 wedgePt = rotation * (normal0 * delta); From 25f395892e00593d4e346942f2f6fde8475037bf Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Fri, 22 Dec 2023 01:56:55 -0800 Subject: [PATCH 13/15] Make the normal extrusion more accurate --- src/manifold/src/manifold.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index d54b9f968..86b17cc91 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -604,17 +604,16 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { for (const int i : {0, 1, 2}) { triPos[i] = pImpl->vertPos_[pImpl->halfedge_[3 * tri + i].startVert]; } - const glm::vec3 normal = radius * pImpl->faceNormal_[tri]; + const glm::dvec3 normal = + (double)radius * (glm::dvec3)pImpl->faceNormal_[tri]; batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { - const float dir = pos.z > 0 ? 1.0f : -1.0f; if (pos.x < 0) { - pos = triPos[0]; + pos = ((glm::dvec3)triPos[0] + (pos.z > 0 ? normal : -normal)); } else if (pos.x > 0) { - pos = triPos[1]; + pos = ((glm::dvec3)triPos[1] + (pos.z > 0 ? normal : -normal)); } else { - pos = triPos[2]; + pos = ((glm::dvec3)triPos[2] + (pos.z > 0 ? normal : -normal)); } - pos += dir * normal; }); }); From 95290c6cb18c8d0c9a8409026e2730e2ace5014e Mon Sep 17 00:00:00 2001 From: Johnathon Selstad Date: Fri, 22 Dec 2023 02:05:01 -0800 Subject: [PATCH 14/15] Simplify --- src/manifold/src/manifold.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 86b17cc91..bbeeddf56 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -607,12 +607,13 @@ Manifold Manifold::Offset(float delta, int circularSegments) const { const glm::dvec3 normal = (double)radius * (glm::dvec3)pImpl->faceNormal_[tri]; batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { + glm::dvec3 offset = (pos.z > 0 ? normal : -normal); if (pos.x < 0) { - pos = ((glm::dvec3)triPos[0] + (pos.z > 0 ? normal : -normal)); + pos = (glm::dvec3)triPos[0] + offset; } else if (pos.x > 0) { - pos = ((glm::dvec3)triPos[1] + (pos.z > 0 ? normal : -normal)); + pos = (glm::dvec3)triPos[1] + offset; } else { - pos = ((glm::dvec3)triPos[2] + (pos.z > 0 ? normal : -normal)); + pos = (glm::dvec3)triPos[2] + offset; } }); }); From 54fa822408a5ec100d344fdbfd65f0fcea6331ef Mon Sep 17 00:00:00 2001 From: Kyle Finn Date: Tue, 26 Dec 2023 15:04:01 -0500 Subject: [PATCH 15/15] Python auto doc (#665) * python autogen docstrings * use function signature hashes in docstring key * remove sys.path debug code * docstrings use param names * autogen docstrings during cmake * document gen_docs.py * more readme, remap mesh_gl references * readme about verify python docstrings, autogen depend on sources --- bindings/python/CMakeLists.txt | 11 + bindings/python/README.md | 40 ++ bindings/python/examples/gyroid_module.py | 4 - bindings/python/gen_docs.py | 109 ++++ bindings/python/manifold3d.cpp | 611 +++++----------------- 5 files changed, 284 insertions(+), 491 deletions(-) create mode 100644 bindings/python/README.md create mode 100644 bindings/python/gen_docs.py diff --git a/bindings/python/CMakeLists.txt b/bindings/python/CMakeLists.txt index 4b1da0ba7..cb9643b54 100644 --- a/bindings/python/CMakeLists.txt +++ b/bindings/python/CMakeLists.txt @@ -24,6 +24,17 @@ target_compile_options(manifold3d PRIVATE ${MANIFOLD_FLAGS} -DMODULE_NAME=manifo target_compile_features(manifold3d PUBLIC cxx_std_17) set_target_properties(manifold3d PROPERTIES OUTPUT_NAME "manifold3d") +message(Python_EXECUTABLE = ${Python_EXECUTABLE}) +add_custom_target( + autogen_docstrings + ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/gen_docs.py + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + BYPRODUCTS autogen_docstrings.inl +) +target_include_directories(manifold3d PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +add_dependencies(autogen_docstrings manifold sdf polygon) +add_dependencies(manifold3d autogen_docstrings) + if(SKBUILD) install( TARGETS manifold3d diff --git a/bindings/python/README.md b/bindings/python/README.md new file mode 100644 index 000000000..c20eac498 --- /dev/null +++ b/bindings/python/README.md @@ -0,0 +1,40 @@ +# Python Bindings + +## Autogenerated Doc-Strings + +Doc-strings for the python API wrapper are generated by gen_docs.py +The script is run automatically during the build to keep python +doc strings fully up-to-date with c++ sources. + +It scrapes documentation comments and c++ function signatures +from the manifold c++ sources, in order to generate a c++ header file +exposing the comments as string variables named by function names. +This allows python bindings to re-use the c++ comments directly. + +Some snake-casing of params is applied for python use case. + +--- + +When modifying the Manifold C++ sources, you may need to update +gen_docs.py. For example, top-level free functions are white-listed, +so if you add a new one, you will need to add it in gen_docs.py. + +Similarly, the list of source files to parse is also white listed, +so if you define functions in new files that need python wrappers, +you will also need to up gen_docs.py. + +To verify that python docs are correct after changes, you can +run the following commends from the manifold repo root: +``` +pip install . +python -c 'import manifold3d; help(manifold3d)' +``` + +Alternateively you could generate stubs with roughly the same info +``` +pip install nanobind-stubgen +pip install . +nanobind-stubgen manifold3d +``` +It will emit some warnings and write a file `manifold3d.pyi` +which will show all the function signatures and docstrings. diff --git a/bindings/python/examples/gyroid_module.py b/bindings/python/examples/gyroid_module.py index e57040531..3a7119dc4 100644 --- a/bindings/python/examples/gyroid_module.py +++ b/bindings/python/examples/gyroid_module.py @@ -15,11 +15,7 @@ """ import math -import sys import numpy as np - -sys.path.append("/Users/k/projects/python/badcad/wip/manifold/build/bindings/python") - from manifold3d import Mesh, Manifold diff --git a/bindings/python/gen_docs.py b/bindings/python/gen_docs.py new file mode 100644 index 000000000..3a7c17d60 --- /dev/null +++ b/bindings/python/gen_docs.py @@ -0,0 +1,109 @@ +from os.path import dirname +from hashlib import md5 +import re + +base = dirname(dirname(dirname(__file__))) + +def snake_case(name): + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + +def python_param_modifier(comment): + # p = f":{snake_case(m[0][1:])}:" + comment = re.sub(r"@(param \w+)", lambda m: f':{snake_case(m[1])}:', comment) + # python API renames `MeshGL` to `Mesh` + comment = re.sub('mesh_gl', 'mesh', comment) + comment = re.sub('MeshGL', 'Mesh', comment) + return comment + + +def method_key(name): + name = re.sub("\+", "_plus", name) + name = re.sub("\-", "_minus", name) + name = re.sub("\^", "_xor", name) + name = re.sub("\=", "_eq", name) + name = re.sub("\:", "_", name) + name = re.sub("\~", "destroy_", name) + return name + + +parens_re = re.compile(r"[^(]+\(([^(]*(\(.*\))*[^(]*\))", flags=re.DOTALL) +args_re = re.compile( + r"^[^,^\(^\)]*(\(.*\))*[^,^\(^\)]*[\s\&\*]([0-9\w]+)\s*[,\)]", flags=re.DOTALL +) + + +def parse_args(s): + par = parens_re.match(s) + if not par: + return None + out = [] + arg_str = par[1] + while m := re.search(args_re, arg_str): + out += [snake_case(m[2])] + arg_str = arg_str[m.span()[1] :] + return out + + +def collect(fname, matcher, param_modifier=python_param_modifier): + comment = "" + with open(fname) as f: + for line in f: + line = line.lstrip() + if line.startswith("/**"): + comment = "" + elif line.startswith("*/"): + pass + elif line.startswith("*") and comment is not None: + comment += line[1:].lstrip() + elif comment and (m := matcher(line)): + while (args := parse_args(line)) is None: + line += next(f) + if len(line) > 500: + break + + method = method_key(snake_case(m[1])) + # comment = re.sub(param_re, param_modifier, comment) + comment = param_modifier(comment) + method = "__".join([method, *args]) + assert method not in comments + comments[method] = comment + comment = "" + + +comments = {} + +method_re = re.compile(r"(\w+::[\w\-\+\^\=\:]+)\(") +function_re = re.compile(r"([\w\-\+\^\=\:]+)\(") + + +# we don't handle inline functions in classes properly +# so instead just white-list functions we want +def select_functions(s): + m = function_re.search(s) + if m and "Triangulate" in m[0]: + return m + if m and "Circular" in m[0]: + return m + return None + + +collect(f"{base}/src/manifold/src/manifold.cpp", lambda s: method_re.search(s)) +collect(f"{base}/src/manifold/src/constructors.cpp", lambda s: method_re.search(s)) +collect( + f"{base}/src/cross_section/src/cross_section.cpp", lambda s: method_re.search(s) +) +collect(f"{base}/src/polygon/src/polygon.cpp", select_functions) +collect(f"{base}/src/utilities/include/public.h", select_functions) + +comments = dict(sorted(comments.items())) + +gen_h = f"autogen_docstrings.inl" +with open(gen_h, "w") as f: + f.write("#pragma once\n\n") + f.write("// --- AUTO GENERATED ---\n") + f.write("// gen_docs.py is run by cmake build\n\n") + f.write("namespace manifold_docstrings {\n") + for key, doc in comments.items(): + f.write(f'const char* {key} = R"___({doc.strip()})___";\n') + f.write("} // namespace manifold_docs") diff --git a/bindings/python/manifold3d.cpp b/bindings/python/manifold3d.cpp index 6e0bd1357..361529caf 100644 --- a/bindings/python/manifold3d.cpp +++ b/bindings/python/manifold3d.cpp @@ -15,6 +15,7 @@ #include #include +#include "autogen_docstrings.inl" // generated in build folder #include "cross_section.h" #include "manifold.h" #include "nanobind/nanobind.h" @@ -202,111 +203,58 @@ std::vector toVector(const T *arr, size_t size) { return std::vector(arr, arr + size); } +using namespace manifold_docstrings; + +// strip original :params: and replace with ours +const std::string manifold__rotate_xyz = + manifold__rotate__x_degrees__y_degrees__z_degrees; +const std::string manifold__rotate__v = + manifold__rotate_xyz.substr(0, manifold__rotate_xyz.find(":param")) + + ":param v: [X, Y, Z] rotation in degrees."; + NB_MODULE(manifold3d, m) { m.doc() = "Python binding for the Manifold library."; m.def("set_min_circular_angle", Quality::SetMinCircularAngle, - nb::arg("angle"), - "Sets an angle constraint the default number of circular segments for " - "the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), " - "and Manifold::Revolve() constructors. The number of segments will be " - "rounded up to the nearest factor of four." - "\n\n" - ":param angle: The minimum angle in degrees between consecutive " - "segments. The angle will increase if the the segments hit the minimum " - "edge length.\n" - "Default is 10 degrees."); + nb::arg("angle"), set_min_circular_angle__angle); m.def("set_min_circular_edge_length", Quality::SetMinCircularEdgeLength, - nb::arg("length"), - "Sets a length constraint the default number of circular segments for " - "the CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), " - "and Manifold::Revolve() constructors. The number of segments will be " - "rounded up to the nearest factor of four." - "\n\n" - ":param length: The minimum length of segments. The length will " - "increase if the the segments hit the minimum angle. Default is 1.0."); + nb::arg("length"), set_min_circular_edge_length__length); m.def("set_circular_segments", Quality::SetCircularSegments, - nb::arg("number"), - "Sets the default number of circular segments for the " - "CrossSection::Circle(), Manifold::Cylinder(), Manifold::Sphere(), and " - "Manifold::Revolve() constructors. Overrides the edge length and angle " - "constraints and sets the number of segments to exactly this value." - "\n\n" - ":param number: Number of circular segments. Default is 0, meaning no " - "constraint is applied."); + nb::arg("number"), set_circular_segments__number); m.def("get_circular_segments", Quality::GetCircularSegments, - nb::arg("radius"), - "Determine the result of the SetMinCircularAngle(), " - "SetMinCircularEdgeLength(), and SetCircularSegments() defaults." - "\n\n" - ":param radius: For a given radius of circle, determine how many " - "default"); + nb::arg("radius"), get_circular_segments__radius); m.def("triangulate", &Triangulate, nb::arg("polygons"), nb::arg("precision") = -1, // TODO document - "Given a list polygons (each polygon shape=(N,2) dtype=float), " - "returns the indices of the triangle vertices as a " - "numpy.ndarray(shape=(N,3), dtype=np.uint32)."); + triangulate__polygons__precision); nb::class_(m, "Manifold") - .def(nb::init<>(), "Construct empty Manifold object") + .def(nb::init<>(), manifold__manifold) .def(nb::init &>(), nb::arg("mesh"), nb::arg("property_tolerance") = nb::list(), - "Convert a MeshGL into a Manifold, retaining its properties and " - "merging onlythe positions according to the merge vectors. Will " - "return an empty Manifoldand set an Error Status if the result is " - "not an oriented 2-manifold. Willcollapse degenerate triangles and " - "unnecessary vertices.\n\n" - "All fields are read, making this structure suitable for a lossless " - "round-tripof data from GetMeshGL. For multi-material input, use " - "ReserveIDs to set aunique originalID for each material, and sort " - "the materials into triangleruns.\n\n" - ":param meshGL: The input MeshGL.\n" - ":param propertyTolerance: A vector of precision values for each " - "property beyond position. If specified, the propertyTolerance " - "vector must have size = numProp - 3. This is the amount of " - "interpolation error allowed before two neighboring triangles are " - "considered to be on a property boundary edge. Property boundary " - "edges will be retained across operations even if thetriangles are " - "coplanar. Defaults to 1e-5, which works well for most properties " - "in the [-1, 1] range.") - .def(nb::self + nb::self, "Boolean union.") - .def(nb::self - nb::self, "Boolean difference.") - .def(nb::self ^ nb::self, "Boolean intersection.") + manifold__manifold__mesh_gl__property_tolerance) + .def(nb::self + nb::self, manifold__operator_plus__q) + .def(nb::self - nb::self, manifold__operator_minus__q) + .def(nb::self ^ nb::self, manifold__operator_xor__q) .def( "hull", [](const Manifold &self) { return self.Hull(); }, - "Compute the convex hull of all points in this manifold.") + manifold__hull) .def_static( "batch_hull", [](std::vector ms) { return Manifold::Hull(ms); }, - nb::arg("manifolds"), - "Compute the convex hull enveloping a set of manifolds.") + nb::arg("manifolds"), manifold__hull__manifolds) .def_static( "hull_points", [](std::vector pts) { return Manifold::Hull(pts); }, - nb::arg("pts"), - "Compute the convex hull enveloping a set of 3d points.") - .def( - "transform", &Manifold::Transform, nb::arg("m"), - "Transform this Manifold in space. The first three columns form a " - "3x3 matrix transform and the last is a translation vector. This " - "operation can be chained. Transforms are combined and applied " - "lazily.\n" - "\n\n" - ":param m: The affine transform matrix to apply to all the vertices.") + nb::arg("pts"), manifold__hull__pts) + .def("transform", &Manifold::Transform, nb::arg("m"), + manifold__transform__m) .def("translate", &Manifold::Translate, nb::arg("t"), - "Move this Manifold in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param t: The vector to add to every vertex.") - .def("scale", &Manifold::Scale, nb::arg("v"), - "Scale this Manifold in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param v: The vector to multiply every vertex by component.") + manifold__translate__v) + .def("scale", &Manifold::Scale, nb::arg("v"), manifold__scale__v) .def( "scale", [](const Manifold &m, float s) { @@ -314,47 +262,21 @@ NB_MODULE(manifold3d, m) { }, nb::arg("s"), "Scale this Manifold in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" + "Transforms are combined and applied lazily.\n\n" ":param s: The scalar to multiply every vertex by component.") - .def("mirror", &Manifold::Mirror, nb::arg("v"), - "Mirror this Manifold in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param mirror: The vector defining the axis of mirroring.") + .def("mirror", &Manifold::Mirror, nb::arg("v"), manifold__mirror__normal) .def( "rotate", [](const Manifold &self, glm::vec3 v) { return self.Rotate(v.x, v.y, v.z); }, - nb::arg("v"), - "Applies an Euler angle rotation to the manifold, first about the X " - "axis, then Y, then Z, in degrees. We use degrees so that we can " - "minimize rounding error, and eliminate it completely for any " - "multiples of 90 degrees. Additionally, more efficient code paths " - "are used to update the manifold when the transforms only rotate by " - "multiples of 90 degrees. This operation can be chained. Transforms " - "are combined and applied lazily." - "\n\n" - ":param v: [X, Y, Z] rotation in degrees.") + nb::arg("v"), manifold__rotate__v.c_str()) + .def("warp", &Manifold::Warp, nb::arg("warp_func"), + manifold__warp__warp_func) + .def("warp_batch", &Manifold::WarpBatch, nb::arg("warp_func"), + manifold__warp_batch__warp_func) .def( - "warp", &Manifold::Warp, nb::arg("f"), - "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." - "\n\n" - ":param f: A function that modifies a given vertex position.") - .def("warp_batch", &Manifold::WarpBatch, nb::arg("f"), - "Same as Manifold.warp but calls `f` with a " - "ndarray(shape=(N,3), dtype=float) and expects an ndarray " - "of the same shape and type in return. The input array can be " - "modified and returned if desired. " - "\n\n" - ":param f: A function that modifies multiple vertex positions.") - .def( - "set_properties", // TODO this needs a batch version! + "set_properties", [](const Manifold &self, int newNumProp, const std::function " - "1.") + manifold__set_properties__num_prop__prop_func) + .def("calculate_curvature", &Manifold::CalculateCurvature, + nb::arg("gaussian_idx"), nb::arg("mean_idx"), + manifold__calculate_curvature__gaussian_idx__mean_idx) + .def("refine", &Manifold::Refine, nb::arg("n"), manifold__refine__n) .def("to_mesh", &Manifold::GetMeshGL, nb::arg("normal_idx") = glm::ivec3(0), - "The most complete output of this library, returning a MeshGL that " - "is designed to easily push into a renderer, including all " - "interleaved vertex properties that may have been input. It also " - "includes relations to all the input meshes that form a part of " - "this result and the transforms applied to each." - "\n\n" - ":param normal_idx: If the original MeshGL inputs that formed this " - "manifold had properties corresponding to normal vectors, you can " - "specify which property channels these are (x, y, z), which will " - "cause this output MeshGL to automatically update these normals " - "according to the applied transforms and front/back side. Each " - "channel must be >= 3 and < numProp, and all original MeshGLs must " - "use the same channels for their normals.") - .def("num_vert", &Manifold::NumVert, - "The number of vertices in the Manifold.") - .def("num_edge", &Manifold::NumEdge, - "The number of edges in the Manifold.") - .def("num_tri", &Manifold::NumTri, - "The number of triangles in the Manifold.") - .def("num_prop", &Manifold::NumProp, - "The number of properties per vertex in the Manifold") - .def("num_prop_vert", &Manifold::NumPropVert, - "The number of property vertices in the Manifold. This will always " - "be >= NumVert, as some physical vertices may be duplicated to " - "account for different properties on different neighboring " - "triangles.") - .def("precision", &Manifold::Precision, - "Returns the precision of this Manifold's vertices, which tracks " - "the approximate rounding error over all the transforms and " - "operations that have led to this state. Any triangles that are " - "colinear within this precision are considered degenerate and " - "removed. This is the value of ε defining " - "[ε-valid](https://github.com/elalish/manifold/wiki/" - "Manifold-Library#definition-of-%CE%B5-valid).") - .def("genus", &Manifold::Genus, - "The genus is a topological property of the manifold, representing " - "the number of \"handles\". A sphere is 0, torus 1, etc. It is only " - "meaningful for a single mesh, so it is best to call Decompose() " - "first.") + manifold__get_mesh_gl__normal_idx) + .def("num_vert", &Manifold::NumVert, manifold__num_vert) + .def("num_edge", &Manifold::NumEdge, manifold__num_edge) + .def("num_tri", &Manifold::NumTri, manifold__num_tri) + .def("num_prop", &Manifold::NumProp, manifold__num_prop) + .def("num_prop_vert", &Manifold::NumPropVert, manifold__num_prop_vert) + .def("precision", &Manifold::Precision, manifold__precision) + .def("genus", &Manifold::Genus, manifold__genus) .def( "volume", [](const Manifold &self) { return self.GetProperties().volume; }, @@ -474,68 +330,22 @@ NB_MODULE(manifold3d, m) { [](const Manifold &self) { return self.GetProperties().surfaceArea; }, "Get the surface area of the manifold\n This is clamped to zero for " "a given face if they are within the Precision().") - .def("original_id", &Manifold::OriginalID, - "If this mesh is an original, this returns its meshID that can be " - "referenced by product manifolds' MeshRelation. If this manifold is " - "a product, this returns -1.") - .def("as_original", &Manifold::AsOriginal, - "This function condenses all coplanar faces in the relation, and " - "collapses those edges. In the process the relation to ancestor " - "meshes is lost and this new Manifold is marked an original. " - "Properties are preserved, so if they do not match across an edge, " - "that edge will be kept.") - .def("is_empty", &Manifold::IsEmpty, - "Does the Manifold have any triangles?") - .def( - "decompose", &Manifold::Decompose, - "This operation returns a vector of Manifolds that are " - "topologically disconnected. If everything is connected, the vector " - "is length one, containing a copy of the original. It is the inverse " - "operation of Compose().") + .def("original_id", &Manifold::OriginalID, manifold__original_id) + .def("as_original", &Manifold::AsOriginal, manifold__as_original) + .def("is_empty", &Manifold::IsEmpty, manifold__is_empty) + .def("decompose", &Manifold::Decompose, manifold__decompose) .def("split", &Manifold::Split, nb::arg("cutter"), - "Split cuts this manifold in two using the cutter manifold. The " - "first result is the intersection, second is the difference. This " - "is more efficient than doing them separately." - "\n\n" - ":param cutter: This is the manifold to cut by.\n") + manifold__split__cutter) .def("split_by_plane", &Manifold::SplitByPlane, nb::arg("normal"), nb::arg("origin_offset"), - "Convenient version of Split() for a half-space." - "\n\n" - ":param normal: This vector is normal to the cutting plane and its " - "length does not matter. The first result is in the direction of " - "this vector, the second result is on the opposite side.\n" - ":param origin_offset: The distance of the plane from the origin in " - "the direction of the normal vector.") - .def( - "trim_by_plane", &Manifold::TrimByPlane, nb::arg("normal"), - nb::arg("origin_offset"), - "Identical to SplitByPlane(), but calculating and returning only the " - "first result." - "\n\n" - ":param normal: This vector is normal to the cutting plane and its " - "length does not matter. The result is in the direction of this " - "vector from the plane.\n" - ":param origin_offset: The distance of the plane from the origin in " - "the direction of the normal vector.") + manifold__split_by_plane__normal__origin_offset) + .def("trim_by_plane", &Manifold::TrimByPlane, nb::arg("normal"), + nb::arg("origin_offset"), + manifold__trim_by_plane__normal__origin_offset) .def("slice", &Manifold::Slice, nb::arg("height"), - "Returns the cross section of this object parallel to the X-Y plane " - "at the specified height. Using a height equal to the bottom of the " - "bounding box will return the bottom faces, while using a height " - "equal to the top of the bounding box will return empty." - "\n\n" - ":param height: The Z-level of the slice, defaulting to zero.") - .def("project", &Manifold::Project, - "Returns a cross section representing the projected outline of this " - "object onto the X-Y plane.") - .def("status", &Manifold::Status, - "Returns the reason for an input Mesh producing an empty Manifold. " - "This Status only applies to Manifolds newly-created from an input " - "Mesh - once they are combined into a new Manifold via operations, " - "the status reverts to NoError, simply processing the problem mesh " - "as empty. Likewise, empty meshes may still show NoError, for " - "instance if they are small enough relative to their precision to " - "be collapsed to nothing.") + manifold__slice__height) + .def("project", &Manifold::Project, manifold__project) + .def("status", &Manifold::Status, manifold__status) .def( "bounding_box", [](const Manifold &self) { @@ -561,81 +371,23 @@ NB_MODULE(manifold3d, m) { }, nb::arg("mesh"), nb::arg("sharpened_edges") = nb::list(), nb::arg("edge_smoothness") = nb::list(), - "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." - "\n\n" - "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." - "\n\n" - ":param mesh: input Mesh.\n" - ":param sharpened_edges: If desired, you can supply a vector of " - "sharpened halfedges, which should in general be a small subset of " - "all halfedges. The halfedge index is " - "(3 * triangle index + [0,1,2] where 0 is the edge between triVert 0 " - "and 1, etc)." - ":param edge_smoothness: must be same length as shapened_edges. " - "Each entry specifies the desired smoothness (between zero and one, " - "with one being the default for all unspecified halfedges)" - "\n\n" - "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.") + // todo params slightly diff + manifold__smooth__mesh_gl__sharpened_edges) .def_static("compose", &Manifold::Compose, nb::arg("manifolds"), - "combine several manifolds into one without checking for " - "intersections.") - .def_static( - "tetrahedron", &Manifold::Tetrahedron, - "Constructs a tetrahedron centered at the origin with one vertex at " - "(1,1,1) and the rest at similarly symmetric points.") - .def_static( - "cube", &Manifold::Cube, nb::arg("size") = glm::vec3{1, 1, 1}, - nb::arg("center") = false, - "Constructs a unit cube (edge lengths all one), by default in the " - "first octant, touching the origin." - "\n\n" - ":param size: The X, Y, and Z dimensions of the box.\n" - ":param center: Set to true to shift the center to the origin.") + manifold__compose__manifolds) + .def_static("tetrahedron", &Manifold::Tetrahedron, manifold__tetrahedron) + .def_static("cube", &Manifold::Cube, nb::arg("size") = glm::vec3{1, 1, 1}, + nb::arg("center") = false, manifold__cube__size__center) .def_static( "cylinder", &Manifold::Cylinder, nb::arg("height"), nb::arg("radius_low"), nb::arg("radius_high") = -1.0f, nb::arg("circular_segments") = 0, nb::arg("center") = false, - "A convenience constructor for the common case of extruding a " - "circle. Can also form cones if both radii are specified." - "\n\n" - ":param height: Z-extent\n" - ":param radius_low: Radius of bottom circle. Must be positive.\n" - ":param radius_high: Radius of top circle. Can equal zero. Default " - "(-1) is equal to radius_low.\n" - ":param circular_segments: How many line segments to use around the " - "circle. Default (-1) is calculated by the static Defaults.\n" - ":param center: Set to true to shift the center to the origin. " - "Default is origin at the bottom.") - .def_static( - "sphere", &Manifold::Sphere, nb::arg("radius"), - nb::arg("circular_segments") = 0, - "Constructs a geodesic sphere of a given radius.\n" - "\n" - ":param radius: Radius of the sphere. Must be positive.\n" - ":param circular_segments: Number of segments along its diameter. " - "This number will always be rounded up to the nearest factor of " - "four, as this sphere is constructed by refining an octahedron. This " - "means there are a circle of vertices on all three of the axis " - "planes. Default is calculated by the static Defaults.") + manifold__cylinder__height__radius_low__radius_high__circular_segments__center) + .def_static("sphere", &Manifold::Sphere, nb::arg("radius"), + nb::arg("circular_segments") = 0, + manifold__sphere__radius__circular_segments) .def_static("reserve_ids", Manifold::ReserveIDs, nb::arg("n"), - "Returns the first of n sequential new unique mesh IDs for " - "marking sets of triangles that can be looked up after " - "further operations. Assign to MeshGL.runOriginalID vector"); + manifold__reserve_ids__n); nb::class_(m, "Mesh") .def( @@ -735,15 +487,15 @@ NB_MODULE(manifold3d, m) { "level_set", [](const std::function &f, std::vector bounds, float edgeLength, float level = 0.0) { - // Same format as Manifold.bounding_box - Box bound = {glm::vec3(bounds[0], bounds[1], bounds[2]), - glm::vec3(bounds[3], bounds[4], bounds[5])}; - - std::function cppToPython = [&f](glm::vec3 v) { - return f(v.x, v.y, v.z); - }; - Mesh result = LevelSet(cppToPython, bound, edgeLength, level, false); - return MeshGL(result); + // Same format as Manifold.bounding_box + Box bound = {glm::vec3(bounds[0], bounds[1], bounds[2]), + glm::vec3(bounds[3], bounds[4], bounds[5])}; + + std::function cppToPython = [&f](glm::vec3 v) { + return f(v.x, v.y, v.z); + }; + Mesh result = LevelSet(cppToPython, bound, edgeLength, level, false); + return MeshGL(result); }, nb::arg("f"), nb::arg("bounds"), nb::arg("edgeLength"), nb::arg("level") = 0.0, @@ -820,30 +572,16 @@ NB_MODULE(manifold3d, m) { "onwards). This class makes use of the " "[Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library " "for polygon clipping (boolean) and offsetting operations.") - .def(nb::init<>(), "Construct empty CrossSection object") + .def(nb::init<>(), cross_section__cross_section) .def(nb::init>, CrossSection::FillRule>(), - nb::arg("polygons"), + nb::arg("contours"), nb::arg("fillrule") = CrossSection::FillRule::Positive, - "Create a 2d cross-section from a set of contours (complex " - "polygons). A boolean union operation (with Positive filling rule " - "by default) performed to combine overlapping polygons and ensure " - "the resulting CrossSection is free of intersections." - "\n\n" - ":param contours: A set of closed paths describing zero or more " - "complex polygons.\n" - ":param fillrule: The filling rule used to interpret polygon " - "sub-regions in contours.") - .def("area", &CrossSection::Area, - "Return the total area covered by complex polygons making up the " - "CrossSection.") - .def("num_vert", &CrossSection::NumVert, - "Return the number of vertices in the CrossSection.") - .def("num_contour", &CrossSection::NumContour, - "Return the number of contours (both outer and inner paths) in the " - "CrossSection.") - .def("is_empty", &CrossSection::IsEmpty, - "Does the CrossSection contain any contours?") + cross_section__cross_section__contours__fillrule) + .def("area", &CrossSection::Area, cross_section__area) + .def("num_vert", &CrossSection::NumVert, cross_section__num_vert) + .def("num_contour", &CrossSection::NumContour, cross_section__num_contour) + .def("is_empty", &CrossSection::IsEmpty, cross_section__is_empty) .def( "bounds", [](const CrossSection &self) { @@ -853,21 +591,11 @@ NB_MODULE(manifold3d, m) { "Return bounding box of CrossSection as tuple(" "min_x, min_y, max_x, max_y)") .def("translate", &CrossSection::Translate, nb::arg("v"), - "Move this CrossSection in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param v: The vector to add to every vertex.") - .def("rotate", &CrossSection::Rotate, nb::arg("angle"), - "Applies a (Z-axis) rotation to the CrossSection, in degrees. This " - "operation can be chained. Transforms are combined and applied " - "lazily." - "\n\n" - ":param degrees: degrees about the Z-axis to rotate.") - .def("scale", &CrossSection::Scale, nb::arg("v"), - "Scale this CrossSection in space. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param v: The vector to multiply every vertex by per component.") + cross_section__translate__v) + .def("rotate", &CrossSection::Rotate, nb::arg("degrees"), + cross_section__rotate__degrees) + .def("scale", &CrossSection::Scale, nb::arg("scale"), + cross_section__scale__scale) .def( "scale", [](const CrossSection &self, float s) { @@ -878,140 +606,49 @@ NB_MODULE(manifold3d, m) { "Transforms are combined and applied lazily." "\n\n" ":param s: The scalar to multiply every vertex by per component.") - .def( - "mirror", &CrossSection::Mirror, nb::arg("ax"), - "Mirror this CrossSection over the arbitrary axis described by the " - "unit form of the given vector. If the length of the vector is zero, " - "an empty CrossSection is returned. This operation can be chained. " - "Transforms are combined and applied lazily." - "\n\n" - ":param ax: the axis to be mirrored over") - .def( - "transform", &CrossSection::Transform, nb::arg("m"), - "Transform this CrossSection in space. The first two columns form a " - "2x2 matrix transform and the last is a translation vector. This " - "operation can be chained. Transforms are combined and applied " - "lazily." - "\n\n" - ":param m: The affine transform matrix to apply to all the vertices.") - .def("warp", &CrossSection::Warp, nb::arg("f"), - "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." - "\n\n" - ":param warpFunc: A function that modifies a given vertex position.") - .def("warp_batch", &CrossSection::WarpBatch, nb::arg("f"), - "Same as CrossSection.warp but calls `f` with a " - "ndarray(shape=(N,2), dtype=float) and expects an ndarray " - "of the same shape and type in return. The input array can be " - "modified and returned if desired. " - "\n\n" - ":param f: A function that modifies multiple vertex positions.") + .def("mirror", &CrossSection::Mirror, nb::arg("ax"), + cross_section__mirror__ax) + .def("transform", &CrossSection::Transform, nb::arg("m"), + cross_section__transform__m) + .def("warp", &CrossSection::Warp, 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("simplify", &CrossSection::Simplify, nb::arg("epsilon") = 1e-6, - "Remove vertices from the contours in this CrossSection that are " - "less than the specified distance epsilon from an imaginary line " - "that passes through its two adjacent vertices. Near duplicate " - "vertices and collinear points will be removed at lower epsilons, " - "with elimination of line segments becoming increasingly aggressive " - "with larger epsilons." - "\n\n" - "It is recommended to apply this function following Offset, in " - "order to clean up any spurious tiny line segments introduced that " - "do not improve quality in any meaningful way. This is particularly " - "important if further offseting operations are to be performed, " - "which would compound the issue.") - .def("offset", &CrossSection::Offset, nb::arg("delta"), - nb::arg("join_type"), nb::arg("miter_limit") = 2.0, - nb::arg("circular_segments") = 0, - "Inflate the contours in CrossSection by the specified delta, " - "handling corners according to the given JoinType." - "\n\n" - ":param delta: Positive deltas will cause the expansion of " - "outlining contours to expand, and retraction of inner (hole) " - "contours. Negative deltas will have the opposite effect.\n" - ":param jt: The join type specifying the treatment of contour joins " - "(corners).\n" - ":param miter_limit: The maximum distance in multiples of delta " - "that vertices can be offset from their original positions with " - "before squaring is applied, when the join type is Miter " - "(default is 2, which is the minimum allowed). See the [Clipper2 " - "MiterLimit](http://www.angusj.com/clipper2/Docs/Units/" - "Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm) " - "page for a visual example.\n" - ":param circular_segments: Number of segments per 360 degrees of " - "JoinType::Round corners (roughly, the number of vertices " - "that will be added to each contour). Default is calculated by the " - "static Quality defaults according to the radius.") - .def(nb::self + nb::self, "Boolean union.") - .def(nb::self - nb::self, "Boolean difference.") - .def(nb::self ^ nb::self, "Boolean intersection.") + cross_section__simplify__epsilon) + .def( + "offset", &CrossSection::Offset, nb::arg("delta"), + nb::arg("join_type"), nb::arg("miter_limit") = 2.0, + nb::arg("circular_segments") = 0, + cross_section__offset__delta__jointype__miter_limit__circular_segments) + .def(nb::self + nb::self, cross_section__operator_plus__q) + .def(nb::self - nb::self, cross_section__operator_minus__q) + .def(nb::self ^ nb::self, cross_section__operator_xor__q) .def( "hull", [](const CrossSection &self) { return self.Hull(); }, - "Compute the convex hull of this cross-section.") + cross_section__hull) .def_static( "batch_hull", [](std::vector cs) { return CrossSection::Hull(cs); }, - nb::arg("cross_sections"), - "Compute the convex hull enveloping a set of cross-sections.") + nb::arg("cross_sections"), cross_section__hull__cross_sections) .def_static( "hull_points", [](std::vector pts) { return CrossSection::Hull(pts); }, - nb::arg("pts"), - "Compute the convex hull enveloping a set of 2d points.") - .def("decompose", &CrossSection::Decompose, - "This operation returns a vector of CrossSections that are " - "topologically disconnected, each containing one outline contour " - "with zero or more holes.") - .def("to_polygons", &CrossSection::ToPolygons, - "Returns the vertices of the cross-section's polygons " - "as a List[ndarray(shape=(*,2), dtype=float)].") + nb::arg("pts"), cross_section__hull__pts) + .def("decompose", &CrossSection::Decompose, cross_section__decompose) + .def("to_polygons", &CrossSection::ToPolygons, cross_section__to_polygons) .def( "extrude", &Manifold::Extrude, nb::arg("height"), nb::arg("n_divisions") = 0, nb::arg("twist_degrees") = 0.0f, nb::arg("scale_top") = std::make_tuple(1.0f, 1.0f), - "Constructs a manifold from the set of polygons by extruding them " - "along the Z-axis.\n" - "\n" - ":param height: Z-extent of extrusion.\n" - ":param nDivisions: Number of extra copies of the crossSection to " - "insert into the shape vertically; especially useful in combination " - "with twistDegrees to avoid interpolation artifacts. Default is " - "none.\n" - ":param twistDegrees: Amount to twist the top crossSection relative " - "to the bottom, interpolated linearly for the divisions in between.\n" - ":param scaleTop: Amount to scale the top (independently in X and " - "Y). If the scale is (0, 0), a pure cone is formed with only a " - "single vertex at the top. Default (1, 1).") - .def( - "revolve", &Manifold::Revolve, nb::arg("circular_segments") = 0, - nb::arg("revolve_degrees") = 360.0, - "Constructs a manifold from the set of polygons by revolving this " - "cross-section around its Y-axis and then setting this as the Z-axis " - "of the resulting manifold. If the polygons cross the Y-axis, only " - "the part on the positive X side is used. Geometrically valid input " - "will result in geometrically valid output.\n" - "\n" - ":param circular_segments: Number of segments along its diameter. " - "Default is calculated by the static Defaults.\n" - ":param revolve_degrees: rotation angle for the sweep.") - .def_static( - "square", &CrossSection::Square, nb::arg("size"), - nb::arg("center") = false, - "Constructs a square with the given XY dimensions. By default it is " - "positioned in the first quadrant, touching the origin. If any " - "dimensions in size are negative, or if all are zero, an empty " - "Manifold will be returned." - "\n\n" - ":param size: The X, and Y dimensions of the square.\n" - ":param center: Set to true to shift the center to the origin.") - .def_static( - "circle", &CrossSection::Circle, nb::arg("radius"), - nb::arg("circular_segments") = 0, - "Constructs a circle of a given radius." - "\n\n" - ":param radius: Radius of the circle. Must be positive.\n" - ":param circular_segments: Number of segments along its diameter. " - "Default is calculated by the static Quality defaults according to " - "the radius."); + manifold__extrude__cross_section__height__n_divisions__twist_degrees__scale_top) + .def("revolve", &Manifold::Revolve, nb::arg("circular_segments") = 0, + nb::arg("revolve_degrees") = 360.0, + manifold__revolve__cross_section__circular_segments__revolve_degrees) + .def_static("square", &CrossSection::Square, nb::arg("size"), + nb::arg("center") = false, + cross_section__square__size__center) + .def_static("circle", &CrossSection::Circle, nb::arg("radius"), + nb::arg("circular_segments") = 0, + cross_section__circle__radius__circular_segments); }