From 126e4d5445976e2f138d7648f12ca7f93a41ea81 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 11 Jul 2026 16:18:59 +0800 Subject: [PATCH] refactor(plugin): split orca.host bindings into host/ by domain PluginHostApi.cpp had grown into one TU holding the module entry point plus three unrelated domains (presets, model/mesh graph, app access), and PluginHostSlicing.cpp mixed ownable geometry value types with the non-owning live print graph. Reorganize the orca.host surface into plugin/host/ with one registrar per domain: - PluginHost.hpp/.cpp entry point (replaces PluginHostApi) - PluginHostBindings.hpp internal per-domain registrar declarations - PluginHostGeometry.cpp BoundingBox, Point, Polygon, ExPolygon + ndarray parsing - PluginHostMesh.hpp/.cpp TriangleMesh snapshot (own TU ahead of planned mesh construct/mutate APIs) - PluginHostPresets.cpp Preset, PresetCollection, PresetBundle - PluginHostModel.cpp scene graph: Model, ModelObject, ModelInstance, ModelVolume - PluginHostApp.cpp Plater + plater()/model()/preset_bundle() accessors - PluginHostSlicing.cpp live print graph only, now with a single lifetime story - PluginHostUi.hpp/.cpp moved unchanged PluginBindingUtils.hpp stays at plugin/ root: it is shared with pluginTypes/ and tests, not host/-specific. No Python-visible change: same submodules, class names and docstrings. Verified with slic3rutils and fff_print suites. --- src/slic3r/CMakeLists.txt | 18 +- src/slic3r/GUI/GUI_App.cpp | 2 +- src/slic3r/plugin/PluginHostApi.cpp | 487 ------------------ src/slic3r/plugin/PluginHostApi.hpp | 13 - src/slic3r/plugin/PluginHostSlicing.hpp | 16 - src/slic3r/plugin/PythonPluginBridge.cpp | 4 +- src/slic3r/plugin/host/PluginHost.cpp | 26 + src/slic3r/plugin/host/PluginHost.hpp | 17 + src/slic3r/plugin/host/PluginHostApp.cpp | 60 +++ src/slic3r/plugin/host/PluginHostBindings.hpp | 16 + src/slic3r/plugin/host/PluginHostGeometry.cpp | 216 ++++++++ src/slic3r/plugin/host/PluginHostMesh.cpp | 105 ++++ src/slic3r/plugin/host/PluginHostMesh.hpp | 28 + src/slic3r/plugin/host/PluginHostModel.cpp | 203 ++++++++ src/slic3r/plugin/host/PluginHostPresets.cpp | 138 +++++ .../plugin/{ => host}/PluginHostSlicing.cpp | 190 +------ src/slic3r/plugin/{ => host}/PluginHostUi.cpp | 4 +- src/slic3r/plugin/{ => host}/PluginHostUi.hpp | 0 .../SlicingPipelinePluginCapability.hpp | 2 +- tests/slic3rutils/test_plugin_host_api.cpp | 10 +- 20 files changed, 838 insertions(+), 717 deletions(-) delete mode 100644 src/slic3r/plugin/PluginHostApi.cpp delete mode 100644 src/slic3r/plugin/PluginHostApi.hpp delete mode 100644 src/slic3r/plugin/PluginHostSlicing.hpp create mode 100644 src/slic3r/plugin/host/PluginHost.cpp create mode 100644 src/slic3r/plugin/host/PluginHost.hpp create mode 100644 src/slic3r/plugin/host/PluginHostApp.cpp create mode 100644 src/slic3r/plugin/host/PluginHostBindings.hpp create mode 100644 src/slic3r/plugin/host/PluginHostGeometry.cpp create mode 100644 src/slic3r/plugin/host/PluginHostMesh.cpp create mode 100644 src/slic3r/plugin/host/PluginHostMesh.hpp create mode 100644 src/slic3r/plugin/host/PluginHostModel.cpp create mode 100644 src/slic3r/plugin/host/PluginHostPresets.cpp rename src/slic3r/plugin/{ => host}/PluginHostSlicing.cpp (62%) rename src/slic3r/plugin/{ => host}/PluginHostUi.cpp (99%) rename src/slic3r/plugin/{ => host}/PluginHostUi.hpp (100%) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index f621e63d67..619723de0b 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -597,12 +597,18 @@ set(SLIC3R_GUI_SOURCES plugin/PythonPluginInterface.hpp plugin/PyPluginPackage.hpp plugin/PluginBindingUtils.hpp - plugin/PluginHostApi.cpp - plugin/PluginHostApi.hpp - plugin/PluginHostUi.cpp - plugin/PluginHostUi.hpp - plugin/PluginHostSlicing.cpp - plugin/PluginHostSlicing.hpp + plugin/host/PluginHost.cpp + plugin/host/PluginHost.hpp + plugin/host/PluginHostBindings.hpp + plugin/host/PluginHostApp.cpp + plugin/host/PluginHostGeometry.cpp + plugin/host/PluginHostMesh.cpp + plugin/host/PluginHostMesh.hpp + plugin/host/PluginHostModel.cpp + plugin/host/PluginHostPresets.cpp + plugin/host/PluginHostSlicing.cpp + plugin/host/PluginHostUi.cpp + plugin/host/PluginHostUi.hpp plugin/CloudPluginService.cpp plugin/CloudPluginService.hpp plugin/PluginFsUtils.cpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 8513af0f86..b593563ad4 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -79,7 +79,7 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/Color.hpp" #include "slic3r/plugin/PluginManager.hpp" -#include "slic3r/plugin/PluginHostUi.hpp" +#include "slic3r/plugin/host/PluginHostUi.hpp" #include "slic3r/plugin/PythonInterpreter.hpp" #include "slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" diff --git a/src/slic3r/plugin/PluginHostApi.cpp b/src/slic3r/plugin/PluginHostApi.cpp deleted file mode 100644 index 4a217f67f2..0000000000 --- a/src/slic3r/plugin/PluginHostApi.cpp +++ /dev/null @@ -1,487 +0,0 @@ -#include "PluginHostApi.hpp" -#include "PluginHostUi.hpp" -#include "PluginHostSlicing.hpp" -#include "PluginBindingUtils.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include - -namespace py = pybind11; - -namespace Slic3r { -namespace { - -GUI::Plater* current_plater() -{ - if (wxTheApp == nullptr) - throw std::runtime_error("OrcaSlicer application is not initialized"); - - GUI::Plater* plater = GUI::wxGetApp().plater(); - if (plater == nullptr) - throw std::runtime_error("Plater is not available"); - - return plater; -} - -PresetBundle* current_preset_bundle() -{ - if (wxTheApp == nullptr) - throw std::runtime_error("OrcaSlicer application is not initialized"); - - PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; - if (preset_bundle == nullptr) - throw std::runtime_error("Preset bundle is not available"); - - return preset_bundle; -} - -// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max. -BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats) -{ - if (stats.number_of_facets == 0) - return BoundingBoxf3(); - return BoundingBoxf3(stats.min.cast(), stats.max.cast()); -} - -// --- Mesh geometry helpers ------------------------------------------------- - -// Zero-copy export of its.vertices / its.indices relies on these Eigen -// row-vectors being tightly packed (no padding between the 3 components). -static_assert(sizeof(stl_vertex) == 3 * sizeof(float), - "stl_vertex must be a packed float[3] for zero-copy numpy export"); -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"); - -// 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. -struct HostTriangleMesh -{ - std::shared_ptr mesh; - const indexed_triangle_set& its() const { return mesh->its; } -}; - -// 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); -} - -py::list current_filament_presets(PresetBundle& bundle) -{ - py::list presets; - for (const std::string& preset_name : bundle.filament_presets) { - Preset* preset = bundle.filaments.find_preset(preset_name); - if (preset == nullptr) - presets.append(py::none()); - else - presets.append(py::cast(preset, py::return_value_policy::reference)); - } - return presets; -} - -PresetCollection& printer_presets(PresetBundle& bundle) -{ - return static_cast(bundle.printers); -} - -} // namespace - -void PluginHostApi::RegisterBindings(pybind11::module_& module) -{ - auto host = module.def_submodule("host", "Host application API"); - - py::enum_(host, "PresetType") - .value("Invalid", Preset::TYPE_INVALID) - .value("Print", Preset::TYPE_PRINT) - .value("SlaPrint", Preset::TYPE_SLA_PRINT) - .value("Filament", Preset::TYPE_FILAMENT) - .value("SlaMaterial", Preset::TYPE_SLA_MATERIAL) - .value("Printer", Preset::TYPE_PRINTER) - .value("PhysicalPrinter", Preset::TYPE_PHYSICAL_PRINTER) - .value("Plate", Preset::TYPE_PLATE) - .value("Model", Preset::TYPE_MODEL); - - py::class_>(host, "Preset") - .def_readonly("type", &Preset::type) - .def_readonly("name", &Preset::name) - .def_readonly("alias", &Preset::alias) - .def_readonly("file", &Preset::file) - .def_readonly("is_default", &Preset::is_default) - .def_readonly("is_external", &Preset::is_external) - .def_readonly("is_system", &Preset::is_system) - .def_readonly("is_visible", &Preset::is_visible) - .def_readonly("is_dirty", &Preset::is_dirty) - .def_readonly("is_compatible", &Preset::is_compatible) - .def_readonly("is_project_embedded", &Preset::is_project_embedded) - .def_readonly("bundle_id", &Preset::bundle_id) - .def("is_user", &Preset::is_user) - .def("is_from_bundle", &Preset::is_from_bundle) - .def("label", &Preset::label, py::arg("no_alias") = false) - .def("config_keys", [](const Preset& preset) { return preset.config.keys(); }) - .def("config_value", [](const Preset& preset, const std::string& key) { - return config_value_or_none(preset.config, key); - }); - - py::class_>(host, "PresetCollection") - .def("size", &PresetCollection::size) - .def("get_selected_preset", [](PresetCollection& collection) -> Preset& { - return collection.get_selected_preset(); - }, py::return_value_policy::reference_internal) - .def("selected_preset", [](PresetCollection& collection) -> Preset& { - return collection.get_selected_preset(); - }, py::return_value_policy::reference_internal) - .def("get_selected_preset_name", &PresetCollection::get_selected_preset_name) - .def("selected_preset_name", &PresetCollection::get_selected_preset_name) - .def("get_edited_preset", [](PresetCollection& collection) -> Preset& { - return collection.get_edited_preset(); - }, py::return_value_policy::reference_internal) - .def("edited_preset", [](PresetCollection& collection) -> Preset& { - return collection.get_edited_preset(); - }, py::return_value_policy::reference_internal) - .def("preset", [](PresetCollection& collection, size_t index) -> Preset& { - if (index >= collection.size()) - throw py::index_error("preset index out of range"); - return collection.preset(index); - }, py::return_value_policy::reference_internal) - .def("find_preset", [](PresetCollection& collection, const std::string& name) -> Preset* { - return collection.find_preset(name); - }, py::return_value_policy::reference_internal) - .def("preset_names", [](const PresetCollection& collection) { - std::vector names; - names.reserve(collection.get_presets().size()); - for (const Preset& preset : collection.get_presets()) - names.push_back(preset.name); - return names; - }); - - py::class_>(host, "PresetBundle") - .def_property_readonly("prints", [](PresetBundle& bundle) -> PresetCollection& { - return bundle.prints; - }, py::return_value_policy::reference_internal) - .def_property_readonly("printers", &printer_presets, py::return_value_policy::reference_internal) - .def_property_readonly("filaments", [](PresetBundle& bundle) -> PresetCollection& { - return bundle.filaments; - }, py::return_value_policy::reference_internal) - .def_property_readonly("sla_prints", [](PresetBundle& bundle) -> PresetCollection& { - return bundle.sla_prints; - }, py::return_value_policy::reference_internal) - .def_property_readonly("sla_materials", [](PresetBundle& bundle) -> PresetCollection& { - return bundle.sla_materials; - }, py::return_value_policy::reference_internal) - .def("current_process_preset", [](PresetBundle& bundle) -> Preset& { - return bundle.prints.get_edited_preset(); - }, py::return_value_policy::reference_internal) - .def("current_print_preset", [](PresetBundle& bundle) -> Preset& { - return bundle.prints.get_edited_preset(); - }, py::return_value_policy::reference_internal) - .def("current_printer_preset", [](PresetBundle& bundle) -> Preset& { - return bundle.printers.get_edited_preset(); - }, py::return_value_policy::reference_internal) - .def("current_filament_preset_names", [](PresetBundle& bundle) { - return bundle.filament_presets; - }) - .def("current_filament_presets", ¤t_filament_presets) - .def("full_config_keys", [](const PresetBundle& bundle) { - return bundle.full_config().keys(); - }) - .def("full_config_value", [](const PresetBundle& bundle, const std::string& key) { - return config_value_or_none(bundle.full_config(), key); - }); - - // Axis-aligned bounding box, returned by value (a copy) so its lifetime is - // independent of the model object it was computed from. Coordinates are in mm. - py::class_(host, "BoundingBox", "Axis-aligned bounding box in millimetres") - .def_property_readonly("defined", [](const BoundingBoxf3& bb) { return bb.defined; }) - .def_property_readonly("min", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.min); }) - .def_property_readonly("max", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.max); }) - .def_property_readonly("size", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.size()); }) - .def_property_readonly("center", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.center()); }) - .def_property_readonly("radius", [](const BoundingBoxf3& bb) { return bb.radius(); }); - - 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(); }) - // Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy. - .def("vertices", [](const HostTriangleMesh& mesh) { - 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())); - }); - }, "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) { - 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())); - }); - }, "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) { - return with_numpy([&] { - 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>(); - for (size_t i = 0; i < normals.size(); ++i) { - view(i, 0) = normals[i].x(); - view(i, 1) = normals[i].y(); - view(i, 2) = normals[i].z(); - } - } - return py::object(std::move(array)); - }); - }, "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; - 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; - 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(); }); - - py::enum_(host, "ModelVolumeType") - .value("Invalid", ModelVolumeType::INVALID) - .value("ModelPart", ModelVolumeType::MODEL_PART) - .value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME) - .value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER) - .value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER) - .value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER); - - py::class_>(host, "ModelVolume") - .def("id", [](const ModelVolume& volume) { return volume.id().id; }) - .def_readonly("name", &ModelVolume::name) - .def("type", &ModelVolume::type) - .def("is_model_part", &ModelVolume::is_model_part) - .def("is_modifier", &ModelVolume::is_modifier) - .def("is_negative_volume", &ModelVolume::is_negative_volume) - .def("is_support_enforcer", &ModelVolume::is_support_enforcer) - .def("is_support_blocker", &ModelVolume::is_support_blocker) - .def("is_support_modifier", &ModelVolume::is_support_modifier) - // Extruder ID is 1-based for FFF, -1 for SLA or support volumes. - .def("extruder_id", &ModelVolume::extruder_id) - .def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); }) - .def("rotation", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_rotation()); }) - .def("scaling_factor", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_scaling_factor()); }) - .def("mirror", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_mirror()); }) - // 4x4 float64 affine matrix mapping this volume into its parent object frame. Requires numpy. - .def("matrix", [](const ModelVolume& volume) { return mat4_to_numpy(volume.get_matrix()); }, - "Volume-to-object 4x4 float64 affine matrix (copy). Requires numpy.") - .def("facets_count", [](const ModelVolume& volume) { return volume.mesh().facets_count(); }) - // Raw (untransformed) mesh volume in mm^3; -1 if it was never computed. - .def("volume", [](const ModelVolume& volume) { return volume.mesh().stats().volume; }) - // Bounding box of the raw (untransformed) mesh, in the volume's local frame. - .def("bounding_box", [](const ModelVolume& volume) { return bbox_from_stats(volume.mesh().stats()); }) - .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() }; - }, "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) - .def("is_seam_painted", &ModelVolume::is_seam_painted) - .def("is_mm_painted", &ModelVolume::is_mm_painted) - .def("is_fuzzy_skin_painted", &ModelVolume::is_fuzzy_skin_painted) - .def("config_keys", [](const ModelVolume& volume) { return volume.config.keys(); }) - .def("config_value", [](const ModelVolume& volume, const std::string& key) { - return config_value_or_none(volume.config.get(), key); - }); - - py::class_>(host, "ModelInstance") - .def("id", [](const ModelInstance& instance) { return instance.id().id; }) - .def_readonly("printable", &ModelInstance::printable) - // True only if the object is printable, this instance is printable and it - // currently sits fully inside the print volume (set during slicing). - .def("is_printable", &ModelInstance::is_printable) - .def("offset", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_offset()); }) - .def("rotation", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_rotation()); }) - .def("scaling_factor", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_scaling_factor()); }) - .def("mirror", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_mirror()); }) - // 4x4 float64 affine matrix mapping the object into world space. Requires numpy. - // World vertices = instance.matrix() @ volume.matrix() applied to mesh vertices. - .def("matrix", [](const ModelInstance& instance) { return mat4_to_numpy(instance.get_matrix()); }, - "Object-to-world 4x4 float64 affine matrix (copy). Requires numpy.") - .def("is_left_handed", &ModelInstance::is_left_handed) - // Assemble-view placement. Each instance carries a second transform used only by - // the Assemble view, set from stored 3mf assemble data or derived from the regular - // transform. Until then (is_assemble_initialized() false) it is identity. - .def("is_assemble_initialized", [](ModelInstance& instance) { return instance.is_assemble_initialized(); }) - .def("assemble_offset", [](const ModelInstance& instance) { - return vec3_to_tuple(instance.get_assemble_transformation().get_offset()); - }) - .def("assemble_rotation", [](const ModelInstance& instance) { - return vec3_to_tuple(instance.get_assemble_transformation().get_rotation()); - }) - // 4x4 float64 affine matrix placing the object in the Assemble view. Requires numpy. - .def("assemble_matrix", [](const ModelInstance& instance) { - return mat4_to_numpy(instance.get_assemble_transformation().get_matrix()); - }, "Assemble-view 4x4 float64 affine matrix (copy). Requires numpy.") - // Offset from the instance origin to its position within the source assembly, - // recorded at import time (e.g. from a STEP assembly). - .def("offset_to_assembly", [](const ModelInstance& instance) { - return vec3_to_tuple(instance.get_offset_to_assembly()); - }) - // World-space bounding box of this instance. - .def("bounding_box", [](ModelInstance& instance) { - const ModelObject* object = instance.get_object(); - if (object == nullptr) - return BoundingBoxf3(); - return object->instance_bounding_box(instance); - }); - - py::class_>(host, "ModelObject") - .def("id", [](const ModelObject& object) { return object.id().id; }) - .def_readonly("name", &ModelObject::name) - .def_readonly("module_name", &ModelObject::module_name) - .def_readonly("input_file", &ModelObject::input_file) - // Import-time flag only: the GUI's printable toggle writes the per-instance - // ModelInstance::printable and never updates this field, so derive an - // object's effective state from its instances. - .def_readonly("printable", &ModelObject::printable) - .def("instance_count", [](const ModelObject& object) { - return object.instances.size(); - }) - .def("volume_count", [](const ModelObject& object) { - return object.volumes.size(); - }) - .def("instances", [](ModelObject& object) { - py::list instances; - for (ModelInstance* instance : object.instances) - instances.append(py::cast(instance, py::return_value_policy::reference)); - return instances; - }) - .def("instance", [](ModelObject& object, size_t index) -> ModelInstance* { - if (index >= object.instances.size()) - throw py::index_error("instance index out of range"); - return object.instances[index]; - }, py::return_value_policy::reference_internal) - .def("volumes", [](ModelObject& object) { - py::list volumes; - for (ModelVolume* volume : object.volumes) - volumes.append(py::cast(volume, py::return_value_policy::reference)); - return volumes; - }) - .def("volume", [](ModelObject& object, size_t index) -> ModelVolume* { - if (index >= object.volumes.size()) - throw py::index_error("volume index out of range"); - return object.volumes[index]; - }, py::return_value_policy::reference_internal) - // World-space bounding box over all instances of this object. - .def("bounding_box", [](const ModelObject& object) { return object.bounding_box_exact(); }) - // Bounding box of the object's raw (untransformed) part meshes — its intrinsic size. - .def("raw_mesh_bounding_box", [](const ModelObject& object) { return object.raw_mesh_bounding_box(); }) - .def("min_z", &ModelObject::min_z) - .def("max_z", &ModelObject::max_z) - .def("facets_count", [](const ModelObject& object) { return object.facets_count(); }) - .def("parts_count", [](const ModelObject& object) { return object.parts_count(); }) - .def("materials_count", [](const ModelObject& object) { return object.materials_count(); }) - .def("mesh_errors_count", [](const ModelObject& object) { return object.get_repaired_errors_count(); }) - .def("is_multiparts", &ModelObject::is_multiparts) - .def("is_cut", &ModelObject::is_cut) - .def("has_custom_layering", &ModelObject::has_custom_layering) - .def("is_fdm_support_painted", &ModelObject::is_fdm_support_painted) - .def("is_seam_painted", &ModelObject::is_seam_painted) - .def("is_mm_painted", &ModelObject::is_mm_painted) - .def("is_fuzzy_skin_painted", &ModelObject::is_fuzzy_skin_painted) - .def("config_keys", [](const ModelObject& object) { - return object.config.keys(); - }) - .def("config_value", [](const ModelObject& object, const std::string& key) { - return config_value_or_none(object.config.get(), key); - }); - - py::class_>(host, "Model") - .def("id", [](const Model& model) { return model.id().id; }) - .def("object_count", [](const Model& model) { - return model.objects.size(); - }) - .def("object", [](Model& model, size_t index) -> ModelObject* { - if (index >= model.objects.size()) - throw py::index_error("model object index out of range"); - return model.objects[index]; - }, py::return_value_policy::reference_internal) - .def("objects", [](Model& model) { - py::list objects; - for (ModelObject* object : model.objects) - objects.append(py::cast(object, py::return_value_policy::reference)); - return objects; - }) - // World-space bounding box of the whole model. bounding_box() is exact; - // bounding_box_approx() is faster and cached. - .def("bounding_box", [](const Model& model) { return model.bounding_box_exact(); }) - .def("bounding_box_approx", [](const Model& model) { return model.bounding_box_approx(); }) - .def("max_z", &Model::max_z) - .def("material_count", [](const Model& model) { return model.materials.size(); }) - .def("is_fdm_support_painted", &Model::is_fdm_support_painted) - .def("is_seam_painted", &Model::is_seam_painted) - .def("is_mm_painted", &Model::is_mm_painted) - .def("is_fuzzy_skin_painted", &Model::is_fuzzy_skin_painted) - .def("current_plate_index", [](const Model& model) { return model.curr_plate_index; }) - .def("designer", [](const Model& model) { - return model.design_info ? model.design_info->Designer : std::string(); - }) - .def("design_id", [](const Model& model) { return model.stl_design_id; }); - - py::class_>(host, "Plater") - .def("model", static_cast(&GUI::Plater::model), py::return_value_policy::reference_internal) - .def("is_project_dirty", &GUI::Plater::is_project_dirty) - .def("is_presets_dirty", &GUI::Plater::is_presets_dirty) - .def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture); - - host.def("plater", ¤t_plater, py::return_value_policy::reference); - host.def("model", []() -> Model& { - return current_plater()->model(); - }, py::return_value_policy::reference); - host.def("preset_bundle", ¤t_preset_bundle, py::return_value_policy::reference); - - // UI: native dialogs and interactive HTML windows for plugins. - PluginHostUi::RegisterBindings(host); - - // Slicing print-graph data model (Print, Layer, Surface, ...). - PluginHostSlicing::RegisterBindings(host); -} - -} // namespace Slic3r diff --git a/src/slic3r/plugin/PluginHostApi.hpp b/src/slic3r/plugin/PluginHostApi.hpp deleted file mode 100644 index 5aaf5882df..0000000000 --- a/src/slic3r/plugin/PluginHostApi.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -namespace Slic3r { - -class PluginHostApi -{ -public: - static void RegisterBindings(pybind11::module_& module); -}; - -} // namespace Slic3r diff --git a/src/slic3r/plugin/PluginHostSlicing.hpp b/src/slic3r/plugin/PluginHostSlicing.hpp deleted file mode 100644 index 3fdbb7bd4e..0000000000 --- a/src/slic3r/plugin/PluginHostSlicing.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once -#include - -namespace Slic3r { - -// Registers the slicing print-graph data model (Print, PrintObject, Layer, -// LayerRegion, Surface, ExPolygon, extrusions, ...) into the `orca.host` -// submodule, in the same raw-class style as PluginHostApi's Model/Preset -// graph. Called from PluginHostApi::RegisterBindings. -class PluginHostSlicing -{ -public: - static void RegisterBindings(pybind11::module_& host); -}; - -} // namespace Slic3r diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 89b9f8eb6f..2711e196bb 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -10,7 +10,7 @@ #include #include "PythonInterpreter.hpp" -#include "PluginHostApi.hpp" +#include "host/PluginHost.hpp" #include "PyPluginPackage.hpp" #include "PyPluginTrampoline.hpp" #include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp" @@ -337,7 +337,7 @@ void bind_python_api(pybind11::module_& m) PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes); ScriptPluginCapability::RegisterBindings(m, pluginTypes); SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes); - PluginHostApi::RegisterBindings(m); + PluginHost::RegisterBindings(m); BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings"; m.def( diff --git a/src/slic3r/plugin/host/PluginHost.cpp b/src/slic3r/plugin/host/PluginHost.cpp new file mode 100644 index 0000000000..524f07f12c --- /dev/null +++ b/src/slic3r/plugin/host/PluginHost.cpp @@ -0,0 +1,26 @@ +#include "PluginHost.hpp" +#include "PluginHostBindings.hpp" +#include "PluginHostUi.hpp" + +namespace Slic3r { + +void PluginHost::RegisterBindings(pybind11::module_& module) +{ + auto host = module.def_submodule("host", "Host application API"); + + // Value types first so the docstring signatures of later registrars + // resolve to the bound Python names. + host_bindings::register_geometry(host); + host_bindings::register_mesh(host); + host_bindings::register_presets(host); + host_bindings::register_model(host); + host_bindings::register_app(host); + + // UI: native dialogs and interactive HTML windows for plugins. + PluginHostUi::RegisterBindings(host); + + // Slicing print-graph data model (Print, Layer, Surface, ...). + host_bindings::register_slicing(host); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHost.hpp b/src/slic3r/plugin/host/PluginHost.hpp new file mode 100644 index 0000000000..4385454cc3 --- /dev/null +++ b/src/slic3r/plugin/host/PluginHost.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace Slic3r { + +// Entry point of the `orca.host` Python API surface. Each domain of the +// surface (geometry, mesh, presets, model, app access, ui, slicing graph) +// lives in its own translation unit in this directory; RegisterBindings +// creates the submodule and runs the per-domain registrars. +class PluginHost +{ +public: + static void RegisterBindings(pybind11::module_& module); +}; + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostApp.cpp b/src/slic3r/plugin/host/PluginHostApp.cpp new file mode 100644 index 0000000000..a17f160bb7 --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostApp.cpp @@ -0,0 +1,60 @@ +#include "PluginHostBindings.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace py = pybind11; + +namespace Slic3r { +namespace { + +GUI::Plater* current_plater() +{ + if (wxTheApp == nullptr) + throw std::runtime_error("OrcaSlicer application is not initialized"); + + GUI::Plater* plater = GUI::wxGetApp().plater(); + if (plater == nullptr) + throw std::runtime_error("Plater is not available"); + + return plater; +} + +PresetBundle* current_preset_bundle() +{ + if (wxTheApp == nullptr) + throw std::runtime_error("OrcaSlicer application is not initialized"); + + PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; + if (preset_bundle == nullptr) + throw std::runtime_error("Preset bundle is not available"); + + return preset_bundle; +} + +} // namespace + +// Access to the live GUI application: the Plater and the module-level +// plater()/model()/preset_bundle() accessors. Everything here is owned by the +// app and only reachable once the GUI is up (the accessors throw before that). +void host_bindings::register_app(py::module_& host) +{ + py::class_>(host, "Plater") + .def("model", static_cast(&GUI::Plater::model), py::return_value_policy::reference_internal) + .def("is_project_dirty", &GUI::Plater::is_project_dirty) + .def("is_presets_dirty", &GUI::Plater::is_presets_dirty) + .def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture); + + host.def("plater", ¤t_plater, py::return_value_policy::reference); + host.def("model", []() -> Model& { + return current_plater()->model(); + }, py::return_value_policy::reference); + host.def("preset_bundle", ¤t_preset_bundle, py::return_value_policy::reference); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostBindings.hpp b/src/slic3r/plugin/host/PluginHostBindings.hpp new file mode 100644 index 0000000000..0f206d5992 --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostBindings.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +// Internal to plugin/host/: the per-domain registrars of the `orca.host` +// surface, one per translation unit, called by PluginHost::RegisterBindings. +namespace Slic3r::host_bindings { + +void register_geometry(pybind11::module_& host); // PluginHostGeometry.cpp +void register_mesh(pybind11::module_& host); // PluginHostMesh.cpp +void register_presets(pybind11::module_& host); // PluginHostPresets.cpp +void register_model(pybind11::module_& host); // PluginHostModel.cpp +void register_app(pybind11::module_& host); // PluginHostApp.cpp +void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp + +} // namespace Slic3r::host_bindings diff --git a/src/slic3r/plugin/host/PluginHostGeometry.cpp b/src/slic3r/plugin/host/PluginHostGeometry.cpp new file mode 100644 index 0000000000..646dffb5d4 --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostGeometry.cpp @@ -0,0 +1,216 @@ +#include "PluginHostBindings.hpp" +#include "slic3r/plugin/PluginBindingUtils.hpp" + +#include +#include // offset/offset_ex/union_ex/diff_ex/intersection_ex +#include + +#include + +#include +#include +#include + +namespace py = pybind11; + +namespace Slic3r { +namespace { +// --- Input path: Python geometry -> C++ Polygon/ExPolygon, with validation. --------------- +// The mutators take scaled integer coords (the same units the read views hand out). A Python +// raise here surfaces as ValueError (pybind translates) so malformed input is rejected up +// front rather than silently corrupting the slicing graph. + +// One (N,2) int64 ndarray -> Polygon. Rejects wrong dtype/shape and degenerate (<3 pt) rings. +// Float / NaN / inf are rejected implicitly: only a signed-integer, 8-byte (coord_t==int64) +// dtype is accepted, and integer arrays cannot hold NaN/inf. +Polygon parse_polygon(py::handle h, const char* who) +{ + if (!py::isinstance(h)) + throw py::value_error(std::string(who) + ": each contour/hole must be an (N,2) int64 ndarray"); + py::array a = py::reinterpret_borrow(h); + if (a.dtype().kind() != 'i' || a.itemsize() != (py::ssize_t) sizeof(coord_t)) + throw py::value_error(std::string(who) + ": polygon coordinates must be int64 (scaled coords)"); + if (a.ndim() != 2 || a.shape(1) != 2) + throw py::value_error(std::string(who) + ": each polygon array must have shape (N,2)"); + if (a.shape(0) < 3) + throw py::value_error(std::string(who) + ": a polygon needs at least 3 points"); + // dtype already validated as int64; forcecast here only guarantees a C-contiguous buffer. + auto arr = py::array_t::ensure(a); + if (!arr) + throw py::value_error(std::string(who) + ": could not read polygon as a contiguous int64 array"); + auto r = arr.unchecked<2>(); + Polygon poly; + poly.points.reserve((size_t) arr.shape(0)); + for (py::ssize_t i = 0; i < arr.shape(0); ++i) + poly.points.emplace_back((coord_t) r(i, 0), (coord_t) r(i, 1)); + return poly; +} + +// Accept a bound orca.host.Polygon (copied) or an (N,2) int64 ndarray. Used by the ExPolygon +// binding, whose constructor/contour-setter/set_holes must accept the Polygon it itself hands +// out (e.g. `ExPolygon(some_polygon_ref)`) in addition to the ndarray-only parse_polygon() path. +Polygon as_polygon(py::handle h, const char* who) +{ + if (py::isinstance(h)) + return h.cast(); + return parse_polygon(h, who); +} +} // namespace + +void host_bindings::register_geometry(py::module_& host) +{ + // ------------------------------------------------------------------ + // Geometry value types of the `orca.host` surface. All use pybind's + // default holder, so plugins can construct and own instances. When + // obtained from the live slicing graph they are non-owning references + // instead — see the lifetime rule in PluginHostSlicing.cpp. + // ------------------------------------------------------------------ + + // Axis-aligned bounding box, returned by value (a copy) so its lifetime is + // independent of the model object it was computed from. Coordinates are in mm. + py::class_(host, "BoundingBox", "Axis-aligned bounding box in millimetres") + .def_property_readonly("defined", [](const BoundingBoxf3& bb) { return bb.defined; }) + .def_property_readonly("min", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.min); }) + .def_property_readonly("max", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.max); }) + .def_property_readonly("size", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.size()); }) + .def_property_readonly("center", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.center()); }) + .def_property_readonly("radius", [](const BoundingBoxf3& bb) { return bb.radius(); }); + + // Point: a constructible value type (default holder, so Python-owned instances + // are freed). Returned-by-reference from Polygon.points, it aliases the buffer; + // x()/y() are Eigen lvalues, so the properties are read/write. p+q / p-q go + // through Eigen expression templates, wrapped back into a Point. + py::class_(host, "Point") + .def(py::init([](coord_t x, coord_t y) { return Point(x, y); }), py::arg("x"), py::arg("y")) + .def_property("x", [](const Point& p) { return p.x(); }, + [](Point& p, coord_t v) { p.x() = v; }) + .def_property("y", [](const Point& p) { return p.y(); }, + [](Point& p, coord_t v) { p.y() = v; }) + .def("__add__", [](const Point& a, const Point& b) { return Point(a + b); }, py::is_operator()) + .def("__sub__", [](const Point& a, const Point& b) { return Point(a - b); }, py::is_operator()) + .def("__mul__", [](const Point& a, double s) { return Point(a.x() * s, a.y() * s); }, py::is_operator()) + .def("__repr__", [](const Point& p) { + return "orca.host.Point(" + std::to_string(p.x()) + ", " + std::to_string(p.y()) + ")"; + }); + + py::class_(host, "Polygon") + .def(py::init<>()) + .def("size", [](const Polygon& p) { return p.points.size(); }) + .def("is_valid", [](const Polygon& p) { return p.is_valid(); }) + .def("is_counter_clockwise", [](const Polygon& p) { return p.is_counter_clockwise(); }) + .def("is_clockwise", [](const Polygon& p) { return p.is_clockwise(); }) + .def("make_counter_clockwise", [](Polygon& p) { return p.make_counter_clockwise(); }, + "Reorient to CCW in place. Returns True if it reversed the winding.") + .def("make_clockwise", [](Polygon& p) { return p.make_clockwise(); }) + .def("area", [](const Polygon& p) { return p.area(); }) + .def("centroid", [](const Polygon& p) { return p.centroid(); }) + .def("contains", [](const Polygon& p, const Point& pt) { return p.contains(pt); }, py::arg("point")) + .def("translate", [](Polygon& p, double x, double y) { p.translate(x, y); }, py::arg("x"), py::arg("y")) + .def("rotate", [](Polygon& p, double angle) { p.rotate(angle); }, py::arg("angle")) + .def("rotate", [](Polygon& p, double angle, const Point& c) { p.rotate(angle, c); }, + py::arg("angle"), py::arg("center")) + .def("douglas_peucker", [](Polygon& p, double tol) { p.douglas_peucker(tol); }, py::arg("tolerance")) + .def("simplify", [](const Polygon& p, double tol) { return p.simplify(tol); }, py::arg("tolerance"), + "Return simplified geometry as a list of Polygon (may split into several).") + .def("offset", [](const Polygon& p, coord_t delta) { return offset(p, (float) delta); }, py::arg("delta"), + "Clipper offset by `delta` scaled units (negative shrinks). Returns [Polygon].") + // --- Point-object idiom: references into the buffer (in-place element edit). --- + .def_property_readonly("points", [](py::object self) { + Polygon& p = self.cast(); + py::list out; + for (Point& pt : p.points) + out.append(py::cast(&pt, py::return_value_policy::reference_internal, self)); + return out; + }, "Vertices as [Point] references into this polygon. Editing a Point mutates the " + "buffer in place. Structural changes (count) go through set_points/append, which " + "invalidate previously returned Point refs and array views (C++ vector semantics).") + .def("append", [](Polygon& p, const Point& pt) { p.points.push_back(pt); }, py::arg("point"), + "Append a vertex. Structural change (count): invalidates previously returned " + "Point refs and array views into this polygon (C++ vector semantics).") + // --- numpy idiom: writable zero-copy (N,2) view (bulk affine edits). --- + .def("as_array", [](py::object self) { + Polygon& p = self.cast(); + return with_numpy([&] { + return py::object(make_writable_rows( + self, p.points.empty() ? nullptr : p.points.front().data(), + (py::ssize_t) p.points.size())); + }); + }, "Vertices as a WRITABLE int64 (N,2) numpy view in scaled coords, aliasing the " + "buffer. Count-preserving in-place edits only; valid during execute(ctx). Requires numpy.") + .def("set_points", [](Polygon& p, py::handle src) { p = parse_polygon(src, "Polygon.set_points"); }, + py::arg("points"), + "Replace all vertices from an (N,2) int64 ndarray (scaled coords). Count-changing; " + "invalidates prior Point refs and array views. Raises ValueError on malformed input."); + + // ExPolygon: default holder (Python-owned instances are freed) so plugins can construct + // their own geometry, not just navigate the live slicing graph. contour/holes accessors + // still use reference_internal, so refs into a graph-owned ExPolygon stay non-owning views + // tied to that owner's lifetime, same as Polygon/Surface. + py::class_(host, "ExPolygon") + .def(py::init([](py::handle contour, py::handle holes) { + // Accept bound Polygons or (N,2) ndarrays for both contour and each hole. + ExPolygon ex; + ex.contour = as_polygon(contour, "ExPolygon.contour"); + if (!holes.is_none()) { + if (!py::isinstance(holes) || py::isinstance(holes)) + throw py::value_error("ExPolygon: holes must be a list of Polygon or (N,2) ndarrays"); + for (py::handle h : py::reinterpret_borrow(holes)) { + Polygon hole = as_polygon(h, "ExPolygon.hole"); + hole.make_clockwise(); + ex.holes.emplace_back(std::move(hole)); + } + } + ex.contour.make_counter_clockwise(); + return ex; + }), py::arg("contour"), py::arg("holes") = py::none(), + "Construct from a Polygon/ndarray contour and optional list of hole Polygons/ndarrays. " + "Orientation is normalized (contour CCW, holes CW).") + .def_property("contour", + [](ExPolygon& e) -> Polygon& { return e.contour; }, + [](ExPolygon& e, py::handle v) { e.contour = as_polygon(v, "ExPolygon.contour"); }, + py::return_value_policy::reference_internal, + "Outer contour (CCW). Read returns a live Polygon ref; assign a Polygon/ndarray to replace it.") + .def_property_readonly("holes", [](py::object self) { + ExPolygon& e = self.cast(); + py::list out; + for (Polygon& h : e.holes) + out.append(py::cast(&h, py::return_value_policy::reference_internal, self)); + return out; + }, "Hole contours (CW) as [Polygon] references (in-place editable). set_holes replaces them.") + .def("set_holes", [](ExPolygon& e, py::handle holes) { + ExPolygon tmp; + if (!py::isinstance(holes) || py::isinstance(holes)) + throw py::value_error("set_holes: expected a list of Polygon or (N,2) ndarrays"); + for (py::handle h : py::reinterpret_borrow(holes)) { + Polygon hole = as_polygon(h, "ExPolygon.set_holes"); + hole.make_clockwise(); + tmp.holes.emplace_back(std::move(hole)); + } + e.holes = std::move(tmp.holes); + }, py::arg("holes"), "Replace all holes. Invalidates prior hole refs (C++ vector semantics).") + .def("translate", [](ExPolygon& e, double x, double y) { e.translate(x, y); }, py::arg("x"), py::arg("y")) + .def("rotate", [](ExPolygon& e, double a) { e.rotate(a); }, py::arg("angle")) + .def("rotate", [](ExPolygon& e, double a, const Point& c) { e.rotate(a, c); }, + py::arg("angle"), py::arg("center")) + .def("scale", [](ExPolygon& e, double f) { e.scale(f); }, py::arg("factor")) + .def("douglas_peucker", [](ExPolygon& e, double t) { e.douglas_peucker(t); }, py::arg("tolerance")) + .def("area", [](const ExPolygon& e) { return e.area(); }) + .def("is_valid", [](const ExPolygon& e) { return e.is_valid(); }) + .def("contains", [](const ExPolygon& e, const Point& p) { return e.contains(p); }, py::arg("point")) + .def("num_contours", [](const ExPolygon& e) { return e.num_contours(); }) + .def("simplify", [](const ExPolygon& e, double t) { return e.simplify(t); }, py::arg("tolerance"), + "Return simplified geometry as [ExPolygon].") + .def("offset", [](const ExPolygon& e, coord_t delta) { return offset_ex(e, (float) delta); }, + py::arg("delta"), "Clipper offset by `delta` scaled units (negative shrinks). Returns [ExPolygon].") + .def("union_ex", [](const ExPolygon& a, const ExPolygon& b) { + return union_ex(ExPolygons{ a, b }); + }, py::arg("other"), "Union with another ExPolygon. Returns [ExPolygon].") + .def("diff_ex", [](const ExPolygon& a, const ExPolygon& b) { + return diff_ex(ExPolygons{ a }, ExPolygons{ b }); + }, py::arg("other"), "This minus `other`. Returns [ExPolygon].") + .def("intersection_ex", [](const ExPolygon& a, const ExPolygon& b) { + return intersection_ex(ExPolygons{ a }, ExPolygons{ b }); + }, py::arg("other"), "Intersection with `other`. Returns [ExPolygon]."); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostMesh.cpp b/src/slic3r/plugin/host/PluginHostMesh.cpp new file mode 100644 index 0000000000..3661f964ed --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostMesh.cpp @@ -0,0 +1,105 @@ +#include "PluginHostBindings.hpp" +#include "PluginHostMesh.hpp" +#include "slic3r/plugin/PluginBindingUtils.hpp" + +#include + +#include +#include +#include + +namespace py = pybind11; + +namespace Slic3r { +namespace { + +// Zero-copy export of its.vertices / its.indices relies on these Eigen +// row-vectors being tightly packed (no padding between the 3 components). +static_assert(sizeof(stl_vertex) == 3 * sizeof(float), + "stl_vertex must be a packed float[3] for zero-copy numpy export"); +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", + "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(); }) + // Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy. + .def("vertices", [](const HostTriangleMesh& mesh) { + 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())); + }); + }, "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) { + 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())); + }); + }, "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) { + return with_numpy([&] { + 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>(); + for (size_t i = 0; i < normals.size(); ++i) { + view(i, 0) = normals[i].x(); + view(i, 1) = normals[i].y(); + view(i, 2) = normals[i].z(); + } + } + return py::object(std::move(array)); + }); + }, "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; + 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; + 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(); }); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostMesh.hpp b/src/slic3r/plugin/host/PluginHostMesh.hpp new file mode 100644 index 0000000000..9aca05c59d --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostMesh.hpp @@ -0,0 +1,28 @@ +#pragma once + +#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. +inline BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats) +{ + if (stats.number_of_facets == 0) + return BoundingBoxf3(); + return BoundingBoxf3(stats.min.cast(), stats.max.cast()); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostModel.cpp b/src/slic3r/plugin/host/PluginHostModel.cpp new file mode 100644 index 0000000000..51fdb180e7 --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostModel.cpp @@ -0,0 +1,203 @@ +#include "PluginHostBindings.hpp" +#include "PluginHostMesh.hpp" +#include "slic3r/plugin/PluginBindingUtils.hpp" + +#include + +#include + +#include + +namespace py = pybind11; + +namespace Slic3r { + +// The scene/document graph: Model -> ModelObject -> ModelInstance/ModelVolume. +// Everything is bound py::nodelete — non-owning references into a graph owned +// by the app (the live Plater model) or by a Print's model snapshot. +void host_bindings::register_model(py::module_& host) +{ + py::enum_(host, "ModelVolumeType") + .value("Invalid", ModelVolumeType::INVALID) + .value("ModelPart", ModelVolumeType::MODEL_PART) + .value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME) + .value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER) + .value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER) + .value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER); + + py::class_>(host, "ModelVolume") + .def("id", [](const ModelVolume& volume) { return volume.id().id; }) + .def_readonly("name", &ModelVolume::name) + .def("type", &ModelVolume::type) + .def("is_model_part", &ModelVolume::is_model_part) + .def("is_modifier", &ModelVolume::is_modifier) + .def("is_negative_volume", &ModelVolume::is_negative_volume) + .def("is_support_enforcer", &ModelVolume::is_support_enforcer) + .def("is_support_blocker", &ModelVolume::is_support_blocker) + .def("is_support_modifier", &ModelVolume::is_support_modifier) + // Extruder ID is 1-based for FFF, -1 for SLA or support volumes. + .def("extruder_id", &ModelVolume::extruder_id) + .def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); }) + .def("rotation", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_rotation()); }) + .def("scaling_factor", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_scaling_factor()); }) + .def("mirror", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_mirror()); }) + // 4x4 float64 affine matrix mapping this volume into its parent object frame. Requires numpy. + .def("matrix", [](const ModelVolume& volume) { return mat4_to_numpy(volume.get_matrix()); }, + "Volume-to-object 4x4 float64 affine matrix (copy). Requires numpy.") + .def("facets_count", [](const ModelVolume& volume) { return volume.mesh().facets_count(); }) + // Raw (untransformed) mesh volume in mm^3; -1 if it was never computed. + .def("volume", [](const ModelVolume& volume) { return volume.mesh().stats().volume; }) + // Bounding box of the raw (untransformed) mesh, in the volume's local frame. + .def("bounding_box", [](const ModelVolume& volume) { return bbox_from_stats(volume.mesh().stats()); }) + .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() }; + }, "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) + .def("is_seam_painted", &ModelVolume::is_seam_painted) + .def("is_mm_painted", &ModelVolume::is_mm_painted) + .def("is_fuzzy_skin_painted", &ModelVolume::is_fuzzy_skin_painted) + .def("config_keys", [](const ModelVolume& volume) { return volume.config.keys(); }) + .def("config_value", [](const ModelVolume& volume, const std::string& key) { + return config_value_or_none(volume.config.get(), key); + }); + + py::class_>(host, "ModelInstance") + .def("id", [](const ModelInstance& instance) { return instance.id().id; }) + .def_readonly("printable", &ModelInstance::printable) + // True only if the object is printable, this instance is printable and it + // currently sits fully inside the print volume (set during slicing). + .def("is_printable", &ModelInstance::is_printable) + .def("offset", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_offset()); }) + .def("rotation", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_rotation()); }) + .def("scaling_factor", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_scaling_factor()); }) + .def("mirror", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_mirror()); }) + // 4x4 float64 affine matrix mapping the object into world space. Requires numpy. + // World vertices = instance.matrix() @ volume.matrix() applied to mesh vertices. + .def("matrix", [](const ModelInstance& instance) { return mat4_to_numpy(instance.get_matrix()); }, + "Object-to-world 4x4 float64 affine matrix (copy). Requires numpy.") + .def("is_left_handed", &ModelInstance::is_left_handed) + // Assemble-view placement. Each instance carries a second transform used only by + // the Assemble view, set from stored 3mf assemble data or derived from the regular + // transform. Until then (is_assemble_initialized() false) it is identity. + .def("is_assemble_initialized", [](ModelInstance& instance) { return instance.is_assemble_initialized(); }) + .def("assemble_offset", [](const ModelInstance& instance) { + return vec3_to_tuple(instance.get_assemble_transformation().get_offset()); + }) + .def("assemble_rotation", [](const ModelInstance& instance) { + return vec3_to_tuple(instance.get_assemble_transformation().get_rotation()); + }) + // 4x4 float64 affine matrix placing the object in the Assemble view. Requires numpy. + .def("assemble_matrix", [](const ModelInstance& instance) { + return mat4_to_numpy(instance.get_assemble_transformation().get_matrix()); + }, "Assemble-view 4x4 float64 affine matrix (copy). Requires numpy.") + // Offset from the instance origin to its position within the source assembly, + // recorded at import time (e.g. from a STEP assembly). + .def("offset_to_assembly", [](const ModelInstance& instance) { + return vec3_to_tuple(instance.get_offset_to_assembly()); + }) + // World-space bounding box of this instance. + .def("bounding_box", [](ModelInstance& instance) { + const ModelObject* object = instance.get_object(); + if (object == nullptr) + return BoundingBoxf3(); + return object->instance_bounding_box(instance); + }); + + py::class_>(host, "ModelObject") + .def("id", [](const ModelObject& object) { return object.id().id; }) + .def_readonly("name", &ModelObject::name) + .def_readonly("module_name", &ModelObject::module_name) + .def_readonly("input_file", &ModelObject::input_file) + // Import-time flag only: the GUI's printable toggle writes the per-instance + // ModelInstance::printable and never updates this field, so derive an + // object's effective state from its instances. + .def_readonly("printable", &ModelObject::printable) + .def("instance_count", [](const ModelObject& object) { + return object.instances.size(); + }) + .def("volume_count", [](const ModelObject& object) { + return object.volumes.size(); + }) + .def("instances", [](ModelObject& object) { + py::list instances; + for (ModelInstance* instance : object.instances) + instances.append(py::cast(instance, py::return_value_policy::reference)); + return instances; + }) + .def("instance", [](ModelObject& object, size_t index) -> ModelInstance* { + if (index >= object.instances.size()) + throw py::index_error("instance index out of range"); + return object.instances[index]; + }, py::return_value_policy::reference_internal) + .def("volumes", [](ModelObject& object) { + py::list volumes; + for (ModelVolume* volume : object.volumes) + volumes.append(py::cast(volume, py::return_value_policy::reference)); + return volumes; + }) + .def("volume", [](ModelObject& object, size_t index) -> ModelVolume* { + if (index >= object.volumes.size()) + throw py::index_error("volume index out of range"); + return object.volumes[index]; + }, py::return_value_policy::reference_internal) + // World-space bounding box over all instances of this object. + .def("bounding_box", [](const ModelObject& object) { return object.bounding_box_exact(); }) + // Bounding box of the object's raw (untransformed) part meshes — its intrinsic size. + .def("raw_mesh_bounding_box", [](const ModelObject& object) { return object.raw_mesh_bounding_box(); }) + .def("min_z", &ModelObject::min_z) + .def("max_z", &ModelObject::max_z) + .def("facets_count", [](const ModelObject& object) { return object.facets_count(); }) + .def("parts_count", [](const ModelObject& object) { return object.parts_count(); }) + .def("materials_count", [](const ModelObject& object) { return object.materials_count(); }) + .def("mesh_errors_count", [](const ModelObject& object) { return object.get_repaired_errors_count(); }) + .def("is_multiparts", &ModelObject::is_multiparts) + .def("is_cut", &ModelObject::is_cut) + .def("has_custom_layering", &ModelObject::has_custom_layering) + .def("is_fdm_support_painted", &ModelObject::is_fdm_support_painted) + .def("is_seam_painted", &ModelObject::is_seam_painted) + .def("is_mm_painted", &ModelObject::is_mm_painted) + .def("is_fuzzy_skin_painted", &ModelObject::is_fuzzy_skin_painted) + .def("config_keys", [](const ModelObject& object) { + return object.config.keys(); + }) + .def("config_value", [](const ModelObject& object, const std::string& key) { + return config_value_or_none(object.config.get(), key); + }); + + py::class_>(host, "Model") + .def("id", [](const Model& model) { return model.id().id; }) + .def("object_count", [](const Model& model) { + return model.objects.size(); + }) + .def("object", [](Model& model, size_t index) -> ModelObject* { + if (index >= model.objects.size()) + throw py::index_error("model object index out of range"); + return model.objects[index]; + }, py::return_value_policy::reference_internal) + .def("objects", [](Model& model) { + py::list objects; + for (ModelObject* object : model.objects) + objects.append(py::cast(object, py::return_value_policy::reference)); + return objects; + }) + // World-space bounding box of the whole model. bounding_box() is exact; + // bounding_box_approx() is faster and cached. + .def("bounding_box", [](const Model& model) { return model.bounding_box_exact(); }) + .def("bounding_box_approx", [](const Model& model) { return model.bounding_box_approx(); }) + .def("max_z", &Model::max_z) + .def("material_count", [](const Model& model) { return model.materials.size(); }) + .def("is_fdm_support_painted", &Model::is_fdm_support_painted) + .def("is_seam_painted", &Model::is_seam_painted) + .def("is_mm_painted", &Model::is_mm_painted) + .def("is_fuzzy_skin_painted", &Model::is_fuzzy_skin_painted) + .def("current_plate_index", [](const Model& model) { return model.curr_plate_index; }) + .def("designer", [](const Model& model) { + return model.design_info ? model.design_info->Designer : std::string(); + }) + .def("design_id", [](const Model& model) { return model.stl_design_id; }); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/host/PluginHostPresets.cpp b/src/slic3r/plugin/host/PluginHostPresets.cpp new file mode 100644 index 0000000000..fffe9e64cf --- /dev/null +++ b/src/slic3r/plugin/host/PluginHostPresets.cpp @@ -0,0 +1,138 @@ +#include "PluginHostBindings.hpp" +#include "slic3r/plugin/PluginBindingUtils.hpp" + +#include +#include + +#include + +#include +#include + +namespace py = pybind11; + +namespace Slic3r { +namespace { + +py::list current_filament_presets(PresetBundle& bundle) +{ + py::list presets; + for (const std::string& preset_name : bundle.filament_presets) { + Preset* preset = bundle.filaments.find_preset(preset_name); + if (preset == nullptr) + presets.append(py::none()); + else + presets.append(py::cast(preset, py::return_value_policy::reference)); + } + return presets; +} + +PresetCollection& printer_presets(PresetBundle& bundle) +{ + return static_cast(bundle.printers); +} + +} // namespace + +void host_bindings::register_presets(py::module_& host) +{ + py::enum_(host, "PresetType") + .value("Invalid", Preset::TYPE_INVALID) + .value("Print", Preset::TYPE_PRINT) + .value("SlaPrint", Preset::TYPE_SLA_PRINT) + .value("Filament", Preset::TYPE_FILAMENT) + .value("SlaMaterial", Preset::TYPE_SLA_MATERIAL) + .value("Printer", Preset::TYPE_PRINTER) + .value("PhysicalPrinter", Preset::TYPE_PHYSICAL_PRINTER) + .value("Plate", Preset::TYPE_PLATE) + .value("Model", Preset::TYPE_MODEL); + + py::class_>(host, "Preset") + .def_readonly("type", &Preset::type) + .def_readonly("name", &Preset::name) + .def_readonly("alias", &Preset::alias) + .def_readonly("file", &Preset::file) + .def_readonly("is_default", &Preset::is_default) + .def_readonly("is_external", &Preset::is_external) + .def_readonly("is_system", &Preset::is_system) + .def_readonly("is_visible", &Preset::is_visible) + .def_readonly("is_dirty", &Preset::is_dirty) + .def_readonly("is_compatible", &Preset::is_compatible) + .def_readonly("is_project_embedded", &Preset::is_project_embedded) + .def_readonly("bundle_id", &Preset::bundle_id) + .def("is_user", &Preset::is_user) + .def("is_from_bundle", &Preset::is_from_bundle) + .def("label", &Preset::label, py::arg("no_alias") = false) + .def("config_keys", [](const Preset& preset) { return preset.config.keys(); }) + .def("config_value", [](const Preset& preset, const std::string& key) { + return config_value_or_none(preset.config, key); + }); + + py::class_>(host, "PresetCollection") + .def("size", &PresetCollection::size) + .def("get_selected_preset", [](PresetCollection& collection) -> Preset& { + return collection.get_selected_preset(); + }, py::return_value_policy::reference_internal) + .def("selected_preset", [](PresetCollection& collection) -> Preset& { + return collection.get_selected_preset(); + }, py::return_value_policy::reference_internal) + .def("get_selected_preset_name", &PresetCollection::get_selected_preset_name) + .def("selected_preset_name", &PresetCollection::get_selected_preset_name) + .def("get_edited_preset", [](PresetCollection& collection) -> Preset& { + return collection.get_edited_preset(); + }, py::return_value_policy::reference_internal) + .def("edited_preset", [](PresetCollection& collection) -> Preset& { + return collection.get_edited_preset(); + }, py::return_value_policy::reference_internal) + .def("preset", [](PresetCollection& collection, size_t index) -> Preset& { + if (index >= collection.size()) + throw py::index_error("preset index out of range"); + return collection.preset(index); + }, py::return_value_policy::reference_internal) + .def("find_preset", [](PresetCollection& collection, const std::string& name) -> Preset* { + return collection.find_preset(name); + }, py::return_value_policy::reference_internal) + .def("preset_names", [](const PresetCollection& collection) { + std::vector names; + names.reserve(collection.get_presets().size()); + for (const Preset& preset : collection.get_presets()) + names.push_back(preset.name); + return names; + }); + + py::class_>(host, "PresetBundle") + .def_property_readonly("prints", [](PresetBundle& bundle) -> PresetCollection& { + return bundle.prints; + }, py::return_value_policy::reference_internal) + .def_property_readonly("printers", &printer_presets, py::return_value_policy::reference_internal) + .def_property_readonly("filaments", [](PresetBundle& bundle) -> PresetCollection& { + return bundle.filaments; + }, py::return_value_policy::reference_internal) + .def_property_readonly("sla_prints", [](PresetBundle& bundle) -> PresetCollection& { + return bundle.sla_prints; + }, py::return_value_policy::reference_internal) + .def_property_readonly("sla_materials", [](PresetBundle& bundle) -> PresetCollection& { + return bundle.sla_materials; + }, py::return_value_policy::reference_internal) + .def("current_process_preset", [](PresetBundle& bundle) -> Preset& { + return bundle.prints.get_edited_preset(); + }, py::return_value_policy::reference_internal) + .def("current_print_preset", [](PresetBundle& bundle) -> Preset& { + return bundle.prints.get_edited_preset(); + }, py::return_value_policy::reference_internal) + .def("current_printer_preset", [](PresetBundle& bundle) -> Preset& { + return bundle.printers.get_edited_preset(); + }, py::return_value_policy::reference_internal) + .def("current_filament_preset_names", [](PresetBundle& bundle) { + return bundle.filament_presets; + }) + .def("current_filament_presets", ¤t_filament_presets) + .def("full_config_keys", [](const PresetBundle& bundle) { + return bundle.full_config().keys(); + }) + .def("full_config_value", [](const PresetBundle& bundle, const std::string& key) { + return config_value_or_none(bundle.full_config(), key); + }); +} + +} // namespace Slic3r diff --git a/src/slic3r/plugin/PluginHostSlicing.cpp b/src/slic3r/plugin/host/PluginHostSlicing.cpp similarity index 62% rename from src/slic3r/plugin/PluginHostSlicing.cpp rename to src/slic3r/plugin/host/PluginHostSlicing.cpp index 1db8120eb7..30e7b45138 100644 --- a/src/slic3r/plugin/PluginHostSlicing.cpp +++ b/src/slic3r/plugin/host/PluginHostSlicing.cpp @@ -1,9 +1,7 @@ -#include "PluginHostSlicing.hpp" -#include "PluginBindingUtils.hpp" +#include "PluginHostBindings.hpp" +#include "slic3r/plugin/PluginBindingUtils.hpp" -#include "libslic3r/libslic3r.h" // unscale<>, scale_ #include "libslic3r/BoundingBox.hpp" -#include "libslic3r/ClipperUtils.hpp" // offset/offset_ex/union_ex/diff_ex/intersection_ex #include "libslic3r/ExPolygon.hpp" #include "libslic3r/Surface.hpp" #include "libslic3r/SurfaceCollection.hpp" @@ -20,47 +18,6 @@ namespace py = pybind11; namespace Slic3r { namespace { -// --- Input path: Python geometry -> C++ ExPolygon/Surface, with validation. --------------- -// The mutators take scaled integer coords (the same units the read views hand out). A Python -// raise here surfaces as ValueError (pybind translates) so malformed input is rejected up -// front rather than silently corrupting the slicing graph. - -// One (N,2) int64 ndarray -> Polygon. Rejects wrong dtype/shape and degenerate (<3 pt) rings. -// Float / NaN / inf are rejected implicitly: only a signed-integer, 8-byte (coord_t==int64) -// dtype is accepted, and integer arrays cannot hold NaN/inf. -static Polygon parse_polygon(py::handle h, const char* who) -{ - if (!py::isinstance(h)) - throw py::value_error(std::string(who) + ": each contour/hole must be an (N,2) int64 ndarray"); - py::array a = py::reinterpret_borrow(h); - if (a.dtype().kind() != 'i' || a.itemsize() != (py::ssize_t) sizeof(coord_t)) - throw py::value_error(std::string(who) + ": polygon coordinates must be int64 (scaled coords)"); - if (a.ndim() != 2 || a.shape(1) != 2) - throw py::value_error(std::string(who) + ": each polygon array must have shape (N,2)"); - if (a.shape(0) < 3) - throw py::value_error(std::string(who) + ": a polygon needs at least 3 points"); - // dtype already validated as int64; forcecast here only guarantees a C-contiguous buffer. - auto arr = py::array_t::ensure(a); - if (!arr) - throw py::value_error(std::string(who) + ": could not read polygon as a contiguous int64 array"); - auto r = arr.unchecked<2>(); - Polygon poly; - poly.points.reserve((size_t) arr.shape(0)); - for (py::ssize_t i = 0; i < arr.shape(0); ++i) - poly.points.emplace_back((coord_t) r(i, 0), (coord_t) r(i, 1)); - return poly; -} - -// Accept a bound orca.host.Polygon (copied) or an (N,2) int64 ndarray. Used by the ExPolygon -// binding, whose constructor/contour-setter/set_holes must accept the Polygon it itself hands -// out (e.g. `ExPolygon(some_polygon_ref)`) in addition to the ndarray-only parse_polygon() path. -static Polygon as_polygon(py::handle h, const char* who) -{ - if (py::isinstance(h)) - return h.cast(); - return parse_polygon(h, who); -} - // Flatten an extrusion graph into a list of leaf ExtrusionPath* while walking the // ORIGINAL Print-owned tree (never a temporary copy): the returned pointers stay // valid for the execute(ctx) lifetime pinned by `owner`, so points() can hand out @@ -104,12 +61,12 @@ static void refresh_lslices_bboxes(Layer& l) } } // namespace -void PluginHostSlicing::RegisterBindings(py::module_& host) +void host_bindings::register_slicing(py::module_& host) { // ------------------------------------------------------------------ // Slicing print-graph data model — raw bindings of the classes the C++ // pipeline itself uses, same nodelete/reference style as the Model and - // Preset graphs in PluginHostApi.cpp. + // Preset graphs in PluginHostModel.cpp / PluginHostPresets.cpp. // // LIFETIME (C++ semantics, the one rule of this API): every object handed // out below is a non-owning reference into the live slicing graph owned by @@ -135,145 +92,10 @@ void PluginHostSlicing::RegisterBindings(py::module_& host) .value("stCount", stCount) .export_values(); - // Point: a constructible value type (default holder, so Python-owned instances - // are freed). Returned-by-reference from Polygon.points, it aliases the buffer; - // x()/y() are Eigen lvalues, so the properties are read/write. p+q / p-q go - // through Eigen expression templates, wrapped back into a Point. - py::class_(host, "Point") - .def(py::init([](coord_t x, coord_t y) { return Point(x, y); }), py::arg("x"), py::arg("y")) - .def_property("x", [](const Point& p) { return p.x(); }, - [](Point& p, coord_t v) { p.x() = v; }) - .def_property("y", [](const Point& p) { return p.y(); }, - [](Point& p, coord_t v) { p.y() = v; }) - .def("__add__", [](const Point& a, const Point& b) { return Point(a + b); }, py::is_operator()) - .def("__sub__", [](const Point& a, const Point& b) { return Point(a - b); }, py::is_operator()) - .def("__mul__", [](const Point& a, double s) { return Point(a.x() * s, a.y() * s); }, py::is_operator()) - .def("__repr__", [](const Point& p) { - return "orca.host.Point(" + std::to_string(p.x()) + ", " + std::to_string(p.y()) + ")"; - }); - - py::class_(host, "Polygon") - .def(py::init<>()) - .def("size", [](const Polygon& p) { return p.points.size(); }) - .def("is_valid", [](const Polygon& p) { return p.is_valid(); }) - .def("is_counter_clockwise", [](const Polygon& p) { return p.is_counter_clockwise(); }) - .def("is_clockwise", [](const Polygon& p) { return p.is_clockwise(); }) - .def("make_counter_clockwise", [](Polygon& p) { return p.make_counter_clockwise(); }, - "Reorient to CCW in place. Returns True if it reversed the winding.") - .def("make_clockwise", [](Polygon& p) { return p.make_clockwise(); }) - .def("area", [](const Polygon& p) { return p.area(); }) - .def("centroid", [](const Polygon& p) { return p.centroid(); }) - .def("contains", [](const Polygon& p, const Point& pt) { return p.contains(pt); }, py::arg("point")) - .def("translate", [](Polygon& p, double x, double y) { p.translate(x, y); }, py::arg("x"), py::arg("y")) - .def("rotate", [](Polygon& p, double angle) { p.rotate(angle); }, py::arg("angle")) - .def("rotate", [](Polygon& p, double angle, const Point& c) { p.rotate(angle, c); }, - py::arg("angle"), py::arg("center")) - .def("douglas_peucker", [](Polygon& p, double tol) { p.douglas_peucker(tol); }, py::arg("tolerance")) - .def("simplify", [](const Polygon& p, double tol) { return p.simplify(tol); }, py::arg("tolerance"), - "Return simplified geometry as a list of Polygon (may split into several).") - .def("offset", [](const Polygon& p, coord_t delta) { return offset(p, (float) delta); }, py::arg("delta"), - "Clipper offset by `delta` scaled units (negative shrinks). Returns [Polygon].") - // --- Point-object idiom: references into the buffer (in-place element edit). --- - .def_property_readonly("points", [](py::object self) { - Polygon& p = self.cast(); - py::list out; - for (Point& pt : p.points) - out.append(py::cast(&pt, py::return_value_policy::reference_internal, self)); - return out; - }, "Vertices as [Point] references into this polygon. Editing a Point mutates the " - "buffer in place. Structural changes (count) go through set_points/append, which " - "invalidate previously returned Point refs and array views (C++ vector semantics).") - .def("append", [](Polygon& p, const Point& pt) { p.points.push_back(pt); }, py::arg("point"), - "Append a vertex. Structural change (count): invalidates previously returned " - "Point refs and array views into this polygon (C++ vector semantics).") - // --- numpy idiom: writable zero-copy (N,2) view (bulk affine edits). --- - .def("as_array", [](py::object self) { - Polygon& p = self.cast(); - return with_numpy([&] { - return py::object(make_writable_rows( - self, p.points.empty() ? nullptr : p.points.front().data(), - (py::ssize_t) p.points.size())); - }); - }, "Vertices as a WRITABLE int64 (N,2) numpy view in scaled coords, aliasing the " - "buffer. Count-preserving in-place edits only; valid during execute(ctx). Requires numpy.") - .def("set_points", [](Polygon& p, py::handle src) { p = parse_polygon(src, "Polygon.set_points"); }, - py::arg("points"), - "Replace all vertices from an (N,2) int64 ndarray (scaled coords). Count-changing; " - "invalidates prior Point refs and array views. Raises ValueError on malformed input."); - - // ExPolygon: default holder (Python-owned instances are freed) so plugins can construct - // their own geometry, not just navigate the live slicing graph. contour/holes accessors - // still use reference_internal, so refs into a graph-owned ExPolygon stay non-owning views - // tied to that owner's lifetime, same as Polygon/Surface above. - py::class_(host, "ExPolygon") - .def(py::init([](py::handle contour, py::handle holes) { - // Accept bound Polygons or (N,2) ndarrays for both contour and each hole. - ExPolygon ex; - ex.contour = as_polygon(contour, "ExPolygon.contour"); - if (!holes.is_none()) { - if (!py::isinstance(holes) || py::isinstance(holes)) - throw py::value_error("ExPolygon: holes must be a list of Polygon or (N,2) ndarrays"); - for (py::handle h : py::reinterpret_borrow(holes)) { - Polygon hole = as_polygon(h, "ExPolygon.hole"); - hole.make_clockwise(); - ex.holes.emplace_back(std::move(hole)); - } - } - ex.contour.make_counter_clockwise(); - return ex; - }), py::arg("contour"), py::arg("holes") = py::none(), - "Construct from a Polygon/ndarray contour and optional list of hole Polygons/ndarrays. " - "Orientation is normalized (contour CCW, holes CW).") - .def_property("contour", - [](ExPolygon& e) -> Polygon& { return e.contour; }, - [](ExPolygon& e, py::handle v) { e.contour = as_polygon(v, "ExPolygon.contour"); }, - py::return_value_policy::reference_internal, - "Outer contour (CCW). Read returns a live Polygon ref; assign a Polygon/ndarray to replace it.") - .def_property_readonly("holes", [](py::object self) { - ExPolygon& e = self.cast(); - py::list out; - for (Polygon& h : e.holes) - out.append(py::cast(&h, py::return_value_policy::reference_internal, self)); - return out; - }, "Hole contours (CW) as [Polygon] references (in-place editable). set_holes replaces them.") - .def("set_holes", [](ExPolygon& e, py::handle holes) { - ExPolygon tmp; - if (!py::isinstance(holes) || py::isinstance(holes)) - throw py::value_error("set_holes: expected a list of Polygon or (N,2) ndarrays"); - for (py::handle h : py::reinterpret_borrow(holes)) { - Polygon hole = as_polygon(h, "ExPolygon.set_holes"); - hole.make_clockwise(); - tmp.holes.emplace_back(std::move(hole)); - } - e.holes = std::move(tmp.holes); - }, py::arg("holes"), "Replace all holes. Invalidates prior hole refs (C++ vector semantics).") - .def("translate", [](ExPolygon& e, double x, double y) { e.translate(x, y); }, py::arg("x"), py::arg("y")) - .def("rotate", [](ExPolygon& e, double a) { e.rotate(a); }, py::arg("angle")) - .def("rotate", [](ExPolygon& e, double a, const Point& c) { e.rotate(a, c); }, - py::arg("angle"), py::arg("center")) - .def("scale", [](ExPolygon& e, double f) { e.scale(f); }, py::arg("factor")) - .def("douglas_peucker", [](ExPolygon& e, double t) { e.douglas_peucker(t); }, py::arg("tolerance")) - .def("area", [](const ExPolygon& e) { return e.area(); }) - .def("is_valid", [](const ExPolygon& e) { return e.is_valid(); }) - .def("contains", [](const ExPolygon& e, const Point& p) { return e.contains(p); }, py::arg("point")) - .def("num_contours", [](const ExPolygon& e) { return e.num_contours(); }) - .def("simplify", [](const ExPolygon& e, double t) { return e.simplify(t); }, py::arg("tolerance"), - "Return simplified geometry as [ExPolygon].") - .def("offset", [](const ExPolygon& e, coord_t delta) { return offset_ex(e, (float) delta); }, - py::arg("delta"), "Clipper offset by `delta` scaled units (negative shrinks). Returns [ExPolygon].") - .def("union_ex", [](const ExPolygon& a, const ExPolygon& b) { - return union_ex(ExPolygons{ a, b }); - }, py::arg("other"), "Union with another ExPolygon. Returns [ExPolygon].") - .def("diff_ex", [](const ExPolygon& a, const ExPolygon& b) { - return diff_ex(ExPolygons{ a }, ExPolygons{ b }); - }, py::arg("other"), "This minus `other`. Returns [ExPolygon].") - .def("intersection_ex", [](const ExPolygon& a, const ExPolygon& b) { - return intersection_ex(ExPolygons{ a }, ExPolygons{ b }); - }, py::arg("other"), "Intersection with `other`. Returns [ExPolygon]."); - // Surface: default holder (Python-owned instances are freed), so plugins can construct // their own Surface(surface_type, expolygon) — not just navigate the live slicing graph. - // expolygon is a reference_internal property, same idiom as Polygon/ExPolygon above. + // expolygon is a reference_internal property, same idiom as the Polygon/ExPolygon + // accessors in PluginHostGeometry.cpp. py::class_(host, "Surface") .def(py::init([](SurfaceType t, const ExPolygon& e) { return Surface(t, e); }), py::arg("surface_type"), py::arg("expolygon")) diff --git a/src/slic3r/plugin/PluginHostUi.cpp b/src/slic3r/plugin/host/PluginHostUi.cpp similarity index 99% rename from src/slic3r/plugin/PluginHostUi.cpp rename to src/slic3r/plugin/host/PluginHostUi.cpp index 7750b8d641..ac54c38337 100644 --- a/src/slic3r/plugin/PluginHostUi.cpp +++ b/src/slic3r/plugin/host/PluginHostUi.cpp @@ -1,7 +1,7 @@ #include "PluginHostUi.hpp" -#include "PluginAuditManager.hpp" -#include "PythonInterpreter.hpp" // PythonGILState +#include "slic3r/plugin/PluginAuditManager.hpp" +#include "slic3r/plugin/PythonInterpreter.hpp" // PythonGILState #include #include diff --git a/src/slic3r/plugin/PluginHostUi.hpp b/src/slic3r/plugin/host/PluginHostUi.hpp similarity index 100% rename from src/slic3r/plugin/PluginHostUi.hpp rename to src/slic3r/plugin/host/PluginHostUi.hpp diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp index c9a504f6be..eda2c70324 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp @@ -12,7 +12,7 @@ namespace Slic3r { // pipeline mutates. The data-model bindings and the mandatory lifetime rule // (valid only during execute(ctx); mutators invalidate references into replaced // containers, like std::vector iterators) live in -// src/slic3r/plugin/PluginHostSlicing.cpp. +// src/slic3r/plugin/host/PluginHostSlicing.cpp. struct SlicingPipelineContext { std::string orca_version; SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice }; diff --git a/tests/slic3rutils/test_plugin_host_api.cpp b/tests/slic3rutils/test_plugin_host_api.cpp index 426ea8d2ff..e31887cfba 100644 --- a/tests/slic3rutils/test_plugin_host_api.cpp +++ b/tests/slic3rutils/test_plugin_host_api.cpp @@ -26,7 +26,7 @@ bool has_attr(const py::handle& object, const char* name) } // namespace -TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Python", "[PluginHostApi][Python]") +TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Python", "[PluginHost][Python]") { py::module_ orca = import_orca_module(); REQUIRE(has_attr(orca, "host")); @@ -111,7 +111,7 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho CHECK(printers.attr("find_preset")(printer_preset.name).attr("name").cast() == printer_preset.name); } -TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHostApi][Python]") +TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]") { py::object host = import_orca_module().attr("host"); @@ -127,7 +127,7 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi } } -TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHostApi][Python]") +TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHost][Python]") { py::object host = import_orca_module().attr("host"); REQUIRE(has_attr(host, "ui")); @@ -149,7 +149,7 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i } } -TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHostApi][Python]") +TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]") { using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinRel; @@ -228,7 +228,7 @@ TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[Pl CHECK(py_modifier.attr("type")().cast() == Slic3r::ModelVolumeType::PARAMETER_MODIFIER); } -TEST_CASE("Plugin host API exposes TriangleMesh geometry to Python", "[PluginHostApi][Python]") +TEST_CASE("Plugin host API exposes TriangleMesh geometry to Python", "[PluginHost][Python]") { using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinRel;