fix: load default config on initial load

This commit is contained in:
Ian Chua
2026-07-16 12:57:59 +08:00
parent a15dcdfb3f
commit b6f98d9592
13 changed files with 60 additions and 103 deletions

View File

@@ -7,12 +7,6 @@
# author = "OrcaSlicer" # author = "OrcaSlicer"
# version = "0.01" # version = "0.01"
# type = "slicing-pipeline" # type = "slicing-pipeline"
#
# [tool.orcaslicer.plugin.settings]
# thickness_mm = "0.3"
# point_distance_mm = "0.8"
# fuzz_holes = "1"
# skip_first_layer = "1"
# /// # ///
"""Fuzzy Slices -- the fuzzy-skin effect applied at slice time. """Fuzzy Slices -- the fuzzy-skin effect applied at slice time.
@@ -44,6 +38,7 @@ Polygon.as_array()/set_points numpy path would be the faster route.
""" """
import math import math
import random import random
import json
import orca import orca
@@ -55,9 +50,9 @@ _DEFAULTS = {
} }
def _params(ctx): def _params(self):
try: try:
src = dict(ctx.params) src = json.loads(self.get_config())
except (AttributeError, TypeError): except (AttributeError, TypeError):
src = {} src = {}
out = {} out = {}
@@ -111,11 +106,14 @@ class FuzzySlices(orca.slicing.SlicingPipelineCapabilityBase):
def get_name(self): def get_name(self):
return "Fuzzy Slices" return "Fuzzy Slices"
def get_default_config(self):
return _DEFAULTS
def execute(self, ctx): def execute(self, ctx):
if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: if ctx.step != orca.slicing.Step.posSlice or ctx.object is None:
return orca.ExecutionResult.success() return orca.ExecutionResult.success()
p = _params(ctx) p = _params(self)
if p["thickness_mm"] <= 0.0 or p["point_distance_mm"] <= 0.0: if p["thickness_mm"] <= 0.0 or p["point_distance_mm"] <= 0.0:
return orca.ExecutionResult.success("Fuzzy Slices: zero thickness/point distance, nothing to do") return orca.ExecutionResult.success("Fuzzy Slices: zero thickness/point distance, nothing to do")

View File

@@ -7,9 +7,6 @@
# author = "OrcaSlicer" # author = "OrcaSlicer"
# version = "0.01" # version = "0.01"
# type = "slicing-pipeline" # 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. """G-code Stamp -- the post-processing half of the slicing-pipeline plugin.
@@ -19,7 +16,7 @@ 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 (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 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 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. ctx.output_name (the final file name). self.get_config() still works.
This sample inserts a single comment line near the top of the file. Because the same 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 capability class can also implement the geometry steps, one plugin can transform slices
@@ -30,21 +27,27 @@ separate working copy), and its output is not reflected in the G-code preview --
viewer maps the pre-post-process file. viewer maps the pre-post-process file.
""" """
import orca import orca
import json
_DEFAULT_STAMP = "processed by the OrcaSlicer G-code Stamp plugin" _DEFAULTS = {
"stamp_text": "processed by the OrcaSlicer G-code Stamp plugin",
}
def _stamp_text(ctx): def _stamp_text(self):
try: try:
text = dict(ctx.params).get("stamp_text", _DEFAULT_STAMP) text = json.loads(self.get_config())["stamp_text"]
except (AttributeError, TypeError): except (AttributeError, TypeError, ValueError, KeyError):
text = _DEFAULT_STAMP text = _DEFAULTS["stamp_text"]
return str(text).replace("\n", " ").strip() or _DEFAULT_STAMP return str(text).replace("\n", " ").strip() or _DEFAULTS["stamp_text"]
class GCodeStamp(orca.slicing.SlicingPipelineCapabilityBase): class GCodeStamp(orca.slicing.SlicingPipelineCapabilityBase):
def get_name(self): def get_name(self):
return "G-code Stamp" return "G-code Stamp"
def get_default_config(self):
return _DEFAULTS
def execute(self, ctx): def execute(self, ctx):
# Only act at the post-process seam; at every geometry step this is a no-op. # Only act at the post-process seam; at every geometry step this is a no-op.
@@ -53,7 +56,7 @@ class GCodeStamp(orca.slicing.SlicingPipelineCapabilityBase):
if not ctx.gcode_path: if not ctx.gcode_path:
return orca.ExecutionResult.success("G-code Stamp: no gcode_path, nothing to do") return orca.ExecutionResult.success("G-code Stamp: no gcode_path, nothing to do")
comment = "; " + _stamp_text(ctx) + " (host=" + (ctx.host or "?") + ")\n" comment = "; " + _stamp_text(self) + " (host=" + (ctx.host or "?") + ")\n"
# Edit the exported G-code in place: keep the original first line first (some flavors # 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. # expect a specific leading line), then insert the stamp right after it.

