feat(plugin): add the slicing-pipeline plugin capability

Introduces a plugin capability that runs Python at the seams of Print::process(),
letting a plugin read and rewrite slicing state as it is computed.

- New slicing_pipeline_plugin config option; selected plugin refs are serialized
  into the print manifest.
- Print gains an injectable hook fired at each pipeline step (posSlice,
  posPerimeters, posInfill, ...). It is a no-op when unset, fires only on genuine
  (re)computation, and never on the use-cache path.
- orca.slicing submodule: SlicingPipelineCapabilityBase plus a trampoline and a
  Step enum. Capabilities read the live graph through zero-copy int64 numpy views
  (contour/holes geometry with unscaled coordinates, flattened toolpath data) and
  edit it through 2D-geometry mutators with cache-invariant refresh.
- GUI dispatcher runs capabilities during slicing under the GIL, turns plugin
  errors into slicing errors, honors cancellation, and adds the plugin picker.
- Ships the InsetEverySlice sample plugin and binding/hook tests.
This commit is contained in:
SoftFever
2026-07-04 04:33:20 +08:00
parent 43bc76d9a9
commit b0bacdd00b
23 changed files with 1622 additions and 38 deletions

View File

@@ -1626,6 +1626,11 @@ void ConfigBase::save_plugin_collection(const std::string& opt_key, const Config
append_ref(val, "post-processing");
} else if (opt_key == "printer_agent") {
append_ref((dynamic_cast<const ConfigOptionString *>(opt))->value, "printer-connection");
} else if (opt_key == "slicing_pipeline_plugin") {
if (const auto* vec = dynamic_cast<const ConfigOptionStrings*>(opt)) {
for (const std::string& val : vec->vserialize())
append_ref(val, "slicing-pipeline");
}
}
// Extend for other plugin-backed settings as needed.
}

View File

