mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
feat: Python Plugins
This commit is contained in:
+245
-7
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user