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:
+4
-1
@@ -113,4 +113,7 @@ __pycache__
|
||||
.vscode/c_cpp_properties.json
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.cache
|
||||
.cache
|
||||
|
||||
# PyCharm
|
||||
.idea
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user