fix: preset resolution

This commit is contained in:
Ian Chua
2026-07-14 19:48:39 +08:00
parent 3daa4336e3
commit 71d0380467
8 changed files with 268 additions and 355 deletions

View File

@@ -615,11 +615,9 @@ set(SLIC3R_GUI_SOURCES
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
plugin/PluginFsUtils.hpp
plugin/CapabilityConfigDocument.cpp
plugin/CapabilityConfigDocument.hpp
plugin/PluginConfig.cpp
plugin/PluginConfig.hpp
plugin/PresetPluginConfig.cpp
plugin/PresetPluginConfig.hpp
plugin/PythonJsonUtils.hpp
plugin/PluginCatalog.cpp

View File

@@ -1,122 +0,0 @@
#include "CapabilityConfigDocument.hpp"
namespace Slic3r {
namespace {
constexpr const char* KEY_PLUGIN = "plugin_key";
constexpr const char* KEY_CAPABILITY = "capability";
constexpr const char* KEY_VERSION = "plugin_version";
constexpr const char* KEY_CAP_CONFIG = "cap_config";
std::string string_field(const nlohmann::json& entry, const char* key)
{
const auto it = entry.find(key);
return it != entry.end() && it->is_string() ? it->get<std::string>() : std::string();
}
bool is_recognized_entry(const nlohmann::json& entry, CapabilityConfigId& id)
{
if (!entry.is_object())
return false;
id.plugin_key = string_field(entry, KEY_PLUGIN);
id.capability = string_field(entry, KEY_CAPABILITY);
return !id.plugin_key.empty() && !id.capability.empty();
}
CapabilityConfigEntry decode_entry(const CapabilityConfigId& id, const nlohmann::json& entry)
{
CapabilityConfigEntry result;
result.id = id;
result.plugin_version = string_field(entry, KEY_VERSION);
const auto cap_it = entry.find(KEY_CAP_CONFIG);
result.cap_config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
return result;
}
} // namespace
CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::json& entries)
{
CapabilityConfigDocument document;
if (!entries.is_array())
return document;
for (const nlohmann::json& entry : entries) {
CapabilityConfigId id;
if (is_recognized_entry(entry, id))
document.m_entries[id] = entry;
else
document.m_opaque_entries.push_back(entry);
}
return document;
}
CapabilityConfigDocument CapabilityConfigDocument::from_root_json(const nlohmann::json& root)
{
const auto entries = root.find(KeyEntries);
return entries != root.end() ? from_entries(*entries) : CapabilityConfigDocument();
}
std::optional<CapabilityConfigEntry> CapabilityConfigDocument::find(const CapabilityConfigId& id) const
{
const auto it = m_entries.find(id);
if (it == m_entries.end())
return std::nullopt;
return decode_entry(it->first, it->second);
}
bool CapabilityConfigDocument::contains(const CapabilityConfigId& id) const
{
return m_entries.find(id) != m_entries.end();
}
bool CapabilityConfigDocument::upsert(CapabilityConfigEntry entry)
{
if (entry.id.plugin_key.empty() || entry.id.capability.empty())
return false;
nlohmann::json serialized = nlohmann::json::object();
const auto existing = m_entries.find(entry.id);
if (existing != m_entries.end() && existing->second.is_object())
serialized = existing->second;
serialized[KEY_PLUGIN] = entry.id.plugin_key;
serialized[KEY_CAPABILITY] = entry.id.capability;
serialized[KEY_VERSION] = entry.plugin_version;
serialized[KEY_CAP_CONFIG] = entry.cap_config;
m_entries[entry.id] = std::move(serialized);
return true;
}
bool CapabilityConfigDocument::erase(const CapabilityConfigId& id)
{
return m_entries.erase(id) != 0;
}
bool CapabilityConfigDocument::empty() const
{
return m_entries.empty() && m_opaque_entries.empty();
}
nlohmann::json CapabilityConfigDocument::serialize_entries() const
{
nlohmann::json result = nlohmann::json::array();
for (const auto& item : m_entries)
result.push_back(item.second);
for (const nlohmann::json& entry : m_opaque_entries)
result.push_back(entry);
return result;
}
nlohmann::json CapabilityConfigDocument::root_json() const
{
nlohmann::json root = nlohmann::json::object();
root[KeyEntries] = serialize_entries();
return root;
}
} // namespace Slic3r

View File

