From c17d9732be2215b9e9353d3c6897a9ad958e7467 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 11 Jul 2026 18:54:07 +0800 Subject: [PATCH] refactor(plugin): bind raw TriangleMesh instead of HostTriangleMesh wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/slic3r/plugin/host/PluginHostMesh.cpp | 80 ++++++++++------------ src/slic3r/plugin/host/PluginHostMesh.hpp | 14 +--- src/slic3r/plugin/host/PluginHostModel.cpp | 7 +- 3 files changed, 46 insertions(+), 55 deletions(-) diff --git a/src/slic3r/plugin/host/PluginHostMesh.cpp b/src/slic3r/plugin/host/PluginHostMesh.cpp index 3661f964ed..28da5c3afc 100644 --- a/src/slic3r/plugin/host/PluginHostMesh.cpp +++ b/src/slic3r/plugin/host/PluginHostMesh.cpp @@ -20,56 +20,52 @@ static_assert(sizeof(stl_vertex) == 3 * sizeof(float), 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"); -// 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 -py::array make_readonly_rows3(const std::shared_ptr& mesh, - const T* data, py::ssize_t rows) -{ - if (rows == 0 || data == nullptr) - return py::array_t(std::vector{ 0, 3 }); - auto* owner = new std::shared_ptr(mesh); - py::capsule base(owner, [](void* p) { - delete reinterpret_cast*>(p); - }); - return make_readonly_rows(base, data, rows); -} - } // namespace void host_bindings::register_mesh(py::module_& host) { - py::class_(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_>(host, "TriangleMesh", "Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.") - .def("vertex_count", [](const HostTriangleMesh& mesh) { return mesh.its().vertices.size(); }) - .def("triangle_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); }) - .def("facets_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); }) - .def("is_empty", [](const HostTriangleMesh& mesh) { return mesh.its().indices.empty(); }) + .def("vertex_count", [](const TriangleMesh& mesh) { return mesh.its.vertices.size(); }) + .def("triangle_count", [](const TriangleMesh& mesh) { return mesh.its.indices.size(); }) + .def("facets_count", [](const TriangleMesh& mesh) { return mesh.its.indices.size(); }) + .def("is_empty", [](const TriangleMesh& mesh) { return mesh.its.indices.empty(); }) // 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(); return with_numpy([&] { - const indexed_triangle_set& its = mesh.its(); - return make_readonly_rows3( - mesh.mesh, - its.vertices.empty() ? nullptr : its.vertices.front().data(), - static_cast(its.vertices.size())); + const std::vector& vertices = mesh.its.vertices; + return py::object(make_readonly_rows( + self, vertices.empty() ? nullptr : vertices.front().data(), + static_cast(vertices.size()))); }); }, "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. - .def("triangles", [](const HostTriangleMesh& mesh) { + .def("triangles", [](py::object self) { + const TriangleMesh& mesh = self.cast(); return with_numpy([&] { - const indexed_triangle_set& its = mesh.its(); - return make_readonly_rows3( - mesh.mesh, - its.indices.empty() ? nullptr : its.indices.front().data(), - static_cast(its.indices.size())); + const std::vector& indices = mesh.its.indices; + return py::object(make_readonly_rows( + self, indices.empty() ? nullptr : indices.front().data(), + static_cast(indices.size()))); }); }, "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. - .def("face_normals", [](const HostTriangleMesh& mesh) { + .def("face_normals", [](const TriangleMesh& mesh) { return with_numpy([&] { - std::vector normals = its_face_normals(mesh.its()); + std::vector normals = its_face_normals(mesh.its); py::array_t array({ static_cast(normals.size()), py::ssize_t(3) }); if (!normals.empty()) { 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.") // numpy-free element access, bounds-checked. - .def("vertex", [](const HostTriangleMesh& mesh, size_t index) { - const std::vector& vertices = mesh.its().vertices; + .def("vertex", [](const TriangleMesh& mesh, size_t index) { + const std::vector& vertices = mesh.its.vertices; if (index >= vertices.size()) throw py::index_error("vertex index out of range"); const stl_vertex& vertex = vertices[index]; return py::make_tuple(vertex.x(), vertex.y(), vertex.z()); }) - .def("triangle", [](const HostTriangleMesh& mesh, size_t index) { - const std::vector& indices = mesh.its().indices; + .def("triangle", [](const TriangleMesh& mesh, size_t index) { + const std::vector& indices = mesh.its.indices; if (index >= indices.size()) throw py::index_error("triangle index out of range"); const stl_triangle_vertex_indices& triangle = indices[index]; return py::make_tuple(triangle[0], triangle[1], triangle[2]); }) - .def("volume", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().volume; }) - .def("bounding_box", [](const HostTriangleMesh& mesh) { return bbox_from_stats(mesh.mesh->stats()); }) - .def("is_manifold", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().manifold(); }); + .def("volume", [](const TriangleMesh& mesh) { return mesh.stats().volume; }) + .def("bounding_box", [](const TriangleMesh& mesh) { return bbox_from_stats(mesh.stats()); }) + .def("is_manifold", [](const TriangleMesh& mesh) { return mesh.stats().manifold(); }); } } // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostMesh.hpp b/src/slic3r/plugin/host/PluginHostMesh.hpp index 9aca05c59d..05e8130973 100644 --- a/src/slic3r/plugin/host/PluginHostMesh.hpp +++ b/src/slic3r/plugin/host/PluginHostMesh.hpp @@ -3,21 +3,11 @@ #include #include -#include - 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 mesh; - const indexed_triangle_set& its() const { return mesh->its; } -}; - // 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) { if (stats.number_of_facets == 0) diff --git a/src/slic3r/plugin/host/PluginHostModel.cpp b/src/slic3r/plugin/host/PluginHostModel.cpp index 51fdb180e7..3b571da230 100644 --- a/src/slic3r/plugin/host/PluginHostModel.cpp +++ b/src/slic3r/plugin/host/PluginHostModel.cpp @@ -6,6 +6,7 @@ #include +#include #include 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(); }) // Full mesh geometry (vertices/triangles) as an immutable snapshot. .def("mesh", [](const ModelVolume& volume) { - return HostTriangleMesh{ volume.get_mesh_shared_ptr() }; + // The volume stores its mesh as shared_ptr (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(volume.get_mesh_shared_ptr()); }, "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("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)