mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
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:
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -1192,7 +1192,6 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"min_feature_size",
|
||||
"min_bead_width",
|
||||
"post_process",
|
||||
"post_process_plugin",
|
||||
"slicing_pipeline_plugin",
|
||||
"plugins",
|
||||
"process_change_extrusion_role_gcode",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<size_t>(step) < sizeof(kStepNames) / sizeof(kStepNames[0])
|
||||
? kStepNames[static_cast<int>(step)] : "Unknown";
|
||||
|
||||
@@ -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 <boost/algorithm/string.hpp>
|
||||
@@ -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<GCodePluginCapability> cap, const PluginCapabilityRef& ref) {
|
||||
GCodePluginContext ctx;
|
||||
auto execute_fn = [&](std::shared_ptr<SlicingPipelinePluginCapability> 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<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")
|
||||
// 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<ConfigOptionStrings>("post_process");
|
||||
const auto* post_process_plugin = config.opt<ConfigOptionStrings>("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<ConfigOptionStrings>("post_process");
|
||||
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)
|
||||
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<ConfigOptionStrings>("plugins"), path, host, output_name);
|
||||
run_post_process_plugins(*slicing_pipeline_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name, config);
|
||||
}
|
||||
} catch (...) {
|
||||
remove_output_name_file();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "PluginHostApi.hpp"
|
||||
#include "PyPluginPackage.hpp"
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
|
||||
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
|
||||
#include "pluginTypes/script/ScriptPluginCapability.hpp"
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
@@ -287,7 +286,6 @@ void bind_python_api(pybind11::module_& m)
|
||||
m.doc() = "OrcaSlicer plugin API";
|
||||
|
||||
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
|
||||
.value("PostProcessing", PluginCapabilityType::PostProcessing)
|
||||
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
|
||||
.value("Automation", PluginCapabilityType::Automation)
|
||||
.value("Analysis", PluginCapabilityType::Analysis)
|
||||
@@ -336,7 +334,6 @@ void bind_python_api(pybind11::module_& m)
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
|
||||
|
||||
// Make sure you register your bindings here
|
||||
GCodePluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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
|
||||
@@ -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).")
|
||||
|
||||
@@ -16,13 +16,23 @@ namespace Slic3r {
|
||||
struct SlicingPipelineContext {
|
||||
std::string orca_version;
|
||||
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
|
||||
Print* print { nullptr }; // always present when dispatched
|
||||
const PrintObject* object { nullptr }; // null for print-wide steps
|
||||
Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
|
||||
const PrintObject* object { nullptr }; // null for print-wide steps and psGCodePostProcess
|
||||
// read-only per-plugin settings, populated by the dispatcher from the
|
||||
// plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as
|
||||
// ctx.params (dict of string->string).
|
||||
std::map<std::string, std::string> params;
|
||||
bool cancelled() const; // -> print->canceled()
|
||||
// Populated ONLY at Step.psGCodePostProcess (the GUI G-code export/post-process seam,
|
||||
// PostProcessor.cpp). gcode_path is the working G-code file on disk that the plugin edits
|
||||
// in place; host is the target ("File", "OctoPrint", ...); output_name mirrors
|
||||
// SLIC3R_PP_OUTPUT_NAME. Empty at every other step.
|
||||
std::string gcode_path;
|
||||
std::string host;
|
||||
std::string output_name;
|
||||
// C++-only config fallback for psGCodePostProcess (no live Print graph there): config_value()
|
||||
// reads it when `print` is null. Not exposed to Python directly. Never dereferenced elsewhere.
|
||||
const DynamicPrintConfig* full_config { nullptr };
|
||||
bool cancelled() const; // -> print->canceled() (false when print is null)
|
||||
};
|
||||
|
||||
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {
|
||||
|
||||
+14
-1
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
#include "SlicingPipelinePluginCapability.hpp"
|
||||
#include "slic3r/plugin/PyPluginTrampoline.hpp"
|
||||
#include "slic3r/plugin/PluginAuditManager.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
namespace Slic3r {
|
||||
class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline<SlicingPipelinePluginCapability> {
|
||||
@@ -9,7 +11,18 @@ public:
|
||||
ExecutionResult execute(SlicingPipelineContext& ctx) override {
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[]{}, PYBIND11_OVERRIDE_PURE,
|
||||
[&]{
|
||||
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
|
||||
// outside data_dir() (a temp/output folder), so writing to it would otherwise be
|
||||
// blocked by the audit sandbox. Grant that folder as a scoped allowed root, mirroring
|
||||
// the former G-code post-processing trampoline. The setup callback runs AFTER the
|
||||
// audit context is constructed, so the scoped root is not cleared by its constructor.
|
||||
// Empty at every other step, so no extra access is granted to the geometry hooks.
|
||||
if (!ctx.gcode_path.empty())
|
||||
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
|
||||
std::filesystem::path(ctx.gcode_path).parent_path());
|
||||
},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
ExecutionResult, SlicingPipelinePluginCapability, execute, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<calls.size();++i) if (calls[i].step==s) return (int)i; return -1; };
|
||||
CHECK(idx(S::Slice) < idx(S::Perimeters));
|
||||
CHECK(idx(S::Perimeters) < idx(S::PrepareInfill)); // prepare-infill fires after perimeters...
|
||||
CHECK(idx(S::PrepareInfill) < idx(S::Infill)); // ...and before the fills are built
|
||||
CHECK(idx(S::posSlice) < idx(S::posPerimeters));
|
||||
CHECK(idx(S::posPerimeters) < idx(S::posPrepareInfill)); // prepare-infill fires after perimeters...
|
||||
CHECK(idx(S::posPrepareInfill) < idx(S::posInfill)); // ...and before the fills are built
|
||||
}
|
||||
|
||||
#include <sstream>
|
||||
@@ -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
|
||||
|
||||
@@ -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<std::string> 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<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);
|
||||
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");
|
||||
|
||||
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<ConfigOptionStrings>("post_process_plugin")->values == refs);
|
||||
CHECK(reloaded.option<ConfigOptionStrings>("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<ConfigOptionStrings>("post_process_plugin", true)->values = refs;
|
||||
original.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = refs;
|
||||
|
||||
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();
|
||||
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]") {
|
||||
@@ -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<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);
|
||||
config.option<ConfigOptionStrings>("post_process_plugin", true)->values = {"pp"};
|
||||
config.option<ConfigOptionStrings>("slicing_pipeline_plugin", true)->values = {"sp"};
|
||||
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;
|
||||
|
||||
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<DynamicPrintConfig> 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<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.
|
||||
|
||||
config.update_plugin_manifest();
|
||||
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"});
|
||||
}
|
||||
|
||||
@@ -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<PluginCapabilityIdentifier, int> 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);
|
||||
}
|
||||
|
||||
@@ -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 <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.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user