@@ -1,10 +1,14 @@
#include "PluginConfig.hpp"
#include <algorithm>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <slic3r/GUI/GUI.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/I18N.hpp>
#include <slic3r/GUI/format.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
@@ -14,12 +18,44 @@
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <stdexcept>
#include <wx/app.h>
#include <wx/utils.h>
namespace Slic3r {
namespace {
constexpr const char* KEY_PLUGIN = "plugin_key";
constexpr const char* KEY_CAPABILITY = "capability";
constexpr const char* KEY_VERSION = "plugin_version";
constexpr const char* KEY_CAP_CONFIG = "cap_config";
std::string string_field(const nlohmann::json& entry, const char* key)
{
const auto it = entry.find(key);
return it != entry.end() && it->is_string() ? it->get<std::string>() : std::string();
}
bool is_recognized_entry(const nlohmann::json& entry, CapabilityConfigId& id)
{
if (!entry.is_object())
return false;
id.plugin_key = string_field(entry, KEY_PLUGIN);
id.capability = string_field(entry, KEY_CAPABILITY);
return !id.plugin_key.empty() && !id.capability.empty();
}
CapabilityConfigEntry decode_entry(const CapabilityConfigId& id, const nlohmann::json& entry)
{
CapabilityConfigEntry result;
result.id = id;
result.plugin_version = string_field(entry, KEY_VERSION);
const auto cap_it = entry.find(KEY_CAP_CONFIG);
result.cap_config = cap_it != entry.end() ? *cap_it : nlohmann::json::object();
return result;
}
// PluginDescriptor::version is overwritten with the latest cloud version on a cloud merge, so it can
// name a version that is not the one on disk; installed_version is what actually loaded.
std::string running_plugin_version(const std::string& plugin_key)
@@ -30,6 +66,19 @@ std::string running_plugin_version(const std::string& plugin_key)
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
}
CapabilityConfigId make_id(const PluginCapabilityIdentifier& id)
{
return CapabilityConfigId{id.plugin_key, id.name};
}
// Null wherever the plugin host runs without the GUI app (the unit tests). wxGetApp() dereferences
// the app unconditionally, so ask wxWidgets instead.
const PresetBundle* active_preset_bundle()
{
const auto* app = dynamic_cast<const GUI::GUI_App*>(wxApp::GetInstance());
return app == nullptr ? nullptr : app->preset_bundle;
}
// PluginLoader stamps both halves onto the instance when it materializes the capability, so the
// caller never supplies them and cannot name another capability's entry. Empty means the instance
// was never materialized: refuse rather than read or clobber a wrong entry.
@@ -42,11 +91,8 @@ std::pair<std::string, std::string> capability_identity(const PluginCapabilityIn
return id;
}
// The identity above plus the type, which decides which preset may override the capability (see
// preset_type_for_capability). Taken from the instance, not the loader's registry: a capability
// calling get_config() from on_load() is not registered yet and must still see its preset's config.
// A raising get_type() costs it only the preset layer — Unknown names no preset, so the base config
// answers.
// The capability type identifies the preset that owns its override. If a plugin raises while
// reporting its type, resolve against the base config instead of failing the capability config API.
PluginCapabilityIdentifier capability_full_identity(const PluginCapabilityInterface& capability, const char* api_name)
{
const auto [plugin_key, capability_name] = capability_identity(capability, api_name);
@@ -63,6 +109,88 @@ PluginCapabilityIdentifier capability_full_identity(const PluginCapabilityInterf
} // namespace
CapabilityConfigDocument CapabilityConfigDocument::from_entries(const nlohmann::json& entries)
{
CapabilityConfigDocument document;
if (!entries.is_array())
return document;
for (const nlohmann::json& entry : entries) {
CapabilityConfigId id;
if (is_recognized_entry(entry, id))
document.m_entries[id] = entry;
else
document.m_opaque_entries.push_back(entry);
}
return document;
}
CapabilityConfigDocument CapabilityConfigDocument::from_root_json(const nlohmann::json& root)
{
const auto entries = root.find(KeyEntries);
return entries != root.end() ? from_entries(*entries) : CapabilityConfigDocument();
}
std::optional<CapabilityConfigEntry> CapabilityConfigDocument::find(const CapabilityConfigId& id) const
{
const auto it = m_entries.find(id);
if (it == m_entries.end())
return std::nullopt;
return decode_entry(it->first, it->second);
}
bool CapabilityConfigDocument::contains(const CapabilityConfigId& id) const
{
return m_entries.find(id) != m_entries.end();
}
bool CapabilityConfigDocument::upsert(CapabilityConfigEntry entry)
{
if (entry.id.plugin_key.empty() || entry.id.capability.empty())
return false;
nlohmann::json serialized = nlohmann::json::object();
const auto existing = m_entries.find(entry.id);
if (existing != m_entries.end() && existing->second.is_object())
serialized = existing->second;
serialized[KEY_PLUGIN] = entry.id.plugin_key;
serialized[KEY_CAPABILITY] = entry.id.capability;
serialized[KEY_VERSION] = entry.plugin_version;
serialized[KEY_CAP_CONFIG] = entry.cap_config;
m_entries[entry.id] = std::move(serialized);
return true;
}
bool CapabilityConfigDocument::erase(const CapabilityConfigId& id)
{
return m_entries.erase(id) != 0;
}
bool CapabilityConfigDocument::empty() const
{
return m_entries.empty() && m_opaque_entries.empty();
}
nlohmann::json CapabilityConfigDocument::serialize_entries() const
{
nlohmann::json result = nlohmann::json::array();
for (const auto& item : m_entries)
result.push_back(item.second);
for (const nlohmann::json& entry : m_opaque_entries)
result.push_back(entry);
return result;
}
nlohmann::json CapabilityConfigDocument::root_json() const
{
nlohmann::json root = nlohmann::json::object();
root[KeyEntries] = serialize_entries();
return root;
}
void PluginConfig::load()
{
const std::string path = plugin_config_file();
@@ -194,6 +322,139 @@ bool PluginConfig::dirty() const
return m_dirty;
}
std::string plugin_overrides_of(const Preset& preset)
{
const auto* opt = dynamic_cast<const ConfigOptionString*>(preset.config.option(PLUGIN_OVERRIDES_OPTION_KEY));
return opt == nullptr ? std::string() : opt->value;
}
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error)
{
document = CapabilityConfigDocument();
error.clear();
if (raw.empty())
return true;
const nlohmann::json parsed = nlohmann::json::parse(raw, nullptr, /* allow_exceptions */ false);
if (parsed.is_discarded()) {
error = "The preset stores invalid plugin capability configuration JSON.";
return false;
}
if (!parsed.is_array()) {
error = "The preset's plugin capability configuration is not an array and cannot be edited.";
return false;
}
document = CapabilityConfigDocument::from_entries(parsed);
return true;
}
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
{
return document.empty() ? std::string() : document.serialize_entries().dump();
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const
{
EffectiveCapabilityConfig result;
result.id = make_id(id);
result.running_plugin_version = running_plugin_version(id.plugin_key);
const BaseConfig base = PluginManager::instance().get_config().get_config(id.plugin_key, id.name);
result.has_base_config = !base.empty();
if (const auto entry = overrides.find(result.id)) {
result.has_preset_override = true;
result.config = entry->cap_config;
result.stored_plugin_version = entry->plugin_version;
return result;
}
if (result.has_base_config) {
result.config = base.config;
result.stored_plugin_version = base.plugin_version;
}
return result;
}
MutationResult PresetPluginConfigService::set_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id,
const nlohmann::json& value) const
{
MutationResult result;
const CapabilityConfigId config_id = make_id(id);
const std::string version = running_plugin_version(id.plugin_key);
// A no-op is a successful unchanged result: re-saving the displayed value must not mark the
// preset dirty.
const auto existing = overrides.find(config_id);
if (existing && existing->cap_config == value && existing->plugin_version == version) {
result.ok = true;
result.effective = get_effective_config(overrides, id);
return result;
}
overrides.upsert({config_id, version, value});
result.ok = true;
result.changed = true;
result.effective = get_effective_config(overrides, id);
return result;
}
MutationResult PresetPluginConfigService::remove_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const
{
MutationResult result;
result.ok = true;
result.changed = overrides.erase(make_id(id));
result.effective = get_effective_config(overrides, id);
return result;
}
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityIdentifier& id)
{
const PresetPluginConfigService service;
CapabilityConfigDocument overrides;
const PresetBundle* bundle = active_preset_bundle();
const Preset* preset = nullptr;
if (bundle != nullptr) {
const std::string type_key = plugin_capability_type_to_string(id.type);
for (const auto& [key, def] : print_config_def.options) {
if (def.plugin_type != type_key)
continue;
const auto& print_options = Preset::print_options();
if (std::find(print_options.begin(), print_options.end(), key) != print_options.end()) {
preset = &bundle->prints.get_edited_preset();
break;
}
const auto& printer_options = Preset::printer_options();
if (std::find(printer_options.begin(), printer_options.end(), key) != printer_options.end()) {
preset = &bundle->printers.get_edited_preset();
break;
}
}
}
if (preset != nullptr) {
std::string error;
if (!parse_plugin_overrides(plugin_overrides_of(*preset), overrides, error)) {
// Text we cannot read is not an override: log it and resolve against the base config.
BOOST_LOG_TRIVIAL(error) << "Preset \"" << preset->name << "\": " << error;
overrides = CapabilityConfigDocument();
}
}
return service.get_effective_config(overrides, id);
}
nlohmann::json capability_get_config(const PluginCapabilityInterface& capability)
{
// Shares its resolution with the dialogs, so a capability reads back exactly the config its UI

View File

@@ -180,36 +180,6 @@ std::vector<PluginCapabilityIdentifier> capabilities_in_use(Preset::Type type, c
return result;
}
Preset::Type preset_type_for_capability(PluginCapabilityType type)
{
if (type == PluginCapabilityType::Unknown)
return Preset::TYPE_INVALID;
// The three typed option lists are disjoint, so the first that holds the key names its owner.
const auto owner_of_option = [](const std::string& key) {
const auto holds = [&key](const std::vector<std::string>& keys) {
return std::find(keys.begin(), keys.end(), key) != keys.end();
};
if (holds(Preset::print_options()))
return Preset::TYPE_PRINT;
if (holds(Preset::filament_options()))
return Preset::TYPE_FILAMENT;
if (holds(Preset::printer_options()))
return Preset::TYPE_PRINTER;
return Preset::TYPE_INVALID;
};
const std::string type_key = plugin_capability_type_to_string(type);
for (const auto& [key, def] : print_config_def.options) {
if (def.plugin_type != type_key)
continue;
const Preset::Type owner = owner_of_option(key);
if (owner != Preset::TYPE_INVALID)
return owner;
}
return Preset::TYPE_INVALID;
}
static std::string resolve_cloud_base_url()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";

View File

@@ -80,13 +80,6 @@ std::string resolve_recovery_url(const PluginCapabilityRef& ref);
std::vector<PluginCapabilityRef> referenced_capabilities(Preset::Type type, const Preset& preset);
std::vector<PluginCapabilityIdentifier> capabilities_in_use(Preset::Type type, const Preset& preset);
// The preset type whose presets can reference capabilities of `type` and therefore carry their
// overrides. Derived from the ConfigDef rather than hardcoded: a plugin-backed option names the
// capability type it accepts (ConfigOptionDef::plugin_type) and belongs to exactly one preset type,
// so declaring the option is all it takes to map a new capability type. TYPE_INVALID when no option
// accepts the type — nothing can reference it, so no preset owns it.
Preset::Type preset_type_for_capability(PluginCapabilityType type);
// The referenced capabilities of the active preset(s) of `type` that are loaded right now: the set
// that can actually be configured. Missing and broken refs are absent, having no instance to ask for
// a config UI or defaults. A loaded-but-disabled capability IS listed — it still has stored config

View File

@@ -1,170 +0,0 @@
#include "PresetPluginConfig.hpp"
#include "PluginManager.hpp"
#include "PluginResolver.hpp"
#include <slic3r/GUI/GUI_App.hpp>
#include <boost/log/trivial.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <wx/app.h>
namespace Slic3r {
namespace {
CapabilityConfigId make_id(const PluginCapabilityIdentifier& id)
{
return CapabilityConfigId{id.plugin_key, id.name};
}
std::string running_plugin_version(const std::string& plugin_key)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().get_catalog().try_get_valid_plugin_descriptor(plugin_key, descriptor))
return {};
return descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version;
}
// Null wherever the plugin host runs without the GUI app (the unit tests). wxGetApp() dereferences
// the app unconditionally, so ask wxWidgets instead.
const PresetBundle* active_preset_bundle()
{
const auto* app = dynamic_cast<const GUI::GUI_App*>(wxApp::GetInstance());
return app == nullptr ? nullptr : app->preset_bundle;
}
// The active preset that can carry an override for a capability of `type`. Edited, not selected: an
// override typed into the tab configures the next slice the same way every other unsaved setting does.
const Preset* active_preset_for(Preset::Type type)
{
const PresetBundle* bundle = active_preset_bundle();
if (bundle == nullptr)
return nullptr;
switch (type) {
case Preset::TYPE_PRINT: return &bundle->prints.get_edited_preset();
case Preset::TYPE_PRINTER: return &bundle->printers.get_edited_preset();
// Deliberately unimplemented. We currently don't support any filament-based
// plugin capabilities. We will defer this to when we have filament-based plugins
// capabilities to have a clearer understanding on how to implement this.
case Preset::TYPE_FILAMENT: return nullptr;
default: return nullptr;
}
}
} // namespace
std::string plugin_overrides_of(const Preset& preset)
{
const auto* opt = dynamic_cast<const ConfigOptionString*>(preset.config.option(PLUGIN_OVERRIDES_OPTION_KEY));
return opt == nullptr ? std::string() : opt->value;
}
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error)
{
document = CapabilityConfigDocument();
error.clear();
if (raw.empty())
return true;
const nlohmann::json parsed = nlohmann::json::parse(raw, nullptr, /* allow_exceptions */ false);
if (parsed.is_discarded()) {
error = "The preset stores invalid plugin capability configuration JSON.";
return false;
}
if (!parsed.is_array()) {
error = "The preset's plugin capability configuration is not an array and cannot be edited.";
return false;
}
document = CapabilityConfigDocument::from_entries(parsed);
return true;
}
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
{
return document.empty() ? std::string() : document.serialize_entries().dump();
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const
{
EffectiveCapabilityConfig result;
result.id = make_id(id);
result.running_plugin_version = running_plugin_version(id.plugin_key);
const BaseConfig base = PluginManager::instance().get_config().get_config(id.plugin_key, id.name);
result.has_base_config = !base.empty();
if (const auto entry = overrides.find(result.id)) {
result.has_preset_override = true;
result.config = entry->cap_config;
result.stored_plugin_version = entry->plugin_version;
return result;
}
if (result.has_base_config) {
result.config = base.config;
result.stored_plugin_version = base.plugin_version;
}
return result;
}
MutationResult PresetPluginConfigService::set_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id,
const nlohmann::json& value) const
{
MutationResult result;
const CapabilityConfigId config_id = make_id(id);
const std::string version = running_plugin_version(id.plugin_key);
// A no-op is a successful unchanged result: re-saving the displayed value must not mark the
// preset dirty.
const auto existing = overrides.find(config_id);
if (existing && existing->cap_config == value && existing->plugin_version == version) {
result.ok = true;
result.effective = get_effective_config(overrides, id);
return result;
}
overrides.upsert({config_id, version, value});
result.ok = true;
result.changed = true;
result.effective = get_effective_config(overrides, id);
return result;
}
MutationResult PresetPluginConfigService::remove_preset_override(CapabilityConfigDocument& overrides,
const PluginCapabilityIdentifier& id) const
{
MutationResult result;
result.ok = true;
result.changed = overrides.erase(make_id(id));
result.effective = get_effective_config(overrides, id);
return result;
}
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityIdentifier& id)
{
const PresetPluginConfigService service;
CapabilityConfigDocument overrides;
if (const Preset* preset = active_preset_for(preset_type_for_capability(id.type))) {
std::string error;
if (!parse_plugin_overrides(plugin_overrides_of(*preset), overrides, error)) {
// Text we cannot read is not an override: log it and resolve against the base config.
BOOST_LOG_TRIVIAL(error) << "Preset \"" << preset->name << "\": " << error;
overrides = CapabilityConfigDocument();
}
}
return service.get_effective_config(overrides, id);
}
} // namespace Slic3r

