feat(plugin)!: merge G-code post-processing into the slicing pipeline as psGCodePostProcess

G-code post-processing is now a step of the slicing-pipeline plugin rather than a
separate capability type. One capability class can transform slices at the geometry
seams AND edit the final G-code, behind a single picker/option.

- Add SlicingPipelineStepPlugin::psGCodePostProcess (bound as
  orca.slicing.Step.psGCodePostProcess). Unlike the geometry steps it fires from the
  GUI export path in PostProcessor.cpp, not from Print::process(): ctx.print/ctx.object
  are None and the plugin edits the file at ctx.gcode_path in place. It may run more
  than once per slice (file export and/or upload) and its output is not shown in the
  preview.
- Extend SlicingPipelineContext with gcode_path/host/output_name and a C++-only
  full_config; config_value() falls back to it when there is no live Print.
- PostProcessor.cpp dispatches SlicingPipelinePluginCapability at psGCodePostProcess,
  driven by the existing slicing_pipeline_plugin option.
- The exported G-code lives outside data_dir(), so the plugin audit sandbox would
  block the write; the trampoline's audit setup grants ctx.gcode_path's folder as a
  scoped allowed root, gated on a non-empty gcode_path so the geometry-step hooks gain
  no extra filesystem access.

BREAKING CHANGE: the separate G-code post-processing capability type is removed.
- orca.gcode.GCodePluginCapabilityBase and orca.PluginType.PostProcessing are gone;
  post-processing plugins migrate to orca.slicing.SlicingPipelineCapabilityBase +
  Step.psGCodePostProcess (and gain ctx.params / ctx.config_value()).
- The post_process_plugin config option is removed; use slicing_pipeline_plugin.
  Presets carrying the old key degrade to the standard unknown-key warning.
- Manifest type = "post-processing" now maps to Unknown (advisory only; the loader
  dispatches on the C++ get_type()).

Also repairs two latent build breaks the branch carried: stale Step enum value usages
in test_slicing_pipeline_hook.cpp and a reference to the removed
ConfigOptionDef::PluginType::None in Tab::on_value_change (now is_plugin_backed()).
Adds the orca_gcode_stamp sample plugin and a psGCodePostProcess binding test.
This commit is contained in:
SoftFever
2026-07-10 19:57:35 +08:00
parent 21ed68963f
commit 19352215da
25 changed files with 288 additions and 200 deletions

View File

@@ -610,7 +610,6 @@ bool PluginLoader::unload_plugin(const std::string& plugin_key, PluginCapability
if (!torn_down_types.insert(cap_type).second)
continue;
switch (cap_type) {
case PluginCapabilityType::PostProcessing: break;
case PluginCapabilityType::PrinterConnection: NetworkAgentFactory::deregister_python_plugin(plugin_key); break;
default: break;
}

View File

@@ -13,7 +13,6 @@
#include "PluginHostApi.hpp"
#include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp"
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
@@ -287,7 +286,6 @@ void bind_python_api(pybind11::module_& m)
m.doc() = "OrcaSlicer plugin API";
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PostProcessing", PluginCapabilityType::PostProcessing)
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Automation", PluginCapabilityType::Automation)
.value("Analysis", PluginCapabilityType::Analysis)
@@ -336,7 +334,6 @@ void bind_python_api(pybind11::module_& m)
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
// Make sure you register your bindings here
GCodePluginCapability::RegisterBindings(m, pluginTypes);
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);

View File

