diff --git a/sandboxes/orca_fuzzy_slices_plugin_any.py b/sandboxes/orca_fuzzy_slices_plugin_any.py index ecff14ca28..428c4906cc 100644 --- a/sandboxes/orca_fuzzy_slices_plugin_any.py +++ b/sandboxes/orca_fuzzy_slices_plugin_any.py @@ -7,12 +7,6 @@ # author = "OrcaSlicer" # version = "0.01" # 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. @@ -44,6 +38,7 @@ Polygon.as_array()/set_points numpy path would be the faster route. """ import math import random +import json import orca @@ -55,9 +50,9 @@ _DEFAULTS = { } -def _params(ctx): +def _params(self): try: - src = dict(ctx.params) + src = json.loads(self.get_config()) except (AttributeError, TypeError): src = {} out = {} @@ -111,11 +106,14 @@ class FuzzySlices(orca.slicing.SlicingPipelineCapabilityBase): def get_name(self): return "Fuzzy Slices" + def get_default_config(self): + return _DEFAULTS + def execute(self, ctx): if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: return orca.ExecutionResult.success() - p = _params(ctx) + p = _params(self) 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") diff --git a/sandboxes/orca_gcode_stamp_plugin_any.py b/sandboxes/orca_gcode_stamp_plugin_any.py index 08273d189a..1846c9e98b 100644 --- a/sandboxes/orca_gcode_stamp_plugin_any.py +++ b/sandboxes/orca_gcode_stamp_plugin_any.py @@ -7,9 +7,6 @@ # 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. @@ -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 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. +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 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. """ 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: - text = dict(ctx.params).get("stamp_text", _DEFAULT_STAMP) - except (AttributeError, TypeError): - text = _DEFAULT_STAMP - return str(text).replace("\n", " ").strip() or _DEFAULT_STAMP + text = json.loads(self.get_config())["stamp_text"] + except (AttributeError, TypeError, ValueError, KeyError): + text = _DEFAULTS["stamp_text"] + return str(text).replace("\n", " ").strip() or _DEFAULTS["stamp_text"] class GCodeStamp(orca.slicing.SlicingPipelineCapabilityBase): def get_name(self): return "G-code Stamp" + + def get_default_config(self): + return _DEFAULTS def execute(self, ctx): # 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: 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 # expect a specific leading line), then insert the stamp right after it. diff --git a/sandboxes/orca_inset_plugin_any.py b/sandboxes/orca_inset_plugin_any.py index 23daf8271d..e05d17e462 100644 --- a/sandboxes/orca_inset_plugin_any.py +++ b/sandboxes/orca_inset_plugin_any.py @@ -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. """ +import json + 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): def get_name(self): return "Inset Every Slice" + def get_default_config(self): + return _DEFAULTS + def execute(self, ctx): if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: 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). - inset_scaled = int(round(INSET_MM / orca.slicing.unscale(1))) + inset_scaled = int(round(inset_mm / orca.slicing.unscale(1))) regions_touched = 0 for layer in ctx.object.layers(): diff --git a/sandboxes/orca_twistify_plugin_example_any.py b/sandboxes/orca_twistify_plugin_example_any.py index 72c746d6b0..5c43e3a147 100644 --- a/sandboxes/orca_twistify_plugin_example_any.py +++ b/sandboxes/orca_twistify_plugin_example_any.py @@ -7,13 +7,6 @@ # author = "OrcaSlicer" # version = "0.02" # 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. @@ -36,7 +29,7 @@ settings table above). The first object layer is untouched (z_rel = 0), so bed adhesion is unaffected. """ import math - +import json import orca _DEFAULTS = { @@ -48,9 +41,9 @@ _DEFAULTS = { } -def _params(ctx): +def _params(self): try: - src = dict(ctx.params) + src = json.loads(self.get_config()) except (AttributeError, TypeError): src = {} out = {} @@ -79,12 +72,15 @@ def _layer_params(z_rel, mm_to_scaled, p): class Twistify(orca.slicing.SlicingPipelineCapabilityBase): def get_name(self): return "Twistify" + + def get_default_config(self): + return _DEFAULTS def execute(self, ctx): if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: return orca.ExecutionResult.success() - p = _params(ctx) + p = _params(self) if _is_identity(p): return orca.ExecutionResult.success("Twistify: identity parameters, nothing to do") diff --git a/src/slic3r/GUI/PostProcessor.cpp b/src/slic3r/GUI/PostProcessor.cpp index bce7ea1814..71363740aa 100644 --- a/src/slic3r/GUI/PostProcessor.cpp +++ b/src/slic3r/GUI/PostProcessor.cpp @@ -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 // 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_plugin_settings(plugin_key); ExecutionResult exec_result; try { diff --git a/src/slic3r/plugin/PluginDescriptor.hpp b/src/slic3r/plugin/PluginDescriptor.hpp index e15dda2666..28c411b176 100644 --- a/src/slic3r/plugin/PluginDescriptor.hpp +++ b/src/slic3r/plugin/PluginDescriptor.hpp @@ -61,7 +61,6 @@ struct PluginDescriptor 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::vector dependencies; // Python dependency requirements declared by plugin package metadata - std::map settings; // [tool.orcaslicer.plugin.settings] table -> per-plugin params (ctx.params) std::vector 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. diff --git a/src/slic3r/plugin/PluginHooks.cpp b/src/slic3r/plugin/PluginHooks.cpp index 6e3a5f9b73..c6aaaf803c 100644 --- a/src/slic3r/plugin/PluginHooks.cpp +++ b/src/slic3r/plugin/PluginHooks.cpp @@ -70,9 +70,6 @@ void install_slicing_pipeline_hook() const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid; ExecutionResult r; 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 // is released between capabilities. PythonGILState gil; @@ -87,9 +84,6 @@ void install_slicing_pipeline_hook() ctx.step = step; ctx.print = &print; 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); } catch (const CanceledException&) { throw; // cancellation must reach process(), never become a slicing error diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index caaca47298..1d43e58bfc 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -555,17 +556,6 @@ std::shared_ptr PluginManager::get_plugin_capability( return nullptr; } -std::map PluginManager::get_plugin_settings(const std::string& plugin_key) const -{ - std::lock_guard lock(m_mutex); - - const Plugin* plugin = find_plugin_locked(plugin_key); - if (plugin == nullptr || !plugin->is_loaded()) - return {}; - - return plugin->descriptor.settings; -} - // ── Lifecycle ─────────────────────────────────────────────────────────────────────────────── 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; } + 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 cancelled = false; std::string registry_error; diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index 052b70579f..153e8cefc9 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -179,8 +179,6 @@ public: 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 get_plugin_settings(const std::string& plugin_key) const; // Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into. void set_cloud_user(const std::string& user_id); diff --git a/src/slic3r/plugin/PythonFileUtils.cpp b/src/slic3r/plugin/PythonFileUtils.cpp index 9a9c117f8f..eeb9bf3a3e 100644 --- a/src/slic3r/plugin/PythonFileUtils.cpp +++ b/src/slic3r/plugin/PythonFileUtils.cpp @@ -187,7 +187,6 @@ bool parse_pep723_toml(const std::string& toml_content, std::string& out_description, std::string& out_author, std::string& out_version, - std::map& out_settings, std::string& error) { out_deps.clear(); @@ -196,7 +195,6 @@ bool parse_pep723_toml(const std::string& toml_content, out_description.clear(); out_author.clear(); out_version.clear(); - out_settings.clear(); 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 == "author") out_author = 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_author, pep_version, - descriptor.settings, pep723_error)) { error = "Failed to parse PEP 723 metadata: " + pep723_error; return false; diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp index 19e32118aa..61c1daae4f 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp @@ -48,9 +48,6 @@ void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py:: py::class_(slicing, "SlicingPipelineContext") .def_readonly("orca_version", &SlicingPipelineContext::orca_version) .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, "Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in " "place; empty at every other step.") diff --git a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp index 7f0f322dce..639c087371 100644 --- a/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp @@ -18,10 +18,6 @@ struct SlicingPipelineContext { SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice }; 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; // 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 diff --git a/tests/slic3rutils/test_plugin_install.cpp b/tests/slic3rutils/test_plugin_install.cpp index c284a315d4..398fc75a4b 100644 --- a/tests/slic3rutils/test_plugin_install.cpp +++ b/tests/slic3rutils/test_plugin_install.cpp @@ -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 read_install_state(plugin_dir, scanned); 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); -} +} \ No newline at end of file