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); }