feat: Python Plugins

This commit is contained in:
Ian Chua
2026-07-02 17:49:36 +08:00
parent 395e070a0e
commit ecddf3d18f
183 changed files with 49955 additions and 2120 deletions
+245 -7
View File
@@ -1,15 +1,25 @@
#include "Plater.hpp"
#include "../Utils/NetworkAgent.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r_version.h"
#include <cstddef>
#include <algorithm>
#include <chrono>
#include <numeric>
#include <limits>
#include <optional>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PluginResolver.hpp>
#include <vector>
#include <string>
#include <regex>
#include <future>
#include <thread>
#include <atomic>
#include <mutex>
#include <boost/algorithm/string.hpp>
#include <boost/iterator/counting_iterator.hpp>
#include <boost/optional.hpp>
@@ -21,6 +31,7 @@
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <wx/msgdlg.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/button.h>
@@ -37,6 +48,8 @@
#include <wx/debug.h>
#include <wx/busyinfo.h>
#include <wx/event.h>
#include <wx/evtloop.h>
#include <wx/timer.h>
#include <wx/wrapsizer.h>
#ifdef _WIN32
#include <wx/richtooltip.h>
@@ -2502,6 +2515,7 @@ void Sidebar::update_all_preset_comboboxes()
auto p_mainframe = wxGetApp().mainframe;
auto cfg = preset_bundle.printers.get_edited_preset().config;
const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin();
if (preset_bundle.use_bbl_network()) {
//only show connection button for not-BBL printer
@@ -2538,7 +2552,8 @@ void Sidebar::update_all_preset_comboboxes()
print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode;
}
p_mainframe->load_printer_url(url, apikey);
if (!use_native_device_tab)
p_mainframe->load_printer_url(url, apikey);
p_mainframe->set_print_button_to_default(print_btn_type);
@@ -2608,8 +2623,7 @@ void Sidebar::update_all_preset_comboboxes()
update_printer_thumbnail();
}
// Orca:: show device tab based on vendor type
p_mainframe->show_device(preset_bundle.use_bbl_device_tab());
p_mainframe->show_device(use_native_device_tab);
p_mainframe->m_tabpanel->SetSelection(p_mainframe->m_tabpanel->GetSelection());
}
@@ -4458,6 +4472,14 @@ struct Plater::priv
bool m_ignore_event{false};
bool m_slice_all{false};
bool m_is_slicing {false};
// Missing-plugin set signatures (sorted full refs joined by '\n'), one per notification. They
// gate plugin-load re-validation and avoid needlessly recreating the notification when the set
// is unchanged. Whether missing plugins block slicing is derived directly from PluginResolver
// (has_missing_plugins()), not cached here.
std::string m_local_missing_shown_sig;
std::string m_cloud_missing_shown_sig;
std::string m_inactive_shown_sig;
std::string m_broken_shown_sig;
bool auto_reslice_pending {false};
bool auto_reslice_after_cancel {false};
bool m_is_publishing {false};
@@ -10361,8 +10383,13 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
update_sidebar();
int old_sel = e.GetOldSelection();
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_device_tab() && new_sel == MainFrame::tpMonitor) {
if (!Slic3r::NetworkAgent::is_network_module_loaded()) {
const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin();
const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin);
if (use_native_device_tab && new_sel == MainFrame::tpMonitor) {
// BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
e.Veto();
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel;
if (q) {
@@ -15944,7 +15971,18 @@ void Plater::reslice()
// and notify user that he should leave it first.
if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
return;
// Keep slicing blocked even if a toolbar/menu path has not refreshed its enabled state yet.
// validate_current_plate also refreshes the missing-plugin notifications and plate readiness.
bool model_fits = false;
bool validate_error = false;
validate_current_plate(model_fits, validate_error);
if (!model_fits || validate_error) {
p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": current plate validation failed; slicing blocked.";
return;
}
// Stop the running (and queued) UI jobs and only proceed if they actually
// get stopped.
unsigned timeout_ms = 10000;
@@ -17852,12 +17890,212 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error)
}*/
}
// Missing-plugin validation (both technologies): block slicing while the active preset(s)
// reference plugin capabilities that are not installed/loadable here. The helper rebuilds the
// missing sets, manages the notifications, and reports whether slicing is blocked. plugins_block_changed
// is set when the block toggled, so the Slice button can be refreshed below.
bool plugins_block_changed = false;
if (refresh_missing_plugin_block(&plugins_block_changed)) {
model_fits = false;
validate_error = true;
}
PartPlate* part_plate = p->partplate_list.get_curr_plate();
part_plate->update_slice_ready_status(model_fits);
part_plate->update_slice_ready_status(model_fits && !validate_error);
// The toolbar Slice button is normally refreshed only by the canvas
// (EVT_GLCANVAS_ENABLE_ACTION_BUTTONS) on geometry updates. When the missing-plugin block toggles
// without a geometry change (e.g. a plugin finishing loading, or a setting edit that drops the
// last missing plugin), refresh it here — AFTER update_slice_ready_status set the plate's
// can_slice() flag that get_enable_slice_status() reads — so the button doesn't lag until the
// next bed click.
if (plugins_block_changed && !p->background_process.running())
p->main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, model_fits && !validate_error);
return;
}
bool Plater::refresh_missing_plugin_block(bool* block_toggled)
{
// PluginResolver owns the per-preset-type missing sets; rebuild them from each preset's own
// "plugins" manifest so the state is always fresh. A plugin is resolved when it is
// installed/loaded, or when no active setting references it any more (refresh drops it then).
// The block is derived solely from PluginResolver; snapshot it before the refresh to detect a
// toggle (so the caller can refresh the Slice button).
const bool was_blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
refresh_missing_plugins(*wxGetApp().preset_bundle);
const auto missing_refs = [](const std::vector<MissingPlugin>& missing) {
std::vector<std::string> refs;
refs.reserve(missing.size());
for (const MissingPlugin& m : missing)
refs.emplace_back(create_full_ref(m.ref));
return refs;
};
const auto signature = [&missing_refs](const std::vector<MissingPlugin>& missing) {
std::vector<std::string> refs = missing_refs(missing);
std::sort(refs.begin(), refs.end());
std::string sig;
for (const std::string& r : refs) { sig += r; sig += '\n'; }
return sig;
};
// Show/refresh the non-closable notification for one missing set. Only (re)create it when the
// set changes; pushing every validate would close+recreate it (flicker, reset hover) since
// validate runs on many triggers. shown_sig also gates plugin-load re-validation.
const auto update = [&](NotificationType type, const std::vector<MissingPlugin>& missing,
std::string* shown_sig, const std::string& header,
const std::string& resolve_label,
std::function<bool(wxEvtHandler*)> resolve_action) {
if (missing.empty()) {
if (!shown_sig->empty()) {
p->notification_manager->close_notification_of_type(type);
shown_sig->clear();
}
return;
}
const std::string sig = signature(missing);
if (*shown_sig != sig) {
std::vector<JumpTo> body;
for (const auto& m : missing)
body.emplace_back(JumpTo{m.ref.capability_name, m.opt, m.opt_type});
p->notification_manager->push_plugin_missing_notification(
type, header, resolve_label, std::move(body), std::move(resolve_action));
*shown_sig = sig;
}
};
const std::vector<MissingPlugin> missing_cloud = get_missing_cloud_plugins();
const std::vector<MissingPlugin> missing_local = get_missing_local_plugins();
const std::vector<std::string> missing_cloud_refs = missing_refs(missing_cloud);
const std::vector<std::string> missing_local_refs = missing_refs(missing_local);
update(NotificationType::OrcaCloudPluginMissingError, missing_cloud,
&p->m_cloud_missing_shown_sig,
_u8L("OrcaCloud plugins required by the current preset are not installed:"),
_u8L("Install Plugins"),
[this, missing_cloud_refs](wxEvtHandler*) { install_missing_cloud_plugins(missing_cloud_refs); return false; });
// "Find on OrcaCloud" is only a suggestion: it opens the browser but cannot resolve the missing
// plugin in-session, so it never closes the notification or unblocks slicing. The user resolves a
// local plugin by installing it or by changing the setting that needs it.
update(NotificationType::OrcaLocalPluginMissingError, missing_local,
&p->m_local_missing_shown_sig,
_u8L("Local plugins required by the current preset are missing:"),
_u8L("Find on OrcaCloud"),
[missing_local_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(missing_local_refs); return false; });
const std::vector<MissingPlugin> inactive = get_inactive_plugins();
const std::vector<MissingPlugin> broken = get_broken_plugins();
const std::vector<std::string> inactive_refs = missing_refs(inactive);
const std::vector<std::string> broken_refs = missing_refs(broken);
update(NotificationType::OrcaPluginInactiveError, inactive,
&p->m_inactive_shown_sig,
_u8L("Plugins required by the current preset are not activated:"),
_u8L("Activate Now"),
[this, inactive_refs](wxEvtHandler*) { enable_inactive_plugins(inactive_refs); return false; });
update(NotificationType::OrcaPluginCapabilityUnavailableError, broken,
&p->m_broken_shown_sig,
_u8L("The installed plugin does not provide the required capability — it may be outdated:"),
_u8L("Find on OrcaCloud"),
[broken_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(broken_refs); return false; });
const bool blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
if (block_toggled)
*block_toggled = (was_blocked != blocked);
return blocked;
}
void Plater::revalidate_current_plate_if_plugins_missing()
{
// Only do work while a missing-plugin notification is up, so the plugin-load hook does not
// trigger a full validation for every plugin that loads during normal startup/use.
if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() &&
p->m_inactive_shown_sig.empty() && p->m_broken_shown_sig.empty())
return;
bool model_fits = true, validate_error = false;
validate_current_plate(model_fits, validate_error);
}
void Plater::install_missing_cloud_plugins(const std::vector<std::string>& cloud_refs)
{
if (cloud_refs.empty())
return;
// Shared between the UI-thread dialog/timer and the resolver's worker thread.
struct InstallProgressState
{
std::atomic<bool> cancel{false};
std::atomic<bool> finished{false};
std::atomic<bool> torn_down{false};
std::mutex mtx;
std::string message;
};
auto state = std::make_shared<InstallProgressState>();
state->message = _u8L("Preparing to install plugins...");
wxWindow* parent = wxGetApp().mainframe;
auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100,
parent, wxPD_APP_MODAL | wxPD_CAN_ABORT);
dialog->Pulse();
// UI-thread timer: animate the pulse, observe the Cancel button, and tear down when the worker
// signals completion. The timer is deleted via CallAfter so it is never freed inside its own
// handler.
auto* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [this, dialog, timer, state](wxTimerEvent&) {
std::string msg;
{
std::lock_guard<std::mutex> lock(state->mtx);
msg = state->message;
}
// Once cancellation is requested, the in-flight plugin still has to finish; reflect that.
if (state->cancel)
msg = _u8L("Cancelling — finishing the current plugin...");
if (!dialog->Pulse(from_u8(msg)))
state->cancel = true;
// Tear down exactly once: Stop() prevents further ticks, but guard so a stale queued tick
// can never double-Destroy the dialog or double-delete the timer.
if (state->finished && !state->torn_down.exchange(true)) {
timer->Stop();
dialog->Destroy();
wxGetApp().CallAfter([timer]() { delete timer; });
revalidate_current_plate_if_plugins_missing();
}
});
timer->Start(100);
PluginInstallProgress progress;
progress.on_plugin_begin = [state](const std::string& name, std::size_t /*index*/, std::size_t /*total*/) {
std::lock_guard<std::mutex> lock(state->mtx);
state->message = (boost::format(_u8L("Installing %1%...")) % name).str();
};
progress.is_cancelled = [state]() { return state->cancel.load(); };
progress.on_finished = [state]() { state->finished = true; };
resolve_missing_plugins(cloud_refs, std::move(progress));
}
void Plater::enable_inactive_plugins(const std::vector<std::string>& refs)
{
if (refs.empty())
return;
// Local and instant — load the plugin and/or enable the capability. The plugin-load callback
// re-validates the plate and clears (or reclassifies) the notification; no progress dialog needed.
resolve_inactive_plugins(refs);
}
bool Plater::plugins_block_slicing() const
{
// Single source of truth: slicing is blocked while PluginResolver still has unresolved plugin
// references — missing (download), inactive (activate), or broken (capability unavailable).
return has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
}
void Plater::open_platesettings_dialog(wxCommandEvent& evt) {
int plate_index = evt.GetInt();
PlateSettingsDialog dlg(this, _L("Plate Settings"), evt.GetString() == "only_layer_sequence");