mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-14 12:37:46 +00:00
refactor(plugin): bind raw TriangleMesh instead of HostTriangleMesh wrapper
Bind libslic3r's TriangleMesh directly with a shared_ptr holder rather than wrapping it in a HostTriangleMesh snapshot struct. ModelVolume.mesh() hands out the volume's own shared_ptr (via const_pointer_cast, which only serves the holder type), so the Python object pins the snapshot exactly as the wrapper did, and the zero-copy views now use the Python object as their array base — deleting the capsule machinery. The wrapper's type-level constness becomes a documented rule instead: handed-out meshes are copy-on-write snapshots shared across threads, so the binding exposes only const methods; a future mutable-mesh API must operate on plugin-owned copies handed back via ModelVolume::set_mesh. No Python-visible change (orca.host.TriangleMesh, same methods/docstrings), and plugins now hold the real class a future set_mesh() will accept. Verified with slic3rutils and fff_print suites.
This commit is contained in:
@@ -20,56 +20,52 @@ static_assert(sizeof(stl_vertex) == 3 * sizeof(float),
|
|||||||
static_assert(sizeof(stl_triangle_vertex_indices) == 3 * sizeof(std::int32_t),
|
static_assert(sizeof(stl_triangle_vertex_indices) == 3 * sizeof(std::int32_t),
|
||||||
"triangle index must be a packed int32[3] for zero-copy numpy export");
|
"triangle index must be a packed int32[3] for zero-copy numpy export");
|
||||||
|
|
||||||
// Read-only, zero-copy (rows, 3) numpy view over a packed T[rows][3] buffer.
|
|
||||||
// The array's base is a capsule owning a strong ref to `mesh`, so the view
|
|
||||||
// stays valid even if the volume's mesh is later replaced on the main thread.
|
|
||||||
template<typename T>
|
|
||||||
py::array make_readonly_rows3(const std::shared_ptr<const TriangleMesh>& mesh,
|
|
||||||
const T* data, py::ssize_t rows)
|
|
||||||
{
|
|
||||||
if (rows == 0 || data == nullptr)
|
|
||||||
return py::array_t<T>(std::vector<py::ssize_t>{ 0, 3 });
|
|
||||||
auto* owner = new std::shared_ptr<const TriangleMesh>(mesh);
|
|
||||||
py::capsule base(owner, [](void* p) {
|
|
||||||
delete reinterpret_cast<std::shared_ptr<const TriangleMesh>*>(p);
|
|
||||||
});
|
|
||||||
return make_readonly_rows<T, 3>(base, data, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void host_bindings::register_mesh(py::module_& host)
|
void host_bindings::register_mesh(py::module_& host)
|
||||||
{
|
{
|
||||||
py::class_<HostTriangleMesh>(host, "TriangleMesh",
|
// The raw libslic3r TriangleMesh, bound with a shared_ptr holder:
|
||||||
|
// ModelVolume.mesh() hands out the volume's own shared_ptr, so the Python
|
||||||
|
// object pins this snapshot even if the volume's mesh is later replaced on
|
||||||
|
// the main thread. The zero-copy views below use the Python object as their
|
||||||
|
// array base, which keeps the buffer alive for each array's lifetime.
|
||||||
|
//
|
||||||
|
// IMMUTABLE BY RULE: handed-out meshes are copy-on-write snapshots SHARED
|
||||||
|
// across threads (a Print's model snapshot and the live GUI model share the
|
||||||
|
// same instance), reached through a const_pointer_cast that only serves the
|
||||||
|
// holder type. Bind only const/read-only methods here. A future mutable-mesh
|
||||||
|
// API must operate on plugin-owned copies handed back via
|
||||||
|
// ModelVolume::set_mesh — never mutate a mesh obtained from the graph.
|
||||||
|
py::class_<TriangleMesh, std::shared_ptr<TriangleMesh>>(host, "TriangleMesh",
|
||||||
"Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.")
|
"Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.")
|
||||||
.def("vertex_count", [](const HostTriangleMesh& mesh) { return mesh.its().vertices.size(); })
|
.def("vertex_count", [](const TriangleMesh& mesh) { return mesh.its.vertices.size(); })
|
||||||
.def("triangle_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
|
.def("triangle_count", [](const TriangleMesh& mesh) { return mesh.its.indices.size(); })
|
||||||
.def("facets_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
|
.def("facets_count", [](const TriangleMesh& mesh) { return mesh.its.indices.size(); })
|
||||||
.def("is_empty", [](const HostTriangleMesh& mesh) { return mesh.its().indices.empty(); })
|
.def("is_empty", [](const TriangleMesh& mesh) { return mesh.its.indices.empty(); })
|
||||||
// Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy.
|
// Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy.
|
||||||
.def("vertices", [](const HostTriangleMesh& mesh) {
|
.def("vertices", [](py::object self) {
|
||||||
|
const TriangleMesh& mesh = self.cast<const TriangleMesh&>();
|
||||||
return with_numpy([&] {
|
return with_numpy([&] {
|
||||||
const indexed_triangle_set& its = mesh.its();
|
const std::vector<stl_vertex>& vertices = mesh.its.vertices;
|
||||||
return make_readonly_rows3<float>(
|
return py::object(make_readonly_rows<float, 3>(
|
||||||
mesh.mesh,
|
self, vertices.empty() ? nullptr : vertices.front().data(),
|
||||||
its.vertices.empty() ? nullptr : its.vertices.front().data(),
|
static_cast<py::ssize_t>(vertices.size())));
|
||||||
static_cast<py::ssize_t>(its.vertices.size()));
|
|
||||||
});
|
});
|
||||||
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
|
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
|
||||||
// Read-only, zero-copy (M, 3) int32 view of triangle vertex indices. Requires numpy.
|
// Read-only, zero-copy (M, 3) int32 view of triangle vertex indices. Requires numpy.
|
||||||
.def("triangles", [](const HostTriangleMesh& mesh) {
|
.def("triangles", [](py::object self) {
|
||||||
|
const TriangleMesh& mesh = self.cast<const TriangleMesh&>();
|
||||||
return with_numpy([&] {
|
return with_numpy([&] {
|
||||||
const indexed_triangle_set& its = mesh.its();
|
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its.indices;
|
||||||
return make_readonly_rows3<std::int32_t>(
|
return py::object(make_readonly_rows<std::int32_t, 3>(
|
||||||
mesh.mesh,
|
self, indices.empty() ? nullptr : indices.front().data(),
|
||||||
its.indices.empty() ? nullptr : its.indices.front().data(),
|
static_cast<py::ssize_t>(indices.size())));
|
||||||
static_cast<py::ssize_t>(its.indices.size()));
|
|
||||||
});
|
});
|
||||||
}, "Read-only zero-copy (M, 3) int32 ndarray of triangle vertex indices. Requires numpy.")
|
}, "Read-only zero-copy (M, 3) int32 ndarray of triangle vertex indices. Requires numpy.")
|
||||||
// One normalized normal per triangle as an (M, 3) float32 copy. Requires numpy.
|
// One normalized normal per triangle as an (M, 3) float32 copy. Requires numpy.
|
||||||
.def("face_normals", [](const HostTriangleMesh& mesh) {
|
.def("face_normals", [](const TriangleMesh& mesh) {
|
||||||
return with_numpy([&] {
|
return with_numpy([&] {
|
||||||
std::vector<Vec3f> normals = its_face_normals(mesh.its());
|
std::vector<Vec3f> normals = its_face_normals(mesh.its);
|
||||||
py::array_t<float> array({ static_cast<py::ssize_t>(normals.size()), py::ssize_t(3) });
|
py::array_t<float> array({ static_cast<py::ssize_t>(normals.size()), py::ssize_t(3) });
|
||||||
if (!normals.empty()) {
|
if (!normals.empty()) {
|
||||||
auto view = array.mutable_unchecked<2>();
|
auto view = array.mutable_unchecked<2>();
|
||||||
@@ -83,23 +79,23 @@ void host_bindings::register_mesh(py::module_& host)
|
|||||||
});
|
});
|
||||||
}, "Per-triangle normalized normals as an (M, 3) float32 ndarray (copy). Requires numpy.")
|
}, "Per-triangle normalized normals as an (M, 3) float32 ndarray (copy). Requires numpy.")
|
||||||
// numpy-free element access, bounds-checked.
|
// numpy-free element access, bounds-checked.
|
||||||
.def("vertex", [](const HostTriangleMesh& mesh, size_t index) {
|
.def("vertex", [](const TriangleMesh& mesh, size_t index) {
|
||||||
const std::vector<stl_vertex>& vertices = mesh.its().vertices;
|
const std::vector<stl_vertex>& vertices = mesh.its.vertices;
|
||||||
if (index >= vertices.size())
|
if (index >= vertices.size())
|
||||||
throw py::index_error("vertex index out of range");
|
throw py::index_error("vertex index out of range");
|
||||||
const stl_vertex& vertex = vertices[index];
|
const stl_vertex& vertex = vertices[index];
|
||||||
return py::make_tuple(vertex.x(), vertex.y(), vertex.z());
|
return py::make_tuple(vertex.x(), vertex.y(), vertex.z());
|
||||||
})
|
})
|
||||||
.def("triangle", [](const HostTriangleMesh& mesh, size_t index) {
|
.def("triangle", [](const TriangleMesh& mesh, size_t index) {
|
||||||
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its().indices;
|
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its.indices;
|
||||||
if (index >= indices.size())
|
if (index >= indices.size())
|
||||||
throw py::index_error("triangle index out of range");
|
throw py::index_error("triangle index out of range");
|
||||||
const stl_triangle_vertex_indices& triangle = indices[index];
|
const stl_triangle_vertex_indices& triangle = indices[index];
|
||||||
return py::make_tuple(triangle[0], triangle[1], triangle[2]);
|
return py::make_tuple(triangle[0], triangle[1], triangle[2]);
|
||||||
})
|
})
|
||||||
.def("volume", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().volume; })
|
.def("volume", [](const TriangleMesh& mesh) { return mesh.stats().volume; })
|
||||||
.def("bounding_box", [](const HostTriangleMesh& mesh) { return bbox_from_stats(mesh.mesh->stats()); })
|
.def("bounding_box", [](const TriangleMesh& mesh) { return bbox_from_stats(mesh.stats()); })
|
||||||
.def("is_manifold", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().manifold(); });
|
.def("is_manifold", [](const TriangleMesh& mesh) { return mesh.stats().manifold(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -3,21 +3,11 @@
|
|||||||
#include <libslic3r/BoundingBox.hpp>
|
#include <libslic3r/BoundingBox.hpp>
|
||||||
#include <libslic3r/TriangleMesh.hpp>
|
#include <libslic3r/TriangleMesh.hpp>
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
// Immutable snapshot of a ModelVolume's mesh. Holding a strong reference to the
|
|
||||||
// const mesh keeps any zero-copy numpy views valid even if the volume's mesh is
|
|
||||||
// later replaced on the main thread. Bound as `orca.host.TriangleMesh` in
|
|
||||||
// PluginHostMesh.cpp; constructed by ModelVolume.mesh() in PluginHostModel.cpp.
|
|
||||||
struct HostTriangleMesh
|
|
||||||
{
|
|
||||||
std::shared_ptr<const TriangleMesh> mesh;
|
|
||||||
const indexed_triangle_set& its() const { return mesh->its; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
|
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
|
||||||
|
// Shared by the TriangleMesh binding (PluginHostMesh.cpp) and the mesh-derived
|
||||||
|
// ModelVolume accessors (PluginHostModel.cpp).
|
||||||
inline BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
|
inline BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
|
||||||
{
|
{
|
||||||
if (stats.number_of_facets == 0)
|
if (stats.number_of_facets == 0)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <pybind11/stl.h>
|
#include <pybind11/stl.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace py = pybind11;
|
namespace py = pybind11;
|
||||||
@@ -52,7 +53,11 @@ void host_bindings::register_model(py::module_& host)
|
|||||||
.def("is_manifold", [](const ModelVolume& volume) { return volume.mesh().stats().manifold(); })
|
.def("is_manifold", [](const ModelVolume& volume) { return volume.mesh().stats().manifold(); })
|
||||||
// Full mesh geometry (vertices/triangles) as an immutable snapshot.
|
// Full mesh geometry (vertices/triangles) as an immutable snapshot.
|
||||||
.def("mesh", [](const ModelVolume& volume) {
|
.def("mesh", [](const ModelVolume& volume) {
|
||||||
return HostTriangleMesh{ volume.get_mesh_shared_ptr() };
|
// The volume stores its mesh as shared_ptr<const TriangleMesh> (a
|
||||||
|
// copy-on-write snapshot); the const_pointer_cast only serves the
|
||||||
|
// binding's shared_ptr holder — the TriangleMesh binding exposes
|
||||||
|
// read-only methods (see the immutability rule in PluginHostMesh.cpp).
|
||||||
|
return std::const_pointer_cast<TriangleMesh>(volume.get_mesh_shared_ptr());
|
||||||
}, "Return the volume's TriangleMesh (local coordinates) for vertex/triangle access.")
|
}, "Return the volume's TriangleMesh (local coordinates) for vertex/triangle access.")
|
||||||
.def("mesh_errors_count", [](const ModelVolume& volume) { return volume.get_repaired_errors_count(); })
|
.def("mesh_errors_count", [](const ModelVolume& volume) { return volume.get_repaired_errors_count(); })
|
||||||
.def("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)
|
.def("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)
|
||||||
|
|||||||
Reference in New Issue
Block a user