#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