Merge branch 'main' into feature/h2c_support_clean_fixes

This commit is contained in:
SoftFever
2026-07-20 00:02:43 +08:00
committed by GitHub
630 changed files with 2317401 additions and 1240939 deletions

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>
@@ -447,6 +460,17 @@ enum class ActionButtonType : int {
abSendGCode
};
// Background for the extruder-group title chip and its edit buttons, matching the StaticGroup
// interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour.
static wxColour extruder_group_chip_bg()
{
#ifdef __WXOSX__
if (!wxGetApp().dark_mode())
return wxColour("#F7F7F7");
#endif
return StateColor::darkModeColorFor(*wxWHITE);
}
// Interactive title row for the sidebar extruder cards: "<title> ( <count> ) [edit]". The count shows the
// extruder's physical nozzle count on multi-nozzle printers (hidden elsewhere, SetCount(-1)), and the
// trailing button opens the manual nozzle-count editor when enabled (a plain dot otherwise).
@@ -455,11 +479,7 @@ class HoverLabel : public wxPanel
public:
HoverLabel(wxWindow *parent, const wxString &label) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
{
#ifdef __WXOSX__
SetBackgroundColour("#F7F7F7");
#else
SetBackgroundColour(*wxWHITE);
#endif
SetBackgroundColour(extruder_group_chip_bg());
auto sizer = new wxBoxSizer(wxHORIZONTAL);
m_label = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
@@ -483,11 +503,7 @@ public:
m_hover_btn = new ScalableButton(this, wxID_ANY, "dot");
m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1));
#ifdef __WXOSX__
m_hover_btn->SetBackgroundColour("#F7F7F7");
#else
m_hover_btn->SetBackgroundColour(*wxWHITE);
#endif
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (m_enabled && m_hover_on_click)
m_hover_on_click();
@@ -537,6 +553,17 @@ public:
void Rescale() { m_hover_btn->msw_rescale(); }
// Re-apply the chip colours on a live light/dark switch (they are set once at construction).
void sys_color_changed()
{
SetBackgroundColour(extruder_group_chip_bg());
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
for (wxStaticText *t : {m_brace_left, m_count, m_brace_right})
t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
Refresh();
}
private:
// Content changed: the cached best size (ours and, transitively, our ancestors') is stale,
// and the parent must re-lay this row or the sizer keeps allocating the old width.
@@ -612,6 +639,15 @@ struct ExtruderGroup : StaticGroup
for (int i = 0; i < 4; ++i)
ams[i]->msw_rescale();
}
void sys_color_changed()
{
if (hover_label)
hover_label->sys_color_changed();
if (btn_edit)
btn_edit->SetBackgroundColour(extruder_group_chip_bg());
Refresh();
}
};
struct Sidebar::priv
@@ -1262,11 +1298,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
//label_ams->SetMinSize({FromDIP(70), -1});
if (index >= 0) {
btn_edit = new ScalableButton(this, wxID_ANY, "dot");
#ifdef __WXOSX__
btn_edit->SetBackgroundColour("#F7F7F7");
#else
btn_edit->SetBackgroundColour(*wxWHITE);
#endif
btn_edit->SetBackgroundColour(extruder_group_chip_bg());
btn_edit->Hide();
btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) {
PopupWindow *window = new AMSCountPopupWindow(this, index);
@@ -3214,6 +3246,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
@@ -3250,7 +3283,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);
@@ -3320,8 +3354,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());
}
@@ -3852,6 +3885,10 @@ void Sidebar::sys_color_changed()
p->image_printer->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE));
p->image_printer_bed->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE));
for (ExtruderGroup *ext : {p->left_extruder, p->right_extruder, p->single_extruder})
if (ext)
ext->sys_color_changed();
// call a kill focus event to ensure new colors applied
for (ComboBox* combo : std::vector<ComboBox*>{p->combo_printer, p->combo_nozzle_dia, p->combo_printer_bed}){
wxFocusEvent fakeEvent(wxEVT_KILL_FOCUS);
@@ -5216,6 +5253,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};
@@ -6007,6 +6052,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
if (this->q->is_view3D_shown())
wxGetApp().open_speed_dial();
});
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
@@ -11162,8 +11211,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) {
@@ -11371,7 +11425,23 @@ void Plater::priv::update_plugin_when_launch(wxCommandEvent &event)
if (!app_config) return;
if (result == wxID_OK) {
app_config->set("update_network_plugin", "true");
// Apply the downloaded update right away and hot-reload the plug-in, the same
// way a manual version switch in Preferences behaves. When a file is still in
// use and cannot be replaced, fall back to installing on the next launch.
bool had_cache = false;
if (wxGetApp().install_network_plugin_from_ota(had_cache)) {
notification_manager->close_notification_of_type(NotificationType::BBLPluginUpdateAvailable);
app_config->set("update_network_plugin", "false");
if (wxGetApp().hot_reload_network_plugin()) {
MessageDialog dlg_ok(wxGetApp().mainframe, _L("Network plug-in switched successfully."), _L("Success"), wxOK | wxICON_INFORMATION);
dlg_ok.ShowModal();
} else {
MessageDialog dlg_fail(wxGetApp().mainframe, _L("Failed to load network plug-in. Please restart the application."), _L("Restart Required"), wxOK | wxICON_WARNING);
dlg_fail.ShowModal();
}
} else {
app_config->set("update_network_plugin", had_cache ? "true" : "false");
}
}
else if (result == wxID_NO) {
app_config->set("update_network_plugin", "false");
@@ -13970,8 +14040,13 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i
_obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
_obj->config.set_key_value("gap_fill_target", new ConfigOptionEnum<GapFillTarget>(GapFillTarget::gftNowhere));
print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0));
// ORCA: print the top surface spiral from the center outwards, so the tiles are comparable.
_obj->config.set_key_value("top_surface_fill_order", new ConfigOptionEnum<SurfaceFillOrder>(SurfaceFillOrder::Outward));
// ORCA: request the calibration's special toolpath order (chords first, center spiral
// last and inside-out) so opposing directions collide into the tactile lip the test
// reads. The special order only applies while the fill order is Default, so reset the
// profile's fill order on the calibration objects; changing the setting on the object
// afterwards deliberately overrides the special order.
_obj->config.set_key_value("calib_flowrate_topinfill_special_order", new ConfigOptionBool(true));
_obj->config.set_key_value("top_surface_fill_order", new ConfigOptionEnum<SurfaceFillOrder>(SurfaceFillOrder::Default));
// extract flowrate from name, filename format: flowrate_xxx
std::string obj_name = _obj->name;
@@ -16790,7 +16865,19 @@ 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;
// Enforce the missing-plugin block at the slicing choke point: menu/keyboard/queued triggers can
// carry a stale enabled state while plugins load asynchronously. refresh_missing_plugin_block
// rebuilds the missing sets and notifications from the active presets without running
// Print::validate; all other validation keeps upstream behavior and is surfaced by
// update_background_process() below.
if (refresh_missing_plugin_block()) {
p->partplate_list.get_curr_plate()->update_slice_ready_status(false);
p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": required plugins missing; slicing blocked.";
return;
}
// Stop the running (and queued) UI jobs and only proceed if they actually
// get stopped.
unsigned timeout_ms = 10000;
@@ -18721,12 +18808,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);
// 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);
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");