// Copyright 2022 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. #include "cross_section.h" #include "manifold.h" #include "pybind11/functional.h" #include "pybind11/numpy.h" #include "pybind11/operators.h" #include "pybind11/pybind11.h" #include "pybind11/stl.h" namespace py = pybind11; using namespace manifold; typedef std::tuple Float2; typedef std::tuple Float3; constexpr auto pyArrayFlags = py::array::c_style | py::array::forcecast; PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MAKE_OPAQUE(std::vector); template std::vector toVector(const py::array_t &arr) { return std::vector(arr.data(), arr.data() + arr.size()); } PYBIND11_MODULE(manifold3d, m) { m.doc() = "Python binding for the Manifold library."; m.def("set_min_circular_angle", Quality::SetMinCircularAngle, "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."); m.def("set_min_circular_edge_length", Quality::SetMinCircularEdgeLength, "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."); m.def("set_circular_segments", Quality::SetCircularSegments, "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."); m.def("get_circular_segments", Quality::GetCircularSegments, "Determine the result of the SetMinCircularAngle(), " "SetMinCircularEdgeLength(), and SetCircularSegments() defaults." "\n\n" ":param radius: For a given radius of circle, determine how many " "default"); py::class_(m, "Manifold") .def(py::init<>()) .def(py::init([](std::vector &manifolds) { Manifold result; 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]; return first; } else { return Manifold(); } }), "Construct manifold as the union of a set of manifolds.") .def(py::self + py::self, "Boolean union.") .def(py::self - py::self, "Boolean difference.") .def(py::self ^ py::self, "Boolean intersection.") .def( "transform", [](Manifold &self, py::array_t &mat) { auto mat_view = mat.unchecked<2>(); 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_view(i, j); } } return self.Transform(mat_glm); }, py::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)); }, py::arg("x") = 0.0f, py::arg("y") = 0.0f, py::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 &self, py::array_t &t) { auto t_view = t.unchecked<1>(); if (t.ndim() != 1 || t.shape(0) != 3) throw std::runtime_error("Invalid vector shape"); return self.Translate(glm::vec3(t_view(0), t_view(1), t_view(2))); }, py::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.") .def( "scale", [](Manifold &self, float scale) { return self.Scale(glm::vec3(scale)); }, py::arg("scale"), "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 &self, py::array_t &scale) { auto scale_view = scale.unchecked<1>(); if (scale.ndim() != 1 || scale.shape(0) != 3) throw std::runtime_error("Invalid vector shape, expected (3)"); glm::vec3 v(scale_view(0), scale_view(1), scale_view(2)); return self.Scale(v); }, py::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( "mirror", [](Manifold &self, py::array_t &mirror) { auto v_view = mirror.unchecked<1>(); if (mirror.ndim() != 1 || mirror.shape(0) != 3) throw std::runtime_error("Invalid vector shape, expected (3)"); glm::vec3 v(v_view(0), v_view(1), v_view(2)); return self.Mirror(v); }, py::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( "rotate", [](Manifold &self, py::array_t &v) { auto v_view = v.unchecked<1>(); if (v.ndim() != 1 || v.shape(0) != 3) throw std::runtime_error("Invalid vector shape, expected (3)"); return self.Rotate(v_view(0), v_view(1), v_view(2)); }, py::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.") .def( "rotate", [](Manifold &self, float xDegrees = 0.0f, float yDegrees = 0.0f, float zDegrees = 0.0f) { return self.Rotate(xDegrees, yDegrees, zDegrees); }, py::arg("x_degrees") = 0.0f, py::arg("y_degrees") = 0.0f, py::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); }); }, py::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 warpFunc: A function that modifies a given vertex position.") .def( "refine", [](Manifold &self, int n) { return self.Refine(n); }, py::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.value().ndim() != 1 || normalIdx.value().shape(0) != 3) throw std::runtime_error("Invalid vector shape, expected (3)"); auto normalIdx_view = normalIdx.value().unchecked<1>(); v = glm::ivec3(normalIdx_view(0), normalIdx_view(1), normalIdx_view(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.", py::arg("normalIdx") = py::none()) .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.") .def( "get_volume", [](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; }, "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 &self) { return self.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::Split, "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.") .def( "split_by_plane", [](Manifold &self, Float3 normal, float originOffset) { return self.SplitByPlane( {std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)}, originOffset); }, py::arg("normal"), py::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); }, py::arg("normal"), py::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 " "the direction of the normal vector.") .def_property_readonly( "bounding_box", [](Manifold &self) { auto b = self.BoundingBox(); py::tuple box = py::make_tuple(b.min[0], b.min[1], b.min[2], b.max[0], b.max[1], b.max[2]); return box; }, "Gets the manifold bounding box as a tuple " "(xmin, ymin, zmin, xmax, ymax, zmax).") .def_static( "smooth", [](const Mesh &mesh) { return Manifold::Smooth(mesh); }, "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 sharpenedEdges: If desired, you can supply a vector of " "sharpened halfedges, which should in general be a small subset of " "all halfedges. Order of entries doesn't matter, as each one " "specifies the desired smoothness (between zero and one, with one " "the default for all unspecified halfedges) and the halfedge index " "(3 * triangle index + [0,1,2] where 0 is the edge between triVert 0 " "and 1, etc)." "\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.") .def_static( "from_mesh", [](const MeshGL &mesh) { return Manifold(mesh); }, py::arg("mesh")) .def_static( "tetrahedron", []() { return 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); }, py::arg("size") = std::make_tuple(1.0f, 1.0f, 1.0f), py::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", [](py::array_t &size, bool center = false) { auto size_view = size.unchecked<1>(); if (size.ndim() != 1 || size.shape(0) != 3) throw std::runtime_error("Invalid vector shape, expected (3)"); return Manifold::Cube( glm::vec3(size_view(0), size_view(1), size_view(2)), center); }, py::arg("size"), py::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); }, py::arg("x"), py::arg("y"), py::arg("z"), py::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); }, py::arg("height"), py::arg("radius_low"), py::arg("radius_high") = -1.0f, py::arg("circular_segments") = 0, py::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 " "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); }, py::arg("radius"), py::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. " "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.") .def_static("reserve_ids", Manifold::ReserveIDs, py::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"); py::class_(m, "Mesh") .def( // note that reshape requires mutable array_t, but this will not // affect the original array passed into the function py::init( [](py::array_t &vertProp, py::array_t &triVerts, const std::optional> &mergeFromVert, const std::optional> &mergeToVert, const std::optional> &runIndex, const std::optional> &runOriginalID, std::optional> &runTransform, const std::optional> &faceID, const std::optional> &halfedgeTangent, float precision) { MeshGL out; out.numProp = vertProp.shape(1); vertProp = vertProp.reshape({-1}); out.vertProperties = toVector(vertProp); if (triVerts.ndim() != 2 || triVerts.shape(1) != 3) throw std::runtime_error( "Invalid tri_verts shape, expected (-1, 3)"); triVerts = triVerts.reshape({-1}); out.triVerts = toVector(triVerts); if (mergeFromVert.has_value()) out.mergeFromVert = toVector(mergeFromVert.value()); if (mergeToVert.has_value()) out.mergeToVert = toVector(mergeToVert.value()); if (runIndex.has_value()) out.runIndex = toVector(runIndex.value()); if (runOriginalID.has_value()) out.runOriginalID = toVector(runOriginalID.value()); if (runTransform.has_value()) { auto runTransform1 = runTransform.value(); if (runTransform1.ndim() != 3 || runTransform1.shape(1) != 4 || runTransform1.shape(2) != 3) throw std::runtime_error( "Invalid run_transform shape, expected (-1, 4, 3)"); runTransform1 = runTransform1.reshape({-1}); out.runTransform = toVector(runTransform1); } if (faceID.has_value()) out.faceID = toVector(faceID.value()); if (halfedgeTangent.has_value()) { auto halfedgeTangent1 = halfedgeTangent.value(); if (halfedgeTangent1.ndim() != 3 || halfedgeTangent1.shape(1) != 3 || halfedgeTangent1.shape(2) != 4) throw std::runtime_error( "Invalid halfedge_tangent shape, expected (-1, 3, 4)"); halfedgeTangent1 = halfedgeTangent1.reshape({-1}); out.halfedgeTangent = toVector(halfedgeTangent1); } return out; }), py::arg("vert_properties"), py::arg("tri_verts"), py::arg("merge_from_vert") = py::none(), py::arg("merge_to_vert") = py::none(), py::arg("run_index") = py::none(), py::arg("run_original_id") = py::none(), py::arg("run_transform") = py::none(), py::arg("face_id") = py::none(), py::arg("halfedge_tangent") = py::none(), py::arg("precision") = 0) .def_property_readonly( "vert_properties", [](const MeshGL &self) { return py::array(self.vertProperties.size(), self.vertProperties.data()) .reshape(std::array{-1, (int)self.numProp}); }) .def_property_readonly("tri_verts", [](const MeshGL &self) { return py::array(self.triVerts.size(), self.triVerts.data()) .reshape(std::array{-1, 3}); }) .def_property_readonly("run_transform", [](const MeshGL &self) { return py::array(self.runTransform.size(), self.runTransform.data()) .reshape(std::array{-1, 4, 3}); }) .def_property_readonly("halfedge_tangent", [](const MeshGL &self) { return py::array(self.halfedgeTangent.size(), self.halfedgeTangent.data()) .reshape(std::array{-1, 3, 4}); }) .def_readonly("merge_from_vert", &MeshGL::mergeFromVert) .def_readonly("merge_to_vert", &MeshGL::mergeToVert) .def_readonly("run_index", &MeshGL::runIndex) .def_readonly("run_original_id", &MeshGL::runOriginalID) .def_readonly("face_id", &MeshGL::faceID); py::enum_(m, "FillRule") .value("EvenOdd", CrossSection::FillRule::EvenOdd, "Only odd numbered sub-regions are filled.") .value("NonZero", CrossSection::FillRule::NonZero, "Only non-zero sub-regions are filled.") .value("Positive", CrossSection::FillRule::Positive, "Only sub-regions with winding counts > 0 are filled.") .value("Negative", CrossSection::FillRule::Negative, "Only sub-regions with winding counts < 0 are filled."); py::enum_(m, "JoinType") .value("Square", CrossSection::JoinType::Square, "Squaring is applied uniformly at all joins where the internal " "join angle is less that 90 degrees. The squared edge will be at " "exactly the offset distance from the join vertex.") .value( "Round", CrossSection::JoinType::Square, "Rounding is applied to all joins that have convex external angles, " "and it maintains the exact offset distance from the join vertex.") .value( "Miter", CrossSection::JoinType::Miter, "There's a necessary limit to mitered joins (to avoid narrow angled " "joins producing excessively long and narrow " "[spikes](http://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/" "Classes/ClipperOffset/Properties/MiterLimit.htm)). So where mitered " "joins would exceed a given maximum miter distance (relative to the " "offset distance), these are 'squared' instead."); py::class_( m, "CrossSection", "Two-dimensional cross sections guaranteed to be without " "self-intersections, or overlaps between polygons (from construction " "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(py::init<>()) .def(py::init([](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])}; } } return CrossSection(simplePolygons, fillrule); }), py::arg("polygons"), py::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?") .def( "translate", [](CrossSection self, Float2 v) { return self.Translate({std::get<0>(v), std::get<1>(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, "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 self, Float2 s) { return self.Scale({std::get<0>(s), std::get<1>(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.") .def( "mirror", [](CrossSection self, Float2 ax) { return self.Mirror({std::get<0>(ax), std::get<1>(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 self, py::array_t &mat) { auto mat_view = mat.unchecked<2>(); 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_view(i, j); } } return self.Transform(mat_glm); }, "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); }); }, py::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("simplify", &CrossSection::Simplify, "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, py::arg("delta"), py::arg("join_type"), py::arg("miter_limit") = 2.0, py::arg("arc_tolerance") = 0.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 circularSegments: 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(py::self + py::self, "Boolean union.") .def(py::self - py::self, "Boolean difference.") .def(py::self ^ py::self, "Boolean intersection.") .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 self) { const Polygons &data = self.ToPolygons(); py::list polygon_list; for (int i = 0; i < data.size(); ++i) { py::list polygon; for (int j = 0; j < data[i].size(); ++j) { auto f = data[i][j]; py::tuple vertex = py::make_tuple(f[0], f[1]); polygon.append(vertex); } polygon_list.append(polygon); } return polygon_list; }, "Returns the vertices of the cross-section's polygons as a " "List[List[Tuple[float, 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); }, py::arg("height"), py::arg("n_divisions") = 0, py::arg("twist_degrees") = 0.0f, py::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", [](CrossSection self, int circularSegments = 0) { return Manifold::Revolve(self, circularSegments); }, py::arg("circular_segments") = 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 circularSegments: Number of segments along its diameter. " "Default is calculated by the static Defaults.") .def_static( "square", [](Float2 dims, bool center) { return CrossSection::Square({std::get<0>(dims), std::get<1>(dims)}, center); }, py::arg("dims"), py::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", [](float radius, int circularSegments) { return CrossSection::Circle(radius, circularSegments); }, py::arg("radius"), py::arg("circularSegments") = 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. " "Default is calculated by the static Quality defaults according to " "the radius."); }