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.
This commit is contained in:
SoftFever
2026-07-11 16:18:59 +08:00
parent a04ce5f81e
commit 126e4d5445
20 changed files with 838 additions and 717 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -1,487 +0,0 @@
#include "PluginHostApi.hpp"
#include "PluginHostUi.hpp"
#include "PluginHostSlicing.hpp"
#include "PluginBindingUtils.hpp"
#include <libslic3r/BoundingBox.hpp>
#include <libslic3r/Model.hpp>
#include <libslic3r/Preset.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <pybind11/numpy.h>
#include <pybind11/stl.h>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <vector>
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<double>(), stats.max.cast<double>());
}
// --- 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<const TriangleMesh> 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<typename T>
py::array make_readonly_rows3(const std::shared_ptr<const TriangleMesh>& mesh,
const T* data, py::ssize_t rows)
{
if (rows == 0 || data == nullptr)
return py::array_t<T>(std::vector<py::ssize_t>{ 0, 3 });
auto* owner = new std::shared_ptr<const TriangleMesh>(mesh);
py::capsule base(owner, [](void* p) {
delete reinterpret_cast<std::shared_ptr<const TriangleMesh>*>(p);
});
return make_readonly_rows<T, 3>(base, data, rows);
}
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<PresetCollection&>(bundle.printers);
}
} // namespace
void PluginHostApi::RegisterBindings(pybind11::module_& module)
{
auto host = module.def_submodule("host", "Host application API");
py::enum_<Preset::Type>(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_<Preset, std::unique_ptr<Preset, py::nodelete>>(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_<PresetCollection, std::unique_ptr<PresetCollection, py::nodelete>>(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<std::string> names;
names.reserve(collection.get_presets().size());
for (const Preset& preset : collection.get_presets())
names.push_back(preset.name);
return names;
});
py::class_<PresetBundle, std::unique_ptr<PresetBundle, py::nodelete>>(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", &current_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_<BoundingBoxf3>(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_<HostTriangleMesh>(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<float>(
mesh.mesh,
its.vertices.empty() ? nullptr : its.vertices.front().data(),
static_cast<py::ssize_t>(its.vertices.size()));
});
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
// Read-only, zero-copy (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<std::int32_t>(
mesh.mesh,
its.indices.empty() ? nullptr : its.indices.front().data(),
static_cast<py::ssize_t>(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<Vec3f> normals = its_face_normals(mesh.its());
py::array_t<float> array({ static_cast<py::ssize_t>(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<stl_vertex>& 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<stl_triangle_vertex_indices>& 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_<ModelVolumeType>(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_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(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_<ModelInstance, std::unique_ptr<ModelInstance, py::nodelete>>(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_<ModelObject, std::unique_ptr<ModelObject, py::nodelete>>(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_<Model, std::unique_ptr<Model, py::nodelete>>(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_<GUI::Plater, std::unique_ptr<GUI::Plater, py::nodelete>>(host, "Plater")
.def("model", static_cast<Model& (GUI::Plater::*)()>(&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", &current_plater, py::return_value_policy::reference);
host.def("model", []() -> Model& {
return current_plater()->model();
}, py::return_value_policy::reference);
host.def("preset_bundle", &current_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

View File

@@ -1,13 +0,0 @@
#pragma once
#include <pybind11/pybind11.h>
namespace Slic3r {
class PluginHostApi
{
public:
static void RegisterBindings(pybind11::module_& module);
};
} // namespace Slic3r

View File

@@ -1,16 +0,0 @@
#pragma once
#include <pybind11/pybind11.h>
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

View File

@@ -10,7 +10,7 @@
#include <pybind11/stl.h>
#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(

View File

@@ -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

View File

@@ -0,0 +1,17 @@
#pragma once
#include <pybind11/pybind11.h>
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

View File

@@ -0,0 +1,60 @@
#include "PluginHostBindings.hpp"
#include <libslic3r/Model.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <memory>
#include <stdexcept>
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_<GUI::Plater, std::unique_ptr<GUI::Plater, py::nodelete>>(host, "Plater")
.def("model", static_cast<Model& (GUI::Plater::*)()>(&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", &current_plater, py::return_value_policy::reference);
host.def("model", []() -> Model& {
return current_plater()->model();
}, py::return_value_policy::reference);
host.def("preset_bundle", &current_preset_bundle, py::return_value_policy::reference);
}
} // namespace Slic3r

View File

@@ -0,0 +1,16 @@
#pragma once
#include <pybind11/pybind11.h>
// 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

View File

@@ -0,0 +1,216 @@
#include "PluginHostBindings.hpp"
#include "slic3r/plugin/PluginBindingUtils.hpp"
#include <libslic3r/BoundingBox.hpp>
#include <libslic3r/ClipperUtils.hpp> // offset/offset_ex/union_ex/diff_ex/intersection_ex
#include <libslic3r/ExPolygon.hpp>
#include <pybind11/stl.h>
#include <string>
#include <utility>
#include <vector>
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<py::array>(h))
throw py::value_error(std::string(who) + ": each contour/hole must be an (N,2) int64 ndarray");
py::array a = py::reinterpret_borrow<py::array>(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<coord_t, py::array::c_style | py::array::forcecast>::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<Polygon>(h))
return h.cast<Polygon>();
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_<BoundingBoxf3>(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_<Point>(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_<Polygon>(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<Polygon&>();
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<Polygon&>();
return with_numpy([&] {
return py::object(make_writable_rows<coord_t, 2>(
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_<ExPolygon>(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<py::sequence>(holes) || py::isinstance<py::str>(holes))
throw py::value_error("ExPolygon: holes must be a list of Polygon or (N,2) ndarrays");
for (py::handle h : py::reinterpret_borrow<py::sequence>(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<ExPolygon&>();
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<py::sequence>(holes) || py::isinstance<py::str>(holes))
throw py::value_error("set_holes: expected a list of Polygon or (N,2) ndarrays");
for (py::handle h : py::reinterpret_borrow<py::sequence>(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

View File

@@ -0,0 +1,105 @@
#include "PluginHostBindings.hpp"
#include "PluginHostMesh.hpp"
#include "slic3r/plugin/PluginBindingUtils.hpp"
#include <pybind11/numpy.h>
#include <cstdint>
#include <memory>
#include <vector>
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<typename T>
py::array make_readonly_rows3(const std::shared_ptr<const TriangleMesh>& mesh,
const T* data, py::ssize_t rows)
{
if (rows == 0 || data == nullptr)
return py::array_t<T>(std::vector<py::ssize_t>{ 0, 3 });
auto* owner = new std::shared_ptr<const TriangleMesh>(mesh);
py::capsule base(owner, [](void* p) {
delete reinterpret_cast<std::shared_ptr<const TriangleMesh>*>(p);
});
return make_readonly_rows<T, 3>(base, data, rows);
}
} // namespace
void host_bindings::register_mesh(py::module_& host)
{
py::class_<HostTriangleMesh>(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<float>(
mesh.mesh,
its.vertices.empty() ? nullptr : its.vertices.front().data(),
static_cast<py::ssize_t>(its.vertices.size()));
});
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
// Read-only, zero-copy (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<std::int32_t>(
mesh.mesh,
its.indices.empty() ? nullptr : its.indices.front().data(),
static_cast<py::ssize_t>(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<Vec3f> normals = its_face_normals(mesh.its());
py::array_t<float> array({ static_cast<py::ssize_t>(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<stl_vertex>& 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<stl_triangle_vertex_indices>& 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

View File

@@ -0,0 +1,28 @@
#pragma once
#include <libslic3r/BoundingBox.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <memory>
namespace Slic3r {
// Immutable snapshot of a ModelVolume's mesh. Holding a strong reference to the
// const mesh keeps any zero-copy numpy views valid even if the volume's mesh is
// later replaced on the main thread. Bound as `orca.host.TriangleMesh` in
// PluginHostMesh.cpp; constructed by ModelVolume.mesh() in PluginHostModel.cpp.
struct HostTriangleMesh
{
std::shared_ptr<const TriangleMesh> mesh;
const indexed_triangle_set& its() const { return mesh->its; }
};
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
inline BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
{
if (stats.number_of_facets == 0)
return BoundingBoxf3();
return BoundingBoxf3(stats.min.cast<double>(), stats.max.cast<double>());
}
} // namespace Slic3r

View File

@@ -0,0 +1,203 @@
#include "PluginHostBindings.hpp"
#include "PluginHostMesh.hpp"
#include "slic3r/plugin/PluginBindingUtils.hpp"
#include <libslic3r/Model.hpp>
#include <pybind11/stl.h>
#include <string>
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_<ModelVolumeType>(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_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(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_<ModelInstance, std::unique_ptr<ModelInstance, py::nodelete>>(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_<ModelObject, std::unique_ptr<ModelObject, py::nodelete>>(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_<Model, std::unique_ptr<Model, py::nodelete>>(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

View File

@@ -0,0 +1,138 @@
#include "PluginHostBindings.hpp"
#include "slic3r/plugin/PluginBindingUtils.hpp"
#include <libslic3r/Preset.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <pybind11/stl.h>
#include <string>
#include <vector>
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<PresetCollection&>(bundle.printers);
}
} // namespace
void host_bindings::register_presets(py::module_& host)
{
py::enum_<Preset::Type>(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_<Preset, std::unique_ptr<Preset, py::nodelete>>(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_<PresetCollection, std::unique_ptr<PresetCollection, py::nodelete>>(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<std::string> names;
names.reserve(collection.get_presets().size());
for (const Preset& preset : collection.get_presets())
names.push_back(preset.name);
return names;
});
py::class_<PresetBundle, std::unique_ptr<PresetBundle, py::nodelete>>(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", &current_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

View File

@@ -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<py::array>(h))
throw py::value_error(std::string(who) + ": each contour/hole must be an (N,2) int64 ndarray");
py::array a = py::reinterpret_borrow<py::array>(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<coord_t, py::array::c_style | py::array::forcecast>::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<Polygon>(h))
return h.cast<Polygon>();
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_<Point>(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_<Polygon>(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<Polygon&>();
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<Polygon&>();
return with_numpy([&] {
return py::object(make_writable_rows<coord_t, 2>(
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_<ExPolygon>(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<py::sequence>(holes) || py::isinstance<py::str>(holes))
throw py::value_error("ExPolygon: holes must be a list of Polygon or (N,2) ndarrays");
for (py::handle h : py::reinterpret_borrow<py::sequence>(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<ExPolygon&>();
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<py::sequence>(holes) || py::isinstance<py::str>(holes))
throw py::value_error("set_holes: expected a list of Polygon or (N,2) ndarrays");
for (py::handle h : py::reinterpret_borrow<py::sequence>(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_<Surface>(host, "Surface")
.def(py::init([](SurfaceType t, const ExPolygon& e) { return Surface(t, e); }),
py::arg("surface_type"), py::arg("expolygon"))

View File

@@ -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 <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/MainFrame.hpp>

View File

@@ -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 };

View File

@@ -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<std::string>() == 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>() == 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;