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

@@ -0,0 +1,74 @@
# /// script
# requires-python = ">=3.12"
#
# [tool.orcaslicer.plugin]
# name = "G-code Stamp"
# description = "Stamps a comment line into the exported G-code at the post-process step (demo)."
# author = "OrcaSlicer"
# version = "0.01"
# type = "slicing-pipeline"
#
# [tool.orcaslicer.plugin.settings]
# stamp_text = "processed by the OrcaSlicer G-code Stamp plugin"
# ///
"""G-code Stamp -- the post-processing half of the slicing-pipeline plugin.
Post-processing is now a step of the slicing pipeline: Step.psGCodePostProcess.
It fires from the G-code export path AFTER the classic post_process scripts, on the
exported G-code file -- NOT from Print::process(). So unlike the geometry steps
(posSlice, posPerimeters, ...) there is no live slicing graph here: ctx.print and
ctx.object are None. Instead the context carries ctx.gcode_path (the working G-code
file on disk, edited IN PLACE), ctx.host ("File", "OctoPrint", ...) and
ctx.output_name (the final file name). ctx.params and ctx.config_value() still work.
This sample inserts a single comment line near the top of the file. Because the same
capability class can also implement the geometry steps, one plugin can transform slices
AND stamp the final G-code; a geometry-only plugin just returns success here.
The step may fire more than once per slice (file export and/or upload each run it on a
separate working copy), and its output is not reflected in the G-code preview -- the
viewer maps the pre-post-process file.
"""
import orca
_DEFAULT_STAMP = "processed by the OrcaSlicer G-code Stamp plugin"
def _stamp_text(ctx):
try:
text = dict(ctx.params).get("stamp_text", _DEFAULT_STAMP)
except (AttributeError, TypeError):
text = _DEFAULT_STAMP
return str(text).replace("\n", " ").strip() or _DEFAULT_STAMP
class GCodeStamp(orca.slicing.SlicingPipelineCapabilityBase):
def get_name(self):
return "G-code Stamp"
def execute(self, ctx):
# Only act at the post-process seam; at every geometry step this is a no-op.
if ctx.step != orca.slicing.Step.psGCodePostProcess:
return orca.ExecutionResult.success()
if not ctx.gcode_path:
return orca.ExecutionResult.success("G-code Stamp: no gcode_path, nothing to do")
comment = "; " + _stamp_text(ctx) + " (host=" + (ctx.host or "?") + ")\n"
# Edit the exported G-code in place: keep the original first line first (some flavors
# expect a specific leading line), then insert the stamp right after it.
with open(ctx.gcode_path, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
insert_at = 1 if lines else 0
lines.insert(insert_at, comment)
with open(ctx.gcode_path, "w", encoding="utf-8") as f:
f.writelines(lines)
return orca.ExecutionResult.success(
"G-code Stamp: stamped '" + (ctx.output_name or ctx.gcode_path) + "'")
@orca.plugin
class GCodeStampPackage(orca.base):
def register_capabilities(self):
orca.register_capability(GCodeStamp)

View File

@@ -2444,9 +2444,9 @@ public:
// "serialized" - vector valued option is entered in a single edit field. Values are separated by a semicolon. // "serialized" - vector valued option is entered in a single edit field. Values are separated by a semicolon.
// "show_value" - even if enum_values / enum_labels are set, still display the value, not the enum label. // "show_value" - even if enum_values / enum_labels are set, still display the value, not the enum label.
std::string gui_flags; std::string gui_flags;
// Capability type of a plugin-backed option, e.g. "post-processing" / "slicing-pipeline" / // Capability type of a plugin-backed option, e.g. "slicing-pipeline" / "printer-connection"
// "printer-connection" (empty for ordinary options). GUIType::plugin_picker filters the plugin // (empty for ordinary options). GUIType::plugin_picker filters the plugin list by it, and it
// list by it, and it resolves the option's "plugins" manifest reference; see is_plugin_backed(). // resolves the option's "plugins" manifest reference; see is_plugin_backed().
std::string plugin_type; std::string plugin_type;
// Whether this option holds plugin capability name(s) that feed the "plugins" manifest -- true // Whether this option holds plugin capability name(s) that feed the "plugins" manifest -- true
// iff it declares a plugin_type. Setting plugin_type is the only step needed to add one. // iff it declares a plugin_type. Setting plugin_type is the only step needed to add one.

View File

@@ -1192,7 +1192,6 @@ static std::vector<std::string> s_Preset_print_options{
"min_feature_size", "min_feature_size",
"min_bead_width", "min_bead_width",
"post_process", "post_process",
"post_process_plugin",
"slicing_pipeline_plugin", "slicing_pipeline_plugin",
"plugins", "plugins",
"process_change_extrusion_role_gcode", "process_change_extrusion_role_gcode",

View File

@@ -125,8 +125,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"printing_by_object_gcode", "printing_by_object_gcode",
"filament_end_gcode", "filament_end_gcode",
"post_process", "post_process",
"post_process_plugin", // "plugins" is the derived manifest backing the plugin-picker options; on its own it only
// "plugins" is the manifest backing post_process_plugin; like it, it only affects G-code export. // affects G-code export. The specific option (e.g. slicing_pipeline_plugin) drives any re-slice.
"plugins", "plugins",
"extruder_clearance_height_to_rod", "extruder_clearance_height_to_rod",
"extruder_clearance_height_to_lid", "extruder_clearance_height_to_lid",

View File

@@ -101,7 +101,10 @@ enum PrintObjectStep {
enum class SlicingPipelineStepPlugin { enum class SlicingPipelineStepPlugin {
posSlice, posPerimeters, posEstimateCurledExtrusions, posPrepareInfill, posInfill, posIroning, posContouring, posSlice, posPerimeters, posEstimateCurledExtrusions, posPrepareInfill, posInfill, posIroning, posContouring,
posSupportMaterial, posDetectOverhangsForLift, posSimplifyPath, psWipeTower, psSkirtBrim posSupportMaterial, posDetectOverhangsForLift, posSimplifyPath, psWipeTower, psSkirtBrim,
// Fires from the GUI G-code export/post-process seam (PostProcessor.cpp), NOT from Print::process().
// At this step the plugin edits the exported G-code file in place; see the binding for the full contract.
psGCodePostProcess
}; };
// A PrintRegion object represents a group of volumes to print // A PrintRegion object represents a group of volumes to print

View File

@@ -5114,20 +5114,10 @@ void PrintConfigDef::init_fff_params()
def->mode = comDevelop; def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings()); def->set_default_value(new ConfigOptionStrings());
def = this->add("post_process_plugin", coStrings);
def->label = L("Post-processing Plugin");
def->tooltip = L("Select a Python plugin to process the output G-code. "
"Plugins are loaded from the orca_plugins directory in your data folder. "
"The plugin will receive the G-code file path and can modify it in place.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "post-processing";
def->full_width = true;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("slicing_pipeline_plugin", coStrings); def = this->add("slicing_pipeline_plugin", coStrings);
def->label = L("Slicing Pipeline Plugin"); 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->tooltip = L("Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, "
"including a final G-code post-processing step. Research/experimental.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker; def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "slicing-pipeline"; def->plugin_type = "slicing-pipeline";
def->full_width = true; def->full_width = true;

View File

@@ -1570,7 +1570,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, ooze_prevention)) ((ConfigOptionBool, ooze_prevention))
((ConfigOptionString, filename_format)) ((ConfigOptionString, filename_format))
((ConfigOptionStrings, post_process)) ((ConfigOptionStrings, post_process))
((ConfigOptionStrings, post_process_plugin))
((ConfigOptionStrings, slicing_pipeline_plugin)) ((ConfigOptionStrings, slicing_pipeline_plugin))
((ConfigOptionString, printer_model)) ((ConfigOptionString, printer_model))
((ConfigOptionFloat, resolution)) ((ConfigOptionFloat, resolution))

View File

@@ -618,9 +618,6 @@ set(SLIC3R_GUI_SOURCES
plugin/PluginAuditManager.hpp plugin/PluginAuditManager.hpp
plugin/PluginResolver.cpp plugin/PluginResolver.cpp
plugin/PluginResolver.hpp plugin/PluginResolver.hpp
plugin/pluginTypes/gcode/GCodePluginCapability.hpp
plugin/pluginTypes/gcode/GCodePluginCapability.cpp
plugin/pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp

View File

@@ -3182,7 +3182,7 @@ bool GUI_App::on_init_inner()
static const char* const kStepNames[] = { static const char* const kStepNames[] = {
"posSlice", "posPerimeters", "posEstimateCurledExtrusions", "posPrepareInfill", "posInfill", "posSlice", "posPerimeters", "posEstimateCurledExtrusions", "posPrepareInfill", "posInfill",
"posIroning", "posContouring", "posSupportMaterial", "posDetectOverhangsForLift", "posIroning", "posContouring", "posSupportMaterial", "posDetectOverhangsForLift",
"posSimplifyPath", "psWipeTower", "psSkirtBrim" "posSimplifyPath", "psWipeTower", "psSkirtBrim", "psGCodePostProcess"
}; // order must match Slic3r::SlicingPipelineStepPlugin }; // order must match Slic3r::SlicingPipelineStepPlugin
const char* step_name = static_cast<size_t>(step) < sizeof(kStepNames) / sizeof(kStepNames[0]) const char* step_name = static_cast<size_t>(step) < sizeof(kStepNames) / sizeof(kStepNames[0])
? kStepNames[static_cast<int>(step)] : "Unknown"; ? kStepNames[static_cast<int>(step)] : "Unknown";

View File

@@ -9,7 +9,7 @@
// file lives in the GUI layer (libslic3r must not depend on pybind11 / PluginManager). // file lives in the GUI layer (libslic3r must not depend on pybind11 / PluginManager).
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.hpp" #include "slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp" #include "slic3r/plugin/PythonInterpreter.hpp"
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
@@ -241,27 +241,39 @@ void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& co
fs.close(); fs.close();
} }
// Run the configured post-processing plugins on `gcode_path` in place. Plugins are executed in-process // Run the configured slicing-pipeline plugins on `gcode_path` in place, at their Step.psGCodePostProcess
// through the embedded Python interpreter. Throws Slic3r::RuntimeError on any failure; the caller is // seam. This is the same capability that runs at the geometry seams inside Print::process(); here it is
// responsible for removing the working copy (see run_post_process_scripts' catch block). // dispatched a final time on the exported G-code, so a plugin can edit slices AND the final G-code from
// Entries are bare capability names; the top-level plugins manifest carries the full plugin refs. // one class. Plugins are executed in-process through the embedded Python interpreter. Throws
// Slic3r::RuntimeError on any failure; the caller removes the working copy (see run_post_process_scripts'
// catch block). Entries are bare capability names; the top-level plugins manifest carries the full refs.
// A geometry-only plugin simply returns success here (it filters on ctx.step), so it costs nothing beyond
// one no-op call, but note any configured pipeline plugin still engages this post-process path (i.e. the
// non-BBL ".pp" working copy) even if it does no G-code work.
static void run_post_process_plugins(const ConfigOptionStrings& capabilities, static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
const ConfigOptionStrings* plugins, const ConfigOptionStrings* plugins,
const std::string& gcode_path, const std::string& gcode_path,
const std::string& host, const std::string& host,
const std::string& output_name) const std::string& output_name,
const DynamicPrintConfig& config)
{ {
// Let plugins observe the (possibly script-updated) target file name, mirroring the script env. // Let plugins observe the (possibly script-updated) target file name, mirroring the script env.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1); boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
const boost::filesystem::path gcode_file(gcode_path); const boost::filesystem::path gcode_file(gcode_path);
auto execute_fn = [&](std::shared_ptr<GCodePluginCapability> cap, const PluginCapabilityRef& ref) { auto execute_fn = [&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
GCodePluginContext ctx; SlicingPipelineContext ctx;
ctx.orca_version = SoftFever_VERSION; ctx.orca_version = SoftFever_VERSION;
ctx.step = SlicingPipelineStepPlugin::psGCodePostProcess;
ctx.gcode_path = gcode_path; ctx.gcode_path = gcode_path;
ctx.host = host; ctx.host = host;
ctx.output_name = output_name; ctx.output_name = output_name;
ctx.full_config = &config; // no live Print here; config_value() reads this
// Hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params (same plugin_key the
// capability was resolved by), mirroring the in-pipeline dispatcher in GUI_App.cpp.
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key);
ExecutionResult exec_result; ExecutionResult exec_result;
try { try {
@@ -298,11 +310,12 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << " completed successfully"; BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << " completed successfully";
}; };
execute_capabilities_from_refs<GCodePluginCapability>(capabilities, plugins, PluginCapabilityType::PostProcessing, execute_fn); execute_capabilities_from_refs<SlicingPipelinePluginCapability>(capabilities, plugins, PluginCapabilityType::SlicingPipeline, execute_fn);
} }
// Run post-processing scripts ("post_process") and/or post-processing plugins ("post_process_plugin") // Run post-processing scripts ("post_process") and/or the slicing-pipeline plugins' psGCodePostProcess
// if defined. Both run on the same working copy of the G-code (the ".pp" temp when make_copy), so a // step ("slicing_pipeline_plugin") if defined. Both run on the same working copy of the G-code (the
// ".pp" temp when make_copy), so a
// plugin never opens the original file the G-code viewer keeps memory-mapped (a writable open of the // plugin never opens the original file the G-code viewer keeps memory-mapped (a writable open of the
// mapped file fails on Windows with a sharing violation). // mapped file fails on Windows with a sharing violation).
// Returns true if a script or plugin was executed. // Returns true if a script or plugin was executed.
@@ -317,11 +330,13 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
bool run_post_process_scripts( bool run_post_process_scripts(
std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config) std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config)
{ {
// post_process / post_process_plugin are absent in SLA mode, hence the null checks. // post_process / slicing_pipeline_plugin are absent in SLA mode, hence the null checks. G-code
const auto* post_process = config.opt<ConfigOptionStrings>("post_process"); // post-processing is now the psGCodePostProcess step of the slicing-pipeline plugin, so the same
const auto* post_process_plugin = config.opt<ConfigOptionStrings>("post_process_plugin"); // slicing_pipeline_plugin option drives both the geometry seams and this final G-code seam.
const bool have_scripts = post_process != nullptr && !post_process->values.empty(); const auto* post_process = config.opt<ConfigOptionStrings>("post_process");
const bool have_plugins = post_process_plugin != nullptr && !post_process_plugin->values.empty(); const auto* slicing_pipeline_plugin = config.opt<ConfigOptionStrings>("slicing_pipeline_plugin");
const bool have_scripts = post_process != nullptr && !post_process->values.empty();
const bool have_plugins = slicing_pipeline_plugin != nullptr && !slicing_pipeline_plugin->values.empty();
if (!have_scripts && !have_plugins) if (!have_scripts && !have_plugins)
return false; return false;
@@ -469,7 +484,7 @@ bool run_post_process_scripts(
// Run plugins after the scripts so they observe any output_name the scripts produced. A thrown // Run plugins after the scripts so they observe any output_name the scripts produced. A thrown
// exception is handled by the catch below, which removes the temp copy. // exception is handled by the catch below, which removes the temp copy.
if (have_plugins) { if (have_plugins) {
run_post_process_plugins(*post_process_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name); run_post_process_plugins(*slicing_pipeline_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name, config);
} }
} catch (...) { } catch (...) {
remove_output_name_file(); remove_output_name_file();

View File

@@ -9,9 +9,10 @@
namespace Slic3r { namespace Slic3r {
// Run post-processing scripts (the "post_process" option) and/or post-processing plugins (the // Run post-processing scripts (the "post_process" option) and/or the slicing-pipeline plugins'
// "post_process_plugin" option) if defined. Lives in the GUI layer because plugins are executed // Step.psGCodePostProcess seam (the "slicing_pipeline_plugin" option) if defined. Lives in the GUI
// through the embedded-Python PluginManager, which libslic3r must not depend on. // layer because plugins are executed through the embedded-Python PluginManager, which libslic3r must
// not depend on.
// Returns true if a script or plugin was executed. // Returns true if a script or plugin was executed.
// Returns false if neither a post-processing script nor plugin was defined. // Returns false if neither a post-processing script nor plugin was defined.
// Throws an exception on error. // Throws an exception on error.

View File

@@ -1794,7 +1794,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// always carries resolved "name;uuid;capability" references that full_config() and save_to_json() // always carries resolved "name;uuid;capability" references that full_config() and save_to_json()
// then pass downstream as-is -- no separate rebuild anywhere else. // then pass downstream as-is -- no separate rebuild anywhere else.
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key); if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
opt_def && opt_def->plugin_type != ConfigOptionDef::PluginType::None) opt_def && opt_def->is_plugin_backed())
m_config->update_plugin_manifest(); m_config->update_plugin_manifest();
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) { if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
@@ -3080,12 +3080,6 @@ void TabPrint::build()
option.opt.height = 15; option.opt.height = 15;
optgroup->append_single_option_line(option, "others_settings_post_processing_scripts"); optgroup->append_single_option_line(option, "others_settings_post_processing_scripts");
optgroup = page->new_optgroup(L("Post-processing Plugin"), L"param_gcode", 0);
optgroup->hide_labels();
option = optgroup->get_option("post_process_plugin");
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 = page->new_optgroup(L("Slicing Pipeline Plugin"), L"param_gcode", 0);
optgroup->hide_labels(); optgroup->hide_labels();
option = optgroup->get_option("slicing_pipeline_plugin"); option = optgroup->get_option("slicing_pipeline_plugin");

View File

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

View File

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

View File

@@ -10,12 +10,11 @@
namespace Slic3r { 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) inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{ {
switch (type) { switch (type) {
case PluginCapabilityType::PostProcessing: return "post-processing";
case PluginCapabilityType::PrinterConnection: return "printer-connection"; case PluginCapabilityType::PrinterConnection: return "printer-connection";
case PluginCapabilityType::Automation: return "automation"; case PluginCapabilityType::Automation: return "automation";
case PluginCapabilityType::Analysis: return "analysis"; 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) inline std::string plugin_capability_type_display_name(PluginCapabilityType type)
{ {
switch (type) { switch (type) {
case PluginCapabilityType::PostProcessing: return "Post-processing";
case PluginCapabilityType::PrinterConnection: return "Printer connection"; case PluginCapabilityType::PrinterConnection: return "Printer connection";
case PluginCapabilityType::Automation: return "Automation"; case PluginCapabilityType::Automation: return "Automation";
case PluginCapabilityType::Analysis: return "Analysis"; 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)); lowered.push_back(to_lower(ch));
} }
if (lowered == "post-processing")
return PluginCapabilityType::PostProcessing;
if (lowered == "printer-connection") if (lowered == "printer-connection")
return PluginCapabilityType::PrinterConnection; return PluginCapabilityType::PrinterConnection;
if (lowered == "automation") 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("posSimplifyPath", SlicingPipelineStepPlugin::posSimplifyPath) // covers all simplify sub-steps
.value("psWipeTower", SlicingPipelineStepPlugin::psWipeTower) .value("psWipeTower", SlicingPipelineStepPlugin::psWipeTower)
.value("psSkirtBrim", SlicingPipelineStepPlugin::psSkirtBrim) .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(); .export_values();
// The read-graph data model (Surface / ExPolygon / the extrusion tree / LayerRegion / // 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, .def_readonly("params", &SlicingPipelineContext::params,
"read-only dict of this plugin's [tool.orcaslicer.plugin.settings] values " "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']).") "(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 { .def_property_readonly("print", [](const SlicingPipelineContext& ctx) -> py::object {
if (ctx.print == nullptr) if (ctx.print == nullptr)
return py::none(); 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. " }, "orca.host.PrintObject for object-scoped steps, or None for print-wide steps. "
"Valid only during the execute(ctx) call.") "Valid only during the execute(ctx) call.")
.def("config_value", [](const SlicingPipelineContext& ctx, const std::string& key) -> py::object { .def("config_value", [](const SlicingPipelineContext& ctx, const std::string& key) -> py::object {
if (ctx.print == nullptr) // In-pipeline steps read the live Print's full config; at psGCodePostProcess (print == null)
return py::none(); // fall back to the config the export path handed in.
return config_value_or_none(ctx.print->full_print_config(), key); 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"), }, py::arg("key"),
"serialized value of a resolved (full) print config option for this slice, or " "serialized value of a resolved (full) print config option for this slice, or "
"None if absent. Shorthand for ctx.print.config_value(key).") "None if absent. Shorthand for ctx.print.config_value(key).")

View File

@@ -16,13 +16,23 @@ namespace Slic3r {
struct SlicingPipelineContext { struct SlicingPipelineContext {
std::string orca_version; std::string orca_version;
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice }; SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
Print* print { nullptr }; // always present when dispatched Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
const PrintObject* object { nullptr }; // null for print-wide steps const PrintObject* object { nullptr }; // null for print-wide steps and psGCodePostProcess
// read-only per-plugin settings, populated by the dispatcher from the // read-only per-plugin settings, populated by the dispatcher from the
// plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as // plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as
// ctx.params (dict of string->string). // ctx.params (dict of string->string).
std::map<std::string, std::string> params; 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 { class SlicingPipelinePluginCapability : public PluginCapabilityInterface {

View File

@@ -1,6 +1,8 @@
#pragma once #pragma once
#include "SlicingPipelinePluginCapability.hpp" #include "SlicingPipelinePluginCapability.hpp"
#include "slic3r/plugin/PyPluginTrampoline.hpp" #include "slic3r/plugin/PyPluginTrampoline.hpp"
#include "slic3r/plugin/PluginAuditManager.hpp"
#include <filesystem>
namespace Slic3r { namespace Slic3r {
class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline<SlicingPipelinePluginCapability> { class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline<SlicingPipelinePluginCapability> {
@@ -9,7 +11,18 @@ public:
ExecutionResult execute(SlicingPipelineContext& ctx) override { ExecutionResult execute(SlicingPipelineContext& ctx) override {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, ::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); ExecutionResult, SlicingPipelinePluginCapability, execute, ctx);
} }
}; };

View File

@@ -44,20 +44,22 @@ TEST_CASE("SlicingPipeline hook fires once per step per object in order", "[slic
using S = Slic3r::SlicingPipelineStepPlugin; using S = Slic3r::SlicingPipelineStepPlugin;
auto count = [&](S s){ return std::count_if(calls.begin(), calls.end(), [&](const Call& c){ return c.step == s; }); }; auto count = [&](S s){ return std::count_if(calls.begin(), calls.end(), [&](const Call& c){ return c.step == s; }); };
CHECK(count(S::Slice) == 1); CHECK(count(S::posSlice) == 1);
CHECK(count(S::Perimeters) == 1); CHECK(count(S::posPerimeters) == 1);
CHECK(count(S::PrepareInfill) == 1); // the prepare-infill seam fires once per object CHECK(count(S::posPrepareInfill) == 1); // the prepare-infill seam fires once per object
CHECK(count(S::Infill) == 1); CHECK(count(S::posInfill) == 1);
CHECK(count(S::WipeTower) == 1); CHECK(count(S::psWipeTower) == 1);
CHECK(count(S::SkirtBrim) == 1); CHECK(count(S::psSkirtBrim) == 1);
// psGCodePostProcess fires from the GUI export path, never from process():
CHECK(count(S::psGCodePostProcess) == 0);
// print-wide steps carry a null object: // print-wide steps carry a null object:
for (const auto& c : calls) for (const auto& c : calls)
if (c.step == S::WipeTower || c.step == S::SkirtBrim) CHECK(c.obj == nullptr); if (c.step == S::psWipeTower || c.step == S::psSkirtBrim) CHECK(c.obj == nullptr);
// Slice must fire before Perimeters for the same object: // Slice must fire before Perimeters for the same object:
auto idx = [&](S s){ for (size_t i=0;i<calls.size();++i) if (calls[i].step==s) return (int)i; return -1; }; auto idx = [&](S s){ for (size_t i=0;i<calls.size();++i) if (calls[i].step==s) return (int)i; return -1; };
CHECK(idx(S::Slice) < idx(S::Perimeters)); CHECK(idx(S::posSlice) < idx(S::posPerimeters));
CHECK(idx(S::Perimeters) < idx(S::PrepareInfill)); // prepare-infill fires after perimeters... CHECK(idx(S::posPerimeters) < idx(S::posPrepareInfill)); // prepare-infill fires after perimeters...
CHECK(idx(S::PrepareInfill) < idx(S::Infill)); // ...and before the fills are built CHECK(idx(S::posPrepareInfill) < idx(S::posInfill)); // ...and before the fills are built
} }
#include <sstream> #include <sstream>
@@ -418,10 +420,10 @@ TEST_CASE("fill_surfaces mutation cascades at PrepareInfill but not at Infill",
return n; return n;
}; };
using S = Slic3r::SlicingPipelineStepPlugin; using S = Slic3r::SlicingPipelineStepPlugin;
const size_t base = fill_paths(false, S::PrepareInfill); // active hook, no mutation const size_t base = fill_paths(false, S::posPrepareInfill); // active hook, no mutation
CHECK(base > 0); CHECK(base > 0);
CHECK(fill_paths(true, S::PrepareInfill) < base); // mutation before make_fills cascades CHECK(fill_paths(true, S::posPrepareInfill) < base); // mutation before make_fills cascades
CHECK(fill_paths(true, S::Infill) == base); // mutation after make_fills is a no-op (v1) CHECK(fill_paths(true, S::posInfill) == base); // mutation after make_fills is a no-op (v1)
} }
// lslices (the layer's merged islands) are built once in slice() and never rebuilt by // lslices (the layer's merged islands) are built once in slice() and never rebuilt by

View File

@@ -410,14 +410,14 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca_plugins_%%%%-%%%%.json"); const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca_plugins_%%%%-%%%%.json");
const std::vector<std::string> refs = { const std::vector<std::string> refs = {
"local_plugin;;post_process", "local_plugin;;inset",
"cloud_plugin;550e8400-e29b-41d4-a716-446655440000;post_process" "cloud_plugin;550e8400-e29b-41d4-a716-446655440000;inset"
}; };
std::unique_ptr<DynamicPrintConfig> config_ptr( std::unique_ptr<DynamicPrintConfig> config_ptr(
DynamicPrintConfig::new_from_defaults_keys({"post_process_plugin"})); DynamicPrintConfig::new_from_defaults_keys({"slicing_pipeline_plugin"}));
DynamicPrintConfig config = std::move(*config_ptr); DynamicPrintConfig config = std::move(*config_ptr);
config.option<ConfigOptionStrings>("post_process_plugin", true)->values = refs; config.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = refs;
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"); config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
nlohmann::json j; nlohmann::json j;
@@ -425,7 +425,7 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
boost::nowide::ifstream ifs(tmp.string()); boost::nowide::ifstream ifs(tmp.string());
ifs >> j; ifs >> j;
} }
REQUIRE(j["post_process_plugin"] == nlohmann::json(refs)); REQUIRE(j["slicing_pipeline_plugin"] == nlohmann::json(refs));
CHECK_FALSE(j.contains("plugins")); CHECK_FALSE(j.contains("plugins"));
DynamicPrintConfig reloaded = DynamicPrintConfig::full_print_config(); DynamicPrintConfig reloaded = DynamicPrintConfig::full_print_config();
@@ -434,7 +434,7 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
std::string reason; std::string reason;
REQUIRE(reloaded.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0); REQUIRE(reloaded.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0);
CHECK(reason.empty()); CHECK(reason.empty());
CHECK(reloaded.option<ConfigOptionStrings>("post_process_plugin")->values == refs); CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
fs::remove(tmp); fs::remove(tmp);
} }
@@ -446,17 +446,17 @@ TEST_CASE("plugin capability references survive string-map serialization", "[Con
}; };
DynamicPrintConfig original = DynamicPrintConfig::full_print_config(); DynamicPrintConfig original = DynamicPrintConfig::full_print_config();
original.option<ConfigOptionStrings>("post_process_plugin", true)->values = refs; original.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = refs;
std::map<std::string, std::string> serialized{ std::map<std::string, std::string> serialized{
{"post_process_plugin", original.option<ConfigOptionStrings>("post_process_plugin")->serialize()} {"slicing_pipeline_plugin", original.option<ConfigOptionStrings>("slicing_pipeline_plugin")->serialize()}
}; };
CHECK(serialized["post_process_plugin"].find("\"master_plugin;;header-stamp\"") != std::string::npos); CHECK(serialized["slicing_pipeline_plugin"].find("\"master_plugin;;header-stamp\"") != std::string::npos);
DynamicPrintConfig reloaded = DynamicPrintConfig::full_print_config(); DynamicPrintConfig reloaded = DynamicPrintConfig::full_print_config();
reloaded.load_string_map(serialized, ForwardCompatibilitySubstitutionRule::Disable); reloaded.load_string_map(serialized, ForwardCompatibilitySubstitutionRule::Disable);
CHECK(reloaded.option<ConfigOptionStrings>("post_process_plugin")->values == refs); CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
} }
TEST_CASE("parse_capability_ref parses local and cloud references", "[Config][plugin]") { TEST_CASE("parse_capability_ref parses local and cloud references", "[Config][plugin]") {
@@ -502,14 +502,13 @@ struct PluginResolverFixture {
TEST_CASE_METHOD(PluginResolverFixture, TEST_CASE_METHOD(PluginResolverFixture,
"update_plugin_manifest derives references generically from plugin-backed options", "update_plugin_manifest derives references generically from plugin-backed options",
"[Config][plugins]") { "[Config][plugins]") {
// Both scalar (printer_agent) and vector (post_process_plugin, slicing_pipeline_plugin) options // Both scalar (printer_agent) and vector (slicing_pipeline_plugin) options opt in via a non-empty
// opt in via a non-empty ConfigOptionDef::plugin_type (is_plugin_backed) and are resolved with it // ConfigOptionDef::plugin_type (is_plugin_backed) and are resolved with it -- there is no hardcoded
// -- there is no hardcoded per-option switch. printer_agent in particular relies on its plugin_type // per-option switch. printer_agent in particular relies on its plugin_type metadata being wired up
// metadata being wired up (it is edited via a dedicated widget, not the plugin_picker). // (it is edited via a dedicated widget, not the plugin_picker).
std::unique_ptr<DynamicPrintConfig> config_ptr(DynamicPrintConfig::new_from_defaults_keys( std::unique_ptr<DynamicPrintConfig> config_ptr(DynamicPrintConfig::new_from_defaults_keys(
{"post_process_plugin", "slicing_pipeline_plugin", "printer_agent"})); {"slicing_pipeline_plugin", "printer_agent"}));
DynamicPrintConfig config = std::move(*config_ptr); DynamicPrintConfig config = std::move(*config_ptr);
config.option<ConfigOptionStrings>("post_process_plugin", true)->values = {"pp"};
config.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = {"sp"}; config.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = {"sp"};
config.option<ConfigOptionString>("printer_agent", true)->value = "agent"; config.option<ConfigOptionString>("printer_agent", true)->value = "agent";
@@ -517,23 +516,22 @@ TEST_CASE_METHOD(PluginResolverFixture,
const std::vector<std::string> manifest = config.option<ConfigOptionStrings>("plugins")->values; const std::vector<std::string> manifest = config.option<ConfigOptionStrings>("plugins")->values;
using Catch::Matchers::VectorContains; using Catch::Matchers::VectorContains;
REQUIRE_THAT(manifest, VectorContains(std::string("pp;;post-processing")));
REQUIRE_THAT(manifest, VectorContains(std::string("sp;;slicing-pipeline"))); REQUIRE_THAT(manifest, VectorContains(std::string("sp;;slicing-pipeline")));
REQUIRE_THAT(manifest, VectorContains(std::string("agent;;printer-connection"))); REQUIRE_THAT(manifest, VectorContains(std::string("agent;;printer-connection")));
CHECK(manifest.size() == 3); CHECK(manifest.size() == 2);
} }
TEST_CASE_METHOD(PluginResolverFixture, TEST_CASE_METHOD(PluginResolverFixture,
"update_plugin_manifest de-duplicates references and skips unset options", "update_plugin_manifest de-duplicates references and skips unset options",
"[Config][plugins]") { "[Config][plugins]") {
std::unique_ptr<DynamicPrintConfig> config_ptr(DynamicPrintConfig::new_from_defaults_keys( std::unique_ptr<DynamicPrintConfig> config_ptr(DynamicPrintConfig::new_from_defaults_keys(
{"post_process_plugin", "printer_agent"})); {"slicing_pipeline_plugin", "printer_agent"}));
DynamicPrintConfig config = std::move(*config_ptr); DynamicPrintConfig config = std::move(*config_ptr);
config.option<ConfigOptionStrings>("post_process_plugin", true)->values = {"x", "x"}; // duplicate config.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = {"x", "x"}; // duplicate
// printer_agent stays at its default empty value -> contributes nothing to the manifest. // printer_agent stays at its default empty value -> contributes nothing to the manifest.
config.update_plugin_manifest(); config.update_plugin_manifest();
const std::vector<std::string> manifest = config.option<ConfigOptionStrings>("plugins")->values; const std::vector<std::string> manifest = config.option<ConfigOptionStrings>("plugins")->values;
CHECK(manifest == std::vector<std::string>{"x;;post-processing"}); CHECK(manifest == std::vector<std::string>{"x;;slicing-pipeline"});
} }

View File

@@ -8,9 +8,9 @@ using Slic3r::PluginCapabilityIdentifier;
using Slic3r::PluginCapabilityType; using Slic3r::PluginCapabilityType;
TEST_CASE("PluginCapabilityIdentifier equality includes plugin_key", "[plugin][identifier]") { TEST_CASE("PluginCapabilityIdentifier equality includes plugin_key", "[plugin][identifier]") {
PluginCapabilityIdentifier a{PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}; PluginCapabilityIdentifier a{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"};
PluginCapabilityIdentifier b{PluginCapabilityType::PostProcessing, "Cleanup", "b.py"}; PluginCapabilityIdentifier b{PluginCapabilityType::SlicingPipeline, "Cleanup", "b.py"};
PluginCapabilityIdentifier a2{PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}; PluginCapabilityIdentifier a2{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"};
CHECK(a == a2); CHECK(a == a2);
CHECK_FALSE(a == b); // same (type,name), different plugin_key -> distinct CHECK_FALSE(a == b); // same (type,name), different plugin_key -> distinct
@@ -18,8 +18,8 @@ TEST_CASE("PluginCapabilityIdentifier equality includes plugin_key", "[plugin][i
TEST_CASE("PluginCapabilityIdentifier is usable as a hash-map key", "[plugin][identifier]") { TEST_CASE("PluginCapabilityIdentifier is usable as a hash-map key", "[plugin][identifier]") {
std::unordered_map<PluginCapabilityIdentifier, int> m; std::unordered_map<PluginCapabilityIdentifier, int> m;
m[{PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}] = 1; m[{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}] = 1;
m[{PluginCapabilityType::PostProcessing, "Cleanup", "b.py"}] = 2; // no collision m[{PluginCapabilityType::SlicingPipeline, "Cleanup", "b.py"}] = 2; // no collision
CHECK(m.size() == 2); CHECK(m.size() == 2);
CHECK(m.at({PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}) == 1); CHECK(m.at({PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}) == 1);
} }

View File

@@ -81,7 +81,8 @@ TEST_CASE("orca.slicing module: Step enum, context, and a Python capability can
REQUIRE(py::hasattr(orca, "slicing")); REQUIRE(py::hasattr(orca, "slicing"));
py::object slicing = orca.attr("slicing"); py::object slicing = orca.attr("slicing");
CHECK(py::hasattr(slicing, "Step")); CHECK(py::hasattr(slicing, "Step"));
CHECK(py::hasattr(slicing.attr("Step"), "Slice")); CHECK(py::hasattr(slicing.attr("Step"), "posSlice"));
CHECK(py::hasattr(slicing.attr("Step"), "psGCodePostProcess"));
CHECK(py::hasattr(slicing, "SlicingPipelineContext")); CHECK(py::hasattr(slicing, "SlicingPipelineContext"));
CHECK(py::hasattr(slicing, "SlicingPipelineCapabilityBase")); CHECK(py::hasattr(slicing, "SlicingPipelineCapabilityBase"));
@@ -126,6 +127,79 @@ TEST_CASE("orca.slicing is workflow-only: context exposes raw print/object; view
CHECK(pyctx.attr("object").is_none()); CHECK(pyctx.attr("object").is_none());
} }
#include "libslic3r/PrintConfig.hpp" // DynamicPrintConfig for the psGCodePostProcess context
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <sstream>
// psGCodePostProcess is the merged post-processing seam: no live Print (print/object are None), the
// plugin edits the file at ctx.gcode_path in place, and ctx.config_value() falls back to the config
// the export path handed in. Exercising the real bindings by calling the Python execute() directly
// (not the C++ audit trampoline) keeps this a pure binding-surface test.
TEST_CASE("orca.slicing psGCodePostProcess context: file edit in place + config fallback", "[slicing_pipeline]") {
namespace fs = boost::filesystem;
ensure_python_initialized();
import_orca_module();
py::gil_scoped_acquire gil;
const fs::path gpath = fs::temp_directory_path() / fs::unique_path("orca_pp_%%%%-%%%%.gcode");
{
boost::nowide::ofstream ofs(gpath.string());
ofs << "; header\nG1 X0 Y0\n";
}
// Config the plugin reads back through ctx.config_value() (there is no live Print at this step).
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("layer_height", new Slic3r::ConfigOptionFloat(0.2));
Slic3r::SlicingPipelineContext ctx;
ctx.orca_version = "test";
ctx.step = Slic3r::SlicingPipelineStepPlugin::psGCodePostProcess;
ctx.gcode_path = gpath.string();
ctx.host = "File";
ctx.output_name = "final.gcode";
ctx.full_config = &config; // print stays null
py::object pyctx = py::cast(&ctx, py::return_value_policy::reference);
CHECK(pyctx.attr("gcode_path").cast<std::string>() == gpath.string());
CHECK(pyctx.attr("host").cast<std::string>() == "File");
CHECK(pyctx.attr("output_name").cast<std::string>() == "final.gcode");
CHECK(pyctx.attr("print").is_none());
CHECK(pyctx.attr("object").is_none());
CHECK(pyctx.attr("step").cast<Slic3r::SlicingPipelineStepPlugin>()
== Slic3r::SlicingPipelineStepPlugin::psGCodePostProcess);
CHECK_FALSE(pyctx.attr("cancelled")().cast<bool>()); // null print -> not cancelled
// config_value() resolves from full_config when print is null; unknown keys are None.
CHECK_FALSE(pyctx.attr("config_value")("layer_height").is_none());
CHECK(pyctx.attr("config_value")("this_key_does_not_exist").is_none());
// A Python capability edits the file in place through ctx.gcode_path. Calling execute() directly
// in Python dispatches to the Python method (no C++ trampoline), so this needs no audit context.
py::module_ main = py::module_::import("__main__");
main.attr("_pp_ctx") = pyctx;
py::exec(R"(
import orca
class Stamp(orca.slicing.SlicingPipelineCapabilityBase):
def get_name(self): return "stamp"
def execute(self, ctx):
assert ctx.step == orca.slicing.Step.psGCodePostProcess
assert ctx.print is None and ctx.object is None
with open(ctx.gcode_path, "a") as f:
f.write("; stamped by " + ctx.host + "\n")
return orca.ExecutionResult.success("ok")
_pp_result = Stamp().execute(_pp_ctx)
)");
CHECK(main.attr("_pp_result").attr("message").cast<std::string>() == std::string("ok"));
std::string contents;
{
boost::nowide::ifstream ifs(gpath.string());
std::stringstream ss; ss << ifs.rdbuf(); contents = ss.str();
}
CHECK(contents.find("; stamped by File") != std::string::npos);
fs::remove(gpath);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Toolpath helpers for the raw-graph tests. // Toolpath helpers for the raw-graph tests.
// //