Update Python bindings (#445)

* Added more Python bindings (CrossSection) and some clarifying Python examples on how to create polygons etc
* Returned extrude from Python example and renamed accordingly.
* Removed PolygonWrapper class from Python bindings
This commit is contained in:
Johnny Bigert
2023-06-08 10:52:10 -07:00
committed by GitHub
parent 2643c291ed
commit f6431112a8
3 changed files with 101 additions and 87 deletions
+4 -1
View File
@@ -113,4 +113,7 @@ __pycache__
.vscode/c_cpp_properties.json
*.sublime-project
*.sublime-workspace
.cache
.cache
# PyCharm
.idea
+38
View File
@@ -0,0 +1,38 @@
from pymanifold import CrossSection, FillRule
def run():
# create a polygon
polygon_points = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]
polygons_points = [polygon_points]
# create a cross-section
cross_section = CrossSection(polygons_points)
polygons = cross_section.to_polygons()
polygon = polygons[0]
if set(polygon) != set(polygon_points):
raise Exception(f"{polygon=} differs from {polygon_points=}")
# extrude a polygon to create a manifold
extruded_polygon = cross_section.extrude(10.0)
eps = 0.001
observed_volume = extruded_polygon.get_volume()
expected_volume = 10.0
if abs(observed_volume - expected_volume) > eps:
raise Exception(f"{observed_volume=} differs from {expected_volume=}")
observed_surface_area = extruded_polygon.get_surface_area()
expected_surface_area = 42.0
if abs(observed_surface_area - expected_surface_area) > eps:
raise Exception(f"{observed_surface_area=} differs from {expected_surface_area=}")
# get bounding box from manifold
observed_bbox = extruded_polygon.bounding_box
expected_bbox = (0.0, 0.0, 0.0, 1.0, 1.0, 10.0)
if observed_bbox != expected_bbox:
raise Exception(f"{observed_bbox=} differs from {expected_bbox=}")
return extruded_polygon
if __name__ == "__main__":
run()
+59 -86
View File
@@ -27,10 +27,6 @@ using namespace manifold;
typedef std::tuple<float, float> Float2;
typedef std::tuple<float, float, float> Float3;
struct PolygonsWrapper {
std::unique_ptr<Polygons> polygons;
};
PYBIND11_MODULE(pymanifold, m) {
m.doc() =
"Python binding for the manifold library. Please check the C++ "
@@ -95,7 +91,7 @@ PYBIND11_MODULE(pymanifold, m) {
.def(py::self ^ py::self, "Boolean intersection.")
.def(
"transform",
[](Manifold self, py::array_t<float> &mat) {
[](Manifold &self, py::array_t<float> &mat) {
auto mat_view = mat.unchecked<2>();
if (mat_view.shape(0) != 3 || mat_view.shape(1) != 4)
throw std::runtime_error("Invalid matrix shape");
@@ -116,7 +112,7 @@ PYBIND11_MODULE(pymanifold, m) {
":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) {
[](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,
@@ -128,7 +124,7 @@ PYBIND11_MODULE(pymanifold, m) {
":param z: Z axis translation. (default 0.0).")
.def(
"translate",
[](Manifold self, py::array_t<float> &t) {
[](Manifold &self, py::array_t<float> &t) {
auto t_view = t.unchecked<1>();
if (t.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
@@ -140,8 +136,8 @@ PYBIND11_MODULE(pymanifold, m) {
"\n\n"
":param v: The vector to add to every vertex.")
.def("scale",
static_cast<Manifold (*)(Manifold, float)>(
[](Manifold self, float scale) {
static_cast<Manifold (*)(Manifold &, float)>(
[](Manifold &self, float scale) {
return self.Scale(glm::vec3(scale));
}),
py::arg("scale"),
@@ -152,7 +148,7 @@ PYBIND11_MODULE(pymanifold, m) {
"vertices.")
.def(
"scale",
[](Manifold self, py::array_t<float> &scale) {
[](Manifold &self, py::array_t<float> &scale) {
auto scale_view = scale.unchecked<1>();
if (scale_view.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
@@ -166,7 +162,7 @@ PYBIND11_MODULE(pymanifold, m) {
":param v: The vector to multiply every vertex by component.")
.def(
"rotate",
[](Manifold self, py::array_t<float> &v) {
[](Manifold &self, py::array_t<float> &v) {
auto v_view = v.unchecked<1>();
if (v_view.shape(0) != 3)
throw std::runtime_error("Invalid vector shape");
@@ -184,7 +180,7 @@ PYBIND11_MODULE(pymanifold, m) {
":param v: [X, Y, Z] rotation in degrees.")
.def(
"rotate",
[](Manifold self, float xDegrees = 0.0f, float yDegrees = 0.0f,
[](Manifold &self, float xDegrees = 0.0f, float yDegrees = 0.0f,
float zDegrees = 0.0f) {
return self.Rotate(xDegrees, yDegrees, zDegrees);
},
@@ -203,7 +199,7 @@ PYBIND11_MODULE(pymanifold, m) {
":param z: Z rotation in degrees. (default 0.0).")
.def(
"warp",
[](Manifold self, const std::function<Float3(Float3)> &f) {
[](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);
@@ -213,7 +209,7 @@ PYBIND11_MODULE(pymanifold, m) {
},
py::arg("f"))
.def(
"refine", [](Manifold self, int n) { return self.Refine(n); },
"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 "
@@ -254,12 +250,12 @@ PYBIND11_MODULE(pymanifold, m) {
"first.")
.def(
"get_volume",
[](Manifold self) { return self.GetProperties().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; },
[](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,
@@ -275,7 +271,7 @@ PYBIND11_MODULE(pymanifold, m) {
.def("is_empty", &Manifold::IsEmpty,
"Does the Manifold have any triangles?")
.def(
"decompose", [](Manifold self) { return self.Decompose(); },
"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 "
@@ -286,7 +282,7 @@ PYBIND11_MODULE(pymanifold, m) {
"is more efficient than doing them separately.")
.def(
"split_by_plane",
[](Manifold self, Float3 normal, float originOffset) {
[](Manifold &self, Float3 normal, float originOffset) {
return self.SplitByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
@@ -301,7 +297,7 @@ PYBIND11_MODULE(pymanifold, m) {
"the direction of the normal vector.")
.def(
"trim_by_plane",
[](Manifold self, Float3 normal, float originOffset) {
[](Manifold &self, Float3 normal, float originOffset) {
return self.TrimByPlane(
{std::get<0>(normal), std::get<1>(normal), std::get<2>(normal)},
originOffset);
@@ -315,6 +311,16 @@ PYBIND11_MODULE(pymanifold, m) {
"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; "
@@ -436,62 +442,6 @@ PYBIND11_MODULE(pymanifold, m) {
"marking sets of triangles that can be looked up after "
"further operations. Assign to MeshGL.runOriginalID vector");
py::class_<PolygonsWrapper>(m, "Polygons")
.def(py::init([](std::vector<std::vector<Float2>> &polygons) {
std::vector<SimplePolygon> simplePolygons(polygons.size());
for (int i = 0; i < polygons.size(); i++) {
std::vector<glm::vec2> vertices(polygons[i].size());
for (int j = 0; j < polygons[i].size(); j++) {
vertices[j] = {std::get<0>(polygons[i][j]),
std::get<1>(polygons[i][j])};
}
simplePolygons[i] = {vertices};
}
return PolygonsWrapper{std::make_unique<Polygons>(simplePolygons)};
}),
py::arg("polygons"),
"Construct the Polygons object from a list of simple polygon, "
"where each simple polygon is a list of points (pair of floats).")
.def(
"extrude",
[](PolygonsWrapper &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.polygons, 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",
[](PolygonsWrapper &self, int circularSegments = 0) {
return Manifold::Revolve(*self.polygons, 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.");
py::class_<Mesh>(m, "Mesh")
.def(py::init([](py::array_t<float> &vertPos, py::array_t<int> &triVerts,
py::array_t<float> &vertNormal,
@@ -624,12 +574,22 @@ PYBIND11_MODULE(pymanifold, m) {
"[Clipper2](http://www.angusj.com/clipper2/Docs/Overview.htm) library "
"for polygon clipping (boolean) and offsetting operations.")
.def(py::init<>())
.def(py::init(
[](PolygonsWrapper &contours, CrossSection::FillRule fillrule) {
return CrossSection(*contours.polygons, fillrule);
}),
py::arg("contours"),
py::arg("fillrule") = CrossSection::FillRule::Positive)
.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++) {
std::vector<glm::vec2> vertices(polygons[i].size());
for (int j = 0; j < polygons[i].size(); j++) {
vertices[j] = {std::get<0>(polygons[i][j]),
std::get<1>(polygons[i][j])};
}
simplePolygons[i] = {vertices};
}
return CrossSection(simplePolygons, fillrule);
}),
py::arg("polygons"),
py::arg("fillrule") = CrossSection::FillRule::Positive,
"Construct a 2d cross-section from a set of contours.")
.def("area", &CrossSection::Area)
.def("num_vert", &CrossSection::NumVert)
.def("num_contour", &CrossSection::NumContour)
@@ -678,11 +638,24 @@ PYBIND11_MODULE(pymanifold, m) {
.def(py::self - py::self, "Boolean difference.")
.def(py::self ^ py::self, "Boolean intersection.")
.def("decompose", &CrossSection::Decompose)
.def("to_polygons",
[](CrossSection self) {
return PolygonsWrapper{
std::make_unique<Polygons>(self.ToPolygons())};
})
.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,