View File

@@ -25,21 +25,39 @@ A surface may split into several islands or vanish when shrunk; both are handled
No numpy required: the whole edit is expressed with the host geometry classes. No numpy required: the whole edit is expressed with the host geometry classes.
""" """
import json
import orca import orca
INSET_MM = 1.0 _DEFAULTS = {
"inset_mm": 1.0, # inward offset applied to every slice
}
def _inset_mm(self):
try:
return float(json.loads(self.get_config())["inset_mm"])
except (AttributeError, TypeError, ValueError, KeyError):
return _DEFAULTS["inset_mm"]
class InsetEverySlice(orca.slicing.SlicingPipelineCapabilityBase): class InsetEverySlice(orca.slicing.SlicingPipelineCapabilityBase):
def get_name(self): def get_name(self):
return "Inset Every Slice" return "Inset Every Slice"
def get_default_config(self):
return _DEFAULTS
def execute(self, ctx): def execute(self, ctx):
if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: if ctx.step != orca.slicing.Step.posSlice or ctx.object is None:
return orca.ExecutionResult.success() return orca.ExecutionResult.success()
inset_mm = _inset_mm(self)
if inset_mm <= 0.0:
return orca.ExecutionResult.success("Inset: zero inset, nothing to do")
# Millimeters -> scaled integer units via the *live* scale (never hardcode 1e6). # Millimeters -> scaled integer units via the *live* scale (never hardcode 1e6).
inset_scaled = int(round(INSET_MM / orca.slicing.unscale(1))) inset_scaled = int(round(inset_mm / orca.slicing.unscale(1)))
regions_touched = 0 regions_touched = 0
for layer in ctx.object.layers(): for layer in ctx.object.layers():

View File