@@ -50,6 +50,8 @@ using namespace nlohmann;
namespace Slic3r {
Print::SlicingPipelineHookFn Print::s_slicing_pipeline_hook_fn = nullptr;
template class PrintState<PrintStep, psCount>;
template class PrintState<PrintObjectStep, posCount>;
@@ -277,7 +279,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_rotation_angle") {
steps.emplace_back(psSkirtBrim);
} else if (
opt_key == "initial_layer_print_height"
opt_key == "slicing_pipeline_plugin"
|| opt_key == "initial_layer_print_height"
|| opt_key == "nozzle_diameter"
|| opt_key == "filament_shrink"
|| opt_key == "filament_shrinkage_compensation_z"
@@ -2201,6 +2204,11 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (time_cost_with_cache)
*time_cost_with_cache = 0;
{
const auto* sp = this->config().option<ConfigOptionStrings>("slicing_pipeline_plugin");
m_pipeline_plugin_active = s_slicing_pipeline_hook_fn && sp && !sp->values.empty();
}
name_tbb_thread_pool_threads_set_locale();
//compute the PrintObject with the same geometries
@@ -2310,20 +2318,36 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": total object counts %1% in current print, need to slice %2%")%m_objects.size()%need_slicing_objects.size();
BOOST_LOG_TRIVIAL(info) << "Starting the slicing process." << log_memory_info();
if (!use_cache) {
// Fire the SlicingPipeline hook for `obj` iff it just (re)computed `pstep` this pass.
auto hook_after = [this](PrintObject* obj, bool was_done, PrintObjectStep pstep, SlicingPipelineStep sstep) {
if (m_pipeline_plugin_active && !was_done && obj->is_step_done(pstep))
run_pipeline_hook(sstep, obj);
};
// SlicingPipeline: dedicated slice loop so the Slice boundary is hookable before perimeters.
for (PrintObject *obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
obj->make_perimeters();
}
else {
if (obj->set_started(posSlice))
obj->set_done(posSlice);
if (obj->set_started(posPerimeters))
obj->set_done(posPerimeters);
const bool was_done = obj->is_step_done(posSlice);
obj->slice();
hook_after(obj, was_done, posSlice, SlicingPipelineStep::Slice);
} else {
if (obj->set_started(posSlice)) obj->set_done(posSlice); // shared/duplicate — no hook
}
}
for (PrintObject *obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
const bool was_done = obj->is_step_done(posPerimeters);
obj->make_perimeters(); // slice() inside is a no-op: posSlice already DONE
hook_after(obj, was_done, posPerimeters, SlicingPipelineStep::Perimeters);
} else {
if (obj->set_started(posPerimeters)) obj->set_done(posPerimeters);
}
}
for (PrintObject *obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
const bool was_done = obj->is_step_done(posEstimateCurledExtrusions);
obj->estimate_curled_extrusions();
hook_after(obj, was_done, posEstimateCurledExtrusions, SlicingPipelineStep::EstimateCurledExtrusions);
}
else {
if (obj->set_started(posEstimateCurledExtrusions))
@@ -2332,7 +2356,9 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
for (PrintObject *obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
const bool was_done = obj->is_step_done(posInfill);
obj->infill();
hook_after(obj, was_done, posInfill, SlicingPipelineStep::Infill);
}
else {
if (obj->set_started(posPrepareInfill))
@@ -2343,7 +2369,9 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
for (PrintObject *obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
const bool was_done = obj->is_step_done(posIroning);
obj->ironing();
hook_after(obj, was_done, posIroning, SlicingPipelineStep::Ironing);
}
else {
if (obj->set_started(posIroning))
@@ -2355,13 +2383,22 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
for (PrintObject *obj : m_objects) {
bool need_contouring = need_slicing_objects.count(obj) != 0 && obj->need_z_contouring();
if (need_contouring) {
const bool was_done = obj->is_step_done(posContouring);
obj->contour_z();
hook_after(obj, was_done, posContouring, SlicingPipelineStep::Contouring);
} else {
if (obj->set_started(posContouring))
obj->set_done(posContouring);
}
}
// SlicingPipeline: support runs in the parallel block below; the hook must fire in a
// sequential loop afterward. Snapshot per-object done-state just before the parallel_for.
std::vector<char> sup_was_done(m_objects.size(), 1);
if (m_pipeline_plugin_active)
for (size_t i = 0; i < m_objects.size(); ++i)
sup_was_done[i] = m_objects[i]->is_step_done(posSupportMaterial) ? 1 : 0;
tbb::parallel_for(tbb::blocked_range<int>(0, int(m_objects.size())),
[this, need_slicing_objects](const tbb::blocked_range<int>& range) {
for (int i = range.begin(); i < range.end(); i++) {
@@ -2377,9 +2414,17 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
);
if (m_pipeline_plugin_active)
for (size_t i = 0; i < m_objects.size(); ++i)
if (need_slicing_objects.count(m_objects[i]) != 0 && !sup_was_done[i]
&& m_objects[i]->is_step_done(posSupportMaterial))
run_pipeline_hook(SlicingPipelineStep::SupportMaterial, m_objects[i]);
for (PrintObject* obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
const bool was_done = obj->is_step_done(posDetectOverhangsForLift);
obj->detect_overhangs_for_lift();
hook_after(obj, was_done, posDetectOverhangsForLift, SlicingPipelineStep::DetectOverhangsForLift);
}
else {
if (obj->set_started(posDetectOverhangsForLift))
@@ -2456,6 +2501,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
this->set_done(psWipeTower);
if (m_pipeline_plugin_active) run_pipeline_hook(SlicingPipelineStep::WipeTower, nullptr);
}
if (this->has_wipe_tower()) {
@@ -2581,6 +2627,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
this->finalize_first_layer_convex_hull();
this->set_done(psSkirtBrim);
if (m_pipeline_plugin_active) run_pipeline_hook(SlicingPipelineStep::SkirtBrim, nullptr);
if (time_cost_with_cache) {
end_time = (long long)Slic3r::Utils::get_current_time_utc();
@@ -2591,7 +2638,13 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
for (PrintObject *obj : m_objects) {
if (((!use_cache)&&(need_slicing_objects.count(obj) != 0))
|| (use_cache &&(re_slicing_objects.count(obj) != 0))){
const bool was_done = obj->is_step_done(posSimplifyPath);
obj->simplify_extrusion_path();
// Unlike every other seam (all inside the `if (!use_cache)` block above), this loop is
// shared with the use_cache path (re_slicing_objects), so `!use_cache` must be checked
// explicitly here to keep hooks from ever firing on cache-loaded (plugin-final) objects.
if (!use_cache && m_pipeline_plugin_active && !was_done && obj->is_step_done(posSimplifyPath))
run_pipeline_hook(SlicingPipelineStep::SimplifyPath, obj);
}
else {
if (obj->set_started(posSimplifyPath))

View File

@@ -882,6 +882,11 @@ enum FilamentCompatibilityType {
InvalidTemperatureRange
};
enum class SlicingPipelineStep {
Slice, Perimeters, EstimateCurledExtrusions, Infill, Ironing, Contouring,
SupportMaterial, DetectOverhangsForLift, SimplifyPath, WipeTower, SkirtBrim
};
// The complete print tray with possibly multiple objects.
class Print : public PrintBaseWithState<PrintStep, psCount>
{
@@ -891,6 +896,11 @@ private: // Prevents erroneous use by other classes.
typedef std::pair<PrintObject *, bool> PrintObjectInfo;
public:
using SlicingPipelineHookFn = std::function<void(Print&, const PrintObject*, SlicingPipelineStep)>;
// Cross-layer injection (mirrors ConfigBase::set_resolve_capability_fn): the GUI/plugin
// layer registers a dispatcher; libslic3r stays free of any plugin/Python dependency.
static void set_slicing_pipeline_hook_fn(SlicingPipelineHookFn fn) { s_slicing_pipeline_hook_fn = std::move(fn); }
Print() = default;
virtual ~Print() { this->clear(); }
@@ -1147,6 +1157,13 @@ private:
// Islands of objects and their supports extruded at the 1st layer.
Polygons first_layer_islands() const;
static SlicingPipelineHookFn s_slicing_pipeline_hook_fn;
bool m_pipeline_plugin_active { false };
void run_pipeline_hook(SlicingPipelineStep step, const PrintObject* object) {
if (m_pipeline_plugin_active && s_slicing_pipeline_hook_fn)
s_slicing_pipeline_hook_fn(*this, object, step);
}
PrintConfig m_config;
PrintObjectConfig m_default_object_config;
PrintRegionConfig m_default_region_config;

View File

@@ -5122,6 +5122,16 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("slicing_pipeline_plugin", coStrings);
def->label = L("Slicing Pipeline Plugin");
def->tooltip = L("Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data. Research/experimental.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "slicing-pipeline";
def->support_plugin = true;
def->full_width = true;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("printer_model", coString);
def->label = L("Printer type");
def->tooltip = L("Type of the printer.");

View File

@@ -1571,6 +1571,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionString, filename_format))
((ConfigOptionStrings, post_process))
((ConfigOptionStrings, post_process_plugin))
((ConfigOptionStrings, slicing_pipeline_plugin))
((ConfigOptionString, printer_model))
((ConfigOptionFloat, resolution))
((ConfigOptionFloats, retraction_minimum_travel))

View File

@@ -621,6 +621,10 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp
plugin/pluginTypes/slicingPipeline/SlicingNumpy.hpp
pchheader.cpp
pchheader.hpp
Utils/ASCIIFolding.cpp

View File

@@ -81,6 +81,7 @@
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/PluginHostUi.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include "slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include "GUI.hpp"
#include "GUI_Utils.hpp"
@@ -3122,6 +3123,61 @@ bool GUI_App::on_init_inner()
return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name;
});
// Orca: register the slicing-pipeline plugin dispatcher (mirrors set_resolve_capability_fn: the
// GUI/plugin layer supplies the Python bridge so libslic3r stays free of any plugin dependency).
// Print::process() fires this hook at each pipeline seam on the slicing worker thread; here we run
// the picker-selected SlicingPipeline capabilities. Per capability we acquire the GIL, honor
// cancellation, and convert a plugin failure into a (non-critical) SlicingError so it surfaces as a
// slicing-error notification rather than the fatal-crash dialog.
Slic3r::Print::set_slicing_pipeline_hook_fn(
[](Slic3r::Print& print, const Slic3r::PrintObject* object, Slic3r::SlicingPipelineStep step) {
const auto* caps = print.config().option<ConfigOptionStrings>("slicing_pipeline_plugin");
// `plugins` is a dynamic-only manifest key (not a static PrintConfig member), so it
// must be read from the full/dynamic config -- reading it off print.config() (the
// static PrintConfig) always yields nullptr and skips every capability. Mirrors the
// post-process path (PostProcessor.cpp, via BackgroundSlicingProcess::full_print_config()).
const auto* plugs = print.full_print_config().option<ConfigOptionStrings>("plugins");
if (caps == nullptr || caps->values.empty())
return;
Slic3r::execute_capabilities_from_refs<Slic3r::SlicingPipelinePluginCapability>(
*caps, plugs, Slic3r::PluginCapabilityType::SlicingPipeline,
[&](std::shared_ptr<Slic3r::SlicingPipelinePluginCapability> cap, const Slic3r::PluginCapabilityRef& ref) {
Slic3r::ExecutionResult r;
try {
// GIL is acquired per capability (not once for the whole dispatch) so it is
// released between capabilities. ctx is built inside this scope because
// ctx.owner is a py::capsule: it must be created and destroyed while the GIL
// is held (ctx destructs before `gil`, so its capsule is decref'd under GIL).
PythonGILState gil;
// throw_if_canceled() is protected on PrintBase; canceled() is the public
// equivalent check (same cancel flag), so honor cancellation via it.
if (print.canceled())
throw Slic3r::CanceledException();
Slic3r::SlicingPipelineContext ctx;
ctx.orca_version = SoftFever_VERSION;
ctx.step = step;
ctx.print = &print;
ctx.object = object;
// No-op-destructor capsule threaded into every zero-copy numpy array as its
// base. It references `print` but frees nothing: `print` is owned by libslic3r
// and outlives the hook, and arrays are valid only during this execute() call.
ctx.owner = pybind11::capsule(&print, [](void*) {});
r = cap->execute(ctx);
} catch (const Slic3r::CanceledException&) {
throw; // cancellation must reach process(), never become a slicing error
} catch (const std::exception& ex) {
// A Python raise reaches here as pybind11::error_already_set; surface it as a
// (non-critical) slicing error instead of a crash.
throw Slic3r::SlicingError(std::string("Slicing pipeline plugin '") +
ref.capability_name + "' error: " + ex.what());
}
if (r.status == Slic3r::PluginResult::FatalError)
throw Slic3r::SlicingError(std::string("Slicing pipeline plugin '") +
ref.capability_name + "' error: " + r.message);
});
});
// Set cloud plugin directory from previous session so cloud-installed
// plugins are discovered even before the network agent is ready.
const std::string preset_folder = app_config->get("preset_folder");

View File

@@ -3079,6 +3079,12 @@ void TabPrint::build()
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
optgroup = page->new_optgroup(L("Slicing Pipeline Plugin"), L"param_gcode", 0);
optgroup->hide_labels();
option = optgroup->get_option("slicing_pipeline_plugin");
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
optgroup = page->new_optgroup(L("Notes"), "note", 0);
option = optgroup->get_option("notes");
option.opt.full_width = true;

View File

@@ -102,10 +102,14 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
{
PluginManager& plugin_mgr = PluginManager::instance();
// Log prefix derived from the capability type so each capability family (Post-processing,
// Slicing Pipeline, ...) tags its dispatch diagnostics with its own display name.
const std::string tag = plugin_capability_type_display_name(type);
const bool has_any = std::any_of(capabilities.values.begin(), capabilities.values.end(),
[](const std::string& s) { return !s.empty(); });
if (has_any && !plugin_mgr.get_loader().wait_for_all_plugin_loads(std::chrono::seconds(10))) {
BOOST_LOG_TRIVIAL(warning) << "Post-process: timed out waiting for plugin loads; unresolved capabilities will be skipped";
BOOST_LOG_TRIVIAL(warning) << tag << ": timed out waiting for plugin loads; unresolved capabilities will be skipped";
}
for (const std::string& capability : capabilities.values) {
@@ -127,7 +131,7 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
}
if (!ref) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no plugin reference found for capability '" << capability << "'; skipping";
BOOST_LOG_TRIVIAL(warning) << tag << ": no plugin reference found for capability '" << capability << "'; skipping";
continue;
}
@@ -136,19 +140,19 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
cap = plugin_mgr.get_loader().get_plugin_capability_by_name(plugin_key, type, cap_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no loaded capability '" << cap_name
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name
<< "' for plugin '" << plugin_key << "'; skipping";
continue;
}
if (!cap->enabled) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
continue;
}
auto plugin_capability = std::dynamic_pointer_cast<T>(cap->instance);
if (!plugin_capability) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
<< "' (plugin_key=" << cap->plugin_key
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
continue;

View File

@@ -16,6 +16,7 @@
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
namespace py = pybind11;
@@ -294,6 +295,7 @@ void bind_python_api(pybind11::module_& m)
.value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("Unknown", PluginCapabilityType::Unknown)
.export_values();
@@ -337,6 +339,7 @@ void bind_python_api(pybind11::module_& m)
GCodePluginCapability::RegisterBindings(m, pluginTypes);
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);
PluginHostApi::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";

View File

@@ -10,7 +10,7 @@
namespace Slic3r {
enum class PluginCapabilityType { PostProcessing = 0, PrinterConnection, Automation, Analysis, Importer, Exporter, Visualization, Script, Unknown };
enum class PluginCapabilityType { PostProcessing = 0, PrinterConnection, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{
@@ -23,6 +23,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
case PluginCapabilityType::Exporter: return "exporter";
case PluginCapabilityType::Visualization: return "visualization";
case PluginCapabilityType::Script: return "script";
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
default: return "unknown";
}
}
@@ -38,6 +39,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
case PluginCapabilityType::Exporter: return "Exporter";
case PluginCapabilityType::Visualization: return "Visualization";
case PluginCapabilityType::Script: return "Script";
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
default: return "Unknown";
}
}
@@ -67,6 +69,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
return PluginCapabilityType::Visualization;
if (lowered == "script")
return PluginCapabilityType::Script;
if (lowered == "slicing-pipeline")
return PluginCapabilityType::SlicingPipeline;
return PluginCapabilityType::Unknown;
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include "libslic3r/Point.hpp"
namespace Slic3r {
// Point/Point3 must be tightly packed for zero-copy views. coord_t = int64_t.
static_assert(sizeof(Point) == 2 * sizeof(coord_t), "Point must be 2 packed coord_t");
static_assert(sizeof(Point3) == 3 * sizeof(coord_t), "Point3 must be 3 packed coord_t");
// Zero-copy, read-only (rows, N) numpy view over `data`, pinned alive by `owner`.
// T is the element scalar (coord_t=int64 for slicing coords). Mirrors PluginHostApi's
// capsule + setflags(write=false) pattern, generalized over column count and owner.
template<typename T, int N>
pybind11::array make_readonly_rows(pybind11::capsule owner, const T* data, pybind11::ssize_t rows)
{
namespace py = pybind11;
py::array_t<T> arr(
{ rows, (py::ssize_t)N },
{ (py::ssize_t)(N * sizeof(T)), (py::ssize_t)sizeof(T) },
data, owner);
arr.attr("setflags")(py::arg("write") = false);
return std::move(arr);
}
} // namespace Slic3r

View File

@@ -0,0 +1,372 @@
#include "SlicingPipelinePluginCapability.hpp"
#include "SlicingPipelinePluginCapabilityTrampoline.hpp"
#include "SlicingNumpy.hpp" // make_readonly_rows
#include "libslic3r/libslic3r.h" // unscale<>, live SCALING_FACTOR
#include "libslic3r/ExtrusionEntity.hpp" // ExtrusionPath/Loop/MultiPath, role_to_string
#include "libslic3r/ExtrusionEntityCollection.hpp" // ExtrusionEntityCollection
#include <pybind11/stl.h>
#include <vector>
namespace py = pybind11;
namespace Slic3r {
bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); }
namespace {
// Zero-copy read-only int64 (N,2) view over a Polygon's points, pinned by `owner`.
// coord_t == int64; Point is asserted tightly packed in SlicingNumpy.hpp.
static py::array polygon_rows(const py::capsule& owner, const Polygon& poly)
{
const Points& p = poly.points;
return make_readonly_rows<coord_t, 2>(
owner, p.empty() ? nullptr : p.front().data(), (py::ssize_t) p.size());
}
// 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
// zero-copy views into path->polyline.points.
//
// This is deliberately NOT ExtrusionEntityCollection::flatten(): flatten() only
// unwraps nested collections (is_collection() is true solely for collections) and
// returns them by value, so it would (a) dangle if we viewed into the copy and
// (b) leave ExtrusionLoop/ExtrusionMultiPath intact — dropping every perimeter
// loop, since dynamic_cast<ExtrusionPath*> fails on a loop. We descend into
// loops/multipaths here to reach their contained paths.
static void collect_extrusion_paths(const ExtrusionEntity* ee, std::vector<const ExtrusionPath*>& out)
{
if (ee == nullptr)
return;
if (const auto* coll = dynamic_cast<const ExtrusionEntityCollection*>(ee)) {
for (const ExtrusionEntity* child : coll->entities)
collect_extrusion_paths(child, out);
} else if (const auto* loop = dynamic_cast<const ExtrusionLoop*>(ee)) {
for (const ExtrusionPath& p : loop->paths)
out.push_back(&p);
} else if (const auto* mp = dynamic_cast<const ExtrusionMultiPath*>(ee)) {
for (const ExtrusionPath& p : mp->paths)
out.push_back(&p);
} else if (const auto* path = dynamic_cast<const ExtrusionPath*>(ee)) {
// Catches ExtrusionPath and its subclasses (Sloped/Contoured/Oriented) last,
// after the composite types above have been ruled out.
out.push_back(path);
}
}
// Build a Python list of PathData over an extrusion collection, each entry pinned by `owner`.
static py::list path_data_list(const py::capsule& owner, const ExtrusionEntityCollection& coll)
{
std::vector<const ExtrusionPath*> paths;
collect_extrusion_paths(&coll, paths);
py::list out;
for (const ExtrusionPath* p : paths)
out.append(PathData{ p, owner });
return out;
}
// --- Task 11 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;
}
// One Python entry -> ExPolygon. Accepts either a bare (N,2) ndarray (contour only) or a
// [contour, [hole, ...]] sequence. Orientation is normalized (contour CCW, holes CW) so
// downstream area/offset math is correct regardless of the caller's winding.
static ExPolygon parse_expolygon(py::handle entry, const char* who)
{
ExPolygon ex;
if (py::isinstance<py::array>(entry)) {
ex.contour = parse_polygon(entry, who);
} else if (py::isinstance<py::sequence>(entry) && !py::isinstance<py::str>(entry)) {
py::sequence seq = py::reinterpret_borrow<py::sequence>(entry);
if (py::len(seq) < 1)
throw py::value_error(std::string(who) + ": a [contour, holes] entry needs a contour");
ex.contour = parse_polygon(seq[0], who);
if (py::len(seq) >= 2) {
// Type-check the holes element up front: a non-sequence (e.g. an int) would otherwise
// reach reinterpret_borrow<py::sequence> and raise a bare Python TypeError on iteration,
// whereas the API contract is ValueError for malformed input (str is excluded because it
// is iterable but never a valid holes container).
py::object holes_obj = seq[1];
if (!py::isinstance<py::sequence>(holes_obj) || py::isinstance<py::str>(holes_obj))
throw py::value_error(std::string(who) + ": the holes element must be a list of (N,2) int64 ndarrays");
for (py::handle hh : py::reinterpret_borrow<py::sequence>(holes_obj)) {
Polygon hole = parse_polygon(hh, who);
hole.make_clockwise();
ex.holes.emplace_back(std::move(hole));
}
}
} else {
throw py::value_error(std::string(who) + ": each entry must be an (N,2) ndarray or a [contour, holes] pair");
}
ex.contour.make_counter_clockwise();
return ex;
}
// A non-empty Python list of entries -> ExPolygons (each entry parsed + oriented).
static ExPolygons parse_expolygon_list(py::handle list_h, const char* who)
{
if (!py::isinstance<py::sequence>(list_h) || py::isinstance<py::str>(list_h))
throw py::value_error(std::string(who) + ": expected a list of polygons");
ExPolygons out;
for (py::handle entry : py::reinterpret_borrow<py::sequence>(list_h))
out.emplace_back(parse_expolygon(entry, who));
if (out.empty())
throw py::value_error(std::string(who) + ": expected a non-empty list of polygons");
return out;
}
// Build Surfaces from a Python list, carrying surface_type (and the other per-surface
// attributes) forward from the collection being replaced, or defaulting to stInternal when
// the region had no prior surfaces.
static Surfaces surfaces_from_py(py::handle list_h, const SurfaceCollection& replaced, const char* who)
{
ExPolygons ex = parse_expolygon_list(list_h, who);
const Surface tmpl = replaced.surfaces.empty() ? Surface(stInternal) : replaced.surfaces.front();
Surfaces out;
out.reserve(ex.size());
for (ExPolygon& e : ex)
out.emplace_back(Surface(tmpl, std::move(e)));
return out;
}
} // namespace
void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_<PluginCapabilityType>& pluginTypes) {
(void) pluginTypes; // matches gcode/script/printerAgent; Step is a fresh enum below.
auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental).");
py::enum_<SlicingPipelineStep>(slicing, "Step")
.value("Slice", SlicingPipelineStep::Slice)
.value("Perimeters", SlicingPipelineStep::Perimeters)
.value("EstimateCurledExtrusions", SlicingPipelineStep::EstimateCurledExtrusions)
.value("Infill", SlicingPipelineStep::Infill) // fires after prepare+infill
.value("Ironing", SlicingPipelineStep::Ironing)
.value("Contouring", SlicingPipelineStep::Contouring)
.value("SupportMaterial", SlicingPipelineStep::SupportMaterial)
.value("DetectOverhangsForLift", SlicingPipelineStep::DetectOverhangsForLift)
.value("SimplifyPath", SlicingPipelineStep::SimplifyPath) // covers all simplify sub-steps
.value("WipeTower", SlicingPipelineStep::WipeTower)
.value("SkirtBrim", SlicingPipelineStep::SkirtBrim)
.export_values();
// --- Read-graph geometry views (see header for the mandatory lifetime rule). ---
// Every array/view below is valid ONLY during the execute(ctx) call that produced it.
py::enum_<SurfaceType>(slicing, "SurfaceType")
.value("stTop", stTop)
.value("stBottom", stBottom)
.value("stBottomBridge", stBottomBridge)
.value("stInternalAfterExternalBridge", stInternalAfterExternalBridge)
.value("stInternal", stInternal)
.value("stInternalSolid", stInternalSolid)
.value("stInternalBridge", stInternalBridge)
.value("stSecondInternalBridge", stSecondInternalBridge)
.value("stInternalVoid", stInternalVoid)
.value("stPerimeter", stPerimeter)
.value("stCount", stCount)
.export_values();
// Scaled integer coordinate -> millimeters. Reads the live SCALING_FACTOR at call
// time (1e-6 normal, 1e-5 for beds > 2147mm), so it is never cached.
slicing.def("unscale", [](coord_t v) { return unscale<double>(v); }, py::arg("coord"),
"Convert a scaled integer coordinate to millimeters (reads the live SCALING_FACTOR).");
py::class_<ExPolygonView>(slicing, "ExPolygonView")
.def("contour", [](const ExPolygonView& v) { return polygon_rows(v.owner, v.ex->contour); },
"Outer contour as a read-only int64 (N,2) numpy view in scaled coords. "
"Valid only during the execute(ctx) call.")
.def("holes", [](const ExPolygonView& v) {
py::list out;
for (const Polygon& h : v.ex->holes)
out.append(polygon_rows(v.owner, h));
return out;
}, "List of hole contours (CW), each a read-only int64 (N,2) numpy view. "
"Valid only during the execute(ctx) call.");
py::class_<SurfaceView>(slicing, "SurfaceView")
.def_property_readonly("surface_type", [](const SurfaceView& v) { return v.s->surface_type; })
.def_property_readonly("thickness", [](const SurfaceView& v) { return v.s->thickness; })
.def_property_readonly("bridge_angle", [](const SurfaceView& v) { return v.s->bridge_angle; })
.def_property_readonly("extra_perimeters", [](const SurfaceView& v) { return v.s->extra_perimeters; })
.def_property_readonly("expolygon", [](const SurfaceView& v) {
return ExPolygonView{ &v.s->expolygon, v.owner };
}, "This surface's geometry as an ExPolygonView. Valid only during the execute(ctx) call.")
// MUTATOR (Task 11). Reclassify this surface's type (e.g. SurfaceType.stInternalSolid).
// set_type reassigns surface_type ONLY — it does not replace the geometry. Writes through
// the const view by const_cast (the Surface is non-const in the live slicing graph).
// Valid only during the execute(ctx) call.
.def("set_type", [](const SurfaceView& v, SurfaceType type) {
const_cast<Surface*>(v.s)->surface_type = type;
}, py::arg("surface_type"),
"Reclassify this surface's SurfaceType (reassigns surface_type only; the geometry "
"is unchanged). Valid only during the execute(ctx) call.");
// A flattened toolpath. Read-only in v1 (mutation is a later phase). role/width/
// height/mm3_per_mm are plain scalars; points() materializes a zero-copy array.
py::class_<PathData>(slicing, "PathData")
.def("points", [](const PathData& p) {
const Points3& pts = p.path->polyline.points;
return make_readonly_rows<coord_t, 3>(
p.owner, pts.empty() ? nullptr : pts.front().data(), (py::ssize_t) pts.size());
}, "Path vertices as a read-only int64 (N,3) numpy view in scaled coords "
"(the polyline is natively 3D on this branch). Valid only during the execute(ctx) call.")
.def_property_readonly("role", [](const PathData& p) {
return ExtrusionEntity::role_to_string(p.path->role());
}, "Extrusion role as a human-readable string (e.g. \"Outer wall\", \"Sparse infill\").")
.def_property_readonly("width", [](const PathData& p) { return p.path->width; })
.def_property_readonly("height", [](const PathData& p) { return p.path->height; })
.def_property_readonly("mm3_per_mm", [](const PathData& p) { return p.path->mm3_per_mm; });
py::class_<LayerRegionView>(slicing, "LayerRegionView")
.def("slices", [](const LayerRegionView& v) {
py::list out;
for (const Surface& s : v.r->slices.surfaces)
out.append(SurfaceView{ &s, v.owner });
return out;
}, "Sliced surfaces (typed top/bottom/internal) as [SurfaceView]. "
"Valid only during the execute(ctx) call.")
.def("fill_surfaces", [](const LayerRegionView& v) {
py::list out;
for (const Surface& s : v.r->fill_surfaces.surfaces)
out.append(SurfaceView{ &s, v.owner });
return out;
}, "Surfaces prepared for infill as [SurfaceView]. "
"Valid only during the execute(ctx) call.")
.def("perimeters", [](const LayerRegionView& v) {
return path_data_list(v.owner, v.r->perimeters);
}, "Perimeter toolpaths flattened to [PathData] (nested collections and "
"loops decomposed into their paths). Valid only during the execute(ctx) call.")
.def("fills", [](const LayerRegionView& v) {
return path_data_list(v.owner, v.r->fills);
}, "Infill toolpaths flattened to [PathData] (nested collections and loops "
"decomposed into their paths). Valid only during the execute(ctx) call.")
// MUTATOR (Task 11). Replace this region's sliced surfaces. `polygons` is a list of
// (N,2) int64 ndarrays (scaled coords) or [contour, [holes...]] pairs; orientation is
// normalized (contour CCW, holes CW) and surface_type is carried forward from the
// replaced surfaces (else stInternal). Writes through the const view by const_cast.
.def("set_slices", [](const LayerRegionView& v, py::object polygons) {
auto* region = const_cast<LayerRegion*>(v.r);
region->slices.set(surfaces_from_py(polygons, region->slices, "set_slices"));
}, py::arg("polygons"),
"Replace this region's sliced surfaces from a list of (N,2) int64 ndarrays (scaled "
"coords) or [contour, [holes...]] pairs (orientation normalized: contour CCW / holes "
"CW; surface_type carried forward from the replaced surfaces, else stInternal).\n"
"MUTATION-CASCADE: at the Slice boundary this is the primary, fully-supported entry "
"point -- the split slice loop runs make_perimeters() afterward, so the change cascades "
"into perimeters and everything downstream (final G-code).\n"
"PERSISTENCE (v1 limitation): the mutation is written into region->slices, but the "
"pre-hook geometry is also retained in each Layer's raw_slices backup (taken by "
"slice() BEFORE this hook fires). The mutation therefore survives only while posSlice "
"stays cached AND perimeters are not re-run from those restored raw slices: "
"make_perimeters() calls restore_untyped_slices(), which overwrites slices from "
"raw_slices, so a config change that re-runs perimeters without re-slicing (e.g. "
"wall_loops) silently reverts to the original geometry while posSlice stays cached "
"(this hook does NOT re-fire). Re-selecting the plugin -- or any other "
"posSlice-invalidating change -- re-fires this hook and re-applies the mutation. "
"Propagating the mutation into raw_slices is a known v1 limitation.\n"
"DUPLICATES: identical objects share Layer*, so the mutation on the object that slices "
"is automatically seen by its duplicates; objects that must mutate independently must "
"not be identical.\n"
"Raises ValueError on malformed input. Valid only during the execute(ctx) call.")
// MUTATOR (Task 11). Replace this region's fill (infill-prep) surfaces; identical input
// format and validation to set_slices.
.def("set_fill_surfaces", [](const LayerRegionView& v, py::object polygons) {
auto* region = const_cast<LayerRegion*>(v.r);
region->fill_surfaces.set(surfaces_from_py(polygons, region->fill_surfaces, "set_fill_surfaces"));
}, py::arg("polygons"),
"Replace this region's fill (infill-prep) surfaces; same input format/validation as "
"set_slices.\n"
"MUTATION-CASCADE: at the Infill boundary this changes the stored surfaces but does NOT "
"regenerate the already-built `fills` toolpaths in v1.\n"
"Raises ValueError on malformed input. Valid only during the execute(ctx) call.");
py::class_<LayerView>(slicing, "LayerView")
.def_property_readonly("slice_z", [](const LayerView& v) { return v.l->slice_z; })
.def_property_readonly("print_z", [](const LayerView& v) { return v.l->print_z; })
.def_property_readonly("height", [](const LayerView& v) { return v.l->height; })
.def("lslices", [](const LayerView& v) {
py::list out;
for (const ExPolygon& e : v.l->lslices)
out.append(ExPolygonView{ &e, v.owner });
return out;
}, "Merged per-layer islands as [ExPolygonView]. "
"Valid only during the execute(ctx) call.")
.def("regions", [](const LayerView& v) {
py::list out;
for (const LayerRegion* r : v.l->regions())
out.append(LayerRegionView{ r, v.owner });
return out;
}, "Per-region views as [LayerRegionView]. "
"Valid only during the execute(ctx) call.")
// MUTATOR (Task 11). Replace this layer's merged islands (lslices) and refresh the
// cache-invariant `lslices_bboxes` (one BoundingBox per island via get_extents). Same
// input format/validation as LayerRegionView.set_slices. Writes through the const view
// by const_cast.
.def("set_lslices", [](const LayerView& v, py::object islands) {
auto* layer = const_cast<Layer*>(v.l);
layer->lslices = parse_expolygon_list(islands, "set_lslices");
layer->lslices_bboxes.clear();
layer->lslices_bboxes.reserve(layer->lslices.size());
for (const ExPolygon& island : layer->lslices)
layer->lslices_bboxes.emplace_back(get_extents(island));
}, py::arg("islands"),
"Replace this layer's merged islands (lslices) from a list of (N,2) int64 ndarrays "
"(scaled coords) or [contour, [holes...]] pairs, and refresh lslices_bboxes (one "
"bounding box per island via get_extents) so the bbox cache stays consistent. Same "
"input format/validation as LayerRegionView.set_slices. Raises ValueError on malformed "
"input. Valid only during the execute(ctx) call.");
py::class_<PrintObjectView>(slicing, "PrintObjectView")
.def("layers", [](const PrintObjectView& v) {
py::list out;
for (const Layer* l : v.o->layers())
out.append(LayerView{ l, v.owner });
return out;
}, "Object layers as [LayerView]. Valid only during the execute(ctx) call.");
py::class_<SlicingPipelineContext>(slicing, "SlicingPipelineContext")
.def_readonly("orca_version", &SlicingPipelineContext::orca_version)
.def_readonly("step", &SlicingPipelineContext::step)
.def_property_readonly("object", [](const SlicingPipelineContext& ctx) -> py::object {
if (ctx.object == nullptr)
return py::none();
return py::cast(PrintObjectView{ ctx.object, ctx.owner });
}, "PrintObjectView for object-scoped steps, or None for print-wide steps. "
"Valid only during the execute(ctx) call.")
.def("cancelled", &SlicingPipelineContext::cancelled);
py::class_<SlicingPipelinePluginCapability, PluginCapabilityInterface,
PySlicingPipelinePluginCapabilityTrampoline,
std::shared_ptr<SlicingPipelinePluginCapability>>(slicing, "SlicingPipelineCapabilityBase")
.def(py::init<>())
.def("get_type", &SlicingPipelinePluginCapability::get_type)
.def("execute", &SlicingPipelinePluginCapability::execute);
}
} // namespace Slic3r