@@ -10,12 +10,11 @@
namespace Slic3r {
enum class PluginCapabilityType { PostProcessing = 0, PrinterConnection, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{
switch (type) {
case PluginCapabilityType::PostProcessing: return "post-processing";
case PluginCapabilityType::PrinterConnection: return "printer-connection";
case PluginCapabilityType::Automation: return "automation";
case PluginCapabilityType::Analysis: return "analysis";
@@ -31,7 +30,6 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
inline std::string plugin_capability_type_display_name(PluginCapabilityType type)
{
switch (type) {
case PluginCapabilityType::PostProcessing: return "Post-processing";
case PluginCapabilityType::PrinterConnection: return "Printer connection";
case PluginCapabilityType::Automation: return "Automation";
case PluginCapabilityType::Analysis: return "Analysis";
@@ -53,8 +51,6 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
lowered.push_back(to_lower(ch));
}
if (lowered == "post-processing")
return PluginCapabilityType::PostProcessing;
if (lowered == "printer-connection")
return PluginCapabilityType::PrinterConnection;
if (lowered == "automation")

View File

@@ -1,30 +0,0 @@
#include "GCodePluginCapability.hpp"
#include "GCodePluginCapabilityTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
namespace Slic3r {
void GCodePluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
{
(void) pluginTypes;
auto gcode = module.def_submodule("gcode", "G-code API");
py::class_<GCodePluginContext, PluginContext>(gcode, "GCodePluginContext", "Context shared with G-code plugins")
.def(py::init<>())
.def_readwrite("gcode_path", &GCodePluginContext::gcode_path)
.def_readwrite("host", &GCodePluginContext::host)
.def_readwrite("output_name", &GCodePluginContext::output_name);
py::class_<GCodePluginCapability, PluginCapabilityInterface, PyGCodePluginCapabilityTrampoline, std::shared_ptr<GCodePluginCapability>>(gcode, "GCodePluginCapabilityBase")
.def(py::init<>())
.def("get_type", &GCodePluginCapability::get_type)
.def("execute", &GCodePluginCapability::execute);
}
} // namespace Slic3r

View File

@@ -1,27 +0,0 @@
#ifndef slic3r_GCodePluginCapability_hpp_
#define slic3r_GCodePluginCapability_hpp_
#include "../../PythonPluginInterface.hpp"
namespace Slic3r {
struct GCodePluginContext : public PluginContext {
std::string gcode_path;
std::string host;
std::string output_name;
};
class GCodePluginCapability : public PluginCapabilityInterface
{
public:
PluginCapabilityType get_type() const override { return PluginCapabilityType::PostProcessing; }
virtual ExecutionResult execute(const GCodePluginContext& ctx) = 0;
static void RegisterBindings(pybind11::module_ &module,
pybind11::enum_<PluginCapabilityType> &pluginTypes);
};
} // namespace Slic3r
#endif /* slic3r_GCodePluginCapability_hpp_ */

View File

@@ -1,35 +0,0 @@
#ifndef slic3r_GCodePluginCapabilityTrampoline_hpp_
#define slic3r_GCodePluginCapabilityTrampoline_hpp_
#include <filesystem>
#include "../../PyPluginTrampoline.hpp"
#include "../../PluginAuditManager.hpp"
#include "GCodePluginCapability.hpp"
namespace Slic3r {
class PyGCodePluginCapabilityTrampoline : public PyPluginCommonTrampoline<GCodePluginCapability>
{
public:
using PyPluginCommonTrampoline<GCodePluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute(const GCodePluginContext& ctx) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[&] {
// G-code post-processing plugins may also write into the folder holding the
// current temp G-code file, in addition to the globally-allowed data_dir().
// The setup callback runs AFTER the context is constructed so the scoped root
// is not cleared by ScopedPluginAuditContext's constructor.
if (!ctx.gcode_path.empty())
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
std::filesystem::path(ctx.gcode_path).parent_path());
},
PYBIND11_OVERRIDE_PURE, ExecutionResult, GCodePluginCapability, execute, ctx);
}
};
} // namespace Slic3r
#endif

View File

@@ -26,6 +26,13 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
.value("posSimplifyPath", SlicingPipelineStepPlugin::posSimplifyPath) // covers all simplify sub-steps
.value("psWipeTower", SlicingPipelineStepPlugin::psWipeTower)
.value("psSkirtBrim", SlicingPipelineStepPlugin::psSkirtBrim)
// Post-process seam: fires in the GUI export path AFTER the classic post_process scripts, on the
// exported G-code file. Unlike every step above it is NOT fired by Print::process(): ctx.print and
// ctx.object are None; instead ctx.gcode_path / ctx.host / ctx.output_name are set and the plugin
// edits the file at ctx.gcode_path IN PLACE. May fire more than once per slice (file export and/or
// upload each fire once, on separate working copies) and its output is not reflected in the G-code
// preview (the viewer maps the pre-post-process file). ctx.config_value()/ctx.params still work.
.value("psGCodePostProcess", SlicingPipelineStepPlugin::psGCodePostProcess)
.export_values();
// The read-graph data model (Surface / ExPolygon / the extrusion tree / LayerRegion /
@@ -44,6 +51,14 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
.def_readonly("params", &SlicingPipelineContext::params,
"read-only dict of this plugin's [tool.orcaslicer.plugin.settings] values "
"(string->string). Parse the values you need, e.g. float(ctx.params['rate']).")
.def_readonly("gcode_path", &SlicingPipelineContext::gcode_path,
"Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in "
"place; empty at every other step.")
.def_readonly("host", &SlicingPipelineContext::host,
"Target host at Step.psGCodePostProcess (\"File\", \"OctoPrint\", ...); empty otherwise.")
.def_readonly("output_name", &SlicingPipelineContext::output_name,
"Final output G-code name at Step.psGCodePostProcess (mirrors SLIC3R_PP_OUTPUT_NAME); "
"empty otherwise.")
.def_property_readonly("print", [](const SlicingPipelineContext& ctx) -> py::object {
if (ctx.print == nullptr)
return py::none();
@@ -61,9 +76,13 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
}, "orca.host.PrintObject for object-scoped steps, or None for print-wide steps. "
"Valid only during the execute(ctx) call.")
.def("config_value", [](const SlicingPipelineContext& ctx, const std::string& key) -> py::object {
if (ctx.print == nullptr)
return py::none();
return config_value_or_none(ctx.print->full_print_config(), key);
// In-pipeline steps read the live Print's full config; at psGCodePostProcess (print == null)
// fall back to the config the export path handed in.
if (ctx.print != nullptr)
return config_value_or_none(ctx.print->full_print_config(), key);
if (ctx.full_config != nullptr)
return config_value_or_none(*ctx.full_config, key);
return py::none();
}, py::arg("key"),
"serialized value of a resolved (full) print config option for this slice, or "
"None if absent. Shorthand for ctx.print.config_value(key).")

View File

@@ -16,13 +16,23 @@ namespace Slic3r {
struct SlicingPipelineContext {
std::string orca_version;
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
Print* print { nullptr }; // always present when dispatched
const PrintObject* object { nullptr }; // null for print-wide steps
Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
const PrintObject* object { nullptr }; // null for print-wide steps and psGCodePostProcess
// read-only per-plugin settings, populated by the dispatcher from the
// plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as
// ctx.params (dict of string->string).
std::map<std::string, std::string> params;
bool cancelled() const; // -> print->canceled()
// Populated ONLY at Step.psGCodePostProcess (the GUI G-code export/post-process seam,
// PostProcessor.cpp). gcode_path is the working G-code file on disk that the plugin edits
// in place; host is the target ("File", "OctoPrint", ...); output_name mirrors
// SLIC3R_PP_OUTPUT_NAME. Empty at every other step.
std::string gcode_path;
std::string host;
std::string output_name;
// C++-only config fallback for psGCodePostProcess (no live Print graph there): config_value()
// reads it when `print` is null. Not exposed to Python directly. Never dereferenced elsewhere.
const DynamicPrintConfig* full_config { nullptr };
bool cancelled() const; // -> print->canceled() (false when print is null)
};
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {

View File

@@ -1,6 +1,8 @@
#pragma once
#include "SlicingPipelinePluginCapability.hpp"
#include "slic3r/plugin/PyPluginTrampoline.hpp"
#include "slic3r/plugin/PluginAuditManager.hpp"
#include <filesystem>
namespace Slic3r {
class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline<SlicingPipelinePluginCapability> {
@@ -9,7 +11,18 @@ public:
ExecutionResult execute(SlicingPipelineContext& ctx) override {
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[]{}, PYBIND11_OVERRIDE_PURE,
[&]{
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
// outside data_dir() (a temp/output folder), so writing to it would otherwise be
// blocked by the audit sandbox. Grant that folder as a scoped allowed root, mirroring
// the former G-code post-processing trampoline. The setup callback runs AFTER the
// audit context is constructed, so the scoped root is not cleared by its constructor.
// Empty at every other step, so no extra access is granted to the geometry hooks.
if (!ctx.gcode_path.empty())
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
std::filesystem::path(ctx.gcode_path).parent_path());
},
PYBIND11_OVERRIDE_PURE,
ExecutionResult, SlicingPipelinePluginCapability, execute, ctx);
}
};