diff --git a/sandboxes/orca_gcode_stamp_plugin_any.py b/sandboxes/orca_gcode_stamp_plugin_any.py new file mode 100644 index 0000000000..08273d189a --- /dev/null +++ b/sandboxes/orca_gcode_stamp_plugin_any.py @@ -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) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 07d71a52c6..0d776c7599 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2444,9 +2444,9 @@ public: // "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. std::string gui_flags; - // Capability type of a plugin-backed option, e.g. "post-processing" / "slicing-pipeline" / - // "printer-connection" (empty for ordinary options). GUIType::plugin_picker filters the plugin - // list by it, and it resolves the option's "plugins" manifest reference; see is_plugin_backed(). + // Capability type of a plugin-backed option, e.g. "slicing-pipeline" / "printer-connection" + // (empty for ordinary options). GUIType::plugin_picker filters the plugin list by it, and it + // resolves the option's "plugins" manifest reference; see is_plugin_backed(). std::string plugin_type; // 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. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 23d006b70d..be48fc2730 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1192,7 +1192,6 @@ static std::vector s_Preset_print_options{ "min_feature_size", "min_bead_width", "post_process", - "post_process_plugin", "slicing_pipeline_plugin", "plugins", "process_change_extrusion_role_gcode", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index dbbca5ab28..066a8a287f 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -125,8 +125,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "printing_by_object_gcode", "filament_end_gcode", "post_process", - "post_process_plugin", - // "plugins" is the manifest backing post_process_plugin; like it, it only affects G-code export. + // "plugins" is the derived manifest backing the plugin-picker options; on its own it only + // affects G-code export. The specific option (e.g. slicing_pipeline_plugin) drives any re-slice. "plugins", "extruder_clearance_height_to_rod", "extruder_clearance_height_to_lid", diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index a3521dcc8e..9830878a1c 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -101,7 +101,10 @@ enum PrintObjectStep { enum class SlicingPipelineStepPlugin { 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 diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f021af0478..7cd82a355c 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5114,20 +5114,10 @@ void PrintConfigDef::init_fff_params() def->mode = comDevelop; 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->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->plugin_type = "slicing-pipeline"; def->full_width = true; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 104e4a9c3d..192ea662bd 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1570,7 +1570,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, ooze_prevention)) ((ConfigOptionString, filename_format)) ((ConfigOptionStrings, post_process)) - ((ConfigOptionStrings, post_process_plugin)) ((ConfigOptionStrings, slicing_pipeline_plugin)) ((ConfigOptionString, printer_model)) ((ConfigOptionFloat, resolution)) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 29fb286cf1..f621e63d67 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -618,9 +618,6 @@ set(SLIC3R_GUI_SOURCES plugin/PluginAuditManager.hpp plugin/PluginResolver.cpp 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.cpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 3150eda1c8..8513af0f86 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3182,7 +3182,7 @@ bool GUI_App::on_init_inner() static const char* const kStepNames[] = { "posSlice", "posPerimeters", "posEstimateCurledExtrusions", "posPrepareInfill", "posInfill", "posIroning", "posContouring", "posSupportMaterial", "posDetectOverhangsForLift", - "posSimplifyPath", "psWipeTower", "psSkirtBrim" + "posSimplifyPath", "psWipeTower", "psSkirtBrim", "psGCodePostProcess" }; // order must match Slic3r::SlicingPipelineStepPlugin const char* step_name = static_cast(step) < sizeof(kStepNames) / sizeof(kStepNames[0]) ? kStepNames[static_cast(step)] : "Unknown"; diff --git a/src/slic3r/GUI/PostProcessor.cpp b/src/slic3r/GUI/PostProcessor.cpp index 2e63835529..afb7932eb3 100644 --- a/src/slic3r/GUI/PostProcessor.cpp +++ b/src/slic3r/GUI/PostProcessor.cpp @@ -9,7 +9,7 @@ // file lives in the GUI layer (libslic3r must not depend on pybind11 / PluginManager). #include "libslic3r/Config.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 @@ -241,27 +241,39 @@ void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& co fs.close(); } -// Run the configured post-processing plugins on `gcode_path` in place. Plugins are executed in-process -// through the embedded Python interpreter. Throws Slic3r::RuntimeError on any failure; the caller is -// responsible for removing the working copy (see run_post_process_scripts' catch block). -// Entries are bare capability names; the top-level plugins manifest carries the full plugin refs. +// Run the configured slicing-pipeline plugins on `gcode_path` in place, at their Step.psGCodePostProcess +// seam. This is the same capability that runs at the geometry seams inside Print::process(); here it is +// dispatched a final time on the exported G-code, so a plugin can edit slices AND the final G-code from +// 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, const ConfigOptionStrings* plugins, const std::string& gcode_path, 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. boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1); const boost::filesystem::path gcode_file(gcode_path); - auto execute_fn = [&](std::shared_ptr cap, const PluginCapabilityRef& ref) { - GCodePluginContext ctx; + auto execute_fn = [&](std::shared_ptr cap, const PluginCapabilityRef& ref) { + SlicingPipelineContext ctx; ctx.orca_version = SoftFever_VERSION; + ctx.step = SlicingPipelineStepPlugin::psGCodePostProcess; ctx.gcode_path = gcode_path; ctx.host = host; 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; 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"; }; - execute_capabilities_from_refs(capabilities, plugins, PluginCapabilityType::PostProcessing, execute_fn); + execute_capabilities_from_refs(capabilities, plugins, PluginCapabilityType::SlicingPipeline, execute_fn); } -// Run post-processing scripts ("post_process") and/or post-processing plugins ("post_process_plugin") -// if defined. Both run on the same working copy of the G-code (the ".pp" temp when make_copy), so a +// Run post-processing scripts ("post_process") and/or the slicing-pipeline plugins' psGCodePostProcess +// 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 // mapped file fails on Windows with a sharing violation). // 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( 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. - const auto* post_process = config.opt("post_process"); - const auto* post_process_plugin = config.opt("post_process_plugin"); - const bool have_scripts = post_process != nullptr && !post_process->values.empty(); - const bool have_plugins = post_process_plugin != nullptr && !post_process_plugin->values.empty(); + // post_process / slicing_pipeline_plugin are absent in SLA mode, hence the null checks. G-code + // post-processing is now the psGCodePostProcess step of the slicing-pipeline plugin, so the same + // slicing_pipeline_plugin option drives both the geometry seams and this final G-code seam. + const auto* post_process = config.opt("post_process"); + const auto* slicing_pipeline_plugin = config.opt("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) 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 // exception is handled by the catch below, which removes the temp copy. if (have_plugins) { - run_post_process_plugins(*post_process_plugin, config.opt("plugins"), path, host, output_name); + run_post_process_plugins(*slicing_pipeline_plugin, config.opt("plugins"), path, host, output_name, config); } } catch (...) { remove_output_name_file(); diff --git a/src/slic3r/GUI/PostProcessor.hpp b/src/slic3r/GUI/PostProcessor.hpp index c85c5a511c..1c0fc09240 100644 --- a/src/slic3r/GUI/PostProcessor.hpp +++ b/src/slic3r/GUI/PostProcessor.hpp @@ -9,9 +9,10 @@ namespace Slic3r { -// Run post-processing scripts (the "post_process" option) and/or post-processing plugins (the -// "post_process_plugin" option) if defined. Lives in the GUI layer because plugins are executed -// through the embedded-Python PluginManager, which libslic3r must not depend on. +// Run post-processing scripts (the "post_process" option) and/or the slicing-pipeline plugins' +// Step.psGCodePostProcess seam (the "slicing_pipeline_plugin" option) if defined. Lives in the GUI +// 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 false if neither a post-processing script nor plugin was defined. // Throws an exception on error. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index fcb5c575ea..b8e469ea92 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -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() // then pass downstream as-is -- no separate rebuild anywhere else. 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(); if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) { @@ -3080,12 +3080,6 @@ void TabPrint::build() option.opt.height = 15; 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->hide_labels(); option = optgroup->get_option("slicing_pipeline_plugin"); diff --git a/src/slic3r/plugin/PluginLoader.cpp b/src/slic3r/plugin/PluginLoader.cpp index 6d3f90651c..3f0257d6c0 100644 --- a/src/slic3r/plugin/PluginLoader.cpp +++ b/src/slic3r/plugin/PluginLoader.cpp @@ -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; } diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 2c12e04975..89b9f8eb6f 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -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_(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); diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index abb552f8e3..c3232c3b98 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -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") diff --git a/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.cpp b/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.cpp deleted file mode 100644 index 0639bdb774..0000000000 --- a/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "GCodePluginCapability.hpp" - -#include "GCodePluginCapabilityTrampoline.hpp" - -#include -#include - -namespace py = pybind11; - -namespace Slic3r { - -void GCodePluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_& pluginTypes) -{ - (void) pluginTypes; - - auto gcode = module.def_submodule("gcode", "G-code API"); - - py::class_(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_>(gcode, "GCodePluginCapabilityBase") - .def(py::init<>()) - .def("get_type", &GCodePluginCapability::get_type) - .def("execute", &GCodePluginCapability::execute); -} - -} // namespace Slic3r diff --git a/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.hpp deleted file mode 100644 index d935fcff1d..0000000000 --- a/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.hpp +++ /dev/null @@ -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_ &pluginTypes); -}; - -} // namespace Slic3r - -#endif /* slic3r_GCodePluginCapability_hpp_ */ diff --git a/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp deleted file mode 100644 index 83bcdc1a70..0000000000 --- a/src/slic3r/plugin/pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef slic3r_GCodePluginCapabilityTrampoline_hpp_ -#define slic3r_GCodePluginCapabilityTrampoline_hpp_ - -#include - -#include "../../PyPluginTrampoline.hpp" -#include "../../PluginAuditManager.hpp" -#include "GCodePluginCapability.hpp" - -namespace Slic3r { -class PyGCodePluginCapabilityTrampoline : public PyPluginCommonTrampoline -{ -public: - using PyPluginCommonTrampoline::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 diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp index bd2bca71c7..2828c0496e 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp @@ -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).") diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp index d3c30ba92c..c9a504f6be 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp @@ -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 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 { diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp index c979e4f086..5dc14c74b9 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp @@ -1,6 +1,8 @@ #pragma once #include "SlicingPipelinePluginCapability.hpp" #include "slic3r/plugin/PyPluginTrampoline.hpp" +#include "slic3r/plugin/PluginAuditManager.hpp" +#include namespace Slic3r { class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline { @@ -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); } }; diff --git a/tests/fff_print/test_slicing_pipeline_hook.cpp b/tests/fff_print/test_slicing_pipeline_hook.cpp index 1f7db91e4c..9e035afea4 100644 --- a/tests/fff_print/test_slicing_pipeline_hook.cpp +++ b/tests/fff_print/test_slicing_pipeline_hook.cpp @@ -44,20 +44,22 @@ TEST_CASE("SlicingPipeline hook fires once per step per object in order", "[slic using S = Slic3r::SlicingPipelineStepPlugin; 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::Perimeters) == 1); - CHECK(count(S::PrepareInfill) == 1); // the prepare-infill seam fires once per object - CHECK(count(S::Infill) == 1); - CHECK(count(S::WipeTower) == 1); - CHECK(count(S::SkirtBrim) == 1); + CHECK(count(S::posSlice) == 1); + CHECK(count(S::posPerimeters) == 1); + CHECK(count(S::posPrepareInfill) == 1); // the prepare-infill seam fires once per object + CHECK(count(S::posInfill) == 1); + CHECK(count(S::psWipeTower) == 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: 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: auto idx = [&](S s){ for (size_t i=0;i @@ -418,10 +420,10 @@ TEST_CASE("fill_surfaces mutation cascades at PrepareInfill but not at Infill", return n; }; 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(fill_paths(true, S::PrepareInfill) < 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::posPrepareInfill) < base); // mutation before make_fills cascades + 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 diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 38d857c765..071f4e359a 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -410,14 +410,14 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[ namespace fs = boost::filesystem; const fs::path tmp = fs::temp_directory_path() / fs::unique_path("orca_plugins_%%%%-%%%%.json"); const std::vector refs = { - "local_plugin;;post_process", - "cloud_plugin;550e8400-e29b-41d4-a716-446655440000;post_process" + "local_plugin;;inset", + "cloud_plugin;550e8400-e29b-41d4-a716-446655440000;inset" }; std::unique_ptr config_ptr( - DynamicPrintConfig::new_from_defaults_keys({"post_process_plugin"})); + DynamicPrintConfig::new_from_defaults_keys({"slicing_pipeline_plugin"})); DynamicPrintConfig config = std::move(*config_ptr); - config.option("post_process_plugin", true)->values = refs; + config.option("slicing_pipeline_plugin", true)->values = refs; config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"); 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()); ifs >> j; } - REQUIRE(j["post_process_plugin"] == nlohmann::json(refs)); + REQUIRE(j["slicing_pipeline_plugin"] == nlohmann::json(refs)); CHECK_FALSE(j.contains("plugins")); 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; REQUIRE(reloaded.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0); CHECK(reason.empty()); - CHECK(reloaded.option("post_process_plugin")->values == refs); + CHECK(reloaded.option("slicing_pipeline_plugin")->values == refs); fs::remove(tmp); } @@ -446,17 +446,17 @@ TEST_CASE("plugin capability references survive string-map serialization", "[Con }; DynamicPrintConfig original = DynamicPrintConfig::full_print_config(); - original.option("post_process_plugin", true)->values = refs; + original.option("slicing_pipeline_plugin", true)->values = refs; std::map serialized{ - {"post_process_plugin", original.option("post_process_plugin")->serialize()} + {"slicing_pipeline_plugin", original.option("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(); reloaded.load_string_map(serialized, ForwardCompatibilitySubstitutionRule::Disable); - CHECK(reloaded.option("post_process_plugin")->values == refs); + CHECK(reloaded.option("slicing_pipeline_plugin")->values == refs); } TEST_CASE("parse_capability_ref parses local and cloud references", "[Config][plugin]") { @@ -502,14 +502,13 @@ struct PluginResolverFixture { TEST_CASE_METHOD(PluginResolverFixture, "update_plugin_manifest derives references generically from plugin-backed options", "[Config][plugins]") { - // Both scalar (printer_agent) and vector (post_process_plugin, slicing_pipeline_plugin) options - // opt in via a non-empty ConfigOptionDef::plugin_type (is_plugin_backed) and are resolved with it - // -- there is no hardcoded per-option switch. printer_agent in particular relies on its plugin_type - // metadata being wired up (it is edited via a dedicated widget, not the plugin_picker). + // Both scalar (printer_agent) and vector (slicing_pipeline_plugin) options opt in via a non-empty + // ConfigOptionDef::plugin_type (is_plugin_backed) and are resolved with it -- there is no hardcoded + // per-option switch. printer_agent in particular relies on its plugin_type metadata being wired up + // (it is edited via a dedicated widget, not the plugin_picker). std::unique_ptr 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); - config.option("post_process_plugin", true)->values = {"pp"}; config.option("slicing_pipeline_plugin", true)->values = {"sp"}; config.option("printer_agent", true)->value = "agent"; @@ -517,23 +516,22 @@ TEST_CASE_METHOD(PluginResolverFixture, const std::vector manifest = config.option("plugins")->values; 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("agent;;printer-connection"))); - CHECK(manifest.size() == 3); + CHECK(manifest.size() == 2); } TEST_CASE_METHOD(PluginResolverFixture, "update_plugin_manifest de-duplicates references and skips unset options", "[Config][plugins]") { std::unique_ptr config_ptr(DynamicPrintConfig::new_from_defaults_keys( - {"post_process_plugin", "printer_agent"})); + {"slicing_pipeline_plugin", "printer_agent"})); DynamicPrintConfig config = std::move(*config_ptr); - config.option("post_process_plugin", true)->values = {"x", "x"}; // duplicate + config.option("slicing_pipeline_plugin", true)->values = {"x", "x"}; // duplicate // printer_agent stays at its default empty value -> contributes nothing to the manifest. config.update_plugin_manifest(); const std::vector manifest = config.option("plugins")->values; - CHECK(manifest == std::vector{"x;;post-processing"}); + CHECK(manifest == std::vector{"x;;slicing-pipeline"}); } diff --git a/tests/slic3rutils/test_plugin_capability_identifier.cpp b/tests/slic3rutils/test_plugin_capability_identifier.cpp index fe9c94d3e7..575ac4c8a5 100644 --- a/tests/slic3rutils/test_plugin_capability_identifier.cpp +++ b/tests/slic3rutils/test_plugin_capability_identifier.cpp @@ -8,9 +8,9 @@ using Slic3r::PluginCapabilityIdentifier; using Slic3r::PluginCapabilityType; TEST_CASE("PluginCapabilityIdentifier equality includes plugin_key", "[plugin][identifier]") { - PluginCapabilityIdentifier a{PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}; - PluginCapabilityIdentifier b{PluginCapabilityType::PostProcessing, "Cleanup", "b.py"}; - PluginCapabilityIdentifier a2{PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}; + PluginCapabilityIdentifier a{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}; + PluginCapabilityIdentifier b{PluginCapabilityType::SlicingPipeline, "Cleanup", "b.py"}; + PluginCapabilityIdentifier a2{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}; CHECK(a == a2); 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]") { std::unordered_map m; - m[{PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}] = 1; - m[{PluginCapabilityType::PostProcessing, "Cleanup", "b.py"}] = 2; // no collision + m[{PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}] = 1; + m[{PluginCapabilityType::SlicingPipeline, "Cleanup", "b.py"}] = 2; // no collision CHECK(m.size() == 2); - CHECK(m.at({PluginCapabilityType::PostProcessing, "Cleanup", "a.py"}) == 1); + CHECK(m.at({PluginCapabilityType::SlicingPipeline, "Cleanup", "a.py"}) == 1); } diff --git a/tests/slic3rutils/test_slicing_pipeline_bindings.cpp b/tests/slic3rutils/test_slicing_pipeline_bindings.cpp index 4a6d36ff01..35f49241b6 100644 --- a/tests/slic3rutils/test_slicing_pipeline_bindings.cpp +++ b/tests/slic3rutils/test_slicing_pipeline_bindings.cpp @@ -81,7 +81,8 @@ TEST_CASE("orca.slicing module: Step enum, context, and a Python capability can REQUIRE(py::hasattr(orca, "slicing")); py::object slicing = orca.attr("slicing"); 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, "SlicingPipelineCapabilityBase")); @@ -126,6 +127,79 @@ TEST_CASE("orca.slicing is workflow-only: context exposes raw print/object; view CHECK(pyctx.attr("object").is_none()); } +#include "libslic3r/PrintConfig.hpp" // DynamicPrintConfig for the psGCodePostProcess context +#include +#include +#include + +// 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() == gpath.string()); + CHECK(pyctx.attr("host").cast() == "File"); + CHECK(pyctx.attr("output_name").cast() == "final.gcode"); + CHECK(pyctx.attr("print").is_none()); + CHECK(pyctx.attr("object").is_none()); + CHECK(pyctx.attr("step").cast() + == Slic3r::SlicingPipelineStepPlugin::psGCodePostProcess); + CHECK_FALSE(pyctx.attr("cancelled")().cast()); // 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("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. //