View File

@@ -0,0 +1,65 @@
#pragma once
#include "slic3r/plugin/PythonPluginInterface.hpp"
#include "libslic3r/Print.hpp" // SlicingPipelineStep, PrintObject
#include "libslic3r/Layer.hpp" // Layer, LayerRegion, SurfaceCollection
#include "libslic3r/Surface.hpp" // Surface, SurfaceType
#include "libslic3r/ExPolygon.hpp" // ExPolygon, Polygon
#include <pybind11/pybind11.h>
#include <string>
namespace Slic3r {
// ---------------------------------------------------------------------------
// Read-graph geometry views (Task 8).
//
// LIFETIME (mandatory): each view is a thin, non-owning wrapper holding a raw
// pointer into a buffer owned by the Print / PrintObject that the slicing
// pipeline mutates and frees between steps. A view — and every numpy array a
// view hands out (ExPolygonView::contour()/holes()) — is valid ONLY for the
// duration of the execute(ctx) call that produced it. The `owner` capsule pins
// the owning SlicingPipelineContext's Print* alive for the array's lifetime,
// but the underlying std::vector storage may be reallocated by the next
// pipeline step, so a Python plugin MUST NOT stash a view or an array across
// execute() calls or read one after execute() returns. Read now, copy what you
// need, and let the views go.
//
// Read accessors are zero-copy and non-owning as described above. The 2D-geometry
// mutators added in Task 11 (LayerRegionView.set_slices/set_fill_surfaces,
// LayerView.set_lslices, SurfaceView.set_type) write THROUGH these const views by
// const_cast: the pointed-to Layer/LayerRegion/Surface are genuinely non-const
// (owned mutably by the Print; the dispatcher merely hands them out as const), the
// same pattern the C++ slicing-pipeline hook uses. Mutations take effect on the live
// slicing graph and cascade per the per-method contract documented in the bindings.
// ---------------------------------------------------------------------------
struct ExPolygonView { const ExPolygon* ex; pybind11::capsule owner; };
struct SurfaceView { const Surface* s; pybind11::capsule owner; };
struct LayerRegionView { const LayerRegion* r; pybind11::capsule owner; };
struct LayerView { const Layer* l; pybind11::capsule owner; };
struct PrintObjectView { const PrintObject* o; pybind11::capsule owner; };
// A single flattened toolpath (Task 9). `path` points into a Print-owned
// ExtrusionEntityCollection (a LayerRegion's `perimeters`/`fills`); like every
// view above it is non-owning and valid ONLY during the producing execute(ctx)
// call, with `owner` pinning that Print* alive for any array points() hands out.
struct PathData { const ExtrusionPath* path; pybind11::capsule owner; };
struct SlicingPipelineContext {
std::string orca_version;
SlicingPipelineStep step { SlicingPipelineStep::Slice };
Print* print { nullptr }; // always present
const PrintObject* object { nullptr }; // null for print-wide steps
// Capsule pinning `print` alive for any zero-copy array a view hands out.
// Populated by Task 10's dispatcher; a default (empty) capsule is fine for
// print-wide steps and for unit tests exercising views over static data.
pybind11::capsule owner;
bool cancelled() const; // -> print->canceled()
};
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {
public:
PluginCapabilityType get_type() const override { return PluginCapabilityType::SlicingPipeline; }
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
};
} // namespace Slic3r

View File

@@ -0,0 +1,16 @@
#pragma once
#include "SlicingPipelinePluginCapability.hpp"
#include "slic3r/plugin/PyPluginTrampoline.hpp"
namespace Slic3r {
class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline<SlicingPipelinePluginCapability> {
public:
using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute(SlicingPipelineContext& ctx) override {
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[]{}, PYBIND11_OVERRIDE_PURE,
ExecutionResult, SlicingPipelinePluginCapability, execute, ctx);
}
};
} // namespace Slic3r