@@ -7,13 +7,6 @@
# author = "OrcaSlicer" # author = "OrcaSlicer"
# version = "0.02" # version = "0.02"
# type = "slicing-pipeline" # type = "slicing-pipeline"
#
# [tool.orcaslicer.plugin.settings]
# twist_deg_per_mm = "1.0"
# taper_per_mm = "0.0"
# wobble_ampl_mm = "0.0"
# wobble_period_mm = "20.0"
# min_scale = "0.05"
# /// # ///
"""Twistify -- twist/taper/wobble any model at slice time. """Twistify -- twist/taper/wobble any model at slice time.
@@ -36,7 +29,7 @@ settings table above). The first object layer is untouched (z_rel = 0), so bed
adhesion is unaffected. adhesion is unaffected.
""" """
import math import math
import json
import orca import orca
_DEFAULTS = { _DEFAULTS = {
@@ -48,9 +41,9 @@ _DEFAULTS = {
} }
def _params(ctx): def _params(self):
try: try:
src = dict(ctx.params) src = json.loads(self.get_config())
except (AttributeError, TypeError): except (AttributeError, TypeError):
src = {} src = {}
out = {} out = {}
@@ -79,12 +72,15 @@ def _layer_params(z_rel, mm_to_scaled, p):
class Twistify(orca.slicing.SlicingPipelineCapabilityBase): class Twistify(orca.slicing.SlicingPipelineCapabilityBase):
def get_name(self): def get_name(self):
return "Twistify" return "Twistify"
def get_default_config(self):
return _DEFAULTS
def execute(self, ctx): def execute(self, ctx):
if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: if ctx.step != orca.slicing.Step.posSlice or ctx.object is None:
return orca.ExecutionResult.success() return orca.ExecutionResult.success()
p = _params(ctx) p = _params(self)
if _is_identity(p): if _is_identity(p):
return orca.ExecutionResult.success("Twistify: identity parameters, nothing to do") return orca.ExecutionResult.success("Twistify: identity parameters, nothing to do")

View File

@@ -273,7 +273,6 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
// Hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params (same plugin_key the // 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. // 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; const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
ctx.params = PluginManager::instance().get_plugin_settings(plugin_key);
ExecutionResult exec_result; ExecutionResult exec_result;
try { try {

View File

@@ -61,7 +61,6 @@ struct PluginDescriptor
std::string entry_path; // Full path to the installed plugin entry file std::string entry_path; // Full path to the installed plugin entry file
std::string entry_package; // Import package/module used for package-based loading std::string entry_package; // Import package/module used for package-based loading
std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata
std::map<std::string, std::string> settings; // [tool.orcaslicer.plugin.settings] table -> per-plugin params (ctx.params)
std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available. std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available.
std::string error; // Blocking error message. Non-empty means the plugin is in an error state. std::string error; // Blocking error message. Non-empty means the plugin is in an error state.

View File

@@ -70,9 +70,6 @@ void install_slicing_pipeline_hook()
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid; const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
ExecutionResult r; ExecutionResult r;
try { try {
// Read manager state before acquiring the GIL so this path does not take
// m_mutex in the opposite order to plugin teardown.
const auto plugin_settings = PluginManager::instance().get_plugin_settings(plugin_key);
// GIL is acquired per capability (not once for the whole dispatch) so it // GIL is acquired per capability (not once for the whole dispatch) so it
// is released between capabilities. // is released between capabilities.
PythonGILState gil; PythonGILState gil;
@@ -87,9 +84,6 @@ void install_slicing_pipeline_hook()
ctx.step = step; ctx.step = step;
ctx.print = &print; ctx.print = &print;
ctx.object = object; ctx.object = object;
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
// (same plugin_key the capability was resolved by, so it always matches).
ctx.params = plugin_settings;
r = cap->execute(ctx); r = cap->execute(ctx);
} catch (const CanceledException&) { } catch (const CanceledException&) {
throw; // cancellation must reach process(), never become a slicing error throw; // cancellation must reach process(), never become a slicing error

View File

@@ -21,6 +21,7 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginLoader.hpp> #include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp> #include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
@@ -555,17 +556,6 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
return nullptr; return nullptr;
} }
std::map<std::string, std::string> PluginManager::get_plugin_settings(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr || !plugin->is_loaded())
return {};
return plugin->descriptor.settings;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────────────────── // ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
@@ -858,6 +848,16 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
return; return;
} }
for (const auto& cap : plugin.capabilities) {
auto config = cap->get_default_config();
if (config.empty())
continue;
if (m_config.has_config(plugin_key, cap->name()))
continue;
m_config.save_config(plugin_key, cap->name(), plugin.descriptor.installed_version, config);
}
bool committed = false; bool committed = false;
bool cancelled = false; bool cancelled = false;
std::string registry_error; std::string registry_error;

View File

@@ -179,8 +179,6 @@ public:
void set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled); void set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled);
// The plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown). This will be replaced once the config is merged in.
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) const;
// Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into. // Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into.
void set_cloud_user(const std::string& user_id); void set_cloud_user(const std::string& user_id);

View File

