Merge remote-tracking branch 'public/feat/plugin-feature' into feature/speed-dial

# Conflicts:
#	src/slic3r/GUI/PluginsDialog.cpp
This commit is contained in:
Andrew
2026-07-13 19:39:42 +08:00
59 changed files with 3814 additions and 846 deletions

View File

@@ -79,7 +79,7 @@
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/PluginHostUi.hpp"
#include "slic3r/plugin/host/PluginHostUi.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include "GUI.hpp"
@@ -2702,6 +2702,54 @@ std::string get_system_info()
return out.str();
}
// wx/app-level plugin wiring, kept in one place: subscriptions to plugin
// loader events that drive GUI policy (plugins dialog refresh, network-agent
// registration, plate revalidation). The libslic3r dispatch hooks are NOT
// wired here -- PluginManager::initialize() installs those via
// plugin_hooks::install().
void GUI_App::init_plugin_gui_wiring()
{
PluginManager& plugin_mgr = PluginManager::instance();
auto refresh_plugins_dialog = [] {
if (!wxTheApp)
return;
GUI_App* app = &GUI::wxGetApp();
if (app->is_closing())
return;
app->CallAfter([app] {
if (!app->is_closing() && app->m_plugins_dlg)
app->m_plugins_dlg->update_plugin_dialog_ui();
});
};
plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
// current plate (on the UI thread) so the notification clears once its plugin is available.
if (wxTheApp && !wxGetApp().is_closing())
wxGetApp().CallAfter([]() {
if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
});
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
});
}
bool GUI_App::on_init_inner()
{
wxLog::SetActiveTarget(new wxBoostLog());
@@ -3105,25 +3153,12 @@ bool GUI_App::on_init_inner()
on_init_network();
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically.
// initialize() also installs the libslic3r hooks (capability resolver,
// slicing-pipeline dispatcher) via plugin_hooks::install() -- no
// per-capability wiring belongs here.
PluginManager& plugin_mgr = PluginManager::instance();
plugin_mgr.initialize();
ConfigBase::set_resolve_capability_fn([&plugin_mgr](const std::string& cap_name, const std::string& cap_type) {
auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type));
if (!plugin_cap)
return std::string();
PluginDescriptor descriptor;
if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor))
return std::string();
// Cloud plugins are resolved at runtime via the UUID in the middle field, so the first
// field keeps the friendly display name. Local plugins are looked up by plugin_key (the
// first field, with an empty UUID), so emit the plugin_key to keep them resolvable.
const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key;
return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name;
});
// Set cloud plugin directory from previous session so cloud-installed
// plugins are discovered even before the network agent is ready.
const std::string preset_folder = app_config->get("preset_folder");
@@ -3134,43 +3169,7 @@ bool GUI_App::on_init_inner()
plugin_mgr.discover_plugins(false, true);
auto refresh_plugins_dialog = [] {
if (!wxTheApp)
return;
GUI_App* app = &GUI::wxGetApp();
if (app->is_closing())
return;
app->CallAfter([app] {
if (!app->is_closing() && app->m_plugins_dlg)
app->m_plugins_dlg->update_plugin_dialog_ui();
});
};
plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
// current plate (on the UI thread) so the notification clears once its plugin is available.
if (wxTheApp && !wxGetApp().is_closing())
wxGetApp().CallAfter([]() {
if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
});
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
});
init_plugin_gui_wiring();
// Register all AppAction sources
m_action_registry.add_source(std::make_unique<ScriptActionSource>());

View File

@@ -776,6 +776,9 @@ private:
bool on_init_network(bool try_backup = false);
void init_networking_callbacks();
void init_app_config();
// GUI-side subscriptions to plugin loader events (dialog refresh,
// network-agent registration, plate revalidation).
void init_plugin_gui_wiring();
void remove_old_networking_plugins();
void drain_pending_events(int timeout_ms);
bool wait_for_network_idle(int timeout_ms);

View File

@@ -103,9 +103,9 @@ ScriptRunOutcome run_script_plugin_capability(const std::string& plugin_key, con
// ModelObject*/ModelVolume*/ModelInstance* aliases into host data and can mint ObjectIDs,
// which libslic3r requires on the main thread (ObjectID.hpp's non-atomic s_last_id). Running
// here makes those reads/instantiations legal and means nothing mutates the model underneath
// a run. The trade-off is that a slow execute() freezes the UI: the contract (see
// plugin_development.md) is to keep execute() quick and offload heavy work to the plugin's own
// threading.Thread. orca.host.ui calls already no-op their main-thread marshaling here.
// a run. The trade-off is that a slow execute() freezes the UI, so the contract is to keep
// execute() quick and offload heavy work to the plugin's own threading.Thread. orca.host.ui
// calls already no-op their main-thread marshaling here.
{
wxBusyCursor busy;
try {

View File

@@ -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();

View 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.

View File

@@ -1790,6 +1790,13 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
return;
}
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so the edited preset
// 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->is_plugin_backed())
m_config->update_plugin_manifest();
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
if (auto printer_tab = dynamic_cast<TabPrinter*>(this))
printer_tab->on_gcode_flavor_changed();
@@ -3073,9 +3080,9 @@ 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 = page->new_optgroup(L("Slicing Pipeline Plugin"), L"param_gcode", 0);
optgroup->hide_labels();
option = optgroup->get_option("post_process_plugin");
option = optgroup->get_option("slicing_pipeline_plugin");
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");