mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-24 11:22:07 +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:
@@ -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