mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-18 08:22:06 +00:00
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:
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user