Files
manifold/bindings/python/manifold3d.cpp
2023-07-23 14:34:42 +08:00

877 lines
43 KiB
C++

// 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<float, float> Float2;
typedef std::tuple<float, float, float> Float3;
constexpr auto pyArrayFlags = py::array::c_style | py::array::forcecast;
PYBIND11_MAKE_OPAQUE(std::vector<int>);
PYBIND11_MAKE_OPAQUE(std::vector<uint32_t>);
PYBIND11_MAKE_OPAQUE(std::vector<float>);
template <typename T>
std::vector<T> toVector(const py::array_t<T> &arr) {
return std::vector<T>(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_<Manifold>(m, "Manifold")
.def(py::init<>())
.def(py::init([](std::vector<Manifold> &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<float> &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<float> &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<float> &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<float> &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<float> &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<Float3(Float3)> &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<py::array_t<uint32_t>> &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 &epsilon; defining "
"[&epsilon;-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<float> &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_<MeshGL>(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<float, pyArrayFlags> &vertProp,
py::array_t<int, pyArrayFlags> &triVerts,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&mergeFromVert,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&mergeToVert,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&runIndex,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&runOriginalID,
std::optional<py::array_t<float, pyArrayFlags>> &runTransform,
const std::optional<py::array_t<uint32_t, pyArrayFlags>>
&faceID,
const std::optional<py::array_t<float, pyArrayFlags>>
&halfedgeTangent,
float precision) {
MeshGL out;
out.numProp = vertProp.shape(1);
vertProp = vertProp.reshape({-1});
out.vertProperties = toVector<float>(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<uint32_t>(triVerts);
if (mergeFromVert.has_value())
out.mergeFromVert = toVector<uint32_t>(mergeFromVert.value());
if (mergeToVert.has_value())
out.mergeToVert = toVector<uint32_t>(mergeToVert.value());
if (runIndex.has_value())
out.runIndex = toVector<uint32_t>(runIndex.value());
if (runOriginalID.has_value())
out.runOriginalID = toVector<uint32_t>(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<float>(runTransform1);
}
if (faceID.has_value())
out.faceID = toVector<uint32_t>(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<float>(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<int, 2>{-1, (int)self.numProp});
})
.def_property_readonly("tri_verts",
[](const MeshGL &self) {
return py::array(self.triVerts.size(),
self.triVerts.data())
.reshape(std::array<int, 2>{-1, 3});
})
.def_property_readonly("run_transform",
[](const MeshGL &self) {
return py::array(self.runTransform.size(),
self.runTransform.data())
.reshape(std::array<int, 3>{-1, 4, 3});
})
.def_property_readonly("halfedge_tangent",
[](const MeshGL &self) {
return py::array(self.halfedgeTangent.size(),
self.halfedgeTangent.data())
.reshape(std::array<int, 3>{-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_<CrossSection::FillRule>(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_<CrossSection::JoinType>(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_<CrossSection>(
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<std::vector<Float2>> &polygons,
CrossSection::FillRule fillrule) {
std::vector<SimplePolygon> simplePolygons(polygons.size());
for (int i = 0; i < polygons.size(); i++) {
simplePolygons[i] = std::vector<glm::vec2>(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<float> &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<Float2(Float2)> &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, <B>when the join type is Miter</B> "
"(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 "
"<B>JoinType::Round</B> 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.");
}