@@ -187,7 +187,6 @@ bool parse_pep723_toml(const std::string& toml_content,
std::string& out_description, std::string& out_description,
std::string& out_author, std::string& out_author,
std::string& out_version, std::string& out_version,
std::map<std::string, std::string>& out_settings,
std::string& error) std::string& error)
{ {
out_deps.clear(); out_deps.clear();
@@ -196,7 +195,6 @@ bool parse_pep723_toml(const std::string& toml_content,
out_description.clear(); out_description.clear();
out_author.clear(); out_author.clear();
out_version.clear(); out_version.clear();
out_settings.clear();
TomlSection section = TomlSection::Root; TomlSection section = TomlSection::Root;
@@ -274,10 +272,6 @@ bool parse_pep723_toml(const std::string& toml_content,
else if (key == "description") out_description = unquote_toml_string(val); else if (key == "description") out_description = unquote_toml_string(val);
else if (key == "author") out_author = unquote_toml_string(val); else if (key == "author") out_author = unquote_toml_string(val);
else if (key == "version") out_version = unquote_toml_string(val); else if (key == "version") out_version = unquote_toml_string(val);
} else if (section == TomlSection::OrcaPluginSettings) {
// collect every key as a string; the plugin parses (int/float/...) what it needs.
if (!key.empty())
out_settings[key] = unquote_toml_string(val);
} }
} }
@@ -675,7 +669,6 @@ bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginD
pep_desc, pep_desc,
pep_author, pep_author,
pep_version, pep_version,
descriptor.settings,
pep723_error)) { pep723_error)) {
error = "Failed to parse PEP 723 metadata: " + pep723_error; error = "Failed to parse PEP 723 metadata: " + pep723_error;
return false; return false;

View File

@@ -48,9 +48,6 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::
py::class_<SlicingPipelineContext>(slicing, "SlicingPipelineContext") py::class_<SlicingPipelineContext>(slicing, "SlicingPipelineContext")
.def_readonly("orca_version", &SlicingPipelineContext::orca_version) .def_readonly("orca_version", &SlicingPipelineContext::orca_version)
.def_readonly("step", &SlicingPipelineContext::step) .def_readonly("step", &SlicingPipelineContext::step)
.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, .def_readonly("gcode_path", &SlicingPipelineContext::gcode_path,
"Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in " "Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in "
"place; empty at every other step.") "place; empty at every other step.")

View File

@@ -18,10 +18,6 @@ struct SlicingPipelineContext {
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice }; SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
const PrintObject* object { nullptr }; // null for print-wide steps and 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;
// Populated ONLY at Step.psGCodePostProcess (the GUI G-code export/post-process seam, // 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 // 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 // in place; host is the target ("File", "OctoPrint", ...); output_name mirrors

View File

@@ -120,38 +120,4 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header
read_install_state(plugin_dir, scanned); read_install_state(plugin_dir, scanned);
CHECK(scanned.installed_version == "1.2.0"); CHECK(scanned.installed_version == "1.2.0");
} }
TEST_CASE("install_plugin parses [tool.orcaslicer.plugin.settings] into descriptor.settings", "[PluginInstall]")
{
ScopedDataDir data_dir_guard("plugin-settings");
// A PEP-723 header with a per-plugin settings sub-table. Values stay strings; the plugin
// parses what it needs (ctx.params). This is the source Twistify reads its knobs from.
const std::string contents =
"# /// script\n"
"# requires-python = \">=3.12\"\n"
"#\n"
"# [tool.orcaslicer.plugin]\n"
"# name = \"Settings Plugin\"\n"
"# type = \"slicing-pipeline\"\n"
"#\n"
"# [tool.orcaslicer.plugin.settings]\n"
"# twist_deg_per_mm = \"1.5\"\n"
"# taper_per_mm = \"-0.004\"\n"
"# ///\n"
"print('ok')\n";
const fs::path py = write_py_file(data_dir_guard.dir / "src", "settings.py", contents);
PluginDescriptor descriptor;
std::string error;
const bool installed = plugin_loader::install_plugin(py, /*cloud_user_id=*/"", descriptor, error);
REQUIRE(installed);
CHECK(error.empty());
REQUIRE(descriptor.settings.count("twist_deg_per_mm") == 1);
CHECK(descriptor.settings.at("twist_deg_per_mm") == "1.5");
CHECK(descriptor.settings.at("taper_per_mm") == "-0.004");
// Identity keys are NOT captured as settings (they belong to [tool.orcaslicer.plugin]).
CHECK(descriptor.settings.count("name") == 0);
}