View File

@@ -63,10 +63,8 @@ public:
};
// The same resolution against the preset that is active right now, rather than a document the caller
// holds: this is what a running capability reads through the Python config API. Only one preset type
// can reference a given capability type (preset_type_for_capability), so there is exactly one preset
// to consult. Falls back to the base config for filament capabilities (see active_preset_for) and in
// a host with no preset bundle (the plugin unit tests).
// holds: this is what a running capability reads through the Python config API. It falls back to the
// base config when no active preset bundle is available.
EffectiveCapabilityConfig active_capability_config(const PluginCapabilityIdentifier& id);
} // namespace Slic3r

View File

@@ -72,18 +72,3 @@ TEST_CASE("referenced_capabilities skips malformed manifest entries", "[PluginRe
CHECK(capability_names(referenced_capabilities(Preset::TYPE_PRINT, preset)) == std::vector<std::string>{"CapA"});
}
TEST_CASE("preset_type_for_capability names the preset type whose options reference the capability", "[PluginResolver]")
{
// Read out of the ConfigDef: declaring a plugin_type on an option is what puts its capability
// type on this map.
CHECK(preset_type_for_capability(PluginCapabilityType::SlicingPipeline) == Preset::TYPE_PRINT);
CHECK(preset_type_for_capability(PluginCapabilityType::PrinterConnection) == Preset::TYPE_PRINTER);
}
TEST_CASE("preset_type_for_capability leaves capability types no option accepts unowned", "[PluginResolver]")
{
// No option accepts them, so no preset can override them: they read config.json alone.
CHECK(preset_type_for_capability(PluginCapabilityType::Automation) == Preset::TYPE_INVALID);
CHECK(preset_type_for_capability(PluginCapabilityType::Unknown) == Preset::TYPE_INVALID);
}