From 790a01061545afee7009e0e0f8eaa3d28fc8b2bc Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 21 Aug 2026 14:40:35 +0800 Subject: [PATCH 01/57] feat: plater notification API for plugins --- src/slic3r/plugin/host/PluginHostUi.cpp | 103 ++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/src/slic3r/plugin/host/PluginHostUi.cpp b/src/slic3r/plugin/host/PluginHostUi.cpp index c098ea3224..8fc3f12877 100644 --- a/src/slic3r/plugin/host/PluginHostUi.cpp +++ b/src/slic3r/plugin/host/PluginHostUi.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include +#include #include #include #include @@ -44,16 +46,20 @@ namespace { struct GilSafeCallable { py::object fn; + std::atomic_bool active{true}; explicit GilSafeCallable(py::object f) : fn(std::move(f)) {} + void disable() + { + active.store(false, std::memory_order_release); + PythonGILState gil; + if (gil) + fn = py::object(); + else + (void) fn.release(); + } ~GilSafeCallable() { - if (fn) { - PythonGILState gil; - if (gil) - fn = py::object(); - else - (void) fn.release(); - } + disable(); } }; using CallablePtr = std::shared_ptr; @@ -166,11 +172,34 @@ public: } return out; } + void bind_callback(const CallablePtr& callback, const std::string& plugin_key) + { + if (!callback) + return; + std::lock_guard lk(m_mtx); + m_callbacks[plugin_key].push_back(callback); + } + std::vector take_callbacks_for_plugin(const std::string& plugin_key) + { + std::lock_guard lk(m_mtx); + auto it = m_callbacks.find(plugin_key); + if (it == m_callbacks.end()) + return {}; + std::vector callbacks; + callbacks.reserve(it->second.size()); + for (const std::weak_ptr& weak_callback : it->second) { + if (auto callback = weak_callback.lock()) + callbacks.push_back(std::move(callback)); + } + m_callbacks.erase(it); + return callbacks; + } private: std::mutex m_mtx; std::unordered_map m_resources; std::unordered_map m_owners; + std::unordered_map>> m_callbacks; int m_next_id{1}; }; @@ -448,6 +477,46 @@ void progress_close(int id) }); } +void plater_notification(NotificationManager::NotificationLevel notification_level, const std::string& text, + const std::string& hypertext, py::object on_click) +{ + const std::string plugin_key = PluginAuditManager::instance().current_plugin(); + CallablePtr holder = make_holder(std::move(on_click)); + if (holder) + UiRegistry::instance().bind_callback(holder, plugin_key); + + std::function callback; + if (holder) { + callback = [holder](wxEvtHandler*) -> bool { + if (!holder->active.load(std::memory_order_acquire)) + return false; + + PythonGILState gil; + if (!gil) + return false; + try { + py::object result = holder->fn(); + return result.is_none() || result.cast(); + } catch (py::error_already_set& e) { + BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what(); + PyErr_Clear(); + return false; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised: " << e.what(); + return false; + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "orca.host.ui notification callback raised an unknown exception"; + return false; + } + }; + } + + run_on_ui_blocking([notification_level, text, hypertext, callback = std::move(callback)]() mutable { + wxGetApp().plater()->get_notification_manager()->push_notification(NotificationType::CustomNotification, notification_level, text, + hypertext, std::move(callback)); + }); +} + } // namespace void PluginHostUi::RegisterBindings(pybind11::module_& host) @@ -530,6 +599,23 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host) ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"), py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE, "Create a native progress dialog and return a ProgressDialog handle."); + + py::enum_(ui, "NotificationLevel") + .value("ProgressBarNotificationLevel", NotificationManager::NotificationLevel::ProgressBarNotificationLevel) + .value("HintNotificationLevel", NotificationManager::NotificationLevel::HintNotificationLevel) + .value("RegularNotificationLevel", NotificationManager::NotificationLevel::RegularNotificationLevel) + .value("PrintInfoNotificationLevel", NotificationManager::NotificationLevel::PrintInfoNotificationLevel) + .value("PrintInfoShortNotificationLevel", NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel) + .value("ImportantNotificationLevel", NotificationManager::NotificationLevel::ImportantNotificationLevel) + .value("WarningNotificationLevel", NotificationManager::NotificationLevel::WarningNotificationLevel) + .value("SeriousWarningNotificationLevel", NotificationManager::NotificationLevel::SeriousWarningNotificationLevel) + .value("ErrorNotificationLevel", NotificationManager::NotificationLevel::ErrorNotificationLevel) + .export_values(); + + ui.def("push_notification", &plater_notification, py::arg("notification_level"), py::arg("text"), + py::arg("hyper_text") = "", py::arg("on_click") = py::none(), + "Push a plater notification. hyper_text is an underlined label; on_click() is called when it is clicked " + "and may return True to close the notification."); } void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key) @@ -538,6 +624,9 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key) return; auto teardown = [plugin_key]() { + for (auto& callback : UiRegistry::instance().take_callbacks_for_plugin(plugin_key)) + callback->disable(); + // Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on // forced teardown (intended); the resource destructor still cleans the registry. for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) { From 8740696e751ab615bb404be86b817f6446666686 Mon Sep 17 00:00:00 2001 From: yw4z Date: Mon, 7 Sep 2026 13:16:31 +0300 Subject: [PATCH 02/57] init --- resources/images/param_add.svg | 12 +-- src/slic3r/GUI/Field.cpp | 108 ++++++-------------------- src/slic3r/GUI/Field.hpp | 7 +- src/slic3r/GUI/OptionsGroup.cpp | 21 +++-- src/slic3r/GUI/PluginPickerDialog.cpp | 81 +++++++++++++------ src/slic3r/GUI/PluginPickerDialog.hpp | 9 ++- 6 files changed, 111 insertions(+), 127 deletions(-) diff --git a/resources/images/param_add.svg b/resources/images/param_add.svg index 71ea5092af..b00140b68a 100644 --- a/resources/images/param_add.svg +++ b/resources/images/param_add.svg @@ -1,8 +1,4 @@ - - - - Layer 1 - - - - + + + + \ No newline at end of file diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 142cf70522..d25106397e 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -2154,6 +2154,7 @@ void PrinterAgentChoice::msw_rescale() void PluginField::BUILD() { auto* panel = new wxPanel(m_parent, wxID_ANY); + panel->SetBackgroundColour(*wxWHITE); wxGetApp().UpdateDarkUI(panel); window = panel; @@ -2196,9 +2197,8 @@ void PluginField::rebuild_ui() m_rows.clear(); m_standalone_add_btn = nullptr; - if (m_values.empty()) { - add_empty_state_row(); - } else { + add_empty_state_row(); + if (!m_values.empty()) { for (size_t i = 0; i < m_values.size(); ++i) add_plugin_row(display_name_for_value(m_values[i]), i == m_values.size() - 1); } @@ -2215,94 +2215,43 @@ void PluginField::rebuild_ui() void PluginField::add_empty_state_row() { - const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1); - auto row_sizer = new wxBoxSizer(wxHORIZONTAL); - - wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, _L("No plugin selected"), - wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord), - wxTE_READONLY); - display->SetEditable(false); - wxGetApp().UpdateDarkUI(display); - display->SetToolTip(_L("No plugin selected")); - - auto add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString, - button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16); - wxGetApp().UpdateDarkUI(add_btn); - add_btn->SetToolTip(_L("Add plugin")); + auto add_btn = new Button(window, _L("Add plugin"), "param_add", 0, 16); + add_btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter); add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_clicked(); }); - row_sizer->Add(display, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); - row_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL); - m_main_sizer->Add(row_sizer, 0, wxEXPAND); - - PluginRow row; - row.display = display; - row.add_btn = add_btn; - row.sizer = row_sizer; - m_rows.push_back(row); + m_main_sizer->Add(add_btn, 0, wxEXPAND | wxBOTTOM, window->FromDIP(SidebarProps::ContentMarginV())); m_standalone_add_btn = add_btn; } void PluginField::add_plugin_row(const wxString& value, bool is_last) { - const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1); auto row_sizer = new wxBoxSizer(wxHORIZONTAL); - ScalableButton* select_btn = new ScalableButton(window, wxID_ANY, "search", wxEmptyString, - button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16); - wxGetApp().UpdateDarkUI(select_btn); - select_btn->SetToolTip(_L("Select plugin")); - - wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, value, - wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord), - wxTE_READONLY); - display->SetEditable(false); - wxGetApp().UpdateDarkUI(display); + ComboBox* display = new ComboBox(window, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, 0, NULL, wxCB_READONLY | CB_NO_DROP_ICON); + display->SetIcon("edit"); display->SetToolTip(get_tooltip_text(value)); - ScalableButton* remove_btn = nullptr; - if (!m_opt.readonly) { - remove_btn = new ScalableButton(window, wxID_ANY, "cross", wxEmptyString, - button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16); - wxGetApp().UpdateDarkUI(remove_btn); - remove_btn->SetToolTip(_L("Remove plugin")); - } + ScalableButton* remove_btn = new ScalableButton(window, wxID_ANY, "cross", wxEmptyString, + wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16); + remove_btn->SetToolTip(_L("Remove plugin")); - ScalableButton* add_btn = nullptr; - if (is_last && !m_opt.readonly) { - add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString, - button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16); - wxGetApp().UpdateDarkUI(add_btn); - add_btn->SetToolTip(_L("Add plugin")); - add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_clicked(); }); - } + if (m_opt.readonly) + remove_btn->Disable(); const size_t row_index = m_rows.size(); - select_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_select_clicked(row_index); }); - if (remove_btn) - remove_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_remove_clicked(row_index); }); + display->Bind(wxEVT_LEFT_DOWN, [this, row_index](wxMouseEvent& ) { on_select_clicked(row_index); }); + remove_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_remove_clicked(row_index); }); - row_sizer->Add(select_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); - row_sizer->Add(display, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); - if (remove_btn) - row_sizer->Add(remove_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); - if (add_btn) - row_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL); - else if (!m_opt.readonly) { - // Reserve space equal to the add button so all rows align. - row_sizer->Add(button_size.GetWidth(), button_size.GetHeight(), 0, wxALIGN_CENTER_VERTICAL); - } + row_sizer->Add(display , 1, wxALIGN_CENTER_VERTICAL); + row_sizer->Add(remove_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, window->FromDIP(SidebarProps::ElementSpacing())); - const int bottom_gap = is_last ? 0 : 4; - m_main_sizer->Add(row_sizer, 0, wxEXPAND | (bottom_gap > 0 ? wxBOTTOM : 0), bottom_gap); + m_main_sizer->Add(row_sizer, 0, wxEXPAND | wxBOTTOM, window->FromDIP(is_last ? SidebarProps::ContentMarginV() : 4)); PluginRow row; - row.select_btn = select_btn; row.display = display; row.remove_btn = remove_btn; - row.add_btn = add_btn; row.sizer = row_sizer; m_rows.push_back(row); } @@ -2354,9 +2303,9 @@ void PluginField::on_add_clicked() m_values.push_back(selected); m_value = m_values; - rebuild_ui(); - - on_change_field(); + // Defer: don't destroy the clicked button from inside its own handler. + if(window) + window->CallAfter([this]() {rebuild_ui(); on_change_field();}); } void PluginField::on_remove_clicked(size_t index) @@ -2367,8 +2316,9 @@ void PluginField::on_remove_clicked(size_t index) m_values.erase(m_values.begin() + index); m_value = m_values; - rebuild_ui(); - on_change_field(); + // Defer: don't destroy the clicked button from inside its own handler. + if(window) + window->CallAfter([this]() {rebuild_ui(); on_change_field();}); } wxString PluginField::get_row_value(size_t index) const @@ -2382,7 +2332,7 @@ void PluginField::set_row_value(size_t index, const wxString& value) { if (index >= m_rows.size() || !m_rows[index].display) return; - m_rows[index].display->ChangeValue(value); + m_rows[index].display->SetValue(value); m_rows[index].display->SetToolTip(get_tooltip_text(value)); } @@ -2425,14 +2375,10 @@ boost::any& PluginField::get_value() void PluginField::enable() { for (auto& row : m_rows) { - if (row.select_btn) - row.select_btn->Enable(); if (row.display) row.display->Enable(); if (row.remove_btn) row.remove_btn->Enable(); - if (row.add_btn) - row.add_btn->Enable(); } if (m_standalone_add_btn) m_standalone_add_btn->Enable(); @@ -2441,14 +2387,10 @@ void PluginField::enable() void PluginField::disable() { for (auto& row : m_rows) { - if (row.select_btn) - row.select_btn->Disable(); if (row.display) row.display->Disable(); if (row.remove_btn) row.remove_btn->Disable(); - if (row.add_btn) - row.add_btn->Disable(); } if (m_standalone_add_btn) m_standalone_add_btn->Disable(); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 5d5d549427..6773d7a3f4 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -25,6 +25,7 @@ #include "wxExtensions.hpp" #include "Widgets/SpinInput.hpp" #include "Widgets/TextInput.hpp" +#include "Widgets/ComboBox.hpp" #ifdef __WXMSW__ #define wxMSW true @@ -532,10 +533,8 @@ public: private: struct PluginRow { - ScalableButton* select_btn { nullptr }; - wxTextCtrl* display { nullptr }; + ComboBox* display { nullptr }; ScalableButton* remove_btn { nullptr }; - ScalableButton* add_btn { nullptr }; wxBoxSizer* sizer { nullptr }; }; @@ -553,7 +552,7 @@ private: wxBoxSizer* m_main_sizer { nullptr }; std::vector m_rows; std::vector m_values; - ScalableButton* m_standalone_add_btn { nullptr }; + Button* m_standalone_add_btn { nullptr }; std::function m_selector; }; diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 25c13c4b8d..7d63556eff 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -698,10 +698,15 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt) Slic3r::PluginManager& manager = Slic3r::PluginManager::instance(); const Slic3r::PluginCapabilityType plugin_type = Slic3r::plugin_capability_type_from_string(opt.plugin_type); if (plugin_type == Slic3r::PluginCapabilityType::Unknown) { - const std::string message = opt.plugin_type.empty() - ? "This setting does not specify a plugin capability type." - : "This setting specifies an unrecognized plugin capability type: '" + opt.plugin_type + "'."; - wxMessageBox(from_u8(message), _L("Plugin Selection"), wxOK | wxICON_WARNING, m_parent); + MessageDialog dlg(m_parent, + opt.plugin_type.empty() ? _L("This setting does not specify a plugin capability type.") + : _L("This setting specifies an unrecognized plugin capability type: ") + "'" + opt.plugin_type + "'.", + _L("Plugin Selection"), + wxOK | wxICON_WARNING + ); + dlg.CenterOnParent(); + dlg.ShowModal(); + return {}; } @@ -714,7 +719,13 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt) }); if (caps.empty()) { - wxMessageBox(_L("No plugins capabilities available for this type.\nEnable or install some to use."), _L("Plugin Selection"), wxOK | wxICON_INFORMATION, m_parent); + MessageDialog dlg(m_parent, + _L("No plugins capabilities available for this type.\nEnable or install some to use."), + _L("Plugin Selection"), + wxOK | wxICON_INFORMATION + ); + dlg.CenterOnParent(); + dlg.ShowModal(); return {}; } diff --git a/src/slic3r/GUI/PluginPickerDialog.cpp b/src/slic3r/GUI/PluginPickerDialog.cpp index d0c387b0c0..6710b00d16 100644 --- a/src/slic3r/GUI/PluginPickerDialog.cpp +++ b/src/slic3r/GUI/PluginPickerDialog.cpp @@ -9,12 +9,16 @@ #include "GUI.hpp" #include "I18N.hpp" +#include "GUI_App.hpp" + +#include "Widgets/DialogButtons.hpp" + namespace Slic3r { namespace GUI { PluginPickerDialog::PluginPickerDialog(wxWindow* parent, const wxString& plugin_type_label, const std::vector& plugins) - : wxDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label)) + : DPIDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label)) , m_plugins(plugins) , m_capability_mode(false) { @@ -25,7 +29,7 @@ PluginPickerDialog::PluginPickerDialog(wxWindow* parent, PluginPickerDialog::PluginPickerDialog(wxWindow* parent, const wxString& plugin_type_label, std::vector capabilities) - : wxDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label)) + : DPIDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label)) , m_capabilities(std::move(capabilities)) , m_capability_mode(true) { @@ -35,12 +39,18 @@ PluginPickerDialog::PluginPickerDialog(wxWindow* parent, void PluginPickerDialog::build_ui(const wxString& plugin_type_label) { + SetBackgroundColour(*wxWHITE); + const bool has_plugins = !m_plugins.empty(); auto* top_sizer = new wxBoxSizer(wxVERTICAL); auto* info_text = new wxStaticText(this, wxID_ANY, wxString::Format(_L("Choose a %s plugin from the list below."), plugin_type_label)); - top_sizer->Add(info_text, 0, wxALL | wxEXPAND, 10); + info_text->SetFont(Label::Body_14); + info_text->SetForegroundColour(wxColour("#363636")); + top_sizer->Add(info_text, 0, wxALL | wxEXPAND, FromDIP(10)); + + top_sizer->AddSpacer(FromDIP(5)); wxArrayString choices; choices.reserve(m_plugins.size()); @@ -51,54 +61,69 @@ void PluginPickerDialog::build_ui(const wxString& plugin_type_label) choices.Add(label); } - m_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + m_choice = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, wxCB_READONLY); + for (const wxString &opt : choices) { m_choice->Append(opt); } + if (has_plugins) { m_choice->SetSelection(0); - m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) { + m_choice->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { update_description(evt.GetSelection()); }); } else { m_choice->Enable(false); } - top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, 10); + top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(10)); m_description = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_description->SetFont(Label::Body_14); + m_description->SetForegroundColour(wxColour("#363636")); m_description->Wrap(400); - top_sizer->Add(m_description, 0, wxALL | wxEXPAND, 10); + top_sizer->Add(m_description, 0, wxALL | wxEXPAND, FromDIP(10)); if (has_plugins) update_description(0); else m_description->SetLabel(_L("No plugins found for this type.")); - auto* button_sizer = new wxStdDialogButtonSizer(); - auto* ok_button = new wxButton(this, wxID_OK); - ok_button->Enable(has_plugins); - button_sizer->AddButton(ok_button); - button_sizer->AddButton(new wxButton(this, wxID_CANCEL)); - button_sizer->Realize(); + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); - top_sizer->Add(button_sizer, 0, wxALL | wxALIGN_RIGHT, 10); + dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { EndModal(wxID_OK); }); + dlg_btns->GetOK()->Enable(has_plugins); + + dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { EndModal(wxID_CANCEL); }); + + top_sizer->Add(dlg_btns, 0, wxEXPAND); SetSizerAndFit(top_sizer); + + wxGetApp().UpdateDlgDarkUI(this); } void PluginPickerDialog::build_capability_ui(const wxString& plugin_type_label) { + SetBackgroundColour(*wxWHITE); + const bool has_capabilities = !m_capabilities.empty(); auto* top_sizer = new wxBoxSizer(wxVERTICAL); auto* info_text = new wxStaticText(this, wxID_ANY, wxString::Format(_L("Choose a %s plugin from the list below."), plugin_type_label)); - top_sizer->Add(info_text, 0, wxALL | wxEXPAND, 10); + info_text->SetFont(Label::Body_14); + info_text->SetForegroundColour(wxColour("#363636")); + + top_sizer->Add(info_text, 0, wxALL | wxEXPAND, FromDIP(10)); + + top_sizer->AddSpacer(FromDIP(5)); wxArrayString choices; choices.reserve(m_capabilities.size()); for (const auto& cap : m_capabilities) choices.Add(cap.label); - m_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + m_choice = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0, NULL, wxCB_READONLY); + for (const wxString &opt : choices) { m_choice->Append(opt); } + if (has_capabilities) { m_choice->SetSelection(0); m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) { @@ -108,27 +133,31 @@ void PluginPickerDialog::build_capability_ui(const wxString& plugin_type_label) m_choice->Enable(false); } - top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, 10); + top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(10)); m_description = new wxStaticText(this, wxID_ANY, wxEmptyString); + m_description->SetFont(Label::Body_14); + m_description->SetForegroundColour(wxColour("#363636")); m_description->Wrap(400); - top_sizer->Add(m_description, 0, wxALL | wxEXPAND, 10); + top_sizer->Add(m_description, 0, wxALL | wxEXPAND, FromDIP(10)); if (has_capabilities) update_capability_description(0); else m_description->SetLabel(_L("No plugins found for this type.")); - auto* button_sizer = new wxStdDialogButtonSizer(); - auto* ok_button = new wxButton(this, wxID_OK); - ok_button->Enable(has_capabilities); - button_sizer->AddButton(ok_button); - button_sizer->AddButton(new wxButton(this, wxID_CANCEL)); - button_sizer->Realize(); + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); - top_sizer->Add(button_sizer, 0, wxALL | wxALIGN_RIGHT, 10); + dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { EndModal(wxID_OK); }); + dlg_btns->GetOK()->Enable(has_capabilities); + + dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { EndModal(wxID_CANCEL); }); + + top_sizer->Add(dlg_btns, 0, wxEXPAND); SetSizerAndFit(top_sizer); + + wxGetApp().UpdateDlgDarkUI(this); } PluginPickerDialog::CapabilityEntry PluginPickerDialog::selected_capability() const @@ -187,4 +216,6 @@ void PluginPickerDialog::update_description(int selection) Layout(); } +void PluginPickerDialog::on_dpi_changed(const wxRect &suggested_rect) {} + }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PluginPickerDialog.hpp b/src/slic3r/GUI/PluginPickerDialog.hpp index 0d676eb04d..ee0f64b818 100644 --- a/src/slic3r/GUI/PluginPickerDialog.hpp +++ b/src/slic3r/GUI/PluginPickerDialog.hpp @@ -11,9 +11,12 @@ #include "slic3r/plugin/PluginManager.hpp" +#include "GUI_Utils.hpp" +#include "Widgets/ComboBox.hpp" + namespace Slic3r { namespace GUI { -class PluginPickerDialog : public wxDialog +class PluginPickerDialog : public DPIDialog { public: // Entry for capability-level selection (plugin_type non-empty path). @@ -40,13 +43,15 @@ public: // Returns the {plugin_key, name} of the selected capability (capability path). CapabilityEntry selected_capability() const; + void on_dpi_changed(const wxRect &suggested_rect) override; + private: void build_ui(const wxString& plugin_type_label); void build_capability_ui(const wxString& plugin_type_label); void update_description(int selection); void update_capability_description(int selection); - wxChoice* m_choice { nullptr }; + ComboBox* m_choice { nullptr }; wxStaticText* m_description { nullptr }; std::vector m_plugins; std::vector m_capabilities; From a5d0d33df32e1af8b6084cfe2f4275b55c43ed70 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Thu, 10 Sep 2026 12:39:55 +0800 Subject: [PATCH 03/57] Register Instance Copies and Moves with Their Plate An instance added with "+" was never registered with the plate it landed on, and moving an instance only re-registered instance 0 of its object, so a copy dragged onto another plate stayed unknown to that plate's registry. The plate's filament list, its wipe tower preview and the position clamp all read that registry, so a multi-filament copy moved onto a single-filament plate drew no tower there and its tower position was never clamped. Register new copies at creation, notify exactly the instances a move changed (every instance of the object when one of its parts moved), and drop the registry entry when a copy is removed again. --- src/slic3r/GUI/GLCanvas3D.cpp | 16 +++++++++++++++- src/slic3r/GUI/Plater.cpp | 8 +++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 556faaa763..676d310f7b 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -5059,7 +5059,21 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) } //BBS: notify instance updates to part plater list - m_selection.notify_instance_update(-1, 0); + // Only what moved: the selected instances, or every instance of an object one of whose + // parts moved. Notifying a plate about an instance that stayed put invalidates its slice + // result, and notifying instance 0 alone left a moved copy unregistered on its new plate. + { + std::set> notified; + for (unsigned int i : m_selection.get_volume_idxs()) { + const GLVolume* v = m_volumes.volumes[i]; + const int object_idx = v->object_idx(); + if (object_idx < 0 || object_idx >= static_cast(m_model->objects.size())) + continue; + const std::pair key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx()); + if (notified.insert(key).second) + m_selection.notify_instance_update(key.first, key.second); + } + } // Fixes sinking/flying instances (snaps object to buildplate) for (const std::pair& i : done) { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 109d7b3c10..3aab1184c3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17653,6 +17653,10 @@ void Plater::increase_instances(size_t num) model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); // p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); } + // Register the copies with the plate they land on before the scene reloads: the plate's + // filament list and wipe tower preview are read from that registry. + for (size_t i = model_object->instances.size() - num; i < model_object->instances.size(); ++i) + p->partplate_list.notify_instance_update(obj_idx, static_cast(i)); #ifdef SUPPORT_AUTO_CENTER if (p->get_config("autocenter") == "true") @@ -17683,8 +17687,10 @@ void Plater::decrease_instances(size_t num) ModelObject* model_object = p->model.objects[obj_idx]; if (model_object->instances.size() > num) { - for (size_t i = 0; i < num; ++ i) + for (size_t i = 0; i < num; ++ i) { + p->partplate_list.notify_instance_removed(obj_idx, static_cast(model_object->instances.size()) - 1); model_object->delete_last_instance(); + } p->update(); // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar().obj_list()->decrease_object_instances(obj_idx, num); From 8c8e6fd0695e390796d4102f36baaae2380d4aad Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Thu, 10 Sep 2026 12:39:55 +0800 Subject: [PATCH 04/57] Let the Remaining Per-Plate Object Scans See Every Instance get_extruders() and estimate_wipe_tower_size() already ask whether any instance of an object sits on the plate; the support-less extruder scan, the mixed-filament risk check and the nozzle/filament compatibility check still tested instance 0 only, so an object whose copy - not its original - was placed on the plate was skipped by all three. --- src/slic3r/GUI/PartPlate.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 90e2c96ab6..93730fdafb 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1917,7 +1917,7 @@ std::vector PartPlate::get_extruders_without_support(bool conside_custom_gc const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { - if (!contain_instance_totally(obj_idx, 0)) + if (!contain_any_instance_totally(obj_idx)) continue; ModelObject* mo = m_model->objects[obj_idx]; @@ -2088,7 +2088,7 @@ bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConf "which may significantly increase waste and the risk of nozzle / waste-chute clogging."); for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) { - if (!contain_instance_totally(obj_idx, 0)) + if (!contain_any_instance_totally(obj_idx)) continue; ModelObject *mo = m_model->objects[obj_idx]; int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1; @@ -2307,7 +2307,7 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig return wipe_tower_size; for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { - if (!use_global_objects && !contain_instance_totally(obj_idx, 0)) + if (!use_global_objects && !contain_any_instance_totally(obj_idx)) continue; BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box(); From 7888452666c31bab80d1dd0675a642f2e41c5d31 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 10 Sep 2026 05:39:14 -0500 Subject: [PATCH 05/57] build: clear 7 warning categories across 26 sites (#15615) * build: clear 2 warnings - cast the NSTextField the class check already proved mainframe_text_field is NSTextField* and was assigned a bare NSView*, which Clang reports as -Wincompatible-pointer-types. Both assignments sit inside if ([viewObject class] == [NSTextField self]), so the runtime type is already guaranteed, and the line above the second one casts the same variable the same way to call setTextColor. macOS only, since nothing else compiles this file. * build: clear 6 warning categories from the clang-cl inventory -Wmissing-braces (9). Aggregates whose first member is itself an aggregate. GUID's fourth member is BYTE[8], so the trailing eight bytes take their own braces. The others were reaching for zero-initialization with {0} and say {} now. bbs_3mf's backup Task ends in an anonymous union, which needs braces of its own; those braces initialize the union's first member rather than the one named at the call site, so the RemoveBackup site says so in a comment. -Wmacro-redefined (11). SendMultiMachinePage.hpp defines five names that Preferences.hpp, PresetBundleDialog.hpp, ExportPresetBundleDialog.hpp and TroubleshootDialog.hpp also define with different values, so the value in force depended on include order. All nine of this file's DESIGN_ macros take the SEND_ prefix it already uses for its own macros, values unchanged, so a DESIGN_ name added elsewhere later cannot collide with it again. They read as one page-local palette, a 900 to 400 gray ramp plus sizes, so the four with no current readers stay: dropping them would leave gaps in a named scale. test_marchingsquares.cpp defines NOMINMAX, which libslic3r already passes as a PUBLIC compile definition, so it takes the #ifndef guard the other suites use. -Wbraced-scalar-init (3). Two PushStyleVar calls resolve to the float overload, so the braces were initializing a scalar. ConfigOptionFloatsNullable already takes an initializer_list, so the inner braces did the same thing. -Wmicrosoft-goto (2). Both gotos in copy_file_gui jump forward over the initialization of size, dwRead and dwWrite, which only MSVC accepts. Those declarations move up to join the others at the top of the function. -Wunused-private-field (3). Every use of ColourPicker's m_clrData and m_picker_widget is behind !defined(__linux__), so on Linux they are written and never read; the members now carry the same guard. ParamsPanel's m_size_move is read nowhere. Tab has its own, which is the one Tab.cpp uses. -Wnonportable-include-path (2). BaseException.h asked for "stackwalker.h" and the file on disk is StackWalker.h. --- src/dev-utils/BaseException.h | 2 +- src/dev-utils/StackWalker.cpp | 2 +- src/libslic3r/Format/bbs_3mf.cpp | 8 ++++---- src/libslic3r/PrintConfig.cpp | 2 +- src/slic3r/GUI/Field.hpp | 2 ++ src/slic3r/GUI/GUI_App.cpp | 2 +- src/slic3r/GUI/GUI_Utils.cpp | 5 +++-- .../GUI/Gizmos/GizmoObjectManipulation.cpp | 2 +- src/slic3r/GUI/IMSlider.cpp | 2 +- src/slic3r/GUI/MainFrame.cpp | 4 ++-- src/slic3r/GUI/ParamsPanel.hpp | 1 - src/slic3r/GUI/PartPlate.cpp | 2 +- src/slic3r/GUI/SendMultiMachinePage.cpp | 14 +++++++------- src/slic3r/GUI/SendMultiMachinePage.hpp | 18 +++++++++--------- src/slic3r/Utils/MacDarkMode.mm | 4 ++-- tests/libslic3r/test_marchingsquares.cpp | 2 ++ 16 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/dev-utils/BaseException.h b/src/dev-utils/BaseException.h index 2cb65d945e..20b6fb0c89 100644 --- a/src/dev-utils/BaseException.h +++ b/src/dev-utils/BaseException.h @@ -1,7 +1,7 @@ #pragma once #include #include -#include "stackwalker.h" +#include "StackWalker.h" #include class CBaseException : public CStackWalker diff --git a/src/dev-utils/StackWalker.cpp b/src/dev-utils/StackWalker.cpp index 6038196cb0..3ef983cd86 100644 --- a/src/dev-utils/StackWalker.cpp +++ b/src/dev-utils/StackWalker.cpp @@ -425,7 +425,7 @@ LPSTACKINFO CStackWalker::StackWalker(HANDLE hThread, const CONTEXT* context) else c = *context; - STACKFRAME64 sf = {0}; + STACKFRAME64 sf = {}; DWORD imageType; //intel X86 diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 96d98dbe9f..e2091da1db 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -8844,7 +8844,7 @@ public: auto model = object.get_model(); auto o = m_temp_model.add_object(object); int backup_id = model->get_object_backup_id(object); - push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, 1 }); + push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, { 1 } }); } void remove_object_mesh(ModelObject& object) { @@ -8854,7 +8854,7 @@ public: void backup_soon() { boost::lock_guard lock(m_mutex); m_other_changes_backup = true; - m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq }); + m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } }); m_cond.notify_all(); } @@ -8872,7 +8872,7 @@ public: m_ui_tasks.clear(); m_tasks.clear(); } - m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, removeAll }); + m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, { removeAll } }); ++m_task_seq; if (model.is_need_backup()) { m_other_changes = false; @@ -9087,7 +9087,7 @@ public: else m_cond.wait(lock); if (m_interval > 0 && boost::get_system_time() > m_next_backup) { - m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq }); + m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } }); m_next_backup += boost::posix_time::seconds(m_interval); // Maybe wakeup from power sleep if (m_next_backup < boost::get_system_time()) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 1db476eede..4c27995ba0 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5430,7 +5430,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->readonly = false; def->nullable = true; - def->set_default_value(new ConfigOptionFloatsNullable { {0.0} }); + def->set_default_value(new ConfigOptionFloatsNullable { 0.0 }); def = this->add("cooling_tube_retraction", coFloat); def->label = L("Cooling tube position"); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 5d5d549427..74011983c6 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -628,8 +628,10 @@ private: void on_button_click(wxCommandEvent &WXUNUSED(ev)); void save_colors_to_config(); private: +#if !defined(__linux__) && !defined(__LINUX__) wxColourData* m_clrData{nullptr}; wxColourPickerWidget* m_picker_widget{nullptr}; +#endif }; class PointCtrl : public Field { diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 99a829bdcc..6d08f052da 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -598,7 +598,7 @@ wxString file_wildcards(FileType file_type, const std::string &custom_extension) static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } #ifdef WIN32 -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }; static void register_win32_device_notification_event() { diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index bc66d90ffd..10dd29c9c1 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -69,6 +69,8 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std HANDLE handlesrc = nullptr; HANDLE handledst = nullptr; CopyFileResult ret = SUCCESS; + DWORD size = 0; + DWORD dwRead = 0, dwWrite = 0; handlesrc = CreateFile(src.wc_str(), GENERIC_READ, @@ -96,9 +98,8 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std goto __finished; } - DWORD size=GetFileSize(handlesrc,NULL); + size = GetFileSize(handlesrc,NULL); buff = new char[size+1]; - DWORD dwRead=0,dwWrite; result = ReadFile(handlesrc, buff, size, &dwRead, NULL); if (!result) { DWORD errCode = GetLastError(); diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index 7c4a2afd38..5d70fde833 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -702,7 +702,7 @@ bool GizmoObjectManipulation::reset_zero_button(ImGuiWrapper *imgui_wrapper, bo for (int i = 0; i < number; i++) { - char buf[3][64] = {0}; + char buf[3][64] = {}; float buf_size[3] = {0}; for (int j = 0; j < 3; j++) { ImGui::DataTypeFormatString(buf[j], IM_ARRAYSIZE(buf[j]), ImGuiDataType_Double, (void *) &vec[i][j], "%.2f"); diff --git a/src/slic3r/GUI/IMSlider.cpp b/src/slic3r/GUI/IMSlider.cpp index c008963646..0d0d6739f8 100644 --- a/src/slic3r/GUI/IMSlider.cpp +++ b/src/slic3r/GUI/IMSlider.cpp @@ -790,7 +790,7 @@ void IMSlider::draw_ticks(const ImRect& slideable_region) { void IMSlider::show_tooltip(const std::string tooltip) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 6 * m_scale, 3 * m_scale }); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, { 3 * m_scale }); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * m_scale); ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND); ImGui::PushStyleColor(ImGuiCol_Border, { 0,0,0,0 }); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f)); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 167b2b4cda..c734804c22 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1591,7 +1591,7 @@ void MainFrame::register_win32_callbacks() //static GUID GUID_DEVINTERFACE_USB_DEVICE = { 0xA5DCBF10, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED }; //static GUID GUID_DEVINTERFACE_DISK = { 0x53f56307, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b }; //static GUID GUID_DEVINTERFACE_VOLUME = { 0x71a27cdd, 0x812a, 0x11d0, 0xbe, 0xc7, 0x08, 0x00, 0x2b, 0xe2, 0x09, 0x2f }; - static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }; // Register USB HID (Human Interface Devices) notifications to trigger the 3DConnexion enumeration. DEV_BROADCAST_DEVICEINTERFACE NotificationFilter = { 0 }; @@ -1631,7 +1631,7 @@ void MainFrame::register_win32_callbacks() { static constexpr int device_count = 1; - RAWINPUTDEVICE devices[device_count] = { 0 }; + RAWINPUTDEVICE devices[device_count] = {}; // multi-axis mouse (SpaceNavigator, etc.) devices[0].usUsagePage = 0x01; devices[0].usUsage = 0x08; diff --git a/src/slic3r/GUI/ParamsPanel.hpp b/src/slic3r/GUI/ParamsPanel.hpp index 0726db91d3..91bf3d2a7e 100644 --- a/src/slic3r/GUI/ParamsPanel.hpp +++ b/src/slic3r/GUI/ParamsPanel.hpp @@ -66,7 +66,6 @@ class ParamsPanel : public wxPanel { #if __WXOSX__ wxWindow* m_tmp_panel; - int m_size_move = -1; #endif // __WXOSX__ private: diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 90e2c96ab6..9826498fbd 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1112,7 +1112,7 @@ void PartPlate::show_tooltip(const std::string tooltip) { const auto scale = m_plater->get_current_canvas3D()->get_scale(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale}); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, {3 * scale}); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale); ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND); ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0}); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f)); diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index 3a52caec3f..2d1b713264 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -814,13 +814,13 @@ wxBoxSizer* SendMultiMachinePage::create_item_title(wxString title, wxWindow* pa wxBoxSizer* m_sizer_title = new wxBoxSizer(wxHORIZONTAL); auto m_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); - m_title->SetForegroundColour(DESIGN_GRAY800_COLOR); + m_title->SetForegroundColour(SEND_DESIGN_GRAY800_COLOR); m_title->SetFont(::Label::Head_13); m_title->Wrap(-1); m_title->SetToolTip(tooltip); auto m_line = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); - m_line->SetBackgroundColour(DESIGN_GRAY400_COLOR); + m_line->SetBackgroundColour(SEND_DESIGN_GRAY400_COLOR); m_sizer_title->Add(m_title, 0, wxALIGN_CENTER | wxALL, 3); m_sizer_title->Add(0, 0, 0, wxLEFT, 9); @@ -843,7 +843,7 @@ wxBoxSizer* SendMultiMachinePage::create_item_checkbox(wxString title, wxWindow* m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 8); auto checkbox_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); - checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR); + checkbox_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR); checkbox_title->SetFont(::Label::Body_13); auto size = checkbox_title->GetTextExtent(title); @@ -867,12 +867,12 @@ wxBoxSizer* SendMultiMachinePage::create_item_input(wxString str_before, wxStrin { wxBoxSizer* sizer_input = new wxBoxSizer(wxHORIZONTAL); auto input_title = new wxStaticText(parent, wxID_ANY, str_before); - input_title->SetForegroundColour(DESIGN_GRAY900_COLOR); + input_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR); input_title->SetFont(::Label::Body_13); input_title->SetToolTip(tooltip); input_title->Wrap(-1); - auto input = new ::TextInput(parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); + auto input = new ::TextInput(parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, SEND_DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); StateColor input_bg(std::pair(wxColour("#F0F0F1"), StateColor::Disabled), std::pair(*wxWHITE, StateColor::Enabled)); input->SetBackgroundColor(input_bg); input->GetTextCtrl()->SetValue(app_config->get(param)); @@ -880,7 +880,7 @@ wxBoxSizer* SendMultiMachinePage::create_item_input(wxString str_before, wxStrin input->GetTextCtrl()->SetValidator(validator); auto second_title = new wxStaticText(parent, wxID_ANY, str_after, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); - second_title->SetForegroundColour(DESIGN_GRAY900_COLOR); + second_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR); second_title->SetFont(::Label::Body_13); second_title->SetToolTip(tooltip); second_title->Wrap(-1); @@ -1337,7 +1337,7 @@ wxPanel* SendMultiMachinePage::create_page() m_tip_text->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), -1)); m_tip_text->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), -1)); m_tip_text->SetLabel(_L("Please select the devices you would like to manage here (up to 6 devices)")); - m_tip_text->SetForegroundColour(DESIGN_GRAY800_COLOR); + m_tip_text->SetForegroundColour(SEND_DESIGN_GRAY800_COLOR); m_tip_text->SetFont(::Label::Head_20); m_tip_text->Wrap(-1); diff --git a/src/slic3r/GUI/SendMultiMachinePage.hpp b/src/slic3r/GUI/SendMultiMachinePage.hpp index 7d77849bf3..a63bc51bb0 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.hpp +++ b/src/slic3r/GUI/SendMultiMachinePage.hpp @@ -22,15 +22,15 @@ namespace GUI { #define SEND_LEFT_DEV_STATUS 250 #define SEND_LEFT_TAKS_STATUS 180 -#define DESIGN_SELECTOR_NOMORE_COLOR wxColour(248, 248, 248) -#define DESIGN_GRAY900_COLOR wxColour(38, 46, 48) -#define DESIGN_GRAY800_COLOR wxColour(50, 58, 61) -#define DESIGN_GRAY600_COLOR wxColour(144, 144, 144) -#define DESIGN_GRAY400_COLOR wxColour(166, 169, 170) -#define DESIGN_RESOUTION_PREFERENCES wxSize(FromDIP(540), -1) -#define DESIGN_COMBOBOX_SIZE wxSize(FromDIP(140), -1) -#define DESIGN_LARGE_COMBOBOX_SIZE wxSize(FromDIP(160), -1) -#define DESIGN_INPUT_SIZE wxSize(FromDIP(50), -1) +#define SEND_DESIGN_SELECTOR_NOMORE_COLOR wxColour(248, 248, 248) +#define SEND_DESIGN_GRAY900_COLOR wxColour(38, 46, 48) +#define SEND_DESIGN_GRAY800_COLOR wxColour(50, 58, 61) +#define SEND_DESIGN_GRAY600_COLOR wxColour(144, 144, 144) +#define SEND_DESIGN_GRAY400_COLOR wxColour(166, 169, 170) +#define SEND_DESIGN_RESOUTION_PREFERENCES wxSize(FromDIP(540), -1) +#define SEND_DESIGN_COMBOBOX_SIZE wxSize(FromDIP(140), -1) +#define SEND_DESIGN_LARGE_COMBOBOX_SIZE wxSize(FromDIP(160), -1) +#define SEND_DESIGN_INPUT_SIZE wxSize(FromDIP(50), -1) diff --git a/src/slic3r/Utils/MacDarkMode.mm b/src/slic3r/Utils/MacDarkMode.mm index cecd90044b..2bce7835e8 100644 --- a/src/slic3r/Utils/MacDarkMode.mm +++ b/src/slic3r/Utils/MacDarkMode.mm @@ -57,7 +57,7 @@ void set_miniaturizable(void * window) while(viewObject = (NSView *)[viewEnum nextObject]) { if([viewObject class] == [NSTextField self]) { //[(NSTextField*)viewObject setTextColor : NSColor.whiteColor]; - mainframe_text_field = viewObject; + mainframe_text_field = (NSTextField*)viewObject; } } } @@ -74,7 +74,7 @@ void set_title_colour_after_set_title(void * window) while(viewObject = (NSView *)[viewEnum nextObject]) { if([viewObject class] == [NSTextField self]) { [(NSTextField*)viewObject setTextColor : NSColor.whiteColor]; - mainframe_text_field = viewObject; + mainframe_text_field = (NSTextField*)viewObject; } } diff --git a/tests/libslic3r/test_marchingsquares.cpp b/tests/libslic3r/test_marchingsquares.cpp index 6844ecb6ac..9a11f49faa 100644 --- a/tests/libslic3r/test_marchingsquares.cpp +++ b/tests/libslic3r/test_marchingsquares.cpp @@ -1,4 +1,6 @@ +#ifndef NOMINMAX #define NOMINMAX +#endif #include #include "test_utils.hpp" From e8d35fadd45c537d578cedc4eacf548ad7a48920 Mon Sep 17 00:00:00 2001 From: Valerii Bokhan <80919135+valerii-bokhan@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:03:50 +0200 Subject: [PATCH 06/57] Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill (#15206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill For patterns with curved/turning anchor lines (Hilbert Curve, Octagram Spiral), the bridge_over_infill algorithm produced incorrect results: 1. determine_bridging_angle: sampling curved anchor orientations produced noise across all turning directions (0/90/180/270°) instead of a single dominant one, yielding unstable bridge angles with 180° spread. Fix: use the configured infill_direction + 90° directly, bypassing the noisy sampling. The old blind +0.25*PI (Hilbert) and +1/16*PI (Octagram) offsets are removed. 2. construct_anchored_polygon: curved Hilbert/Octagram anchors intersected each vertical scan line many times at wildly different Y positions, producing chaotic polygon sections — holes in random places, bridges over air, rotated bridges. Fix: replace the curved infill polylines with synthetic straight lines parallel to infill_direction, spaced at the real infill line spacing (flow_spacing / density). Lines are centered on the limiting_area bbox center so that after rotation they span the full bridged_area. Anchors are left at full bbox length (not clipped) to guarantee every scan line finds an anchor. Rectilinear and other straight-line patterns are unaffected. Known limitation: some bridge edges may still terminate over air in edge cases where the nearest synthetic anchor line is more than one infill spacing away from the bridge boundary. This will be addressed in a follow-up. * fix: anchor internal bridges to actual sparse infill Preserve real anchors across regions and align plane-path anchor origins with printed infill. Respect lower-layer rotation templates and model alignment, and sample curved bridge boundaries more finely. Add regression coverage for anchor alignment, bridge angles and region isolation, with Orca comments explaining the geometry constraints. Verified 175 FFF tests before the comment-only follow-up; preserve CRLF in modified files. * Fix internal bridge support contacts and separated infill origins Restore anchor contact after bridge smoothing and share per-body pattern origins between anchors and printed infill. Recompute origins when preparation settings change. Cover multiline counts 1, 2 and 3 and add regressions for printed bridge support, separated infill alignment and reslicing. * Add explicit standard headers to PrintObject tests * test: cover surface centering when infill settings change Verify top and bottom Archimedean Chords and Octagram Spiral paths after switching centering modes or toggling separated infills. Compare reslicing against fresh slicing and document dependent infill invalidation. * test: preserve directional surface infill when settings change * perf: index layer islands for connected-body detection * test: use public print pipeline for body centering checks --- src/libslic3r/Fill/Fill.cpp | 75 +++-- src/libslic3r/Fill/Fill.hpp | 6 + src/libslic3r/PrintObject.cpp | 304 +++++++++++------- tests/fff_print/test_fill.cpp | 66 ++++ tests/fff_print/test_printobject.cpp | 442 +++++++++++++++++++++++++++ 5 files changed, 736 insertions(+), 157 deletions(-) diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index dc772580ca..f5386b085c 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -11,7 +11,7 @@ #include "AABBTreeLines.hpp" #include "ExtrusionEntity.hpp" -#include "FillBase.hpp" +#include "Fill.hpp" #include "FillRectilinear.hpp" #include "FillLightning.hpp" #include "FillConcentricInternal.hpp" @@ -1234,6 +1234,33 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p return surface_fills; } +// Orca: Anchors and printed infill must share the same body origin. Keep the choice +// here so per-model surface centering and separated sparse infill cannot drift apart. +static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox) +{ + const auto ¶ms = fill.params; + const auto &config = layer.regions()[fill.region_id]->region().config(); + const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface; + const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && + (params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral); + const bool separate = !external && params.separated_infills && + (is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() || + !config.sparse_infill_rotate_template.value.empty()); + if (per_model || separate) { + double best_overlap = 0.; + for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) { + const double overlap = area(intersection_ex(layer.lslices[i], expoly)); + if (overlap > best_overlap) { + best_overlap = overlap; + const Point center = layer.lslices_separated_component_bboxes[i].center(); + bbox = layer.object()->bounding_box(); + bbox.translate(center.x(), center.y()); + } + } + } + return bbox; +} + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING void export_group_fills_to_svg(const char *path, const std::vector &fills) { @@ -1353,19 +1380,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // Orca: Checking the filling of a centered surface by drawing for each model parts bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface; - bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral; if (is_top_or_bottom) { params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern } - // Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly - // fall through to the default (whole-object) bounding box below. - bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill; - bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills && - ( - is_separable_infill_pattern(surface_fill.params.pattern) || - params.config->solid_infill_rotate_template != "" || - params.config->sparse_infill_rotate_template != "" ); - if( surface_fill.params.pattern == ipLockedZag ) { params.locked_zag = true; params.infill_lock_depth = surface_fill.params.infill_lock_depth; @@ -1389,34 +1406,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.can_reverse = false; for (ExPolygon& expoly : surface_fill.expolygons) { - // Orca: separate infill / per-model pattern centering. - // - // Center the pattern on each connected body of the object independently, so every piece - // is filled exactly as if it were sliced on its own: touching/overlapping parts merge - // into one body sharing a center, while separate parts and disconnected islands (even - // interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each - // island belongs to, and its full bounding box, were resolved in 3D by PrintObject:: - // infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We - // match this fill region to the island it overlaps most, then re-use the whole-object - // bounding box (origin-centered — identical extent to the default, so coverage and cost - // are unchanged) re-centered on that body. - if (is_per_model_center || is_separate_infill) { - double best_overlap = 0.; - BoundingBox best_component; - for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) { - const double overlap = area(intersection_ex(this->lslices[r], expoly)); - if (overlap > best_overlap) { - best_overlap = overlap; - best_component = this->lslices_separated_component_bboxes[r]; - } - } - if (best_component.defined) { - const Point c = best_component.center(); - BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above) - part_bbox.translate(c.x(), c.y()); // re-center on this body - f->set_bounding_box(part_bbox); - } - } // - End: separate infill / per-model pattern centering + // Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface. + f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); if (params.symmetric_infill_y_axis) { @@ -1583,8 +1574,14 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc params.multiline = surface_fill.params.multiline; params.gyroid_optimized = surface_fill.params.gyroid_optimized; params.smooth_factor = surface_fill.params.smooth_factor; + // Orca: Match make_fills() when choosing the origin of plane-path patterns. + // Without the sparse extrusion role, the filler uses each surface's bounds + // instead of the object's bounds, so bridge anchors shift away from printed infill. + params.extrusion_role = surface_fill.params.extrusion_role; for (ExPolygon &expoly : surface_fill.expolygons) { + // Orca: Match the per-body origin of make_fills() before generating physical anchors. + f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); // Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon. f->spacing = surface_fill.params.spacing; surface_fill.surface.expolygon = std::move(expoly); diff --git a/src/libslic3r/Fill/Fill.hpp b/src/libslic3r/Fill/Fill.hpp index e92ab2dee5..b183cf0253 100644 --- a/src/libslic3r/Fill/Fill.hpp +++ b/src/libslic3r/Fill/Fill.hpp @@ -14,6 +14,12 @@ namespace Slic3r { class ExtrusionEntityCollection; class LayerRegion; +class PrintObject; + +// Orca: Share the layer rotation calculation between infill generation and internal +// bridge angle selection so both interpret rotation templates in the same way. +double calculate_infill_rotation_angle(const PrintObject *object, size_t layer_id, + const double &fixed_infill_angle, const std::string &template_string); // An interface class to Perl, aggregating an instance of a Fill and a FillData. class Filler diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index a228bb7436..e147356ea6 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -21,9 +21,11 @@ #include "TriangleMeshSlicer.hpp" #include "Utils.hpp" #include "Fill/FillAdaptive.hpp" +#include "Fill/Fill.hpp" #include "Fill/FillLightning.hpp" #include "Format/STL.hpp" #include "format.hpp" +#include "AABBTreeIndirect.hpp" #include "AABBTreeLines.hpp" #include @@ -672,6 +674,98 @@ void PrintObject::prepare_infill() } // for each region #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ + // Orca: precompute the object's 3D connected bodies for separated infills / per-model + // centering. Two islands belong to the same body when their slices overlap on adjacent + // layers; islands that only overlap in top-down projection but never touch (e.g. interleaved + // chain links) stay separate, matching "split to objects". Each layer island then records + // the full bounding box of its body, so its infill is centered on that body as if it were + // sliced alone. Compute this before bridges so anchors and extrusion share the same origin. + bool needs_separated_components = false; + for (size_t i = 0; i < this->num_printing_regions(); ++ i) { + const PrintRegionConfig &rc = this->printing_region(i).config(); + if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) { + needs_separated_components = true; + break; + } + } + // Orca: Fast path: the feature only changes anything when the object is made of more than one + // connected body. Detect that cheaply the same way as "Split to objects" — more than one + // model part, or a single part whose mesh is splittable (is_splittable() is cached). A single + // body already shares the object center, i.e. the default, so skip the connectivity pass. + if (needs_separated_components) { + int parts = 0; + const ModelVolume *first_part = nullptr; + for (const ModelVolume *v : this->model_object()->volumes) + if (v->is_model_part()) { ++ parts; first_part = v; } + if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable())) + needs_separated_components = false; + } + for (Layer *layer : m_layers) + layer->lslices_separated_component_bboxes.clear(); + if (needs_separated_components) { + const size_t nl = m_layers.size(); + std::vector offset(nl + 1, 0); // Orca: flat index of the first island of each layer + for (size_t i = 0; i < nl; ++ i) + offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); + const size_t nreg = offset[nl]; + // Orca: Union-find over every (layer, island). + std::vector parent(nreg); + for (size_t i = 0; i < nreg; ++ i) parent[i] = i; + auto find = [&parent](size_t x) { + while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } + return x; + }; + auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; }; + // Orca: Index the smaller of two consecutive layers instead of scanning every + // pair of islands. The tree prunes distant boxes on fragmented models; exact + // polygon intersections still decide connectivity for the remaining candidates. + for (size_t i = 0; i + 1 < nl; ++ i) { + m_print->throw_if_canceled(); + size_t layer_a = i, layer_b = i + 1; + if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size()) + std::swap(layer_a, layer_b); + const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b]; + if (lb->lslices.empty()) + continue; + + using IslandTree = AABBTreeIndirect::Tree<2, coord_t>; + std::vector bboxes; + bboxes.reserve(lb->lslices.size()); + for (size_t b = 0; b < lb->lslices.size(); ++ b) + bboxes.emplace_back(b, lb->lslices_bboxes[b]); + IslandTree tree; + tree.build_modify_input(bboxes); + for (size_t a = 0; a < la->lslices.size(); ++ a) { + const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max); + AABBTreeIndirect::traverse(tree, + [&query](const IslandTree::Node &node) { return node.bbox.intersects(query); }, + [&](const IslandTree::Node &node) { + const size_t b = node.idx; + // Orca: Tree boxes include an epsilon, so retain the original box + // filter. Already-connected islands cannot change the partition + // and need no further polygon intersection. + if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) && + find(offset[layer_a] + a) != find(offset[layer_b] + b) && + ! intersection_ex(la->lslices[a], lb->lslices[b]).empty()) + unite(offset[layer_a] + a, offset[layer_b] + b); + return true; + }); + } + } + // Orca: Full bounding box of each body, indexed by its union-find root. + std::vector body_bbox(nreg); + for (size_t i = 0; i < nl; ++ i) + for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a) + body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]); + // Orca: Store the body bbox for every island. + for (size_t i = 0; i < nl; ++ i) { + Layer *layer = m_layers[i]; + layer->lslices_separated_component_bboxes.resize(layer->lslices.size()); + for (size_t a = 0; a < layer->lslices.size(); ++ a) + layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)]; + } + } + // the following step needs to be done before combination because it may need // to remove only half of the combined infill this->bridge_over_infill(); @@ -706,71 +800,6 @@ void PrintObject::infill() if (this->set_started(posInfill)) { m_print->set_status(35, L("Generating infill toolpath")); - // Orca: precompute the object's 3D connected bodies for separated infills / per-model - // centering. Two islands belong to the same body when their slices overlap on adjacent - // layers; islands that only overlap in top-down projection but never touch (e.g. interleaved - // chain links) stay separate, matching "split to objects". Each layer island then records - // the full bounding box of its body, so its infill is centered on that body as if it were - // sliced alone. Done once here, before the parallel fill, and only when a region needs it. - bool needs_separated_components = false; - for (size_t i = 0; i < this->num_printing_regions(); ++ i) { - const PrintRegionConfig &rc = this->printing_region(i).config(); - if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) { - needs_separated_components = true; - break; - } - } - // Fast path: the feature only changes anything when the object is made of more than one - // connected body. Detect that cheaply the same way as "Split to objects" — more than one - // model part, or a single part whose mesh is splittable (is_splittable() is cached). A single - // body already shares the object center, i.e. the default, so skip the connectivity pass. - if (needs_separated_components) { - int parts = 0; - const ModelVolume *first_part = nullptr; - for (const ModelVolume *v : this->model_object()->volumes) - if (v->is_model_part()) { ++ parts; first_part = v; } - if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable())) - needs_separated_components = false; - } - for (Layer *layer : m_layers) - layer->lslices_separated_component_bboxes.clear(); - if (needs_separated_components) { - const size_t nl = m_layers.size(); - std::vector offset(nl + 1, 0); // flat index of the first island of each layer - for (size_t i = 0; i < nl; ++ i) - offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); - const size_t nreg = offset[nl]; - // Union-find over every (layer, island). - std::vector parent(nreg); - for (size_t i = 0; i < nreg; ++ i) parent[i] = i; - auto find = [&parent](size_t x) { - while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } - return x; - }; - auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; }; - // Join islands that overlap between two consecutive layers. - for (size_t i = 0; i + 1 < nl; ++ i) { - const Layer *la = m_layers[i], *lb = m_layers[i + 1]; - for (size_t a = 0; a < la->lslices.size(); ++ a) - for (size_t b = 0; b < lb->lslices.size(); ++ b) - if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) && - ! intersection_ex(la->lslices[a], lb->lslices[b]).empty()) - unite(offset[i] + a, offset[i + 1] + b); - } - // Full bounding box of each body, indexed by its union-find root. - std::vector body_bbox(nreg); - for (size_t i = 0; i < nl; ++ i) - for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a) - body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]); - // Store the body bbox for every island. - for (size_t i = 0; i < nl; ++ i) { - Layer *layer = m_layers[i]; - layer->lslices_separated_component_bboxes.resize(layer->lslices.size()); - for (size_t a = 0; a < layer->lslices.size(); ++ a) - layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)]; - } - } - const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first; const auto& support_fill_octree = this->m_adaptive_fill_octrees.second; @@ -1401,8 +1430,6 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_anchor_max" || opt_key == "top_surface_line_width" || opt_key == "bottom_surface_density" - || opt_key == "center_of_surface_pattern" - || opt_key == "separated_infills" || opt_key == "initial_layer_line_width" || opt_key == "small_area_infill_flow_compensation" || opt_key == "lateral_lattice_angle_1" @@ -1410,6 +1437,10 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_overhang_angle") { steps.emplace_back(posInfill); } else if (opt_key == "sparse_infill_pattern" + // Orca: Body centering now also determines bridge anchors during preparation. + // Invalidating preparation also invalidates infill, including top/bottom surfaces. + || opt_key == "center_of_surface_pattern" + || opt_key == "separated_infills" || opt_key == "sparse_infill_smooth_factor" || opt_key == "symmetric_infill_y_axis" || opt_key == "infill_shift_step" @@ -3009,21 +3040,12 @@ void PrintObject::bridge_over_infill() return diff(layers_sparse_infill, not_sparse_infill); }; - // LAMBDA do determine optimal bridging angle - auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors, InfillPattern dominant_pattern, double infill_direction) { + // Orca: Derive the fallback bridge direction from the supplied anchor geometry. + // Pattern-specific angle selection belongs at the call site, where the supporting + // layer and region are known; this helper must not override it with a base config angle. + auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors) { AABBTreeLines::LinesDistancer lines_tree(anchors); - // Orca: since 3D Honeycomb was "fixed" by forcing coordf_t layerHeight = scale_(1.0), this is no longer needed. - // CorssHatch also does not need fixed angle. - // - // Check it the infill that require a fixed infill angle. - //switch (dominant_pattern) { - //case ip3DHoneycomb: - //case ipCrossHatch: - // return (infill_direction + 45.0) * 2.0 * M_PI / 360.; - //default: break; - //} - std::map counted_directions; for (const Polygon &p : bridged_area) { double acc_distance = 0; @@ -3089,18 +3111,15 @@ void PrintObject::bridge_over_infill() if (bridging_angle == 0) { bridging_angle = 0.001; } - switch (dominant_pattern) { - case ipHilbertCurve: bridging_angle += 0.25 * PI; break; - case ipOctagramSpiral: bridging_angle += (1.0 / 16.0) * PI; break; - default: break; - } return bridging_angle; }; - // LAMBDA that will fill given polygons with lines, exapand the lines to the nearest anchor, and reconstruct polygons from the newly - // generated lines - auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle) { + // Orca: Extend scan sections to the nearest anchors and reconstruct the bridge area. + // scan_spacing controls boundary sampling independently of the extrusion spacing; + // anchoring overlap and smoothing thresholds still use the physical bridging flow. + auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle, + coord_t scan_spacing, bool restore_anchors = false) { auto lines_rotate = [](Lines &lines, double cos_angle, double sin_angle) { for (Line &l : lines) { double ax = double(l.a.x()); @@ -3127,12 +3146,12 @@ void PrintObject::bridge_over_infill() BoundingBox bb_x = get_extents(bridged_area); BoundingBox bb_y = get_extents(anchors); - const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + bridging_flow.scaled_spacing() - 1) / bridging_flow.scaled_spacing(); + const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + scan_spacing - 1) / scan_spacing; std::vector vertical_lines(n_vlines); for (size_t i = 0; i < n_vlines; i++) { - // Orca: Make sure the line is placed in the middle of the extrusion - // coord_t x = bb_x.min.x() + i * bridging_flow.scaled_spacing(); - coord_t x = bb_x.min.x() + (i + 0.5) * bridging_flow.scaled_spacing(); + // Orca: Sample the center of each reconstructed strip. Its edges lie + // half a scan step away, even when the sampling is finer than extrusion. + coord_t x = bb_x.min.x() + (i + 0.5) * scan_spacing; coord_t y_min = bb_y.min.y() - bridging_flow.scaled_spacing(); coord_t y_max = bb_y.max.y() + bridging_flow.scaled_spacing(); vertical_lines[i].a = Point{x, y_min}; @@ -3155,7 +3174,11 @@ void PrintObject::bridge_over_infill() auto anchors_intersections = anchors_and_walls_tree.intersections_with_line(vertical_lines[i]); for (Line §ion : polygon_sections[i]) { - auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a, + // Orca: A repaired boundary may already overlap its anchor by one flow width. + // Include that overlap in the search so restoring rounded corners does not + // extend every already anchored section into the next sparse infill cell. + const coord_t overlap = restore_anchors ? bridging_flow.scaled_width() + SCALED_EPSILON : 0; + auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a + Point{0, overlap}, [](const Point &a, const std::pair &b) { return a.y() > b.first.y(); }); @@ -3164,7 +3187,7 @@ void PrintObject::bridge_over_infill() section.a.y() -= bridging_flow.scaled_width() * (0.5 + 0.5); } - auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b, + auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b - Point{0, overlap}, [](const Point &a, const std::pair &b) { return a.y() < b.first.y(); }); @@ -3194,7 +3217,9 @@ void PrintObject::bridge_over_infill() }); } - // reconstruct polygon from polygon sections + // Orca: Reconstruct the polygon from scan sections. At discontinuities and + // strip starts/ends, use half the scan step for the X offsets; using half an + // extrusion spacing would overlap the finer strips and distort curved anchors. struct TracedPoly { Points lows; @@ -3220,8 +3245,8 @@ void PrintObject::bridge_over_infill() 36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) { traced_poly.lows.push_back(candidate->a); } else { - traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); - traced_poly.lows.push_back(candidate->a - Point{bridging_flow.scaled_spacing() / 2, 0}); + traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0}); + traced_poly.lows.push_back(candidate->a - Point{scan_spacing / 2, 0}); traced_poly.lows.push_back(candidate->a); } @@ -3229,8 +3254,8 @@ void PrintObject::bridge_over_infill() 36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) { traced_poly.highs.push_back(candidate->b); } else { - traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); - traced_poly.highs.push_back(candidate->b - Point{bridging_flow.scaled_spacing() / 2, 0}); + traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0}); + traced_poly.highs.push_back(candidate->b - Point{scan_spacing / 2, 0}); traced_poly.highs.push_back(candidate->b); } segment_added = true; @@ -3238,9 +3263,9 @@ void PrintObject::bridge_over_infill() } if (!segment_added) { - // Zero overlapping segments, we just close this polygon - traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); - traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); + // Orca: No section continues this strip; close at its right edge. + traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0}); + traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0}); Polygon &new_poly = expanded_bridged_area.emplace_back(std::move(traced_poly.lows)); new_poly.points.insert(new_poly.points.end(), traced_poly.highs.rbegin(), traced_poly.highs.rend()); traced_poly.lows.clear(); @@ -3255,9 +3280,9 @@ void PrintObject::bridge_over_infill() for (const auto &segment : polygon_slice) { if (used_segments.find(&segment) == used_segments.end()) { TracedPoly &new_tp = current_traced_polys.emplace_back(); - new_tp.lows.push_back(segment.a - Point{bridging_flow.scaled_spacing() / 2, 0}); + new_tp.lows.push_back(segment.a - Point{scan_spacing / 2, 0}); new_tp.lows.push_back(segment.a); - new_tp.highs.push_back(segment.b - Point{bridging_flow.scaled_spacing() / 2, 0}); + new_tp.highs.push_back(segment.b - Point{scan_spacing / 2, 0}); new_tp.highs.push_back(segment.b); } } @@ -3364,7 +3389,10 @@ void PrintObject::bridge_over_infill() total_fill_area = closing(total_fill_area, float(SCALED_EPSILON)); expansion_area = closing(expansion_area, float(SCALED_EPSILON)); expansion_area = intersection(expansion_area, deep_infill_area); - Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing)); + // Orca: Preserve the real lower-layer anchors for every candidate in this + // layer. Replacing this shared set for one pattern also changes later regions, + // and synthetic straight lines can claim support where no infill is printed. + const Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing)); Polygons internal_unsupported_area = shrink(deep_infill_area, spacing * 4.5); #ifdef DEBUG_BRIDGE_OVER_INFILL @@ -3375,6 +3403,9 @@ void PrintObject::bridge_over_infill() std::vector expanded_surfaces; expanded_surfaces.reserve(surfaces_by_layer[lidx].size()); for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) { + const auto ®ion_config = candidate.region->region().config(); + const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve || + region_config.sparse_infill_pattern == ipOctagramSpiral; const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true); Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing()); area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area); @@ -3403,20 +3434,40 @@ void PrintObject::bridge_over_infill() to_lines(area_to_be_bridge), to_lines(boundary_plines), to_lines(anchors), to_lines(expansion_area)); #endif - double bridging_angle = 0; - if (!anchors.empty()) { - bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors), - candidate.region->region().config().sparse_infill_pattern.value, - candidate.region->region().config().infill_direction.value); - } else { - // use expansion boundaries as anchors. - // Also, use Infill pattern that is neutral for angle determination, since there are no infill lines. - bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(boundary_plines), InfillPattern::ipLine, 0); + double bridging_angle = -1.; + if (!anchors.empty() && turning_pattern) { + // Orca: Keep adjacent bridges over Hilbert/Octagram aligned despite + // their many local turning directions. Use the lower layer's rotation, + // since that is the infill supporting the bridge, not the current layer's. + for (const LayerRegion *lower_region : layer->lower_layer->regions()) { + // Orca: Apply the configured direction only if the same region has + // sparse infill below this bridge. A height modifier may put another + // pattern underneath, requiring the geometry-based fallback below. + if (&lower_region->region() != &candidate.region->region() || + intersection(area_to_be_bridge, to_polygons(lower_region->fill_surfaces.filter_by_type(stInternal))).empty()) + continue; + bridging_angle = calculate_infill_rotation_angle(po, layer->lower_layer->id(), region_config.infill_direction.value, + region_config.sparse_infill_rotate_template.value) + 0.5 * PI; + // Orca: Apply model alignment as infill generation does, then normalize + // the undirected bridge angle to [0, PI), including negative rotations. + if (region_config.align_infill_direction_to_model) { + const auto &m = po->trafo().matrix(); + bridging_angle += std::atan2(double(m(1, 0)), double(m(0, 0))); + } + bridging_angle = std::fmod(bridging_angle, PI); + if (bridging_angle < 0.) + bridging_angle += PI; + break; + } } + // Orca: A different region below (e.g. a height modifier) needs the actual anchor + // directions. When there are no sparse anchors, use the expansion boundaries. + if (bridging_angle < 0.) + bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors.empty() ? boundary_plines : anchors)); - // ORCA: Internal bridge angle override + // Orca: Preserve the user's absolute or relative internal bridge angle + // override after automatic direction selection. if (candidate.region->region().config().internal_bridge_angle.value > 0) { - const auto ®ion_config = candidate.region->region().config(); const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value); if (region_config.relative_bridge_angle.value) bridging_angle += custom_angle_rad; @@ -3429,11 +3480,19 @@ void PrintObject::bridge_over_infill() } } + // Orca: Changing the bridge direction must not change its physical supports. + // Extend to actual sparse infill or the existing boundary anchors, never to + // a synthetic grid that merely has the same nominal angle and spacing. boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end()); if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) { boundary_plines = intersection_pl(boundary_plines, expand(area_to_be_bridge, scale_(10))); } - Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle); + // Orca: Use four samples per extrusion spacing for Hilbert/Octagram so the + // reconstructed boundary follows rounded anchors instead of cutting corners. + // Keep the original step for other patterns and at least one coordinate unit + // after integer division. This changes boundary accuracy, not infill density. + const coord_t scan_spacing = std::max(coord_t(1), flow.scaled_spacing() / (turning_pattern ? 4 : 1)); + Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing); // Check collision with other expanded surfaces { @@ -3447,7 +3506,9 @@ void PrintObject::bridge_over_infill() } } if (reconstruct) { - bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle); + // Orca: Retain the same sampling accuracy when matching a nearby + // bridge's direction; rebuilding must not lose the curved supports. + bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing); } } @@ -3455,6 +3516,13 @@ void PrintObject::bridge_over_infill() // bridging_area = opening(bridging_area, flow.scaled_spacing()); bridging_area = opening(bridging_area, flow.scaled_spacing() * 0.75); bridging_area = closing(bridging_area, flow.scaled_spacing()); + // Orca: Opening/closing can pull rounded bridge ends away from their real + // supports. Restore those contacts after smoothing, preserving the cleaned + // area and the selected angle; do not smooth the restored contacts again. + if (turning_pattern && !bridging_area.empty()) { + bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow, + bridging_angle, scan_spacing, true)); + } bridging_area = intersection(bridging_area, limiting_area); bridging_area = intersection(bridging_area, total_fill_area); bridging_area = diff(bridging_area, total_top_area); diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 04e5b61831..aa81570e56 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -9,6 +9,7 @@ #include #include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" @@ -1229,3 +1230,68 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", " REQUIRE(smooth.point_count > sharp.point_count); REQUIRE(smooth.sharp_turns < sharp.sharp_turns); } + +TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][InternalBridge][Regression]") +{ + // Orca: Compare generated anchors with actual extrusion across plane-path patterns, + // smoothing, multiline and rotations; an origin shift must not pass as valid support. + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords"); + const std::string smoothing = GENERATE("0%", "100%"); + const int multiline = GENERATE(1, 2); + const bool rotated = GENERATE(false, true); + const bool separated = GENERATE(false, true); + CAPTURE(pattern, smoothing, multiline, rotated, separated); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", smoothing}, + {"fill_multiline", multiline}, + {"infill_direction", 45}, + {"sparse_infill_rotate_template", rotated ? "0,25,50" : ""}, + {"align_infill_direction_to_model", rotated}, + {"separated_infills", separated}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"resolution", 0.012}}); + Print print; + Model model; + TriangleMesh mesh = make_cube(30, 24, 1); + if (separated) { + // Orca: Two disconnected bodies in one object must each use their own infill origin. + TriangleMesh second = make_cube(30, 24, 1); + second.translate(50, 0, 0); + mesh.merge(second); + } + Slic3r::Test::init_print({mesh}, print, model, config, nullptr, false); + if (rotated) { + model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(23.))); + print.apply(model, config); + } + print.process(); + + const Layer &layer = *print.objects().front()->get_layer(4); + Polylines printed; + for (const LayerRegion *region : layer.regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) + if (entity->role() == erInternalInfill) + entity->collect_polylines(printed); + REQUIRE_FALSE(printed.empty()); + const AABBTreeLines::LinesDistancer printed_tree(to_lines(printed)); + + // Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently. + const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr), + shrink(to_polygons(layer.lslices), scale_(3.))); + REQUIRE_FALSE(anchors.empty()); + double max_distance = 0.; + for (const Polyline &path : anchors) + for (const Point &point : path.equally_spaced_points(scale_(0.25))) + max_distance = std::max(max_distance, printed_tree.distance_from_lines(point)); + // Orca: Allow only the configured simplification tolerance; infill-scale offsets + // would hide anchors that no longer coincide with printed lines. + CHECK(unscale(max_distance) <= config.opt_float("resolution")); +} diff --git a/tests/fff_print/test_printobject.cpp b/tests/fff_print/test_printobject.cpp index fb7fe2c1fd..a373a1ad39 100644 --- a/tests/fff_print/test_printobject.cpp +++ b/tests/fff_print/test_printobject.cpp @@ -4,11 +4,18 @@ #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/GCodeReader.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/AABBTreeLines.hpp" #include "test_helpers.hpp" +#include #include +#include #include +#include +#include +#include using namespace Slic3r; using namespace Slic3r::Test; @@ -130,3 +137,438 @@ TEST_CASE("Initial layer height is honored", "[PrintObject]") REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4)); REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4)); } + +static TriangleMesh internal_bridge_step() +{ + // Orca: The smaller tower leaves a shoulder whose solid skin needs internal bridges + // over the sparse infill in the base, without relying on an external model file. + TriangleMesh mesh = make_cube(30, 24, 3); + TriangleMesh tower = make_cube(14, 10, 1); + tower.translate(8, 7, 3); + mesh.merge(tower); + return mesh; +} + +static DynamicPrintConfig internal_bridge_config(const std::string &pattern, int multiline) +{ + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"fill_multiline", multiline}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", "100%"}, + {"infill_direction", 45}, + {"internal_bridge_angle", 0}, + {"thick_internal_bridges", true}, + {"top_shell_layers", 3}, + {"bottom_shell_layers", 2}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}}); + return config; +} + +TEST_CASE("Internal bridge angles follow the lower infill layer and model rotation", "[PrintObject][InternalBridge][Regression]") +{ + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral"); + // Orca: Cover both a central line (odd counts) and offset pairs (even counts). + const int multiline = GENERATE(1, 2, 3); + CAPTURE(multiline); + const double rotation = GENERATE(23., -123.); + const std::vector cycle{10., 30., 70.}; + auto config = internal_bridge_config(pattern, multiline); + config.set_deserialize_strict({{"sparse_infill_rotate_template", "10,30,70"}, + {"align_infill_direction_to_model", true}, + {"separated_infills", false}}); + Print print; + Model model; + init_print({internal_bridge_step()}, print, model, config, nullptr, false); + model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(rotation))); + print.apply(model, config); + print.process(); + const PrintObject &object = *print.objects().front(); + size_t bridges = 0; + for (size_t i = 1; i < object.layer_count(); ++i) { + // Orca: The support is one layer below the bridge. Check the template and model + // rotation together, including normalization when the resulting angle is negative. + double expected = std::fmod(cycle[(i - 1) % cycle.size()] + 90. + rotation, 180.); + if (expected < 0.) expected += 180.; + for (const LayerRegion *region : object.get_layer(i)->regions()) + for (const Surface *surface : region->fill_surfaces.filter_by_type(stInternalBridge)) { + CAPTURE(pattern, rotation, i); + CHECK_THAT(Geometry::rad2deg(surface->bridge_angle), Catch::Matchers::WithinAbs(expected, 0.001)); + ++bridges; + } + } + REQUIRE(bridges > 0); +} + +TEST_CASE("Turning infill does not replace the anchors of another region", "[PrintObject][InternalBridge][Regression]") +{ + // Orca: Keep the right-hand region fixed while changing the left-hand pattern in the + // same object. Its bridge areas must be independent of a previous candidate's anchors. + const int multiline = GENERATE(1, 2, 3); + CAPTURE(multiline); + auto right_bridges = [multiline](const std::string &left_pattern) { + auto config = internal_bridge_config(left_pattern, multiline); + Print print; + Model model; + init_print({internal_bridge_step()}, print, model, config, nullptr, false); + TriangleMesh right = internal_bridge_step(); + right.translate(50, 0, 0); + ModelVolume *volume = model.objects.front()->add_volume(std::move(right)); + volume->config.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + volume->config.set_key_value("infill_direction", new ConfigOptionFloat(17.)); + print.apply(model, config); + print.process(); + std::map result; + const PrintObject &object = *print.objects().front(); + for (size_t i = 0; i < object.layer_count(); ++i) + for (const LayerRegion *region : object.get_layer(i)->regions()) + if (region->region().config().infill_direction == 17.) + polygons_append(result[i], to_polygons(region->fill_surfaces.filter_by_type(stInternalBridge))); + return result; + }; + const auto baseline = right_bridges("rectilinear"); + const auto actual = right_bridges(GENERATE("hilbertcurve", "octagramspiral")); + REQUIRE(actual.size() == baseline.size()); + double total_area = 0.; + for (const auto &[layer, expected] : baseline) { + CAPTURE(layer); + const auto &polys = actual.at(layer); + CHECK(area(diff(expected, polys)) < scaled(1.) * scaled(1.) * 1e-6); + CHECK(area(diff(polys, expected)) < scaled(1.) * scaled(1.) * 1e-6); + total_area += area(expected); + } + REQUIRE(total_area > 0.); +} + +TEST_CASE("Rounded internal bridges end on printed support", "[PrintObject][InternalBridge][Regression]") +{ + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral"); + const bool separated = GENERATE(false, true); + CAPTURE(pattern, separated); + auto config = internal_bridge_config(pattern, 1); + config.set_deserialize_strict({{"infill_wall_overlap", "0%"}, {"separated_infills", separated}}); + TriangleMesh mesh = internal_bridge_step(); + if (separated) { + TriangleMesh second = internal_bridge_step(); + second.translate(50, 0, 0); + mesh.merge(second); + } + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + print.process(); + + // Orca: Check final extrusion endpoints after polygon cleanup and fill generation. + // A correct bridge angle and correct sparse anchors alone do not guarantee contact. + const PrintObject &object = *print.objects().front(); + size_t checked = 0; + for (size_t i = 1; i < object.layer_count(); ++i) { + Polygons support; + Polylines walls; + for (const LayerRegion *region : object.get_layer(i - 1)->regions()) { + region->perimeters.polygons_covered_by_width(support, 0.f); + region->fills.polygons_covered_by_width(support, 0.f); + region->perimeters.collect_polylines(walls); + } + REQUIRE_FALSE(support.empty()); + const AABBTreeLines::LinesDistancer support_tree(to_lines(union_(support))); + const AABBTreeLines::LinesDistancer wall_tree(to_lines(walls)); + for (const LayerRegion *region : object.get_layer(i)->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (entity->role() != erInternalBridgeInfill) + continue; + const auto *path = dynamic_cast(entity); + REQUIRE(path != nullptr); + for (const Line &line : path->polyline.to_polyline().lines()) { + // Orca: Sample span ends, excluding short connectors and wall overlap. + if (line.length() < scale_(std::max(0.7, 3. * path->width))) + continue; + for (const Point &point : {line.a, line.b}) { + if (wall_tree.distance_from_lines(point) <= scale_(0.5)) + continue; + CAPTURE(i, point.x(), point.y()); + const double gap = unscale(support_tree.distance_from_lines(point)) - 0.5 * path->width; + CHECK(gap <= 0.1); + ++checked; + } + } + } + } + REQUIRE(checked > 0); +} + +TEST_CASE("Enabling separated infill recomputes body origins", "[PrintObject][InternalBridge][Regression]") +{ + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords"); + CAPTURE(pattern); + auto footprint = [&](bool reslice) { + auto config = internal_bridge_config(pattern, 2); + config.set_deserialize_strict({{"separated_infills", !reslice}}); + TriangleMesh mesh = internal_bridge_step(); + TriangleMesh second = internal_bridge_step(); + second.translate(50, 0, 0); + mesh.merge(second); + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + print.process(); + if (reslice) { + // Orca: Enabling centering after a completed slice must rebuild the body + // origins now shared by bridge preparation and printed infill. + config.set_deserialize_strict({{"separated_infills", true}}); + print.apply(model, config); + print.process(); + } + Polygons result; + for (const LayerRegion *region : print.objects().front()->get_layer(4)->regions()) + region->fills.polygons_covered_by_width(result, 0.f); + return union_(result); + }; + const Polygons fresh = footprint(false); + const Polygons resliced = footprint(true); + REQUIRE_FALSE(fresh.empty()); + CHECK(area(diff(fresh, resliced)) < scaled(1.) * scaled(1.) * 1e-6); + CHECK(area(diff(resliced, fresh)) < scaled(1.) * scaled(1.) * 1e-6); +} + +TEST_CASE("Surface centering survives changes to separated infill settings", "[PrintObject][SurfaceInfill][Regression]") +{ + const std::string pattern = GENERATE("archimedeanchords", "octagramspiral"); + const std::string initial_center = GENERATE("each_surface", "each_model", "each_assembly"); + const std::string final_center = GENERATE("each_surface", "each_model", "each_assembly"); + const bool separated = GENERATE(false, true); + const std::string top_order = GENERATE("default", "outward", "inward"); + const std::string bottom_order = top_order == "outward" ? "inward" : top_order == "inward" ? "outward" : "default"; + const std::string density = GENERATE("80%", "100%"); + const bool change_center = initial_center != final_center; + CAPTURE(pattern, initial_center, final_center, separated, top_order, bottom_order, density); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"top_surface_pattern", pattern}, + {"bottom_surface_pattern", pattern}, + {"top_surface_fill_order", top_order}, + {"bottom_surface_fill_order", bottom_order}, + {"top_surface_density", density}, + {"bottom_surface_density", density}, + {"center_of_surface_pattern", initial_center}, + {"separated_infills", change_center ? separated : !separated}, + {"sparse_infill_pattern", "rectilinear"}, + {"sparse_infill_density", "15%"}, + {"top_shell_layers", 2}, + {"bottom_shell_layers", 2}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}}); + + // Orca: Two disconnected bodies exercise per-body centering. The offset tower also + // makes each-surface and each-model centering differ on the top surfaces. + TriangleMesh mesh = make_cube(30, 24, 2); + TriangleMesh tower = make_cube(12, 10, 1); + tower.translate(4, 3, 2); + mesh.merge(tower); + TriangleMesh second = mesh; + second.translate(50, 0, 0); + mesh.merge(second); + + // Orca: Equal footprints can hide reordered or reversed paths. Retain their point + // sequences and ordering protection to cover the directional surface behavior too. + struct SurfaceFillSnapshot { + std::map> paths; + bool protected_order = true; + }; + auto surface_fills = [](const Print &print) { + std::map, SurfaceFillSnapshot> result; + const PrintObject &object = *print.objects().front(); + for (size_t i = 0; i < object.layer_count(); ++i) { + auto collect = [&](const auto &self, const ExtrusionEntity &entity, bool no_sort) -> void { + if (const auto *collection = dynamic_cast(&entity)) { + for (const ExtrusionEntity *child : collection->entities) + self(self, *child, no_sort || collection->no_sort); + } else if (entity.role() == erTopSolidInfill || entity.role() == erBottomSurface) { + const auto *path = dynamic_cast(&entity); + REQUIRE(path != nullptr); + auto &snapshot = result[{i, entity.role()}]; + // Orca: The centered test model has one body on either side of X=0. + // Their traversal order may vary; preserve path order within each body. + Points points = path->polyline.to_polyline().points; + REQUIRE_FALSE(points.empty()); + snapshot.paths[points.front().x() > 0].push_back(std::move(points)); + snapshot.protected_order &= no_sort && !path->can_reverse(); + } + }; + for (const LayerRegion *region : object.get_layer(i)->regions()) + collect(collect, region->fills, false); + } + return result; + }; + + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + print.process(); + const auto initial = surface_fills(print); + config.set_deserialize_strict({{"center_of_surface_pattern", final_center}, {"separated_infills", separated}}); + print.apply(model, config); + // Orca: Preparation owns the body origins, and its invalidation must also force + // regeneration of top/bottom extrusion paths, even when sparse infill is unchanged. + CHECK_FALSE(print.objects().front()->is_step_done(posPrepareInfill)); + CHECK_FALSE(print.objects().front()->is_step_done(posInfill)); + print.process(); + const auto resliced = surface_fills(print); + + Print fresh_print; + Model fresh_model; + init_print({mesh}, fresh_print, fresh_model, config, nullptr, false); + fresh_print.process(); + const auto fresh = surface_fills(fresh_print); + REQUIRE_FALSE(fresh.empty()); + REQUIRE(resliced.size() == fresh.size()); + std::set roles; + bool changed_paths = false; + for (const auto &entry : fresh) { + CAPTURE(entry.first.first, entry.first.second); + REQUIRE_FALSE(entry.second.paths.empty()); + roles.insert(entry.first.second); + REQUIRE(resliced.count(entry.first) == 1); + REQUIRE(initial.count(entry.first) == 1); + const auto &actual = resliced.at(entry.first); + const auto &expected = entry.second; + const auto &before = initial.at(entry.first); + CHECK((actual.paths == expected.paths)); + if (!change_center) + CHECK((actual.paths == before.paths)); + if (top_order != "default") { + CHECK(expected.protected_order); + CHECK(actual.protected_order); + CHECK(before.protected_order); + } + changed_paths |= expected.paths != before.paths; + } + CHECK(roles.count(erTopSolidInfill) == 1); + CHECK(roles.count(erBottomSurface) == 1); + // Orca: Guard against a vacuous comparison: changing surface centering must change + // the printed pattern, while toggling separated sparse infill must leave it alone. + CHECK(changed_paths == change_center); +} + +TEST_CASE("Separated infill keeps fragmented and nested bodies independent", "[PrintObject][SurfaceInfill][Regression]") +{ + constexpr size_t grid_size = 8; + TriangleMesh mesh; + auto add_box = [&](double x, double y, double width, double depth) { + TriangleMesh box = make_cube(width, depth, 0.6); + box.translate(x, y, 0); + mesh.merge(box); + }; + // Orca: Many small islands exercise spatial pruning and the tree's original + // island indices. A pillar inside a frame also overlaps its bounding box, + // but must remain a separate body because it lies entirely inside the hole. + for (size_t x = 0; x < grid_size; ++ x) + for (size_t y = 0; y < grid_size; ++ y) + add_box(6 * x, 6 * y, 3, 3); + add_box(54, 0, 20, 4); + add_box(54, 16, 20, 4); + add_box(54, 0, 4, 20); + add_box(70, 0, 4, 20); + add_box(62, 8, 4, 4); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"separated_infills", true}, + {"center_of_surface_pattern", "each_surface"}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"elefant_foot_compensation", 0}, + {"wall_loops", 1}}); + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + // Orca: Prepare body bounds through the public pipeline, then inspect the object read-only. + print.process(); + const PrintObject &object = *print.objects().front(); + REQUIRE(object.layer_count() > 1); + for (const Layer *layer : object.layers()) { + REQUIRE(layer->lslices.size() == grid_size * grid_size + 2); + REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size()); + size_t holes = 0; + for (size_t i = 0; i < layer->lslices.size(); ++ i) { + const BoundingBox &body = layer->lslices_separated_component_bboxes[i]; + const BoundingBox &island = layer->lslices_bboxes[i]; + CHECK(body.min == island.min); + CHECK(body.max == island.max); + holes += layer->lslices[i].holes.size(); + } + CHECK(holes == 1); + } +} + +TEST_CASE("Body centering survives islands merging and splitting between layers", "[PrintObject][SurfaceInfill][Regression]") +{ + const bool separated = GENERATE(false, true); + CAPTURE(separated); + // Orca: Four posts join through horizontal then vertical rails, creating a + // cycle of overlaps before splitting into four islands again. This exercises + // redundant connections and indexing either adjacent layer. A fifth post + // stays separate at every height. + TriangleMesh mesh; + for (int x : {0, 8}) + for (int y : {0, 8}) { + TriangleMesh post = make_cube(4, 4, 1); + post.translate(x, y, 0); + mesh.merge(post); + } + for (int y : {0, 8}) { + TriangleMesh rail = make_cube(12, 4, 0.2); + rail.translate(0, y, 0.2); + mesh.merge(rail); + } + for (int x : {0, 8}) { + TriangleMesh rail = make_cube(4, 12, 0.2); + rail.translate(x, 0, 0.4); + mesh.merge(rail); + } + TriangleMesh isolated = make_cube(4, 4, 1); + isolated.translate(20, 0, 0); + mesh.merge(isolated); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"separated_infills", separated}, + {"center_of_surface_pattern", separated ? "each_surface" : "each_model"}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"elefant_foot_compensation", 0}, + {"wall_loops", 1}}); + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + // Orca: Prepare body bounds through the public pipeline, then inspect the object read-only. + print.process(); + const PrintObject &object = *print.objects().front(); + REQUIRE(object.layer_count() == 5); + REQUIRE(object.get_layer(0)->lslices.size() == 5); + REQUIRE(object.get_layer(1)->lslices.size() == 3); + REQUIRE(object.get_layer(2)->lslices.size() == 3); + REQUIRE(object.get_layer(4)->lslices.size() == 5); + + BoundingBox isolated_bbox = object.get_layer(0)->lslices_bboxes.front(); + for (const BoundingBox &bbox : object.get_layer(0)->lslices_bboxes) + if (bbox.min.x() > isolated_bbox.min.x()) + isolated_bbox = bbox; + BoundingBox connected_bbox; + for (const Layer *layer : object.layers()) + for (const BoundingBox &bbox : layer->lslices_bboxes) + if (bbox.min.x() < isolated_bbox.min.x()) + connected_bbox.merge(bbox); + for (const Layer *layer : object.layers()) { + REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size()); + for (size_t i = 0; i < layer->lslices.size(); ++ i) { + const BoundingBox &expected = layer->lslices_bboxes[i].min.x() < isolated_bbox.min.x() ? connected_bbox : isolated_bbox; + const BoundingBox &actual = layer->lslices_separated_component_bboxes[i]; + CHECK(actual.min == expected.min); + CHECK(actual.max == expected.max); + } + } +} From a93c6ea67b11376ed27c80acea7878b9fbfbf270 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 10 Sep 2026 19:29:58 +0800 Subject: [PATCH 07/57] hotfix: system bundles being copied from resources folder on every startup --- src/slic3r/Utils/PresetUpdater.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index b328c43cca..23957f6500 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1106,8 +1106,8 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); - if (enabled_config_update) { - if (is_vendor_installed(vendor_name)) { + if (is_vendor_installed(vendor_name)) { + if (enabled_config_update) { if (is_vendor_enabled) { // Orca: whichever form of the vendor resources ships at the newer // version is the one installing lays down, and the one to judge @@ -1122,17 +1122,12 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); bundles.insert(vendor_name); } - } - else { - //need to be removed because not installed + } else { + // need to be removed because not installed remove_installed_vendor(vendor_name); } } - else if (is_vendor_enabled) { - bundles.insert(vendor_name); - } - } - else if (is_vendor_enabled) { + } else if (is_vendor_enabled) { bundles.insert(vendor_name); } } From 29b3282c8b73ca2518a8045369a938e666c3a8ac Mon Sep 17 00:00:00 2001 From: yw4z Date: Thu, 10 Sep 2026 15:45:00 +0300 Subject: [PATCH 08/57] Update PluginPickerDialog.cpp --- src/slic3r/GUI/PluginPickerDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/PluginPickerDialog.cpp b/src/slic3r/GUI/PluginPickerDialog.cpp index c0ff656fd1..5109b3b483 100644 --- a/src/slic3r/GUI/PluginPickerDialog.cpp +++ b/src/slic3r/GUI/PluginPickerDialog.cpp @@ -124,7 +124,7 @@ void PluginPickerDialog::build_capability_ui(const wxString& plugin_type_label) if (has_capabilities) { m_choice->SetSelection(0); - m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) { + m_choice->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { update_capability_description(evt.GetSelection()); }); } else { From d09c3568c553c324fd2684193b8fe9dbd3142b67 Mon Sep 17 00:00:00 2001 From: yw4z Date: Thu, 10 Sep 2026 17:02:29 +0300 Subject: [PATCH 09/57] match style of progress dialog --- src/slic3r/GUI/PluginsDialog.hpp | 6 +++--- src/slic3r/GUI/Widgets/ProgressDialog.cpp | 9 ++++++--- src/slic3r/GUI/Widgets/ProgressDialog.hpp | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp index e663de79e4..98b46cf3e3 100644 --- a/src/slic3r/GUI/PluginsDialog.hpp +++ b/src/slic3r/GUI/PluginsDialog.hpp @@ -107,12 +107,12 @@ private: const wxString& title, const wxString& message, int maximum = 100, - int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, + int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button bool finish_after_dialog_destroyed = false) { const auto alive = m_alive; - wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, this, style); - wxTimer* timer = new wxTimer(); + ProgressDialog* progress = new ProgressDialog(title, message, maximum, this, style); + wxTimer* timer = new wxTimer(); timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) { if (alive->load(std::memory_order_acquire) && progress) diff --git a/src/slic3r/GUI/Widgets/ProgressDialog.cpp b/src/slic3r/GUI/Widgets/ProgressDialog.cpp index 53842ecaea..c16f3bcf1f 100644 --- a/src/slic3r/GUI/Widgets/ProgressDialog.cpp +++ b/src/slic3r/GUI/Widgets/ProgressDialog.cpp @@ -178,7 +178,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int wxBoxSizer *sizer_1line = new wxBoxSizer(wxHORIZONTAL); m_msg = new wxStaticText(m_panel_1line, wxID_ANY, wxEmptyString, wxDefaultPosition, PROGRESSDIALOG_SIMPLEBOOK_SIZE, 0); m_msg->Wrap(-1); - m_msg->SetFont(::Label::Body_13); + m_msg->SetFont(::Label::Body_14); m_msg->SetForegroundColour(PROGRESSDIALOG_GREY_700); sizer_1line->Add(m_msg, 0, wxALIGN_CENTER, 0); m_panel_1line->SetSizer(sizer_1line); @@ -188,7 +188,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int wxBoxSizer *sizer_2line = new wxBoxSizer(wxVERTICAL); m_msg_2line = new wxStaticText(m_panel_2line, wxID_ANY, wxEmptyString, wxDefaultPosition, PROGRESSDIALOG_SIMPLEBOOK_SIZE, 0); m_msg_2line->Wrap(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x); - m_msg_2line->SetFont(::Label::Body_13); + m_msg_2line->SetFont(::Label::Body_14); m_msg_2line->SetForegroundColour(PROGRESSDIALOG_GREY_700); m_msg_2line->SetMaxSize(wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1)); sizer_2line->Add(m_msg_2line, 1, wxALL, 0); @@ -204,7 +204,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0); m_msg->Wrap(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x); - m_msg->SetFont(::Label::Body_13); + m_msg->SetFont(::Label::Body_14); m_msg->SetForegroundColour(PROGRESSDIALOG_GREY_700); m_msg_sizer->Add(m_msg, 0, wxEXPAND | wxALL, 0); @@ -228,6 +228,9 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int if (!HasPDFlag(wxPD_NO_PROGRESS)) { m_gauge = new wxGauge(this, wxID_ANY, maximum, wxDefaultPosition, PROGRESSDIALOG_GAUGE_SIZE, gauge_style); m_gauge->SetValue(0); + m_gauge->SetForegroundColour(wxColour("#009688")); + m_gauge->SetBackgroundColour(wxColour("#D9D9D9")); + wxGetApp().UpdateDarkUI(m_gauge); m_sizer_main->Add(m_gauge, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28)); } diff --git a/src/slic3r/GUI/Widgets/ProgressDialog.hpp b/src/slic3r/GUI/Widgets/ProgressDialog.hpp index bb770298a9..9ebf31b194 100644 --- a/src/slic3r/GUI/Widgets/ProgressDialog.hpp +++ b/src/slic3r/GUI/Widgets/ProgressDialog.hpp @@ -18,7 +18,7 @@ class WXDLLIMPEXP_FWD_CORE wxWindowDisabler; #define PROGRESSDIALOG_GAUGE_SIZE wxSize(FromDIP(320), FromDIP(6)) #define PROGRESSDIALOG_CANCEL_BUTTON_SIZE wxSize(FromDIP(60), FromDIP(24)) #define PROGRESSDIALOG_DEF_BK wxColour(255,255,255) -#define PROGRESSDIALOG_GREY_700 wxColour(107,107,107) +#define PROGRESSDIALOG_GREY_700 wxColour(54,54,54) // #363636 label color #define wxPD_NO_PROGRESS 0x0100 From a640e32a19583378d68618efba5b44911a6b7cd2 Mon Sep 17 00:00:00 2001 From: yw4z Date: Thu, 10 Sep 2026 17:29:25 +0300 Subject: [PATCH 10/57] fix build error --- src/slic3r/GUI/PluginsDialog.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp index 98b46cf3e3..ec68d12969 100644 --- a/src/slic3r/GUI/PluginsDialog.hpp +++ b/src/slic3r/GUI/PluginsDialog.hpp @@ -2,6 +2,7 @@ #define slic3r_PluginsDialog_hpp_ #include "Widgets/WebViewHostDialog.hpp" +#include "Widgets/ProgressDialog.hpp" #include "PluginSource.hpp" #include "PluginStatus.hpp" #include "PluginSort.hpp" From d97dea2c41d554db3fd115886ce6b67e5beb146c Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 10 Sep 2026 16:08:54 -0500 Subject: [PATCH 11/57] build: clear 10 driver warnings from CGAL's fp flag pair under clang-cl (#15629) --- src/libslic3r/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index e860f50280..ffc6b5cee6 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -558,6 +558,12 @@ if (_opts) target_compile_options(libslic3r_cgal PRIVATE "${_opts_bad}") endif() +if (IS_CLANG_CL) + # CGAL passes /fp:strict /fp:except-. clang-cl reports the second as overriding part of + # the first; the settings cc1 receives are the same ones MSVC produces from that pair. + target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option) +endif () + target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs) if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround From 0a630738f1e6467f7602b4948a901b8381daab78 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:15:06 +0300 Subject: [PATCH 12/57] Fix untranslated language dialog captions (#15600) --- localization/i18n/OrcaSlicer.pot | 4 - localization/i18n/ca/OrcaSlicer_ca.po | 4 - localization/i18n/cs/OrcaSlicer_cs.po | 4 - localization/i18n/de/OrcaSlicer_de.po | 4 - localization/i18n/en/OrcaSlicer_en.po | 4 - localization/i18n/es/OrcaSlicer_es.po | 4 - localization/i18n/eu/OrcaSlicer_eu.po | 4 - localization/i18n/fr/OrcaSlicer_fr.po | 4 - localization/i18n/hu/OrcaSlicer_hu.po | 4 - localization/i18n/it/OrcaSlicer_it.po | 4 - localization/i18n/ja/OrcaSlicer_ja.po | 4 - localization/i18n/ko/OrcaSlicer_ko.po | 4 - localization/i18n/lt/OrcaSlicer_lt.po | 4 - localization/i18n/nl/OrcaSlicer_nl.po | 4 - localization/i18n/pl/OrcaSlicer_pl.po | 4 - localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 4 - localization/i18n/ru/OrcaSlicer_ru.po | 4 - localization/i18n/sv/OrcaSlicer_sv.po | 4 - localization/i18n/th/OrcaSlicer_th.po | 4 - localization/i18n/tr/OrcaSlicer_tr.po | 4 - localization/i18n/uk/OrcaSlicer_uk.po | 4 - localization/i18n/vi/OrcaSlicer_vi.po | 4 - localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 4 - localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 4 - src/slic3r/GUI/GUI_App.cpp | 221 -------------------- src/slic3r/GUI/GUI_App.hpp | 3 - src/slic3r/GUI/MainFrame.cpp | 89 -------- src/slic3r/GUI/Preferences.cpp | 23 +- src/slic3r/GUI/Widgets/FanControl.cpp | 2 +- 29 files changed, 4 insertions(+), 430 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 92b25f26c0..bbdc0e59be 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -2293,8 +2293,6 @@ msgstr "" msgid "%s has been removed." msgstr "" -msgid "Switching application language" -msgstr "" msgid "Select the language" msgstr "" @@ -8294,8 +8292,6 @@ msgstr "" msgid "Language selection" msgstr "" -msgid "Switching application language while some presets are modified." -msgstr "" msgid "Asia-Pacific" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 91ba0e2f2a..373eb6e9fd 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -2522,8 +2522,6 @@ msgstr "Hi ha una actualització disponible. Obriu el quadre de diàleg del paqu msgid "%s has been removed." msgstr "%s s'ha eliminat." -msgid "Switching application language" -msgstr "Canvi d'idioma de l'aplicació" msgid "Select the language" msgstr "Seleccioneu l'idioma" @@ -8924,8 +8922,6 @@ msgstr "Voleu continuar?" msgid "Language selection" msgstr "Selecció d'idiomes" -msgid "Switching application language while some presets are modified." -msgstr "Canviant l'idioma de l'aplicació mentre es modifiquen alguns perfils." msgid "Asia-Pacific" msgstr "Àsia-Pacífic" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index 9884888723..b0d64c8005 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -2482,8 +2482,6 @@ msgstr "Je k dispozici aktualizace. Otevřete dialog balíčku předvoleb a prov msgid "%s has been removed." msgstr "%s bylo odstraněno." -msgid "Switching application language" -msgstr "Přepnutí jazyka aplikace" msgid "Select the language" msgstr "Zvolte jazyk" @@ -8882,8 +8880,6 @@ msgstr "Chcete pokračovat?" msgid "Language selection" msgstr "Výběr jazyka" -msgid "Switching application language while some presets are modified." -msgstr "Přepnutí jazyka aplikace, když jsou některé předvolby upraveny." msgid "Asia-Pacific" msgstr "Asie-Pacifik" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index bb096c0a23..bd25405cc3 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -2430,8 +2430,6 @@ msgstr "Es ist ein Update verfügbar. Öffnen Sie den Profilbündel-Dialog, um e msgid "%s has been removed." msgstr "%s wurde entfernt." -msgid "Switching application language" -msgstr "Wechsel der Sprache" msgid "Select the language" msgstr "Sprache wählen" @@ -8754,8 +8752,6 @@ msgstr "Möchten Sie fortfahren?" msgid "Language selection" msgstr "Sprachauswahl" -msgid "Switching application language while some presets are modified." -msgstr "Umschalten der Anwendungssprache, während einige Profile geändert werden." msgid "Asia-Pacific" msgstr "Asien-Pazifik" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index b949aea730..35c7f8a87a 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -2289,8 +2289,6 @@ msgstr "" msgid "%s has been removed." msgstr "" -msgid "Switching application language" -msgstr "" msgid "Select the language" msgstr "" @@ -8290,8 +8288,6 @@ msgstr "" msgid "Language selection" msgstr "" -msgid "Switching application language while some presets are modified." -msgstr "" msgid "Asia-Pacific" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index e1d86c2fe8..efe4c7dbcd 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -2356,8 +2356,6 @@ msgstr "Hay una actualización disponible. Abra el cuadro de diálogo del paquet msgid "%s has been removed." msgstr "Se ha eliminado %s." -msgid "Switching application language" -msgstr "Cambiando el idioma de la aplicación" msgid "Select the language" msgstr "Seleccionar el idioma" @@ -8529,8 +8527,6 @@ msgstr "¿Quieres continuar?" msgid "Language selection" msgstr "Selección de idiomas" -msgid "Switching application language while some presets are modified." -msgstr "Cambiando idioma de la aplicación mientras se modifican algunos perfiles." msgid "Asia-Pacific" msgstr "Asia-Pacífico" diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index a475d76ab2..698941018e 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -2390,8 +2390,6 @@ msgstr "Eguneratze bat dago erabilgarri. Ireki aurrezarpen-paketeen elkarrizketa msgid "%s has been removed." msgstr "%s kendu da." -msgid "Switching application language" -msgstr "Aplikazioaren hizkuntza aldatzen" msgid "Select the language" msgstr "Hautatu hizkuntza" @@ -8610,8 +8608,6 @@ msgstr "Jarraitu nahi duzu?" msgid "Language selection" msgstr "Hizkuntza-hautaketa" -msgid "Switching application language while some presets are modified." -msgstr "Aplikazioaren hizkuntza aldatzen ari da aurrezarpen batzuk aldatuta dauden bitartean." msgid "Asia-Pacific" msgstr "Asia-Pazifikoa" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index de356edd7d..6344696234 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -2414,8 +2414,6 @@ msgstr "Une mise à jour est disponible. Ouvrez la boîte de dialogue du paquet msgid "%s has been removed." msgstr "%s a été supprimé." -msgid "Switching application language" -msgstr "Changer la langue de l'application" msgid "Select the language" msgstr "Sélectionner la langue" @@ -8678,8 +8676,6 @@ msgstr "Voulez-vous continuer ?" msgid "Language selection" msgstr "Sélection de la langue" -msgid "Switching application language while some presets are modified." -msgstr "Changement de langue de l’application alors que certains préréglages sont modifiés." msgid "Asia-Pacific" msgstr "Asie-Pacifique" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 2c00949e15..78c06dd4c0 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -2460,8 +2460,6 @@ msgstr "Frissítés érhető el. Nyisd meg a beállításcsomag párbeszédablak msgid "%s has been removed." msgstr "%s eltávolítva." -msgid "Switching application language" -msgstr "Alkalmazás nyelvének váltása" msgid "Select the language" msgstr "Válaszd ki a nyelvet" @@ -8806,8 +8804,6 @@ msgstr "Szeretnéd folytatni?" msgid "Language selection" msgstr "Nyelv kiválasztása" -msgid "Switching application language while some presets are modified." -msgstr "Alkalmazás nyelvének átváltása, miközben egyes beállítások módosultak." msgid "Asia-Pacific" msgstr "Ázsia-Csendes-óceáni térség" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 1aa2f15701..cf5099bca3 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -2466,8 +2466,6 @@ msgstr "È disponibile un aggiornamento. Apri la finestra di dialogo del bundle msgid "%s has been removed." msgstr "%s è stato rimosso." -msgid "Switching application language" -msgstr "Cambio lingua applicazione" msgid "Select the language" msgstr "Seleziona la lingua" @@ -8807,8 +8805,6 @@ msgstr "Vuoi continuare?" msgid "Language selection" msgstr "Selezione lingua" -msgid "Switching application language while some presets are modified." -msgstr "Cambio lingua applicazione durante la modifica di alcuni profili." msgid "Asia-Pacific" msgstr "Asia-Pacifico" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 9067928361..99cd0d0a83 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -2473,8 +2473,6 @@ msgstr "アップデートが利用可能です。プリセットバンドルの msgid "%s has been removed." msgstr "%sを削除しました。" -msgid "Switching application language" -msgstr "アプリケーション言語の切り替え" msgid "Select the language" msgstr "言語を選択" @@ -8825,8 +8823,6 @@ msgstr "続行しますか?" msgid "Language selection" msgstr "言語選択" -msgid "Switching application language while some presets are modified." -msgstr "アプリケーション言語を切り替える時に、プリセットの変更があります" msgid "Asia-Pacific" msgstr "アジア太平洋地域" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index fef0a09398..8d12c90228 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -2481,8 +2481,6 @@ msgstr "사용 가능한 업데이트가 있습니다. 사전 설정 번들 대 msgid "%s has been removed." msgstr "%s이(가) 제거되었습니다." -msgid "Switching application language" -msgstr "응용 프로그램 언어 전환" msgid "Select the language" msgstr "언어 선택" @@ -8860,8 +8858,6 @@ msgstr "계속하시겠습니까?" msgid "Language selection" msgstr "언어 선택" -msgid "Switching application language while some presets are modified." -msgstr "일부 사전 설정이 수정되는 동안 응용 프로그램 언어를 전환합니다." msgid "Asia-Pacific" msgstr "아시아 태평양" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 80273427f0..67d02bef6d 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -2449,8 +2449,6 @@ msgstr "Yra prieinamas atnaujinimas. Atidarykite profilių paketo dialogo langą msgid "%s has been removed." msgstr "%s buvo pašalintas." -msgid "Switching application language" -msgstr "Perjungiama programos kalba" msgid "Select the language" msgstr "Pasirinkite kalbą" @@ -8797,8 +8795,6 @@ msgstr "Ar norite tęsti?" msgid "Language selection" msgstr "Kalbos pasirinkimas" -msgid "Switching application language while some presets are modified." -msgstr "Keičiama programos kalba, kai yra pakeistų profilių." msgid "Asia-Pacific" msgstr "Azija-Ramusis vandenynas" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 720f4ba6f5..eff0fdc6b0 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -2679,8 +2679,6 @@ msgstr "Er is een update beschikbaar. Open het dialoogvenster voor de voorinstel msgid "%s has been removed." msgstr "%s is verwijderd." -msgid "Switching application language" -msgstr "De taal van de applicatie wordt aangepast" msgid "Select the language" msgstr "Kies de taal" @@ -9602,8 +9600,6 @@ msgstr "Wilt u doorgaan?" msgid "Language selection" msgstr "Taal selectie" -msgid "Switching application language while some presets are modified." -msgstr "De taal van de toepassing aanpaasen terwijl sommige voorinstellingen zijn aangepast." msgid "Asia-Pacific" msgstr "Azië-Pacific" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 3d7fb083ef..6f74f4b602 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -2512,8 +2512,6 @@ msgstr "Dostępna jest aktualizacja. Otwórz okno pakietu profili, aby ją zains msgid "%s has been removed." msgstr "%s został usunięty." -msgid "Switching application language" -msgstr "Zmiana języka aplikacji" msgid "Select the language" msgstr "Wybierz język" @@ -9016,8 +9014,6 @@ msgstr "Czy kontynuować?" msgid "Language selection" msgstr "Wybór języka" -msgid "Switching application language while some presets are modified." -msgstr "Zmiana języka aplikacji przy jednoczesnym istniejących zmodyfikowanych ustawieniach." msgid "Asia-Pacific" msgstr "Azja i Pacyfik" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 066b8b310b..6f7bf473c0 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -2356,8 +2356,6 @@ msgstr "Há uma atualização disponível. Abra a caixa de diálogo do pacote de msgid "%s has been removed." msgstr "%s foi removido." -msgid "Switching application language" -msgstr "Alternando o idioma do aplicativo" msgid "Select the language" msgstr "Selecione o idioma" @@ -8572,8 +8570,6 @@ msgstr "Você deseja continuar?" msgid "Language selection" msgstr "Seleção de idioma" -msgid "Switching application language while some presets are modified." -msgstr "Alternando idioma do aplicativo enquanto algumas predefinições são modificadas." msgid "Asia-Pacific" msgstr "Ásia-Pacífico" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index b5e6baffe0..2e33546ae8 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -2431,8 +2431,6 @@ msgstr "Доступно обновление. Проверьте меню па msgid "%s has been removed." msgstr "%s был удалён." -msgid "Switching application language" -msgstr "Изменение языка приложения" msgid "Select the language" msgstr "Выбор языка" @@ -8852,8 +8850,6 @@ msgstr "Хотите продолжить?" msgid "Language selection" msgstr "Выбор языка" -msgid "Switching application language while some presets are modified." -msgstr "Смена языка приложения при изменении некоторых профилей." msgid "Asia-Pacific" msgstr "Азиатско-Тихоокеанский" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index b4e3bf51bc..b9aa481d4c 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -2767,8 +2767,6 @@ msgstr "Det finns en uppdatering tillgänglig. Öppna dialogrutan för förinst msgid "%s has been removed." msgstr "%s har tagits bort." -msgid "Switching application language" -msgstr "Byt applikationsspråk" msgid "Select the language" msgstr "Välj språk" @@ -9694,8 +9692,6 @@ msgstr "Fortsätta?" msgid "Language selection" msgstr "Språkval" -msgid "Switching application language while some presets are modified." -msgstr "Byter språk medans inställningarna ändras." msgid "Asia-Pacific" msgstr "Asien-Stillahavsområdet" diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 935bb83a3b..ee7430015e 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -2456,8 +2456,6 @@ msgstr "มีอัปเดตพร้อมใช้งาน เปิด msgid "%s has been removed." msgstr "ลบ %s แล้ว" -msgid "Switching application language" -msgstr "การเปลี่ยนภาษาของแอปพลิเคชัน" msgid "Select the language" msgstr "เลือกภาษา" @@ -8758,8 +8756,6 @@ msgstr "ต้องการดำเนินการต่อหรือไ msgid "Language selection" msgstr "การเลือกภาษา" -msgid "Switching application language while some presets are modified." -msgstr "การสลับภาษาของแอปพลิเคชันในขณะที่มีการแก้ไขค่าที่ตั้งไว้บางส่วน" msgid "Asia-Pacific" msgstr "เอเชียแปซิฟิก" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 02d54aa39f..0cf9d57412 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -2480,8 +2480,6 @@ msgstr "Kullanılabilir bir güncelleme var. Güncellemek için ön ayar paketi msgid "%s has been removed." msgstr "%s kaldırıldı." -msgid "Switching application language" -msgstr "Uygulama dilini değiştirme" msgid "Select the language" msgstr "Dili seçin" @@ -8860,8 +8858,6 @@ msgstr "Devam etmek istiyor musun?" msgid "Language selection" msgstr "Dil seçimi" -msgid "Switching application language while some presets are modified." -msgstr "Bazı ön ayarlar değiştirilirken uygulama dilinin değiştirilmesi." msgid "Asia-Pacific" msgstr "Asya Pasifik" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index d72a1f9792..ec9e97bae0 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -2424,8 +2424,6 @@ msgstr "Доступне оновлення. Відкрийте вікно на msgid "%s has been removed." msgstr "%s вилучено." -msgid "Switching application language" -msgstr "Зміна мови програми" msgid "Select the language" msgstr "Вибрати мову" @@ -8874,8 +8872,6 @@ msgstr "Ви хочете продовжувати?" msgid "Language selection" msgstr "Вибір мови" -msgid "Switching application language while some presets are modified." -msgstr "Зміна мови програми при зміні деяких профілів." msgid "Asia-Pacific" msgstr "Азіатсько-Тихоокеанський регіон" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index a0758d51ad..a80f47cfc1 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -2568,8 +2568,6 @@ msgstr "Có bản cập nhật khả dụng. Hãy mở hộp thoại gói cài msgid "%s has been removed." msgstr "%s đã bị xóa." -msgid "Switching application language" -msgstr "Đang chuyển ngôn ngữ ứng dụng" msgid "Select the language" msgstr "Chọn ngôn ngữ" @@ -9309,8 +9307,6 @@ msgstr "Bạn có muốn tiếp tục?" msgid "Language selection" msgstr "Chọn ngôn ngữ" -msgid "Switching application language while some presets are modified." -msgstr "Đang chuyển đổi ngôn ngữ ứng dụng trong khi một số preset đã được chỉnh sửa." msgid "Asia-Pacific" msgstr "Châu Á-Thái Bình Dương" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 7c474031c9..faa3419ae5 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -2361,8 +2361,6 @@ msgstr "有更新可用。打开预设包对话框进行更新。" msgid "%s has been removed." msgstr "%s 已被移除。" -msgid "Switching application language" -msgstr "切换应用程序语言" msgid "Select the language" msgstr "选择语言" @@ -8587,8 +8585,6 @@ msgstr "是否继续?" msgid "Language selection" msgstr "语言选择" -msgid "Switching application language while some presets are modified." -msgstr "在切换应用语言之前发现某些参数预设有更改。" msgid "Asia-Pacific" msgstr "亚太" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 83b22ca029..8f17cfdc5d 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -2425,8 +2425,6 @@ msgstr "有可用的更新。請開啟預設組合對話框進行更新。" msgid "%s has been removed." msgstr "%s 已移除。" -msgid "Switching application language" -msgstr "切換應用程式語言" msgid "Select the language" msgstr "選擇語言" @@ -8753,8 +8751,6 @@ msgstr "是否繼續?" msgid "Language selection" msgstr "語言選擇" -msgid "Switching application language while some presets are modified." -msgstr "在切換應用程式語言之前發現某些參數預設有更改。" msgid "Asia-Pacific" msgstr "亞太" diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 6d08f052da..df2d1fccc0 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -7769,21 +7769,6 @@ void GUI_App::stop_http_server() m_http_server.stop(); } -void GUI_App::switch_staff_pick(bool on) -{ - mainframe->m_webview->SendDesignStaffpick(on); -} - -bool GUI_App::switch_language() -{ - if (select_language()) { - recreate_GUI(_L("Switching application language") + dots); - return true; - } else { - return false; - } -} - #ifdef __linux__ static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, const wxLanguageInfo* system_language) @@ -7878,72 +7863,6 @@ int GUI_App::GetSingleChoiceIndex(const wxString& message, #endif } -// select language from the list of installed languages -bool GUI_App::select_language() -{ - wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); - std::vector language_infos; - language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - for (size_t i = 0; i < translations.GetCount(); ++ i) { - const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); - if (langinfo != nullptr) - language_infos.emplace_back(langinfo); - } - sort_remove_duplicates(language_infos); - std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); - - wxArrayString names; - names.Alloc(language_infos.size()); - - // Some valid language should be selected since the application start up. - const wxString active_language_code = current_language_code(); - const wxLanguageInfo* active_language_info = wxLocale::FindLanguageInfo(active_language_code); - const wxLanguage current_language = active_language_info != nullptr ? wxLanguage(active_language_info->Language) : wxLanguage(m_wxLocale->GetLanguage()); - const wxString active_lang_prefix = active_language_code.BeforeFirst('_'); - int init_selection = -1; - int init_selection_alt = -1; - int init_selection_default = -1; - for (size_t i = 0; i < language_infos.size(); ++ i) { - if (wxLanguage(language_infos[i]->Language) == current_language) - // The dictionary matches the active language and country. - init_selection = i; - else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == active_lang_prefix) || - // if the active language is Slovak, mark the Czech language as active. - (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && active_lang_prefix == "sk")) - // The dictionary matches the active language, it does not necessarily match the country. - init_selection_alt = i; - if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") - // This will be the default selection if the active language does not match any dictionary. - init_selection_default = i; - names.Add(language_infos[i]->Description); - } - if (init_selection == -1) - // This is the dictionary matching the active language. - init_selection = init_selection_alt; - if (init_selection != -1) - // This is the language to highlight in the choice dialog initially. - init_selection_default = init_selection; - - const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); - // Try to load a new language. - if (index != -1 && (init_selection == -1 || init_selection != index)) { - const wxLanguageInfo *new_language_info = language_infos[index]; - if (this->load_language(new_language_info->CanonicalName, false)) { - // Save language at application config. - // Which language to save as the selected dictionary language? - // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its - // stability in the future: - // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - // 2) Current locale language may not match the dictionary name, see GH issue #3901 - // m_wxLocale->GetCanonicalName() - // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. - app_config->set("language", new_language_info->CanonicalName.ToUTF8().data()); - return true; - } - } - - return false; -} // Load gettext translation files and activate them at the start of the application, // based on the "language" key stored in the application config. @@ -8330,146 +8249,6 @@ void GUI_App::show_ip_address_enter_dialog_handler(wxCommandEvent& evt) show_modal_ip_address_enter_dialog(mode == -1?false:true, title); } -//void GUI_App::add_config_menu(wxMenuBar *menu) -//void GUI_App::add_config_menu(wxMenu *menu) -//{ -// auto local_menu = new wxMenu(); -// wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); -// -// const auto config_wizard_name = _(ConfigWizard::name(true)); -// const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Open %s"))) % config_wizard_name).str()); -// // Cmd+, is standard on OS X - what about other operating systems? -// if (is_editor()) { -// local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); -// local_menu->Append(config_id_base + ConfigMenuUpdate, _L("Check for Configuration Updates"), _L("Check for configuration updates")); -// local_menu->AppendSeparator(); -// } -// local_menu->Append(config_id_base + ConfigMenuPreferences, _L("Preferences") + dots + -//#ifdef __APPLE__ -// "\tCtrl+,", -//#else -// "\tCtrl+P", -//#endif -// _L("Application preferences")); -// wxMenu* mode_menu = nullptr; -// if (is_editor()) { -// local_menu->AppendSeparator(); -// mode_menu = new wxMenu(); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced Mode")); -// Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); -// Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); -// -// local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s Mode"), SLIC3R_APP_NAME)); -// } -// local_menu->AppendSeparator(); -// local_menu->Append(config_id_base + ConfigMenuLanguage, _L("Language")); -// if (is_editor()) { -// local_menu->AppendSeparator(); -// } -// -// local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { -// switch (event.GetId() - config_id_base) { -// case ConfigMenuWizard: -// run_wizard(ConfigWizard::RR_USER); -// break; -// case ConfigMenuUpdate: -// check_updates(true); -// break; -//#ifdef __linux__ -// case ConfigMenuDesktopIntegration: -// show_desktop_integration_dialog(); -// break; -//#endif -// case ConfigMenuSnapshots: -// //BBS do not support task snapshot -// break; -// case ConfigMenuPreferences: -// { -// //BBS GUI refactor: remove unuse layout logic -// //bool app_layout_changed = false; -// { -// // the dialog needs to be destroyed before the call to recreate_GUI() -// // or sometimes the application crashes into wxDialogBase() destructor -// // so we put it into an inner scope -// PreferencesDialog dlg(mainframe); -// dlg.ShowModal(); -// //BBS GUI refactor: remove unuse layout logic -// //app_layout_changed = dlg.settings_layout_changed(); -// if (dlg.seq_top_layer_only_changed()) -// this->plater_->refresh_print(); -// -// if (dlg.recreate_GUI()) { -// recreate_GUI(_L("Restart application") + dots); -// return; -// } -//#ifdef _WIN32 -// if (is_editor()) { -// if (app_config->get("associate_3mf") == "true") -// associate_3mf_files(); -// if (app_config->get("associate_stl") == "true") -// associate_stl_files(); -// } -// else { -// if (app_config->get("associate_gcode") == "true") -// associate_gcode_files(); -// } -//#endif // _WIN32 -// } -// //BBS GUI refactor: remove unuse layout logic -// /*if (app_layout_changed) { -// // hide full main_sizer for mainFrame -// mainframe->GetSizer()->Show(false); -// mainframe->update_layout(); -// mainframe->select_tab(size_t(0)); -// }*/ -// break; -// } -// case ConfigMenuLanguage: -// { -// /* Before change application language, let's check unsaved changes on 3D-Scene -// * and draw user's attention to the application restarting after a language change -// */ -// { -// // the dialog needs to be destroyed before the call to switch_language() -// // or sometimes the application crashes into wxDialogBase() destructor -// // so we put it into an inner scope -// wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); -// title += " - " + _L("Choose language"); -// //wxMessageDialog dialog(nullptr, -// MessageDialog dialog(nullptr, -// _L("Switching the language requires application restart.\n") + "\n\n" + -// _L("Do you want to continue?"), -// title, -// wxICON_QUESTION | wxOK | wxCANCEL); -// if (dialog.ShowModal() == wxID_CANCEL) -// return; -// } -// -// switch_language(); -// break; -// } -// case ConfigMenuFlashFirmware: -// //BBS FirmwareDialog::run(mainframe); -// break; -// default: -// break; -// } -// }); -// -// using std::placeholders::_1; -// -// if (mode_menu != nullptr) { -// auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; -// mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); -// mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); -// } -// -// // BBS -// //menu->Append(local_menu, _L("Configuration")); -// menu->AppendSubMenu(local_menu, _L("Configuration")); -//} - void GUI_App::open_presetbundledialog(size_t open_on_tab, const std::string& highlight_option) { bool app_layout_changed = false; diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 8bf32df64c..2569e10271 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -569,7 +569,6 @@ public: void start_http_server(const std::string& provider = ORCA_CLOUD_PROVIDER); void start_http_server(int port, const std::string& provider = ORCA_CLOUD_PROVIDER); void stop_http_server(); - void switch_staff_pick(bool on); void on_show_check_privacy_dlg(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER); void show_check_privacy_dlg(wxCommandEvent& evt); @@ -583,7 +582,6 @@ public: void persist_window_geometry(wxTopLevelWindow *window, bool default_maximized = false); void update_ui_from_settings(); - bool switch_language(); bool load_language(wxString language, bool initial); Tab* get_tab(Preset::Type type); @@ -801,7 +799,6 @@ private: bool window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized = false); void window_pos_sanitize(wxTopLevelWindow* window); void window_pos_center(wxTopLevelWindow *window); - bool select_language(); // Dynamic printer agent selection - internal helpers for switch_printer_agent // and the plugin load/unload callbacks (init_plugin_gui_wiring). diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index c734804c22..5f36323d6e 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -3275,98 +3275,9 @@ void MainFrame::init_menubar_as_editor() auto preference_item = new wxMenuItem(parent_menu, ConfigMenuPreferences + config_id_base, _L("Preferences") + "\t" + ctrl + "P", ""); #endif - //auto printer_item = new wxMenuItem(parent_menu, ConfigMenuPrinter + config_id_base, _L("Printer"), ""); - //auto language_item = new wxMenuItem(parent_menu, ConfigMenuLanguage + config_id_base, _L("Switch Language"), ""); -// parent_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent& event) { -// switch (event.GetId() - config_id_base) { -// //case ConfigMenuLanguage: -// //{ -// // /* Before change application language, let's check unsaved changes on 3D-Scene -// // * and draw user's attention to the application restarting after a language change -// // */ -// // { -// // // the dialog needs to be destroyed before the call to switch_language() -// // // or sometimes the application crashes into wxDialogBase() destructor -// // // so we put it into an inner scope -// // wxString title = _L("Language selection"); -// // wxMessageDialog dialog(nullptr, -// // _L("Switching the language requires application restart.\n") + "\n\n" + -// // _L("Do you want to continue?"), -// // title, -// // wxICON_QUESTION | wxOK | wxCANCEL); -// // if (dialog.ShowModal() == wxID_CANCEL) -// // return; -// // } -// -// // wxGetApp().switch_language(); -// // break; -// //} -// //case ConfigMenuWizard: -// //{ -// // wxGetApp().run_wizard(ConfigWizard::RR_USER); -// // break; -// //} -// case ConfigMenuPrinter: -// { -// wxGetApp().params_dialog()->Popup(); -// wxGetApp().get_tab(Preset::TYPE_PRINTER)->restore_last_select_item(); -// break; -// } -// case ConfigMenuPreferences: -// { -// CallAfter([this] { -// PreferencesDialog dlg(this); -// dlg.ShowModal(); -//#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER -// if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -//#else -// if (dlg.seq_top_layer_only_changed()) -//#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER -// plater()->refresh_print(); -//#if ENABLE_CUSTOMIZABLE_FILES_ASSOCIATION_ON_WIN -//#ifdef _WIN32 -// /* -// if (wxGetApp().app_config()->get("associate_3mf") == "true") -// wxGetApp().associate_3mf_files(); -// if (wxGetApp().app_config()->get("associate_stl") == "true") -// wxGetApp().associate_stl_files(); -// /*if (wxGetApp().app_config()->get("associate_step") == "true") -// wxGetApp().associate_step_files();*/ -//#endif // _WIN32 -//#endif -// }); -// break; -// } -// default: -// break; -// } -// }); #ifdef __APPLE__ wxString about_title = wxString::Format(_L("&About %s"), SLIC3R_APP_FULL_NAME); - //auto about_item = new wxMenuItem(parent_menu, OrcaSlicerMenuAbout + bambu_studio_id_base, about_title, ""); - //parent_menu->Bind(wxEVT_MENU, [this, bambu_studio_id_base](wxEvent& event) { - // switch (event.GetId() - bambu_studio_id_base) { - // case OrcaSlicerMenuAbout: - // Slic3r::GUI::about(); - // break; - // case OrcaSlicerMenuPreferences: - // CallAfter([this] { - // PreferencesDialog dlg(this); - // dlg.ShowModal(); - //#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - // if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) - //#else - // if (dlg.seq_top_layer_only_changed()) - //#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - // plater()->refresh_print(); - // }); - // break; - // default: - // break; - // } - //}); - //parent_menu->Insert(0, about_item); append_menu_item( parent_menu, wxID_ANY, _L(about_title), "", [](wxCommandEvent &) { Slic3r::GUI::about();}, diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 08483f3100..7d80147efa 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -507,26 +507,14 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS } } - - // the dialog needs to be destroyed before the call to switch_language() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - MessageDialog msg_wingow(nullptr, _L("Switching languages requires the application to restart.\n") + "\n" + _L("Do you want to continue?"), - L("Language selection"), wxICON_QUESTION | wxOK | wxCANCEL); - if (msg_wingow.ShowModal() == wxID_CANCEL) { + MessageDialog msg_window(nullptr, _L("Switching languages requires the application to restart.\n") + "\n" + _L("Do you want to continue?"), + _L("Language selection"), wxICON_QUESTION | wxOK | wxCANCEL); + if (msg_window.ShowModal() == wxID_CANCEL) { combobox->SetSelection(m_current_language_selected); return; } } - auto check = [](bool yes_or_no) { - // if (yes_or_no) - // return true; - int act_btns = ActionButtons::SAVE; - return wxGetApp().check_and_keep_current_preset_changes(_L("Switching application language"), - _L("Switching application language while some presets are modified."), act_btns); - }; - m_current_language_selected = combobox->GetSelection(); if (m_current_language_selected >= 0 && m_current_language_selected < vlist.size()) { m_pending_language = vlist[m_current_language_selected]->CanonicalName.ToUTF8().data(); @@ -1031,11 +1019,6 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too app_config->set_bool(param, checkbox->GetValue()); app_config->save(); - // if (param == "staff_pick_switch") { - // bool pbool = app_config->get("staff_pick_switch") == "true"; - // wxGetApp().switch_staff_pick(pbool); - // } - if (param == "sync_user_preset") { bool sync = app_config->get("sync_user_preset") == "true" ? true : false; if (sync) { diff --git a/src/slic3r/GUI/Widgets/FanControl.cpp b/src/slic3r/GUI/Widgets/FanControl.cpp index f10553814e..7efdfabf07 100644 --- a/src/slic3r/GUI/Widgets/FanControl.cpp +++ b/src/slic3r/GUI/Widgets/FanControl.cpp @@ -995,7 +995,7 @@ void FanControlPopupNew::init_names(MachineObject* obj) { radio_btn_name[AIR_DUCT::AIR_DUCT_HEATING_INTERNAL_FILT] = _L("Heating"); radio_btn_name[AIR_DUCT::AIR_DUCT_EXHAUST] = _L("Exhaust"); radio_btn_name[AIR_DUCT::AIR_DUCT_FULL_COOLING] = _L("Full Cooling"); - radio_btn_name[AIR_DUCT::AIR_DUCT_INIT] = L("Init"); + radio_btn_name[AIR_DUCT::AIR_DUCT_INIT] = _L("Init"); air_door_func_name[AIR_DOOR::AIR_DOOR_FUNC_CHAMBER] = _L("Chamber"); air_door_func_name[AIR_DOOR::AIR_DOOR_FUNC_INNERLOOP] = _L("Innerloop"); From d127db4d9927021ade3bf9c122fd55aa6c01dac7 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 10 Sep 2026 16:15:39 -0500 Subject: [PATCH 13/57] build: clear 5 warning categories across 19 sites (#15628) --- src/libslic3r/Emboss.cpp | 10 +++++----- src/libslic3r/Fill/FillAdaptive.cpp | 4 ++-- src/libslic3r/GCode/ToolOrderUtils.cpp | 2 +- src/libslic3r/Line.cpp | 4 ++-- src/libslic3r/PrintObject.cpp | 4 +++- src/libslic3r/SLAPrint.cpp | 4 +++- src/slic3r/GUI/DeviceCore/DevMapping.cpp | 4 +++- src/slic3r/GUI/MeshUtils.cpp | 4 ++-- src/slic3r/GUI/Printer/PrinterFileSystem.cpp | 2 +- src/slic3r/Utils/BBLNetworkPlugin.cpp | 2 +- src/slic3r/Utils/BBLPrinterAgent.cpp | 21 +++++++++++++++----- src/slic3r/Utils/PresetUpdater.cpp | 2 +- 12 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index ef144b48d3..34d9a93590 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -968,10 +968,10 @@ EmbossStyles Emboss::get_font_list_by_register() { } // TODO: Fix global function -bool CALLBACK EnumFamCallBack(LPLOGFONT lplf, - LPNEWTEXTMETRIC lpntm, - DWORD FontType, - LPVOID aFontList) +int CALLBACK EnumFamCallBack(const LOGFONT *lplf, + const TEXTMETRIC *lpntm, + DWORD FontType, + LPARAM aFontList) { std::vector *fontList = (std::vector *) (aFontList); @@ -988,7 +988,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() { HDC hDC = GetDC(NULL); std::vector font_names; - EnumFontFamilies(hDC, (LPCTSTR) NULL, (FONTENUMPROC) EnumFamCallBack, + EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack, (LPARAM) &font_names); EmbossStyles font_list; diff --git a/src/libslic3r/Fill/FillAdaptive.cpp b/src/libslic3r/Fill/FillAdaptive.cpp index 344bb529f0..dbaa1f2ac9 100644 --- a/src/libslic3r/Fill/FillAdaptive.cpp +++ b/src/libslic3r/Fill/FillAdaptive.cpp @@ -1395,8 +1395,8 @@ void Filler::_fill_surface_single( } #endif /* ADAPTIVE_CUBIC_INFILL_DEBUG_OUTPUT */ - const auto hook_length = coordf_t(std::min(std::numeric_limits::max(), scale_(params.anchor_length))); - const auto hook_length_max = coordf_t(std::min(std::numeric_limits::max(), scale_(params.anchor_length_max))); + const auto hook_length = coordf_t(scale_(params.anchor_length)); + const auto hook_length_max = coordf_t(scale_(params.anchor_length_max)); Polylines all_polylines_with_hooks = all_polylines.size() > 1 ? connect_lines_using_hooks(std::move(all_polylines), expolygon, this->spacing, hook_length, hook_length_max) : std::move(all_polylines); diff --git a/src/libslic3r/GCode/ToolOrderUtils.cpp b/src/libslic3r/GCode/ToolOrderUtils.cpp index 4e2934d967..4a67477d24 100644 --- a/src/libslic3r/GCode/ToolOrderUtils.cpp +++ b/src/libslic3r/GCode/ToolOrderUtils.cpp @@ -910,7 +910,7 @@ namespace Slic3r unsigned int iterations = (1 << all_extruders.size()); unsigned int final_state = iterations - 1; - std::vector>cache(iterations, std::vector(all_extruders.size(), 0x7fffffff)); + std::vector>cache(iterations, std::vector(all_extruders.size(), std::numeric_limits::max())); std::vector>prev(iterations, std::vector(all_extruders.size(), -1)); cache[1][0] = 0.; for (unsigned int state = 0; state < iterations; ++state) { diff --git a/src/libslic3r/Line.cpp b/src/libslic3r/Line.cpp index c74df3aa59..94453e18f7 100644 --- a/src/libslic3r/Line.cpp +++ b/src/libslic3r/Line.cpp @@ -30,8 +30,8 @@ bool Line::intersection_infinite(const Line &other, Point* point) const return false; double t1 = cross2(v12, v2) / denom; Vec2d result = (a1 + t1 * v1); - if (result.x() > std::numeric_limits::max() || result.x() < std::numeric_limits::lowest() || - result.y() > std::numeric_limits::max() || result.y() < std::numeric_limits::lowest()) { + if (result.x() > double(std::numeric_limits::max()) || result.x() < double(std::numeric_limits::lowest()) || + result.y() > double(std::numeric_limits::max()) || result.y() < double(std::numeric_limits::lowest())) { // Intersection has at least one of the coordinates much bigger (or smaller) than coord_t maximum value (or minimum). // So it can not be stored into the Point without integer overflows. That could mean that input lines are parallel or near parallel. return false; diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index e147356ea6..720a2cdade 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1635,7 +1635,9 @@ bool PrintObject::invalidate_step(PrintObjectStep step) bool PrintObject::invalidate_all_steps() { // First call the "invalidate" functions, which may cancel background processing. - bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps(); + const bool inherited_invalidated = Inherited::invalidate_all_steps(); + const bool print_invalidated = m_print->invalidate_all_steps(); + bool result = inherited_invalidated || print_invalidated; // Then reset some of the depending values. m_slicing_params.valid = false; return result; diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index cdefd3e10e..eb37ca578c 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -1007,7 +1007,9 @@ bool SLAPrintObject::invalidate_step(SLAPrintObjectStep step) bool SLAPrintObject::invalidate_all_steps() { - return Inherited::invalidate_all_steps() | m_print->invalidate_all_steps(); + const bool inherited_invalidated = Inherited::invalidate_all_steps(); + const bool print_invalidated = m_print->invalidate_all_steps(); + return inherited_invalidated || print_invalidated; } double SLAPrintObject::get_elevation() const { diff --git a/src/slic3r/GUI/DeviceCore/DevMapping.cpp b/src/slic3r/GUI/DeviceCore/DevMapping.cpp index 165492c9f6..0040bb05f2 100644 --- a/src/slic3r/GUI/DeviceCore/DevMapping.cpp +++ b/src/slic3r/GUI/DeviceCore/DevMapping.cpp @@ -1,3 +1,5 @@ +#include + #include #include "DevMapping.h" #include "DevFilaSystem.h" @@ -270,7 +272,7 @@ namespace Slic3r std::set picked_tar; for (int k = 0; k < distance_map.size(); k++) { - float min_val = INT_MAX; + float min_val = std::numeric_limits::max(); int picked_src_idx = -1; int picked_tar_idx = -1; for (int i = 0; i < distance_map.size(); i++) diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index bc6c60a360..173c5d2f13 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -297,7 +297,7 @@ void MeshClipper::recalculate_triangles() // it so it lies on our line. This will be the figure to subtract // from the cut. The coordinates must not overflow after the transform, // make the rectangle a bit smaller. - const coord_t size = (std::numeric_limits::max()/2 - scale_(std::max(std::abs(e * a), std::abs(e * b)))) / 4; + const coord_t size = (double(std::numeric_limits::max()/2) - scale_(std::max(std::abs(e * a), std::abs(e * b)))) / 4; Polygons ep {Polygon({Point(-size, 0), Point(size, 0), Point(size, 2*size), Point(-size, 2*size)})}; ep.front().rotate(angle); ep.front().translate(scale_(-e * a), scale_(-e * b)); @@ -352,7 +352,7 @@ void MeshClipper::recalculate_triangles() // To prevent overflow after scaling, downscale the input if needed: double extra_scale = 1.; - coord_t limit = coord_t(std::min(std::numeric_limits::max() / (2. * std::max(1., scale_x)), std::numeric_limits::max() / (2. * std::max(1., scale_y)))); + coord_t limit = coord_t(std::min(double(std::numeric_limits::max()) / (2. * std::max(1., scale_x)), double(std::numeric_limits::max()) / (2. * std::max(1., scale_y)))); coord_t max_coord = 0; for (const Point& pt : exp.contour) max_coord = std::max(max_coord, std::max(std::abs(pt.x()), std::abs(pt.y()))); diff --git a/src/slic3r/GUI/Printer/PrinterFileSystem.cpp b/src/slic3r/GUI/Printer/PrinterFileSystem.cpp index 8ec6909c8c..9aa35e2cff 100644 --- a/src/slic3r/GUI/Printer/PrinterFileSystem.cpp +++ b/src/slic3r/GUI/Printer/PrinterFileSystem.cpp @@ -1803,7 +1803,7 @@ static void* get_function(const char* name) return function; #if defined(_MSC_VER) || defined(_WIN32) - function = GetProcAddress(module, name); + function = reinterpret_cast(GetProcAddress(module, name)); #else function = dlsym(module, name); #endif diff --git a/src/slic3r/Utils/BBLNetworkPlugin.cpp b/src/slic3r/Utils/BBLNetworkPlugin.cpp index 607e7d16d1..d795abf354 100644 --- a/src/slic3r/Utils/BBLNetworkPlugin.cpp +++ b/src/slic3r/Utils/BBLNetworkPlugin.cpp @@ -349,7 +349,7 @@ void* BBLNetworkPlugin::get_function(const char* name) return function; #if defined(_MSC_VER) || defined(_WIN32) - function = GetProcAddress(m_networking_module, name); + function = reinterpret_cast(GetProcAddress(m_networking_module, name)); #else function = dlsym(m_networking_module, name); #endif diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index 5e73edf84c..0c6225cc55 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -8,6 +8,7 @@ #include using json = nlohmann::json; +#include #include namespace Slic3r { @@ -90,6 +91,16 @@ OnMessageFn to_orca_messages(OnMessageFn fn) return [fn = std::move(fn)](std::string dev_id, std::string msg) { fn(std::move(dev_id), BBLPrinterAgent::to_orca_payload(std::move(msg))); }; } +// Retypes a plug-in entry point for an older plug-in generation. The detour through the +// generic function pointer marks the signature change as deliberate, which a direct cast +// between two signatures does not. +template +To as_abi(From fn) +{ + static_assert(std::is_function_v>, "as_abi retypes a function pointer"); + return reinterpret_cast(reinterpret_cast(fn)); +} + } // namespace std::string BBLPrinterAgent::to_orca_filament_id(const std::string& printer_filament_id) const @@ -141,7 +152,7 @@ int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int // series through the legacy form would silently drop MessageFlag sign/encrypt. switch (plugin.network_abi()) { case NetworkAbi::Legacy: { - auto legacy_func = reinterpret_cast(func); + auto legacy_func = as_abi(func); return legacy_func(agent, std::move(dev_id), std::move(json_str), qos); } case NetworkAbi::V0203: @@ -185,7 +196,7 @@ int BBLPrinterAgent::send_message_to_printer(std::string dev_id, std::string jso if (func && agent) { switch (plugin.network_abi()) { case NetworkAbi::Legacy: { - auto legacy_func = reinterpret_cast(func); + auto legacy_func = as_abi(func); return legacy_func(agent, std::move(dev_id), std::move(json_str), qos); } case NetworkAbi::V0203: @@ -275,7 +286,7 @@ int BBLPrinterAgent::bind(std::string dev_ip, std::string dev_id, std::string de switch (plugin.network_abi()) { case NetworkAbi::Legacy: case NetworkAbi::V0203: { - auto older_func = reinterpret_cast(func); + auto older_func = as_abi(func); return older_func(agent, dev_ip, dev_id, sec_link, timezone, improved, update_fn); } case NetworkAbi::Current: @@ -436,9 +447,9 @@ int dispatch_start(CurrentFn func, PrintParams& params, const CallbackFns&... ca params.ams_mapping_info = BBLPrinterAgent::from_orca_payload(std::move(params.ams_mapping_info)); switch (plugin.network_abi()) { case NetworkAbi::Legacy: - return reinterpret_cast(func)(agent, BBLNetworkPlugin::as_legacy(params), callbacks...); + return as_abi(func)(agent, BBLNetworkPlugin::as_legacy(params), callbacks...); case NetworkAbi::V0203: - return reinterpret_cast(func)(agent, BBLNetworkPlugin::as_0203(params), callbacks...); + return as_abi(func)(agent, BBLNetworkPlugin::as_0203(params), callbacks...); case NetworkAbi::Current: return func(agent, std::move(params), callbacks...); default: diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 23957f6500..032f9dbf7a 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1620,7 +1620,7 @@ void PresetUpdater::priv::check_new_vendors(const std::set& system_ Http::get(download_url_str) .timeout_connect(5) .on_progress(check_cancel) - .on_error([&vendor_id, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) { + .on_error([&vendor_id, &retry_count](std::string body, std::string error, unsigned http_status) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id << " (attempt " << retry_count << "/" << max_retries << "): " << error; }) From a49b8927088cde075c8ccfc7dcaf1bedc3b52af9 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:23:31 +0300 Subject: [PATCH 14/57] Fix bridge flow invalidation for zero-gap supports (#15626) --- src/libslic3r/PrintObject.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 720a2cdade..54378b3b16 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1511,13 +1511,9 @@ bool PrintObject::invalidate_state_by_config_options( steps.emplace_back(posPerimeters); steps.emplace_back(posSupportMaterial); } else if (opt_key == "bridge_flow" || opt_key == "internal_bridge_flow") { - if (m_config.support_top_z_distance > 0.) { - // Only invalidate due to bridging if bridging is enabled. - // If later "support_top_z_distance" is modified, the complete PrintObject is invalidated anyway. - steps.emplace_back(posPerimeters); - steps.emplace_back(posInfill); - steps.emplace_back(posSupportMaterial); - } + steps.emplace_back(posPerimeters); + steps.emplace_back(posInfill); + steps.emplace_back(posSupportMaterial); } else if ( opt_key == "wall_generator" || opt_key == "wall_transition_length" From a6cf5cc1e3aecebda1eb9330f88540b58ac53d5c Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Thu, 10 Sep 2026 18:23:30 +0800 Subject: [PATCH 15/57] Add the includes the precompiled header was supplying on macOS A build without SLIC3R_PCH had never been tried on macOS. Three files used what pchheader.hpp happened to include: LocalesUtils.cpp needs and , and the two dialogs need . The GTK port's headers and libstdc++ pull these in transitively, the Cocoa port's headers and libc++ do not. --- src/libslic3r/LocalesUtils.cpp | 2 ++ src/slic3r/GUI/AmsMappingPopup.cpp | 1 + src/slic3r/GUI/PhysicalPrinterDialog.cpp | 1 + 3 files changed, 4 insertions(+) diff --git a/src/libslic3r/LocalesUtils.cpp b/src/libslic3r/LocalesUtils.cpp index 308752cc62..e727b29b09 100644 --- a/src/libslic3r/LocalesUtils.cpp +++ b/src/libslic3r/LocalesUtils.cpp @@ -3,6 +3,8 @@ #ifdef _WIN32 #include #endif +#include +#include #include #include diff --git a/src/slic3r/GUI/AmsMappingPopup.cpp b/src/slic3r/GUI/AmsMappingPopup.cpp index 3e745b0a64..22ffaef034 100644 --- a/src/slic3r/GUI/AmsMappingPopup.cpp +++ b/src/slic3r/GUI/AmsMappingPopup.cpp @@ -11,6 +11,7 @@ #include "MainFrame.hpp" #include "format.hpp" #include "Widgets/ProgressDialog.hpp" +#include #include "Widgets/RoundedRectangle.hpp" #include "Widgets/StaticBox.hpp" diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index 989cf204e1..04317ca46b 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From 3331280b3467e06bde414c162662f92e8774108c Mon Sep 17 00:00:00 2001 From: Alexandre Folle de Menezes Date: Fri, 11 Sep 2026 05:36:59 -0300 Subject: [PATCH 16/57] Verify and improve AI pt_BR translations (#15621) --- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 150 ++++++-------------- 1 file changed, 40 insertions(+), 110 deletions(-) diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 6f7bf473c0..2a3e9a7f53 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -2128,7 +2128,7 @@ msgid "" "OrcaSlicer has attempted to recreate the configuration file.\n" "Please note, application settings will be lost, but printer profiles will not be affected." msgstr "" -"O arquivo de configuração do OrcaSlicer pode estar corrompido e não pode ser analisado.\n" +"O arquivo de configuração do OrcaSlicer pode estar corrompido e não pode ser processado.\n" "O OrcaSlicer tentou recriar o arquivo de configuração.\n" "Por favor, note que as configurações do aplicativo serão perdidas, mas os perfis de impressora não serão afetados." @@ -6675,7 +6675,7 @@ msgid "Failed to fetch model information from printer." msgstr "Falha ao obter informação do modelo da impressora." msgid "Failed to parse model information." -msgstr "Falha ao analisar a informação do modelo." +msgstr "Falha ao processar a informação do modelo." msgid "The .gcode.3mf file contains no G-code data. Please slice it with Orca Slicer and export a new .gcode.3mf file." msgstr "O arquivo .gcode.3mf não contém dados de G-code. Por favor, fatie com Orca Slicer e exporte um novo arquivo .gcode.3mf." @@ -8637,7 +8637,7 @@ msgstr "" "\n" "Deseja baixar e instalar esta versão agora?\n" "\n" -"Observação: o aplicativo pode precisar ser reiniciado após a instalação." +"Nota: o aplicativo pode precisar ser reiniciado após a instalação." msgid "Download Network Plug-in" msgstr "Baixar Plug-in de Rede" @@ -9135,7 +9135,7 @@ msgid "" "Note: When Stealth Mode is enabled, your user profiles will not be backed up to Orca Cloud." msgstr "" "Isso desativa todos os recursos da nuvem, incluindo a sincronização de perfis do Orca Cloud. Usuários que preferem trabalhar totalmente offline podem ativar esta opção.\n" -"Observação: quando o Modo Furtivo está ativado, seus perfis de usuário não serão copiados para o Orca Cloud." +"Nota: quando o Modo Furtivo está ativado, seus perfis de usuário não serão copiados para o Orca Cloud." msgid "Hide login side panel" msgstr "Ocultar painel lateral de autenticação" @@ -9775,7 +9775,6 @@ msgstr "A impressora falhou ao gerar a tabela de mapeamento automático do bico msgid "The current nozzle mapping may produce an extra %0.2f g of waste." msgstr "O mapeamento de bicos atual pode gerar um desperdício adicional de %0.2f g." -# AI Translated #, c-format, boost-format msgid "Recommended filament arrangement saves %s->" msgstr "A disposição de filamento recomendada economiza %s->" @@ -9834,7 +9833,6 @@ msgstr "Este processo determina os valores de fluxo dinâmico para melhorar a qu msgid "Internal" msgstr "Interno" -# AI Translated #, c-format, boost-format msgid "%s space less than 20MB. Timelapse may not save properly. You can turn it off or" msgstr "%s com espaço inferior a 20MB. O timelapse pode não ser salvo corretamente. Você pode desativá-lo ou" @@ -9842,15 +9840,12 @@ msgstr "%s com espaço inferior a 20MB. O timelapse pode não ser salvo corretam msgid "Clean up files" msgstr "Limpar arquivos" -# AI Translated msgid "Low internal storage. This timelapse will overwrite the oldest video files." msgstr "Armazenamento interno baixo. Este timelapse substituirá os arquivos de vídeo mais antigos." -# AI Translated msgid "Low external storage. This timelapse will overwrite the oldest video files." msgstr "Armazenamento externo baixo. Este timelapse substituirá os arquivos de vídeo mais antigos." -# AI Translated msgid "Insufficient external storage for time-lapse photography. Connect to computer to delete files, or use a larger memory card." msgstr "Armazenamento externo insuficiente para fotografia time-lapse. Conecte ao computador para excluir arquivos ou use um cartão de memória maior." @@ -9886,7 +9881,6 @@ msgstr "Atualizando informações dos hotends (%d/%d)." msgid "There are not enough available hotends currently." msgstr "Não há hotends disponíveis em quantidade suficiente no momento." -# AI Translated msgid "Please complete the hotend rack setup and try again." msgstr "Por favor, conclua a configuração do rack de hotend e tente novamente." @@ -9903,11 +9897,9 @@ msgstr "As informações reportadas sobre o hotend podem não ser confiáveis." msgid "The printer has no nozzle matching the slicing file (%s)." msgstr "A impressora não possui um bico compatível com o arquivo de fatiamento (%s)." -# AI Translated msgid "Please install a matching nozzle in the hotend rack, or set the corresponding printer preset while slicing." msgstr "Por favor, instale um bico compatível no rack de hotend, ou defina a predefinição de impressora correspondente ao fatiar." -# AI Translated msgid "The toolhead and hotend rack are full. Please remove at least one hotend before printing." msgstr "A cabeça da ferramenta e o rack de hotend estão cheios. Por favor, remova pelo menos um hotend antes de imprimir." @@ -9933,24 +9925,19 @@ msgstr "ambas extrusoras" msgid "The hardness of current material (%s) exceeds the hardness of %s(%s). Please verify the nozzle or material settings and try again." msgstr "A dureza do material atual (%s) excede a dureza de %s(%s). Verifique as configurações do bico ou do material e tente novamente." -# AI Translated msgid "Your current firmware version cannot start this print job. Please update to the latest version and try again." msgstr "Sua versão atual do firmware não pode iniciar este trabalho de impressão. Atualize para a versão mais recente e tente novamente." -# AI Translated #, c-format, boost-format msgid "The hardness of current material (%s) exceeds the hardness of %s(%s). It may cause nozzle wear, leading to material leakage and unstable flow. Please exercise caution when using it." msgstr "A dureza do material atual (%s) excede a dureza de %s(%s). Isso pode causar desgaste do bico, levando a vazamento de material e fluxo instável. Tenha cuidado ao usá-lo." -# AI Translated msgid "Some filaments may switch between extruders during printing. Manual K-value calibration cannot be applied throughout the entire print, which may affect print quality. Enabling Flow Dynamics Calibration is recommended." msgstr "Alguns filamentos podem alternar entre extrusoras durante a impressão. A calibração manual do valor K não pode ser aplicada durante toda a impressão, o que pode afetar a qualidade da impressão. Recomenda-se ativar a Calibração de Dinâmica de Fluxo." -# AI Translated msgid "There is stringing-prone filament in this file. For best print quality, we recommend switching nozzle clumping detection to Auto mode." -msgstr "Há filamento propenso a fiapos neste arquivo. Para a melhor qualidade de impressão, recomendamos alternar a detecção de aglomeração no bico para o modo Automático." +msgstr "Há filamento propenso a criar fios neste arquivo. Para a melhor qualidade de impressão, recomendamos alternar a detecção de aglomeração no bico para o modo Automático." -# AI Translated msgid "If 'Dynamic Flow Calibration' is set to Auto/On, the system will use the manual calibration value or the default value and skip the flow calibration process. You can perform a manual flow calibration for TPU filament on the 'Calibration' page." msgstr "Se a 'Calibração de Fluxo Dinâmico' estiver definida como Automático/Ativado, o sistema usará o valor de calibração manual ou o valor padrão e ignorará o processo de calibração de fluxo. Você pode realizar uma calibração de fluxo manual para filamento TPU na página 'Calibração'." @@ -10057,7 +10044,6 @@ msgstr "Desative a calibração de fluxo dinâmico para habilitar o valor de flu msgid "This printer does not support printing all plates." msgstr "Esta impressora não suporta a imprimir todas as placas." -# AI Translated #, c-format, boost-format msgid "The current firmware supports a maximum of %s materials. You can either reduce the number of materials to %s or fewer on the Preparation Page, or try updating the firmware. If you are still restricted after the update, please wait for subsequent firmware support." msgstr "O firmware atual suporta no máximo %s materiais. Você pode reduzir o número de materiais para %s ou menos na Página de Preparação, ou tentar atualizar o firmware. Se ainda estiver restrito após a atualização, aguarde o suporte de firmware subsequente." @@ -10065,11 +10051,9 @@ msgstr "O firmware atual suporta no máximo %s materiais. Você pode reduzir o n msgid "The type of external filament is unknown or does not match with the filament type in the slicing file. Please make sure you have installed the correct filament in the external spool." msgstr "O tipo de filamento externo é desconhecido ou não corresponde ao tipo de filamento no arquivo de fatiamento. Certifique-se de ter instalado o filamento correto no carretel externo." -# AI Translated msgid "TPU 90A/TPU 85A are too soft. It is recommended to perform manual flow calibration on the 'Calibration' page. If 'Dynamic Flow Calibration' is set to auto/on, the system will use the previous calibration value and skip the flow calibration process." msgstr "TPU 90A/TPU 85A são muito macios. Recomenda-se realizar a calibração de fluxo manual na página 'Calibração'. Se a 'Calibração de Fluxo Dinâmico' estiver definida como automático/ativado, o sistema usará o valor de calibração anterior e ignorará o processo de calibração de fluxo." -# AI Translated msgid "The filament in the AMS may be insufficient for this print. Please refill or replace it." msgstr "O filamento no AMS pode ser insuficiente para esta impressão. Por favor, reabasteça ou substitua-o." @@ -10146,7 +10130,7 @@ msgid "Failed to post ticket to server" msgstr "Falha ao enviar o ticket para o servidor" msgid "Failed to parse login report reason" -msgstr "Falha ao analisar o motivo do relatório de login" +msgstr "Falha ao processar o motivo do relatório de login" msgid "Receive login report timeout" msgstr "Limite de tempo excedido ao receber o relatório de login" @@ -10242,11 +10226,9 @@ msgstr "Excluir esta predefinição" msgid "Search in preset" msgstr "Pesquisar nas predefinições" -# AI Translated msgid "Synchronization of different extruder drives or nozzle volume types is not supported." msgstr "A sincronização de diferentes acionamentos de extrusora ou tipos de volume do bico não é suportada." -# AI Translated msgid "Synchronize the modification of parameters to the corresponding parameters of another extruder." msgstr "Sincroniza a modificação de parâmetros com os parâmetros correspondentes de outra extrusora." @@ -10316,7 +10298,7 @@ msgid "Are you sure you want to enable this option?" msgstr "Tem certeza de que deseja habilitar esta opção?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" -msgstr "Padrões de preenchimento são projetados para lidar com a rotação automaticamente para garantir a impressão adequada e atingir os efeitos pretendidos (Ex. Giroide, Cúbico). Girar o padrão de preenchimento esparso atual pode causar suporte insuficiente. Prossiga com cautela e verifique cuidadosamente se há possíveis problemas de impressão. Tem certeza de que deseja habilitar esta opção?" +msgstr "Padrões de preenchimento são projetados para lidar com a rotação automaticamente para garantir a impressão adequada e atingir os efeitos pretendidos (ex.: Giroide, Cúbico). Girar o padrão de preenchimento esparso atual pode causar suporte insuficiente. Prossiga com cautela e verifique cuidadosamente se há possíveis problemas de impressão. Tem certeza de que deseja habilitar esta opção?" msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funcionalidade experimental: Retrair e cortar o filamento a uma distância maior durante mudanças de filamento para minimizar a purga. Embora possa reduzir notavelmente a purga, ele também pode elevar o risco de bolhas no bico ou outras complicações de impressão." @@ -10479,7 +10461,6 @@ msgstr "G-code de mudança de tipo de extrusão" msgid "Post-processing Scripts" msgstr "Scripts de pós-processamento" -# AI Translated msgid "Slicing Pipeline Plugin" msgstr "Plugin de Pipeline de Fatiamento" @@ -10533,9 +10514,8 @@ msgstr "Temperatura da câmara de impressão" msgid "Chamber temperature" msgstr "Temperatura da câmara" -# AI Translated msgid "Target chamber temperature, and the minimal chamber temperature at which printing should start" -msgstr "Temperatura da câmara alvo, e a temperatura mínima da câmara na qual a impressão deve começar" +msgstr "Temperatura alvo da câmara, e a temperatura mínima da câmara na qual a impressão deve começar" msgid "Target" msgstr "Alvo" @@ -10773,7 +10753,6 @@ msgstr "Limites de altura da camada" msgid "Z-Hop" msgstr "Z-Hop" -# AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11081,7 +11060,6 @@ msgstr "Se ativo, este diálogo pode ser usado para transferir valores seleciona msgid "One of the presets does not exist" msgstr "Uma das predefinições não existe" -# AI Translated msgid "Compared presets has different printer technology" msgstr "As predefinições comparadas têm tecnologia de impressora diferente" @@ -11416,9 +11394,8 @@ msgstr "Entrar" msgid "Login failed. Please try again." msgstr "Falha no login. Tente novamente." -# AI Translated msgid "parse json failed" -msgstr "falha ao analisar o json" +msgstr "falha ao processar o json" msgid "[Action Required] " msgstr "[Ação Necessária] " @@ -11609,7 +11586,6 @@ msgctxt "Keyboard Shortcut" msgid "Space" msgstr "Espaço" -# AI Translated msgid "Open actions speed dial" msgstr "Abrir menu rápido de ações" @@ -11691,11 +11667,9 @@ msgstr "informações de atualização da versão %s:" msgid "Network plug-in update" msgstr "Atualização do plug-in de rede" -# AI Translated msgid "Click OK to update the Network plug-in now. If a file is in use, the update will be applied the next time Orca Slicer launches." msgstr "Clique em OK para atualizar o plug-in de Rede agora. Se um arquivo estiver em uso, a atualização será aplicada na próxima vez que o Orca Slicer for iniciado." -# AI Translated msgid "A new Network plug-in is available. Do you want to install it?" msgstr "Um novo plug-in de Rede está disponível. Deseja instalá-lo?" @@ -11755,7 +11729,6 @@ msgstr "Nome da impressora" msgid "Where to find your printer's IP and Access Code?" msgstr "Onde encontrar o IP e o Código de Acesso da sua impressora?" -# AI Translated msgid "How to trouble shooting" msgstr "Como solucionar problemas" @@ -11903,11 +11876,9 @@ msgstr "Objeto: %1%" msgid "Parts of the object at these heights may be too thin or the object may have a faulty mesh." msgstr "Partes do objeto nessas alturas podem ser muito finas, ou o objeto pode ter uma malha com falhas." -# AI Translated msgid "Process change extrusion role G-code" msgstr "G-code de mudança de tipo de extrusão do processo" -# AI Translated msgid "Filament change extrusion role G-code" msgstr "G-code de mudança de tipo de extrusão do filamento" @@ -12082,7 +12053,6 @@ msgstr " está muito perto de uma área de exclusão, e colisões vão ocorrer.\ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está muito perto da área de detecção de aglomeração, e ocorrerão colisões.\n" -# AI Translated msgid " is partially outside the printable area, and it cannot be printed.\n" msgstr " está parcialmente fora da área imprimível, e não pode ser impresso.\n" @@ -12098,7 +12068,6 @@ msgstr "Se ainda assim desejar imprimir, você pode ativar a opção em Preferê msgid "No extrusions under current settings." msgstr "Nenhuma extrusão com as configurações atuais." -# AI Translated msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." msgstr "Um filamento misto com gradiente está em uso, mas 'Subcamada de cor mista' está desativado. O gradiente não será impresso." @@ -12138,7 +12107,6 @@ msgstr "Você pode querer reduzir o tamanho do seu modelo ou alterar as configur msgid "Variable layer height is not supported with Organic supports." msgstr "A altura de camada variável não é suportada com suportes Orgânicos." -# AI Translated msgid "The wipe tower filament cannot be a mixed filament." msgstr "O filamento da torre de purga não pode ser um filamento misto." @@ -12775,7 +12743,6 @@ msgstr "" msgid "Internal bridge flow ratio" msgstr "Taxa de fluxo em ponte interna" -# AI Translated msgid "" "This value governs the thickness of the internal bridge layer. This is the first layer over sparse infill so increasing it may increase strength and upper layer quality.\n" "Values above 1.0: Increase the amount of material while maintaining line spacing. This can improve line contact and strength.\n" @@ -13165,11 +13132,9 @@ msgstr "" "A geometria será decimada antes de detectar ângulos agudos. Este parâmetro indica o comprimento mínimo da divergência para a decimação.\n" "0 para desativar." -# AI Translated msgid "Brim ears outer only" -msgstr "Orelhas da borda apenas externas" +msgstr "Apenas orelhas da borda externas" -# AI Translated msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." msgstr "Gera orelhas de rato apenas no contorno externo do modelo, excluindo furos e seções fechadas." @@ -13197,7 +13162,6 @@ msgstr "Por objeto" msgid "Intra-layer order" msgstr "Ordem intra-camada" -# AI Translated msgid "" "Order in which object instances are visited within a single layer, which controls how much travel is spent moving between them.\n" "\n" @@ -13255,7 +13219,7 @@ msgid "mm/s² or %" msgstr "mm/s² ou %" msgid "Acceleration of bridges. If the value is expressed as a percentage (e.g. 50%), it will be calculated based on the outer wall acceleration." -msgstr "Aceleração das pontes. Se o valor for expresso como uma porcentagem (por exemplo, 50%), será calculado com base na aceleração da parede externa." +msgstr "Aceleração das pontes. Se o valor for expresso como uma porcentagem (ex.: 50%), será calculado com base na aceleração da parede externa." msgid "Default filament profile" msgstr "Perfil de filamento padrão" @@ -13835,7 +13799,6 @@ msgstr "Tempo da camada" msgid "The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time." msgstr "A ventoinha de resfriamento de peças será ativado para camadas cujo tempo estimado seja mais curto que esse valor. A velocidade da ventoinha é interpolada entre as velocidades mínima e máxima da ventoinha de acordo com o tempo de impressão da camada." -# AI Translated msgctxt "second" msgid "s" msgstr "s" @@ -14142,61 +14105,47 @@ msgstr "Material de suporte" msgid "Support material is commonly used to print supports and support interfaces." msgstr "O material de suporte é comumente usado para imprimir suportes e interfaces de suporte." -# AI Translated msgid "Is mixed filament" msgstr "É filamento misto" -# AI Translated msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" msgstr "Define se este slot de filamento é um filamento misto composto por vários filamentos físicos" -# AI Translated msgid "Mixed filament components" msgstr "Componentes do filamento misto" -# AI Translated msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" msgstr "Índices (começando em 1) dos filamentos componentes, separados por vírgulas; ex.: \"1,3\"" -# AI Translated msgid "Mixed filament sublayer ratios" msgstr "Proporções de subcamada do filamento misto" -# AI Translated msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" msgstr "Valores de proporção separados por vírgulas cuja soma seja 1.0; ex.: \"0.7,0.3\"" -# AI Translated msgid "Mixed filament gradient" msgstr "Gradiente do filamento misto" -# AI Translated msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." -msgstr "Ativa o modo de gradiente na direção Z para as subcamadas do filamento misto. Quando ativado, as proporções das subcamadas variam linearmente ao longo das camadas." +msgstr "Ativa o modo de gradiente na direção Z para as subcamadas do filamento misto. Quando ativo, as proporções das subcamadas variam linearmente ao longo das camadas." -# AI Translated msgid "Mixed filament gradient range" msgstr "Faixa do gradiente do filamento misto" -# AI Translated msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." msgstr "Proporções inicial e final do primeiro componente no modo de gradiente. Par separado por vírgula; ex.: \"0.10,0.90\" significa de 10% a 90%." -# AI Translated msgid "Mixed filament gradient curve" msgstr "Curva do gradiente do filamento misto" -# AI Translated msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." -msgstr "Curva personalizada opcional, no estilo do Photoshop, que mapeia o progresso em Z para a proporção do primeiro componente. Codificada como pontos de controle separados por barras verticais, no formato \"x,y\" (legado) ou \"x,y,m_in,m_out\" quando é necessário substituir a tangente (um valor vazio ou \"nan\" usa o padrão PCHIP). x está em [0,1]; y é limitado à faixa de proporção configurada; ex.: \"0,0.15|0.5,0.50|1,0.85\". Quando vazio, o gradient_range linear é usado." +msgstr "Curva personalizada opcional no estilo do Photoshop, mapeando o progresso em Z para a proporção do primeiro componente. Codificada como pontos de controle separados por barras verticais, no formato \"x,y\" (legado) ou \"x,y,m_in,m_out\" quando é necessário substituir a tangente (um valor vazio ou \"nan\" usa o padrão PCHIP). X em [0,1]; Y é limitado à faixa de proporção configurada; ex.: \"0,0.15|0.5,0.50|1,0.85\". Quando vazio, o gradient_range linear é usado no lugar." -# AI Translated msgid "Mixed filament per-part gradient" msgstr "Gradiente por peça do filamento misto" -# AI Translated msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." -msgstr "Quando o modo de gradiente está ativado, aplica o gradiente a cada peça de uma montagem de forma independente, em vez de tratar toda a montagem como uma única faixa Z." +msgstr "Quando o modo de gradiente está ativado, aplica o gradiente a cada peça de uma montagem de forma independente em vez de tratar toda a montagem como uma única faixa Z." msgid "Filament printable" msgstr "Filamento imprimível" @@ -14287,7 +14236,7 @@ msgid "Insert solid layers" msgstr "Inserir camadas sólidas" msgid "Insert solid infill at specific layers. Use N to insert every Nth layer, N#K to insert K consecutive solid layers every N layers (K is optional, e.g. '5#' equals '5#1'), or a comma-separated list (e.g. 1,7,9) to insert at explicit layers. Layers are 1-based." -msgstr "Insere preenchimento sólido em camadas específicas. Use N para inserir a cada enésima camada, N#K para inserir K camadas sólidas consecutivas a cada enésima camada (K é opcional, ou seja, '5#' é igual a '5#1'), ou uma lista separada por vírgulas (Ex. 1,7,9) para inserir em camadas esplícitas. Camadas são baseadas em 1." +msgstr "Insere preenchimento sólido em camadas específicas. Use N para inserir a cada enésima camada, N#K para inserir K camadas sólidas consecutivas a cada enésima camada (K é opcional, ex.: '5#' é igual a '5#1'), ou uma lista separada por vírgulas (Ex. 1,7,9) para inserir em camadas esplícitas. Camadas são baseadas em 1." msgid "Fill Multiline" msgstr "Multilinhas de Preenchimento" @@ -14365,11 +14314,9 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" -# AI Translated msgid "Sparse infill smooth factor" msgstr "Fator de suavização do preenchimento esparso" -# AI Translated msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, while 100% produces the largest possible curves between adjacent infill lines." msgstr "Controla o quanto os cantos do preenchimento esparso são arredondados. 0% mantém o trajeto original com cantos vivos, enquanto 100% produz as maiores curvas possíveis entre linhas de preenchimento adjacentes." @@ -14383,10 +14330,10 @@ msgid "Acceleration of inner walls." msgstr "Aceleração das paredes internas." msgid "Acceleration of sparse infill. If the value is expressed as a percentage (e.g. 100%), it will be calculated based on the default acceleration." -msgstr "Aceleração do preenchimento esparso. Se o valor for expresso como uma porcentagem (por exemplo, 100%), será calculado com base na aceleração padrão." +msgstr "Aceleração do preenchimento esparso. Se o valor for expresso como uma porcentagem (ex.: 100%), será calculado com base na aceleração padrão." msgid "Acceleration of internal solid infill. If the value is expressed as a percentage (e.g. 100%), it will be calculated based on the default acceleration." -msgstr "Aceleração do preenchimento sólido interno. Se o valor for expresso como uma porcentagem (por exemplo, 100%), será calculado com base na aceleração padrão." +msgstr "Aceleração do preenchimento sólido interno. Se o valor for expresso como uma porcentagem (ex.: 100%), será calculado com base na aceleração padrão." msgid "This is the printing acceleration for the first layer. Using limited acceleration can improve build plate adhesion." msgstr "Esta é a aceleração para a primeira camada. Usar aceleração limitada melhorar a adesão à placa de impressão." @@ -14490,7 +14437,7 @@ msgid "" "Only available when \"No cooling for the first\" is 0.\n" "Set to -1 to disable it." msgstr "" -"Define uma velocidade exata da ventoinha para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (por exemplo, dutos ABS/ASA no estilo Voron) de uma mesa aquecida. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" +"Define uma velocidade exata da ventoinha para a primeira camada, substituindo todas as outras configurações de resfriamento. Útil para proteger peças impressas em 3D da cabeça da ferramenta (ex.: dutos ABS/ASA no estilo Voron) de uma mesa aquecida. Uma pequena quantidade de fluxo de ar resfria os dutos, sem usar o resfriamento total que pode, em certas condições, prejudicar a aderência da primeira camada.\n" "A partir da segunda camada, o resfriamento normal é retomado.\n" "Se \"Velocidade total da ventoinha na camada\" também estiver definida, a ventoinha aumenta suavemente deste valor na primeira camada até o seu alvo na camada escolhida.\n" "Disponível apenas quando \"Sem resfriamento nas primeiras\" é 0.\n" @@ -14861,7 +14808,7 @@ msgid "" "Set to 0 to deactivate." msgstr "" "Algumas ventoinhas de resfriamento de componentes não conseguem iniciar a rotação quando comandadas abaixo de um determinado ciclo de trabalho PWM. Quando definido acima de 0, qualquer comando de ventoinha de resfriamento de componentes diferente de zero será elevado para pelo menos essa porcentagem, para que a ventoinha inicie de forma confiável. Um comando de ventoinha de 0 (ventoinha desligada) é sempre atendido exatamente. Essa limitação é aplicada após cada outro cálculo da ventoinha (rampa da primeira camada, interpolação do tempo da camada, substituições de saliência/ponte/interface de suporte/alisamento), para que o dimensionamento ainda opere dentro do intervalo [este valor, 100%].\n" -"Se o seu firmware já desativa a ventoinha abaixo de um limite (por exemplo, [fan] off_below: 0.10 do Klipper desliga a ventoinha sempre que o ciclo de trabalho comandado for inferior a 10%), esta opção e o limite do firmware devem idealmente ser definidos com o mesmo valor. A correspondência entre eles (por exemplo, off_below: 0.10 no Klipper e 10% aqui) garante que o fatiador nunca emita um valor diferente de zero que o firmware emitiria a velocidade cai silenciosamente e a ventoinha nunca recebe um valor abaixo daquele que você sabe que ela pode realmente atingir.\n" +"Se o seu firmware já desativa a ventoinha abaixo de um limite (por exemplo, [fan] off_below: 0.10 do Klipper desliga a ventoinha sempre que o ciclo de trabalho comandado for inferior a 10%), esta opção e o limite do firmware devem idealmente ser definidos com o mesmo valor. A correspondência entre eles (ex.: off_below: 0.10 no Klipper e 10% aqui) garante que o fatiador nunca emita um valor diferente de zero que o firmware emitiria a velocidade cai silenciosamente e a ventoinha nunca recebe um valor abaixo daquele que você sabe que ela pode realmente atingir.\n" "Defina como 0 para desativar." msgid "Time cost" @@ -14908,13 +14855,11 @@ msgstr "Com que tipo de G-code a impressora é compatível." msgid "Klipper" msgstr "Klipper" -# AI Translated msgid "Skip G-code config block" msgstr "Omitir o bloco de configuração do G-code" -# AI Translated msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." -msgstr "Não grava o CONFIG_BLOCK (os pares chave/valor da configuração do fatiador) no arquivo G-code. Isso pode ajudar com impressoras cujo firmware trava ao interpretar essas linhas de comentário (por exemplo, Anycubic go-klipper). Observação: o arquivo G-code não conterá mais as configurações do fatiador, então importá-lo de volta no OrcaSlicer não restaurará a configuração." +msgstr "Não grava o CONFIG_BLOCK (os pares chave/valor da configuração do fatiador) no arquivo G-code. Isso pode ajudar com impressoras cujo firmware trava ao interpretar essas linhas de comentário (ex.: Anycubic go-klipper). Nota: o arquivo G-code não conterá mais as configurações do fatiador, então importá-lo de volta no OrcaSlicer não restaurará a configuração." msgid "Pellet Modded Printer" msgstr "Impressora Modificada para Pellets" @@ -14962,13 +14907,13 @@ msgid "Sparse infill rotation template" msgstr "Gabarito de rotação de preenchimento esparso" msgid "Rotate the sparse infill direction per layer using a template of angles. Enter comma-separated degrees (e.g., '0,30,60,90'). Angles are applied in order by layer and repeat when the list ends. Advanced syntax is supported: '+5' rotates +5° every layer; '+5#5' rotates +5° every 5 layers. See the Wiki for details. When a template is set, the standard infill direction setting is ignored. Note: some infill patterns (e.g., Gyroid) control rotation themselves; use with care." -msgstr "Gira a direção do preenchimento esparso por camada usando um gabarito de ângulos. Insira graus separados por vírgula (por exemplo, '0, 30, 60, 90'). Os ângulos são aplicados em ordem por camada e repetidos quando a lista termina. Sintaxe avançada suportada: '+5' gira +5° a cada camada; '+5#5' gira +5° a cada 5 camadas. Consulte a Wiki para obter detalhes. Quando um modelo é definido, a configuração padrão de direção do preenchimento é ignorada. Observação: alguns padrões de preenchimento (por exemplo, Giróide) tem seu próprio controle de rotação, use com cuidado." +msgstr "Gira a direção do preenchimento esparso por camada usando um gabarito de ângulos. Insira graus separados por vírgula (ex.: '0,30,60,90'). Os ângulos são aplicados em ordem por camada e repetidos quando a lista termina. Sintaxe avançada suportada: '+5' gira +5° a cada camada; '+5#5' gira +5° a cada 5 camadas. Consulte a Wiki para obter detalhes. Quando um modelo é definido, a configuração padrão de direção do preenchimento é ignorada. Nota: alguns padrões de preenchimento (ex.: Giróide) tem seu próprio controle de rotação, use com cuidado." msgid "Solid infill rotation template" msgstr "Gabarito de rotação de preenchimento sólido" msgid "This parameter adds a rotation of solid infill direction to each layer according to the specified template. The template is a comma-separated list of angles in degrees, e.g. '0,90'. The first angle is applied to the first layer, the second angle to the second layer, and so on. If there are more layers than angles, the angles will be repeated. Note that not all solid infill patterns support rotation." -msgstr "Este parâmetro adiciona uma rotação da direção do preenchimento sólido a cada camada, de acordo com o gabarito especificado. O gabarito é uma lista de ângulos em graus separados por vírgulas, por exemplo, '0,90'. O primeiro ângulo é aplicado à primeira camada, o segundo ângulo à segunda camada e assim por diante. Se houver mais camadas do que ângulos, os ângulos serão repetidos. Observe que nem todos os padrões de preenchimento sólido suportam rotação." +msgstr "Este parâmetro adiciona uma rotação da direção do preenchimento sólido a cada camada, de acordo com o gabarito especificado. O gabarito é uma lista de ângulos em graus separados por vírgulas, como '0,90'. O primeiro ângulo é aplicado à primeira camada, o segundo ângulo à segunda camada e assim por diante. Se houver mais camadas do que ângulos, os ângulos serão repetidos. Observe que nem todos os padrões de preenchimento sólido suportam rotação." msgid "Skeleton infill density" msgstr "Densidade de preenchimento de esqueleto" @@ -15434,7 +15379,6 @@ msgstr "Força máxima do eixo Y" msgid "The allowed maximum output force of Y axis" msgstr "A força máxima de saída permitida do eixo Y" -# AI Translated msgctxt "Newton" msgid "N" msgstr "N" @@ -15445,7 +15389,6 @@ msgstr "Massa da mesa do eixo Y" msgid "The machine bed mass load of Y axis" msgstr "A carga de massa da mesa do equipamento no eixo Y" -# AI Translated msgctxt "gram" msgid "g" msgstr "g" @@ -15970,7 +15913,7 @@ msgid "Z-hop height" msgstr "Altura de Z-hop" msgid "Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing." -msgstr "Sempre que há uma retração, o bico é levantado um pouco para criar folga entre o bico e a impressão. Isso evita que o bico atinja a impressão ao se mover. Usar linhas em espiral para levantar Z pode evitar stringing." +msgstr "Sempre que há uma retração, o bico é levantado um pouco para criar folga entre o bico e a impressão. Isso evita que o bico atinja a impressão ao se mover. Usar linhas em espiral para levantar Z pode evitar criação de fios." msgid "Z-hop lower boundary" msgstr "Limite inferior do Z-hop" @@ -16051,7 +15994,7 @@ msgid "Has filament switcher" msgstr "Tem trocador de filamentos" msgid "Printer has a filament switcher hardware (e.g., AMS)." -msgstr "A impressora tem um sistema de troca de filamentos (Ex.: AMS)." +msgstr "A impressora tem um sistema de troca de filamentos (ex.: AMS)." msgid "Extra length on restart" msgstr "Comprimento extra na retração" @@ -16168,7 +16111,7 @@ msgid "Scarf joint speed" msgstr "Velocidade da costura em bisel" msgid "This option sets the printing speed for scarf joints. It is recommended to print scarf joints at a slow speed (less than 100 mm/s). It's also advisable to enable 'Extrusion rate smoothing' if the set speed varies significantly from the speed of the outer or inner walls. If the speed specified here is higher than the speed of the outer or inner walls, the printer will default to the slower of the two speeds. When specified as a percentage (e.g., 80%), the speed is calculated based on the respective outer or inner wall speed. The default value is set to 100%." -msgstr "Esta opção define a velocidade de impressão para as costuras em bisel. É recomendável imprimir as costuras em bisel em uma velocidade baixa (menor que 100 mm/s). Também é aconselhável habilitar 'Suavização da taxa de extrusão' se a velocidade definida variar significativamente da velocidade das paredes externas ou internas. Se a velocidade especificada aqui for maior que a velocidade das paredes externas ou internas, a impressora utilizará a mais lenta das duas velocidades. Quando especificado como uma porcentagem (por exemplo, 80%), a velocidade é calculada com base na velocidade do perímetro externo ou interna respectiva. O valor padrão é definido como 100%." +msgstr "Esta opção define a velocidade de impressão para as costuras em bisel. É recomendável imprimir as costuras em bisel em uma velocidade baixa (menor que 100 mm/s). Também é aconselhável habilitar 'Suavização da taxa de extrusão' se a velocidade definida variar significativamente da velocidade das paredes externas ou internas. Se a velocidade especificada aqui for maior que a velocidade das paredes externas ou internas, a impressora utilizará a mais lenta das duas velocidades. Quando especificado como uma porcentagem (ex.: 80%), a velocidade é calculada com base na velocidade do perímetro externo ou interna respectiva. O valor padrão é definido como 100%." msgid "Scarf joint flow ratio" msgstr "Taxa de fluxo da costura em bisel" @@ -16238,7 +16181,7 @@ msgid "Wipe speed" msgstr "Velocidade de limpeza" msgid "The wipe speed is determined by the speed setting specified in this configuration. If the value is expressed as a percentage (e.g. 80%), it will be calculated based on the travel speed setting above. The default value for this parameter is 80%." -msgstr "A velocidade de limpeza é determinada pela velocidade especificada nesta configuração. Se o valor for expresso como uma porcentagem (por exemplo, 80%), será calculado com base na configuração de velocidade de deslocamento acima. O valor padrão para este parâmetro é 80%." +msgstr "A velocidade de limpeza é determinada pela velocidade especificada nesta configuração. Se o valor for expresso como uma porcentagem (ex.: 80%), será calculado com base na configuração de velocidade de deslocamento acima. O valor padrão para este parâmetro é 80%." msgid "Skirt distance" msgstr "Distância da saia" @@ -16276,7 +16219,7 @@ msgstr "" "Um escudo de ar é útil para proteger uma impressão ABS ou ASA de deformações e desprendimento da mesa de impressão devido à corrente de ar. Geralmente, ele é necessário apenas com impressoras de estrutura aberta, ou seja, sem um gabinete.\n" "\n" "Habilitado = a saia é tão alta quanto o objeto impresso mais alto. Caso contrário, 'Altura da saia' é usada.\n" -"Observação: com o escudo de ar ativo, a saia será impressa na distância da saia do objeto. Portanto, se as bordas estiverem ativas, ela pode se cruzar com elas. Para evitar isso, aumente o valor da distância da saia.\n" +"Nota: com o escudo de ar ativo, a saia será impressa na distância da saia do objeto. Portanto, se as bordas estiverem ativas, ela pode se cruzar com elas. Para evitar isso, aumente o valor da distância da saia.\n" msgid "Enabled" msgstr "Ativado" @@ -16417,10 +16360,10 @@ msgid "Preheat steps" msgstr "Passos de pré-aquecimento" msgid "Insert multiple preheat commands (e.g. M104.1). Only useful for Prusa XL. For other printers, please set it to 1." -msgstr "Insire múltiplos comandos de pré-aquecimento (por exemplo, M104.1). Útil apenas para Prusa XL. Para outras impressoras, defina como 1." +msgstr "Insire múltiplos comandos de pré-aquecimento (ex.: M104.1). Útil apenas para Prusa XL. Para outras impressoras, defina como 1." msgid "G-code written at the very top of the output file, before any other content. Useful for adding metadata that printer firmware reads from the first lines of the file (e.g. estimated print time, filament usage). Supports placeholders like {print_time_sec} and {used_filament_length}." -msgstr "Código G escrito no início do arquivo de saída, antes de qualquer outro conteúdo. Útil para adicionar metadados que o firmware da impressora lê das primeiras linhas do arquivo (por exemplo, tempo estimado de impressão, consumo de filamento). Suporta marcadores como {print_time_sec} e {used_filament_length}." +msgstr "Código G escrito no início do arquivo de saída, antes de qualquer outro conteúdo. Útil para adicionar metadados que o firmware da impressora lê das primeiras linhas do arquivo (ex.: tempo estimado de impressão, consumo de filamento). Suporta marcadores como {print_time_sec} e {used_filament_length}." msgid "Start G-code" msgstr "G-code Inicial" @@ -16441,7 +16384,7 @@ msgid "Manual Filament Change" msgstr "Troca Manual de Filamento" msgid "Enable this option to omit the custom Change filament G-code only at the beginning of the print. The tool change command (e.g., T0) will be skipped throughout the entire print. This is useful for manual multi-material printing, where we use M600/PAUSE to trigger the manual filament change action." -msgstr "Ative esta opção para omitir o G-code de troca de filamento personalizado apenas no início da impressão. O comando de troca de ferramenta (por exemplo, T0) será ignorado durante toda a impressão. Isso é útil para impressão manual de vários materiais, onde usamos M600/PAUSE para acionar a ação de troca manual de filamento." +msgstr "Ative esta opção para omitir o G-code de troca de filamento personalizado apenas no início da impressão. O comando de troca de ferramenta (ex.: T0) será ignorado durante toda a impressão. Isso é útil para impressão manual de vários materiais, onde usamos M600/PAUSE para acionar a ação de troca manual de filamento." msgid "Wipe tower type" msgstr "Tipo de torre de purga" @@ -16997,11 +16940,9 @@ msgstr "" "\n" "Definir um valor na configuração de quantidade de retração antes da limpeza abaixo executará qualquer retração em excesso antes da limpeza, caso contrário, será realizada após." -# AI Translated msgid "Mixed color sublayer" msgstr "Subcamada de cor mista" -# AI Translated msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." msgstr "Ativa a divisão em subcamadas de cor mista. Quando ativado, as camadas que contêm filamentos de cor mista são divididas em subcamadas para obter efeitos de mistura de cores." @@ -18061,9 +18002,8 @@ msgstr "A geração da malha do arquivo do modelo falhou ou não há forma váli msgid "The supplied file couldn't be read because it's empty." msgstr "O arquivo fornecido não pôde ser lido porque está vazio." -# AI Translated msgid "The file format is incompatible and cannot be parsed." -msgstr "O formato do arquivo é incompatível e não pode ser lido." +msgstr "O formato do arquivo é incompatível e não pode ser processado." msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato de arquivo desconhecido: o arquivo de entrada deve ter extensão .stl, .obj, .amf(.xml)." @@ -18072,10 +18012,10 @@ msgid "Unknown file format: input file must have .3mf or .zip.amf extension." msgstr "Formato de arquivo desconhecido: o arquivo de entrada deve ter extensão .3mf ou .zip.amf." msgid "load_obj: failed to parse" -msgstr "load_obj: falha ao analisar" +msgstr "load_obj: falha ao processar" msgid "load mtl in obj: failed to parse" -msgstr "carregar mtl em obj: falha ao analisar" +msgstr "carregar mtl em obj: falha ao processsar" msgid "The file contains polygons with more than 4 vertices." msgstr "O arquivo contém polígonos com mais de 4 vértices." @@ -19189,7 +19129,7 @@ msgid "Serial" msgstr "Série" msgid "e.g. Basic, Matte, Silk, Marble" -msgstr "por exemplo, Básico, Fosco, Seda, Mármore" +msgstr "Ex.: Básico, Fosco, Seda, Mármore" msgid "Filament Preset" msgstr "Predefinição de Filamento" @@ -19870,7 +19810,7 @@ msgid "Error. Can't get API token for authorization" msgstr "Erro. ​​Não foi possível obter o token de API para autorização" msgid "Could not parse server response." -msgstr "Não foi possível decifrar a resposta do servidor." +msgstr "Não foi possível processar a resposta do servidor." msgid "Error saving session to file" msgstr "Erro salvando sessão para arquivo" @@ -19938,7 +19878,7 @@ msgstr "O host respondeu, mas não parece ser o Moonraker (falta o result.klippy #, c-format, boost-format msgid "Could not parse Moonraker server response: %s" -msgstr "Não foi possível analisar a resposta do servidor Moonraker: %s" +msgstr "Não foi possível processar a resposta do servidor Moonraker: %s" msgid "Connection to OctoPrint is working correctly." msgstr "A conexão com o OctoPrint funciona corretamente." @@ -20312,7 +20252,6 @@ msgstr "Removido" msgid "Enable smart filament assign: Assign one filament to multiple nozzles to maximize savings" msgstr "Ativar atribuição inteligente de filamento: Atribui um filamento a vários bicos para maximizar a economia" -# AI Translated msgid "File Saving" msgstr "Salvamento de Arquivo" @@ -20499,7 +20438,7 @@ msgid "Connection timed out. Please check if the printer and computer network ar msgstr "Limite de tempo de conexão esgotado. Verifique se a impressora e a rede do computador estão funcionando corretamente e confirme se estão na mesma rede." msgid "The Hostname/IP/URL could not be parsed, please check it and try again." -msgstr "Não foi possível decifrar o Hostname/IP/URL; verifique-o e tente novamente." +msgstr "Não foi possível processar o Hostname/IP/URL; verifique-o e tente novamente." msgid "File/data transfer interrupted. Please check the printer and network, then try it again." msgstr "Transferência de arquivo/dados interrompida. Verifique a impressora e a rede e tente novamente." @@ -21282,15 +21221,6 @@ msgstr "" #~ msgid "Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)" #~ msgstr "Seu sistema não possui codecs H.264 para o GStreamer, que são necessários para reproduzir vídeos. (Tente instalar os pacotes gstreamer1.0-plugins-bad ou gstreamer1.0-libav e depois reinicie o OrcaSlicer?)" -#~ msgid "N" -#~ msgstr "N" - -#~ msgid "g" -#~ msgstr "g" - -#~ msgid "Fila Saving" -#~ msgstr "Econo Filamento" - #~ msgid "" #~ "Layer height is too small.\n" #~ "It will set to min_layer_height\n" @@ -21618,7 +21548,7 @@ msgstr "" #~ "Pontes externas de menor densidade podem ajudar a melhorar a confiabilidade, pois há mais espaço para o ar circular ao redor da ponte extrudada, melhorando sua velocidade de resfriamento. O mínimo é 10%.\n" #~ "\n" #~ "Densidades mais altas podem produzir superfícies de ponte mais lisas, pois as linhas sobrepostas fornecem suporte adicional durante a impressão. O máximo é 120%.\n" -#~ "Observação: Densidade de ponte muito alta pode causar deformação ou sobrextrusão." +#~ "Nota: Densidade de ponte muito alta pode causar deformação ou sobrextrusão." #~ msgid "" #~ "Controls the density (spacing) of internal bridge lines. 100% means solid bridge. Default is 100%.\n" @@ -21663,7 +21593,7 @@ msgstr "" #~ "\n" #~ "Geralmente, é recomendável ter esta opção ativada, a menos que o resfriamento da impressora seja potente o suficiente ou a velocidade de impressão lenta o suficiente para que a curvatura do perímetro não aconteça. Se estiver imprimindo com uma alta velocidade de perímetro externo, este parâmetro pode introduzir pequenos artefatos ao desacelerar devido à grande variação nas velocidades de impressão. Se você notar artefatos, certifique-se de que seu pressure advance esteja ajustado corretamente.\n" #~ "\n" -#~ "Observação: quando esta opção estiver habilitada, os perímetros de saliência são tratados como saliências, o que significa que a velocidade de saliência é aplicada mesmo se o perímetro de saliência for parte de uma ponte. Por exemplo, quando os perímetros estiverem 100% salientes, sem nenhuma parede apoiando-os por baixo, a velocidade de saliência de 100% será aplicada." +#~ "Nota: quando esta opção estiver habilitada, os perímetros de saliência são tratados como saliências, o que significa que a velocidade de saliência é aplicada mesmo se o perímetro de saliência for parte de uma ponte. Por exemplo, quando os perímetros estiverem 100% salientes, sem nenhuma parede apoiando-os por baixo, a velocidade de saliência de 100% será aplicada." #~ msgid "If enabled, bridges are more reliable, can bridge longer distances, but may look worse. If disabled, bridges look better but are reliable just for shorter bridged distances." #~ msgstr "Se ativado, as pontes são mais confiáveis, podem cobrir distâncias maiores, mas podem parecer piores. Se desativado, as pontes ficam melhores, mas são confiáveis apenas para distâncias de ponte mais curtas." @@ -22583,7 +22513,7 @@ msgstr "" #~ msgstr "Contagem máxima de projetos recentes" #~ msgid "This parameter adds a rotation of sparse infill direction to each layer according to the specified template. The template is a comma-separated list of angles in degrees, e.g. '0,90'. The first angle is applied to the first layer, the second angle to the second layer, and so on. If there are more layers than angles, the angles will be repeated. Note that not all sparse infill patterns support rotation." -#~ msgstr "Este parâmetro adiciona uma rotação na direção do preenchimento esparso a cada camada, de acordo com o gabarito especificado. O gabarito é uma lista de ângulos em graus separados por vírgulas, por exemplo, '0,90'. O primeiro ângulo é aplicado à primeira camada, o segundo ângulo à segunda camada e assim por diante. Se houver mais camadas do que ângulos, os ângulos serão repetidos. Observe que nem todos os padrões de preenchimento esparso suportam rotação." +#~ msgstr "Este parâmetro adiciona uma rotação na direção do preenchimento esparso a cada camada, de acordo com o gabarito especificado. O gabarito é uma lista de ângulos em graus separados por vírgulas, como '0,90'. O primeiro ângulo é aplicado à primeira camada, o segundo ângulo à segunda camada e assim por diante. Se houver mais camadas do que ângulos, os ângulos serão repetidos. Observe que nem todos os padrões de preenchimento esparso suportam rotação." #~ msgid "Set Position" #~ msgstr "Definir Posição" From 4ad3d11c7a27e70b5f7303ea3f2af6d835ae4434 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Fri, 11 Sep 2026 03:52:37 -0500 Subject: [PATCH 17/57] Fix Qidi X-Plus 5 chamber heating profiles (#15556) * Fix Qidi X-Plus 5 chamber heating profiles * Restore Qidi ABS Odorless chamber temperature --------- Co-authored-by: yw4z --- .../profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json | 3 +++ .../profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json | 6 ++++++ .../profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json | 6 ++++++ .../profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json | 6 ++++++ .../profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json | 4 ++-- .../Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json | 3 +++ 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json index accf79987f..79a52578a4 100644 --- a/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json +++ b/resources/profiles/Qidi/filament/X5/Generic ABS @X-Plus 5.json @@ -8,6 +8,9 @@ "box_temperature_range_high": [ "45" ], + "chamber_temperature": [ + "55" + ], "close_fan_the_first_x_layers": [ "3" ], diff --git a/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json index 6abc420237..d4af23e160 100644 --- a/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json +++ b/resources/profiles/Qidi/filament/X5/QIDI PA12-CF @X-Plus 5.json @@ -14,9 +14,15 @@ "box_temperature": [ "55" ], + "chamber_temperature": [ + "55" + ], "close_fan_the_first_x_layers": [ "3" ], + "during_print_exhaust_fan_speed": [ + "0" + ], "cool_plate_temp_initial_layer": [ "60" ], diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json index ca5591813d..fec939caa8 100644 --- a/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-CF @X-Plus 5.json @@ -14,9 +14,15 @@ "box_temperature": [ "60" ], + "chamber_temperature": [ + "55" + ], "close_fan_the_first_x_layers": [ "3" ], + "during_print_exhaust_fan_speed": [ + "0" + ], "cool_plate_temp_initial_layer": [ "80" ], diff --git a/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json index 411ae26214..de72deb697 100644 --- a/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json +++ b/resources/profiles/Qidi/filament/X5/QIDI PAHT-GF @X-Plus 5.json @@ -14,9 +14,15 @@ "box_temperature": [ "60" ], + "chamber_temperature": [ + "55" + ], "close_fan_the_first_x_layers": [ "3" ], + "during_print_exhaust_fan_speed": [ + "0" + ], "cool_plate_temp_initial_layer": [ "80" ], diff --git a/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json index 615d895851..f973121e62 100644 --- a/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json +++ b/resources/profiles/Qidi/filament/X5/QIDI PPS-GF @X-Plus 5.json @@ -14,8 +14,8 @@ "box_temperature": [ "65" ], - "chamber_temperatures": [ - "0" + "chamber_temperature": [ + "55" ], "close_fan_the_first_x_layers": [ "3" diff --git a/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json index 2ea27efa4b..82c261fbd5 100644 --- a/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json +++ b/resources/profiles/Qidi/filament/X5/QIDI UltraPA-CF25 @X-Plus 5.json @@ -14,6 +14,9 @@ "box_temperature": [ "60" ], + "chamber_temperature": [ + "55" + ], "close_fan_the_first_x_layers": [ "3" ], From 613dbcb21be6d2a91a3f92cbed12d4fe155bbbc0 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:03:14 -0500 Subject: [PATCH 18/57] Add Flashforge Creator 5 and Creator 5 Pro 0.25 mm nozzle profiles (#15282) * Add Creator 5 0.25 mm nozzle profiles * Fix Creator 5 process profile load order * Bump Flashforge profile version --------- Co-authored-by: yw4z --- resources/profiles/Flashforge.json | 26 +- .../Flashforge Creator 5 0.25 nozzle.json | 311 ++++++++++++++++++ .../Flashforge Creator 5 Pro 0.25 nozzle.json | 311 ++++++++++++++++++ .../machine/Flashforge Creator 5 Pro.json | 2 +- .../machine/Flashforge Creator 5.json | 2 +- .../0.08mm Standard @FF C5 0.25 nozzle.json | 28 ++ .../0.10mm Standard @FF C5 0.25 nozzle.json | 27 ++ .../0.12mm Standard @FF C5 0.25 nozzle.json | 28 ++ .../0.14mm Standard @FF C5 0.25 nozzle.json | 28 ++ 9 files changed, 760 insertions(+), 3 deletions(-) create mode 100644 resources/profiles/Flashforge/machine/Flashforge Creator 5 0.25 nozzle.json create mode 100644 resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro 0.25 nozzle.json create mode 100644 resources/profiles/Flashforge/process/0.08mm Standard @FF C5 0.25 nozzle.json create mode 100644 resources/profiles/Flashforge/process/0.10mm Standard @FF C5 0.25 nozzle.json create mode 100644 resources/profiles/Flashforge/process/0.12mm Standard @FF C5 0.25 nozzle.json create mode 100644 resources/profiles/Flashforge/process/0.14mm Standard @FF C5 0.25 nozzle.json diff --git a/resources/profiles/Flashforge.json b/resources/profiles/Flashforge.json index f3923c435a..f348a4f329 100644 --- a/resources/profiles/Flashforge.json +++ b/resources/profiles/Flashforge.json @@ -1,7 +1,7 @@ { "name": "Flashforge", "url": "", - "version": "02.04.00.05", + "version": "02.04.00.06", "force_update": "0", "description": "Flashforge configurations", "machine_model_list": [ @@ -439,6 +439,22 @@ "name": "0.14mm Standard @FF AD5X 0.25 nozzle", "sub_path": "process/0.14mm Standard @FF AD5X 0.25 nozzle.json" }, + { + "name": "0.08mm Standard @FF C5 0.25 nozzle", + "sub_path": "process/0.08mm Standard @FF C5 0.25 nozzle.json" + }, + { + "name": "0.10mm Standard @FF C5 0.25 nozzle", + "sub_path": "process/0.10mm Standard @FF C5 0.25 nozzle.json" + }, + { + "name": "0.12mm Standard @FF C5 0.25 nozzle", + "sub_path": "process/0.12mm Standard @FF C5 0.25 nozzle.json" + }, + { + "name": "0.14mm Standard @FF C5 0.25 nozzle", + "sub_path": "process/0.14mm Standard @FF C5 0.25 nozzle.json" + }, { "name": "0.14mm Standard @Flashforge AD5M 0.25 Nozzle", "sub_path": "process/0.14mm Standard @Flashforge AD5M 0.25 Nozzle.json" @@ -2903,6 +2919,10 @@ "name": "Flashforge AD5X 0.4 nozzle", "sub_path": "machine/Flashforge AD5X 0.4 nozzle.json" }, + { + "name": "Flashforge Creator 5 0.25 nozzle", + "sub_path": "machine/Flashforge Creator 5 0.25 nozzle.json" + }, { "name": "Flashforge Creator 5 0.4 nozzle", "sub_path": "machine/Flashforge Creator 5 0.4 nozzle.json" @@ -2915,6 +2935,10 @@ "name": "Flashforge Creator 5 0.8 nozzle", "sub_path": "machine/Flashforge Creator 5 0.8 nozzle.json" }, + { + "name": "Flashforge Creator 5 Pro 0.25 nozzle", + "sub_path": "machine/Flashforge Creator 5 Pro 0.25 nozzle.json" + }, { "name": "Flashforge Creator 5 Pro 0.4 nozzle", "sub_path": "machine/Flashforge Creator 5 Pro 0.4 nozzle.json" diff --git a/resources/profiles/Flashforge/machine/Flashforge Creator 5 0.25 nozzle.json b/resources/profiles/Flashforge/machine/Flashforge Creator 5 0.25 nozzle.json new file mode 100644 index 0000000000..54be573cf7 --- /dev/null +++ b/resources/profiles/Flashforge/machine/Flashforge Creator 5 0.25 nozzle.json @@ -0,0 +1,311 @@ +{ + "type": "machine", + "name": "Flashforge Creator 5 0.25 nozzle", + "inherits": "Flashforge Adventurer 5M Pro 0.4 Nozzle", + "from": "system", + "setting_id": "xp3cpTEGYdWjcrMM", + "instantiation": "true", + "adaptive_bed_mesh_margin": "0", + "auxiliary_fan": "1", + "bbl_use_printhost": "0", + "bed_custom_model": "", + "bed_custom_texture": "", + "bed_exclude_area": [ + "0x0" + ], + "bed_mesh_max": "99999,99999", + "bed_mesh_min": "-99999,-99999", + "bed_mesh_probe_distance": "50,50", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]", + "best_object_pos": "0.5,0.5", + "change_extrusion_role_gcode": "", + "change_filament_gcode": "", + "cooling_tube_length": "0", + "cooling_tube_retraction": "0", + "default_bed_type": "", + "default_filament_profile": [ + "Flashforge Generic PLA" + ], + "default_print_profile": "0.12mm Standard @FF C5 0.25 nozzle", + "deretraction_speed": [ + "30", + "30", + "30", + "30" + ], + "disable_m73": "1", + "emit_machine_limits_to_gcode": "1", + "enable_filament_ramming": "0", + "enable_long_retraction_when_cut": "0", + "extra_loading_move": "0", + "extruder_clearance_height_to_lid": "360", + "extruder_clearance_height_to_rod": "60", + "extruder_clearance_radius": "92", + "extruder_colour": [ + "#FCE94F", + "#FCE94F", + "#FCE94F", + "#FCE94F" + ], + "extruder_offset": [ + "0x0", + "0x0", + "0x0", + "0x0" + ], + "fan_kickstart": "0", + "fan_speedup_overhangs": "1", + "fan_speedup_time": "0", + "gcode_flavor": "klipper", + "head_wrap_detect_zone": [], + "high_current_on_filament_swap": "0", + "host_type": "octoprint", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "long_retractions_when_cut": [ + "0", + "0", + "0", + "0" + ], + "machine_end_gcode": ";end_gcode\nG1 X150 Y150 E-1.2 F12000", + "machine_load_filament_time": "0", + "machine_max_acceleration_e": [ + "5000", + "5000" + ], + "machine_max_acceleration_extruding": [ + "30000", + "20000" + ], + "machine_max_acceleration_retracting": [ + "5000", + "5000" + ], + "machine_max_acceleration_travel": [ + "20000", + "20000" + ], + "machine_max_acceleration_x": [ + "30000", + "20000" + ], + "machine_max_acceleration_y": [ + "30000", + "20000" + ], + "machine_max_acceleration_z": [ + "300", + "500" + ], + "machine_max_jerk_e": [ + "2.5", + "2.5" + ], + "machine_max_jerk_x": [ + "9", + "9" + ], + "machine_max_jerk_y": [ + "9", + "9" + ], + "machine_max_jerk_z": [ + "3", + "3" + ], + "machine_max_junction_deviation": [ + "0", + "0" + ], + "machine_max_speed_e": [ + "30", + "30" + ], + "machine_max_speed_x": [ + "600", + "600" + ], + "machine_max_speed_y": [ + "600", + "600" + ], + "machine_max_speed_z": [ + "20", + "20" + ], + "machine_min_extruding_rate": [ + "0", + "0" + ], + "machine_min_travel_rate": [ + "0", + "0" + ], + "machine_pause_gcode": "M25", + "machine_start_gcode": ";start_gcode\nM140 S[bed_temperature_initial_layer_single]\nM106 P101 S0 ; L+R_PLA_Turbo_Fan_0-255\nM106 P2 S0 ; Center_Fresch_Air_Input_Fan_for_PLA30%_(80_0-255) \nM191 S0 ; Chamber temp. max65C\nM106 S0 ; Model_Fan_(Heat_Break_Cooler_80-255)100%=255\nM106 P3 S0 ; Filter_Unit_Fan_0-255(ABS=255)\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F2400\nT[initial_extruder]\nM109 S[nozzle_temperature_initial_layer] T[initial_extruder]\nG1 X256 Y0 Z0.2 F6000\nG1 E5 F{filament_max_volumetric_speed[initial_no_support_extruder]/2.4053*60}\nG1 X216 E10 F{filament_max_volumetric_speed[initial_no_support_extruder]/2.4053*60}\n;start_gcode end", + "machine_tool_change_time": "7", + "machine_unload_filament_time": "0", + "manual_filament_change": "0", + "max_layer_height": [ + "0.14", + "0.14", + "0.14", + "0.14" + ], + "max_resonance_avoidance_speed": "120", + "min_layer_height": [ + "0.08", + "0.08", + "0.08", + "0.08" + ], + "min_resonance_avoidance_speed": "70", + "nozzle_diameter": [ + "0.25", + "0.25", + "0.25", + "0.25" + ], + "nozzle_height": "4", + "nozzle_hrc": "0", + "nozzle_type": "hardened_steel", + "nozzle_volume": "0", + "parking_pos_retraction": "0", + "pellet_modded_printer": "0", + "preferred_orientation": "0", + "printable_area": [ + "0x0", + "256x0", + "256x256", + "0x256" + ], + "printable_height": "256", + "printer_model": "Flashforge Creator 5", + "printer_notes": "", + "printer_settings_id": "Flashforge Creator 5 0.25 nozzle", + "printer_structure": "corexy", + "printer_technology": "FFF", + "printer_variant": "0.25", + "printhost_authorization_type": "key", + "printhost_ssl_ignore_revoke": "0", + "printing_by_object_gcode": "", + "purge_in_prime_tower": "0", + "resonance_avoidance": "0", + "retract_before_wipe": [ + "100%", + "100%", + "100%", + "100%" + ], + "retract_length_toolchange": [ + "3", + "3", + "3", + "3" + ], + "retract_lift_above": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_below": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_enforce": [ + "All Surfaces", + "All Surfaces", + "All Surfaces", + "All Surfaces" + ], + "retract_restart_extra": [ + "0", + "0", + "0", + "0" + ], + "retract_restart_extra_toolchange": [ + "-0.8", + "-0.8", + "-0.8", + "-0.8" + ], + "retract_when_changing_layer": [ + "1", + "1", + "1", + "1" + ], + "retraction_distances_when_cut": [ + "18", + "18", + "18", + "18" + ], + "retraction_length": [ + "0.8", + "0.8", + "0.8", + "0.8" + ], + "retraction_minimum_travel": [ + "2", + "2", + "2", + "2" + ], + "retraction_speed": [ + "30", + "30", + "30", + "30" + ], + "scan_first_layer": "0", + "silent_mode": "0", + "single_extruder_multi_material": "0", + "support_air_filtration": "0", + "support_chamber_temp_control": "0", + "support_multi_bed_types": "1", + "template_custom_gcode": "", + "thumbnails": "140x110/PNG", + "thumbnails_format": "PNG", + "time_cost": "0", + "time_lapse_gcode": "", + "travel_slope": [ + "3", + "3", + "3", + "3" + ], + "upward_compatible_machine": [], + "use_firmware_retraction": "0", + "use_relative_e_distances": "1", + "wipe": [ + "1", + "1", + "1", + "1" + ], + "wipe_distance": [ + "2", + "2", + "2", + "2" + ], + "z_hop": [ + "0.4", + "0.4", + "0.4", + "0.4" + ], + "z_hop_types": [ + "Auto Lift", + "Auto Lift", + "Auto Lift", + "Auto Lift" + ], + "z_offset": "0" +} diff --git a/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro 0.25 nozzle.json b/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro 0.25 nozzle.json new file mode 100644 index 0000000000..3eea1c58cf --- /dev/null +++ b/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro 0.25 nozzle.json @@ -0,0 +1,311 @@ +{ + "type": "machine", + "name": "Flashforge Creator 5 Pro 0.25 nozzle", + "inherits": "Flashforge Adventurer 5M Pro 0.4 Nozzle", + "from": "system", + "setting_id": "rpmib7bIKa85LNMR", + "instantiation": "true", + "adaptive_bed_mesh_margin": "0", + "auxiliary_fan": "1", + "bbl_use_printhost": "0", + "bed_custom_model": "", + "bed_custom_texture": "", + "bed_exclude_area": [ + "0x0" + ], + "bed_mesh_max": "99999,99999", + "bed_mesh_min": "-99999,-99999", + "bed_mesh_probe_distance": "50,50", + "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]", + "best_object_pos": "0.5,0.5", + "change_extrusion_role_gcode": "", + "change_filament_gcode": "", + "cooling_tube_length": "0", + "cooling_tube_retraction": "0", + "default_bed_type": "", + "default_filament_profile": [ + "Flashforge Generic PLA" + ], + "default_print_profile": "0.12mm Standard @FF C5 0.25 nozzle", + "deretraction_speed": [ + "30", + "30", + "30", + "30" + ], + "disable_m73": "1", + "emit_machine_limits_to_gcode": "1", + "enable_filament_ramming": "0", + "enable_long_retraction_when_cut": "0", + "extra_loading_move": "0", + "extruder_clearance_height_to_lid": "360", + "extruder_clearance_height_to_rod": "60", + "extruder_clearance_radius": "92", + "extruder_colour": [ + "#FCE94F", + "#FCE94F", + "#FCE94F", + "#FCE94F" + ], + "extruder_offset": [ + "0x0", + "0x0", + "0x0", + "0x0" + ], + "fan_kickstart": "0", + "fan_speedup_overhangs": "1", + "fan_speedup_time": "0", + "gcode_flavor": "klipper", + "head_wrap_detect_zone": [], + "high_current_on_filament_swap": "0", + "host_type": "octoprint", + "layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]", + "long_retractions_when_cut": [ + "0", + "0", + "0", + "0" + ], + "machine_end_gcode": ";end_gcode\nG1 X150 Y150 E-1.2 F12000", + "machine_load_filament_time": "0", + "machine_max_acceleration_e": [ + "5000", + "5000" + ], + "machine_max_acceleration_extruding": [ + "30000", + "20000" + ], + "machine_max_acceleration_retracting": [ + "5000", + "5000" + ], + "machine_max_acceleration_travel": [ + "20000", + "20000" + ], + "machine_max_acceleration_x": [ + "30000", + "20000" + ], + "machine_max_acceleration_y": [ + "30000", + "20000" + ], + "machine_max_acceleration_z": [ + "300", + "500" + ], + "machine_max_jerk_e": [ + "2.5", + "2.5" + ], + "machine_max_jerk_x": [ + "9", + "9" + ], + "machine_max_jerk_y": [ + "9", + "9" + ], + "machine_max_jerk_z": [ + "3", + "3" + ], + "machine_max_junction_deviation": [ + "0", + "0" + ], + "machine_max_speed_e": [ + "30", + "30" + ], + "machine_max_speed_x": [ + "600", + "600" + ], + "machine_max_speed_y": [ + "600", + "600" + ], + "machine_max_speed_z": [ + "20", + "20" + ], + "machine_min_extruding_rate": [ + "0", + "0" + ], + "machine_min_travel_rate": [ + "0", + "0" + ], + "machine_pause_gcode": "M25", + "machine_start_gcode": ";start_gcode\nM140 S[bed_temperature_initial_layer_single]\nM106 P101 S0 ; L+R_PLA_Turbo_Fan_0-255\nM106 P2 S0 ; Center_Fresch_Air_Input_Fan_for_PLA30%_(80_0-255) \nM191 S0 ; Chamber temp. max65C\nM106 S0 ; Model_Fan_(Heat_Break_Cooler_80-255)100%=255\nM106 P3 S0 ; Filter_Unit_Fan_0-255(ABS=255)\nM104 S[nozzle_temperature_initial_layer]\nG90\nM83\nG1 Z5 F2400\nT[initial_extruder]\nM109 S[nozzle_temperature_initial_layer] T[initial_extruder]\nG1 X256 Y0 Z0.2 F6000\nG1 E5 F{filament_max_volumetric_speed[initial_no_support_extruder]/2.4053*60}\nG1 X216 E10 F{filament_max_volumetric_speed[initial_no_support_extruder]/2.4053*60}\n;start_gcode end", + "machine_tool_change_time": "7", + "machine_unload_filament_time": "0", + "manual_filament_change": "0", + "max_layer_height": [ + "0.14", + "0.14", + "0.14", + "0.14" + ], + "max_resonance_avoidance_speed": "120", + "min_layer_height": [ + "0.08", + "0.08", + "0.08", + "0.08" + ], + "min_resonance_avoidance_speed": "70", + "nozzle_diameter": [ + "0.25", + "0.25", + "0.25", + "0.25" + ], + "nozzle_height": "4", + "nozzle_hrc": "0", + "nozzle_type": "hardened_steel", + "nozzle_volume": "0", + "parking_pos_retraction": "0", + "pellet_modded_printer": "0", + "preferred_orientation": "0", + "printable_area": [ + "0x0", + "256x0", + "256x256", + "0x256" + ], + "printable_height": "256", + "printer_model": "Flashforge Creator 5 Pro", + "printer_notes": "", + "printer_settings_id": "Flashforge Creator 5 Pro 0.25 nozzle", + "printer_structure": "corexy", + "printer_technology": "FFF", + "printer_variant": "0.25", + "printhost_authorization_type": "key", + "printhost_ssl_ignore_revoke": "0", + "printing_by_object_gcode": "", + "purge_in_prime_tower": "0", + "resonance_avoidance": "0", + "retract_before_wipe": [ + "100%", + "100%", + "100%", + "100%" + ], + "retract_length_toolchange": [ + "3", + "3", + "3", + "3" + ], + "retract_lift_above": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_below": [ + "0", + "0", + "0", + "0" + ], + "retract_lift_enforce": [ + "All Surfaces", + "All Surfaces", + "All Surfaces", + "All Surfaces" + ], + "retract_restart_extra": [ + "0", + "0", + "0", + "0" + ], + "retract_restart_extra_toolchange": [ + "-0.8", + "-0.8", + "-0.8", + "-0.8" + ], + "retract_when_changing_layer": [ + "1", + "1", + "1", + "1" + ], + "retraction_distances_when_cut": [ + "18", + "18", + "18", + "18" + ], + "retraction_length": [ + "0.8", + "0.8", + "0.8", + "0.8" + ], + "retraction_minimum_travel": [ + "2", + "2", + "2", + "2" + ], + "retraction_speed": [ + "30", + "30", + "30", + "30" + ], + "scan_first_layer": "0", + "silent_mode": "0", + "single_extruder_multi_material": "0", + "support_air_filtration": "1", + "support_chamber_temp_control": "1", + "support_multi_bed_types": "1", + "template_custom_gcode": "", + "thumbnails": "140x110/PNG", + "thumbnails_format": "PNG", + "time_cost": "0", + "time_lapse_gcode": "", + "travel_slope": [ + "3", + "3", + "3", + "3" + ], + "upward_compatible_machine": [], + "use_firmware_retraction": "0", + "use_relative_e_distances": "1", + "wipe": [ + "1", + "1", + "1", + "1" + ], + "wipe_distance": [ + "2", + "2", + "2", + "2" + ], + "z_hop": [ + "0.4", + "0.4", + "0.4", + "0.4" + ], + "z_hop_types": [ + "Auto Lift", + "Auto Lift", + "Auto Lift", + "Auto Lift" + ], + "z_offset": "0" +} diff --git a/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro.json b/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro.json index bf8cbf3702..4ef02628d1 100644 --- a/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro.json +++ b/resources/profiles/Flashforge/machine/Flashforge Creator 5 Pro.json @@ -2,7 +2,7 @@ "type": "machine_model", "name": "Flashforge Creator 5 Pro", "model_id": "Flashforge-Creator-5-Pro", - "nozzle_diameter": "0.4;0.6;0.8", + "nozzle_diameter": "0.25;0.4;0.6;0.8", "machine_tech": "FFF", "family": "Flashforge", "bed_model": "flashforge_c5_buildplate_model.stl", diff --git a/resources/profiles/Flashforge/machine/Flashforge Creator 5.json b/resources/profiles/Flashforge/machine/Flashforge Creator 5.json index 8d5ac253b7..7ef9c172c0 100644 --- a/resources/profiles/Flashforge/machine/Flashforge Creator 5.json +++ b/resources/profiles/Flashforge/machine/Flashforge Creator 5.json @@ -2,7 +2,7 @@ "type": "machine_model", "name": "Flashforge Creator 5", "model_id": "Flashforge-Creator-5", - "nozzle_diameter": "0.4;0.6;0.8", + "nozzle_diameter": "0.25;0.4;0.6;0.8", "machine_tech": "FFF", "family": "Flashforge", "bed_model": "flashforge_c5_buildplate_model.stl", diff --git a/resources/profiles/Flashforge/process/0.08mm Standard @FF C5 0.25 nozzle.json b/resources/profiles/Flashforge/process/0.08mm Standard @FF C5 0.25 nozzle.json new file mode 100644 index 0000000000..09221f10b2 --- /dev/null +++ b/resources/profiles/Flashforge/process/0.08mm Standard @FF C5 0.25 nozzle.json @@ -0,0 +1,28 @@ +{ + "type": "process", + "name": "0.08mm Standard @FF C5 0.25 nozzle", + "inherits": "0.10mm Standard @FF AD5X 0.25 nozzle", + "from": "system", + "setting_id": "KrOSCGKyvNy9v08u", + "instantiation": "true", + "bottom_solid_infill_flow_ratio": "1", + "compatible_printers": [ + "Flashforge Creator 5 0.25 nozzle", + "Flashforge Creator 5 Pro 0.25 nozzle" + ], + "initial_layer_print_height": "0.1", + "internal_bridge_flow": "1", + "layer_height": "0.08", + "ooze_prevention": "1", + "preheat_time": "20", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "print_settings_id": "0.08mm Standard @FF C5 0.25 nozzle", + "small_perimeter_speed": "30", + "standby_temperature_delta": "-100", + "top_solid_infill_flow_ratio": "1" +} diff --git a/resources/profiles/Flashforge/process/0.10mm Standard @FF C5 0.25 nozzle.json b/resources/profiles/Flashforge/process/0.10mm Standard @FF C5 0.25 nozzle.json new file mode 100644 index 0000000000..47ea8c6322 --- /dev/null +++ b/resources/profiles/Flashforge/process/0.10mm Standard @FF C5 0.25 nozzle.json @@ -0,0 +1,27 @@ +{ + "type": "process", + "name": "0.10mm Standard @FF C5 0.25 nozzle", + "inherits": "0.10mm Standard @FF AD5X 0.25 nozzle", + "from": "system", + "setting_id": "0Mep1gVTvD4RwK53", + "instantiation": "true", + "bottom_solid_infill_flow_ratio": "1", + "compatible_printers": [ + "Flashforge Creator 5 0.25 nozzle", + "Flashforge Creator 5 Pro 0.25 nozzle" + ], + "initial_layer_print_height": "0.1", + "internal_bridge_flow": "1", + "ooze_prevention": "1", + "preheat_time": "20", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "print_settings_id": "0.10mm Standard @FF C5 0.25 nozzle", + "small_perimeter_speed": "30", + "standby_temperature_delta": "-100", + "top_solid_infill_flow_ratio": "1" +} diff --git a/resources/profiles/Flashforge/process/0.12mm Standard @FF C5 0.25 nozzle.json b/resources/profiles/Flashforge/process/0.12mm Standard @FF C5 0.25 nozzle.json new file mode 100644 index 0000000000..df33cdf344 --- /dev/null +++ b/resources/profiles/Flashforge/process/0.12mm Standard @FF C5 0.25 nozzle.json @@ -0,0 +1,28 @@ +{ + "type": "process", + "name": "0.12mm Standard @FF C5 0.25 nozzle", + "inherits": "0.10mm Standard @FF AD5X 0.25 nozzle", + "from": "system", + "setting_id": "rcKQBKZJvCfrfzqV", + "instantiation": "true", + "bottom_solid_infill_flow_ratio": "1", + "compatible_printers": [ + "Flashforge Creator 5 0.25 nozzle", + "Flashforge Creator 5 Pro 0.25 nozzle" + ], + "initial_layer_print_height": "0.1", + "internal_bridge_flow": "1", + "layer_height": "0.12", + "ooze_prevention": "1", + "preheat_time": "20", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "print_settings_id": "0.12mm Standard @FF C5 0.25 nozzle", + "small_perimeter_speed": "30", + "standby_temperature_delta": "-100", + "top_solid_infill_flow_ratio": "1" +} diff --git a/resources/profiles/Flashforge/process/0.14mm Standard @FF C5 0.25 nozzle.json b/resources/profiles/Flashforge/process/0.14mm Standard @FF C5 0.25 nozzle.json new file mode 100644 index 0000000000..fd6f8f560d --- /dev/null +++ b/resources/profiles/Flashforge/process/0.14mm Standard @FF C5 0.25 nozzle.json @@ -0,0 +1,28 @@ +{ + "type": "process", + "name": "0.14mm Standard @FF C5 0.25 nozzle", + "inherits": "0.10mm Standard @FF AD5X 0.25 nozzle", + "from": "system", + "setting_id": "749mQY3CU2jk0MOd", + "instantiation": "true", + "bottom_solid_infill_flow_ratio": "1", + "compatible_printers": [ + "Flashforge Creator 5 0.25 nozzle", + "Flashforge Creator 5 Pro 0.25 nozzle" + ], + "initial_layer_print_height": "0.1", + "internal_bridge_flow": "1", + "layer_height": "0.14", + "ooze_prevention": "1", + "preheat_time": "20", + "print_extruder_id": [ + "1" + ], + "print_extruder_variant": [ + "Direct Drive Standard" + ], + "print_settings_id": "0.14mm Standard @FF C5 0.25 nozzle", + "small_perimeter_speed": "30", + "standby_temperature_delta": "-100", + "top_solid_infill_flow_ratio": "1" +} From 0a3724ed2f106dd40b8a6b6f89c6a83a546cc81c Mon Sep 17 00:00:00 2001 From: mschfh <37435502+mschfh@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:05:17 -0500 Subject: [PATCH 19/57] =?UTF-8?q?fix(profiles):=20set=20PETG=20SuperTack?= =?UTF-8?q?=20temperatures=20to=2060=C2=B0C=20(#15189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: yw4z --- .../Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle.json | 6 ------ .../Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json | 6 ------ .../Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle.json | 6 ------ .../Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle.json | 6 ------ .../Anycubic PETG @Anycubic Kobra X 0.4 nozzle.json | 6 ------ .../Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle.json | 6 ------ .../Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle.json | 6 ------ .../Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle.json | 6 ------ .../Generic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json | 6 ------ .../filament/Generic PETG @Anycubic Kobra X 0.4 nozzle.json | 6 ------ resources/profiles/Anycubic/filament/fdm_filament_pet.json | 6 ++++++ .../Flashforge/filament/Flashforge HS PETG @FF G4 HF.json | 6 ++++++ .../Flashforge/filament/Flashforge HS PETG @FF G4P HF.json | 6 ++++++ .../Flashforge/filament/Flashforge PETG Pro @FF G4 HF.json | 6 ++++++ .../Flashforge/filament/Flashforge PETG Pro @FF G4P HF.json | 6 ++++++ .../filament/Flashforge PETG Transparent @FF G4 HF.json | 6 ++++++ .../filament/Flashforge PETG Transparent @FF G4P HF.json | 6 ++++++ .../profiles/Flashforge/filament/fdm_filament_pet.json | 6 ++++++ 18 files changed, 48 insertions(+), 60 deletions(-) diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle.json index 7fb1d1f66c..4852093cda 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.25 nozzle.json @@ -324,12 +324,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json index 64ec8774b0..16d4b54568 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json @@ -318,12 +318,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle.json index ea95c2b1c5..ebc7e8e514 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.6 nozzle.json @@ -324,12 +324,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle.json index a2cd9ecc5e..978c51007e 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra S1 Max 0.8 nozzle.json @@ -324,12 +324,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra X 0.4 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra X 0.4 nozzle.json index b0c543cebf..e2c6e84c1b 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra X 0.4 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG @Anycubic Kobra X 0.4 nozzle.json @@ -309,12 +309,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle.json index bbe7575b49..0774881a2b 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.4 nozzle.json @@ -318,12 +318,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle.json index d78fcc1d27..e3a405e192 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.6 nozzle.json @@ -324,12 +324,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle.json b/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle.json index cc3e8a15fc..e28e2d859e 100644 --- a/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle.json +++ b/resources/profiles/Anycubic/filament/Anycubic PETG-CF @Anycubic Kobra S1 Max 0.8 nozzle.json @@ -324,12 +324,6 @@ "slow_down_min_speed": [ "10" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json b/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json index e025d0587c..b44b6ecc11 100644 --- a/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json +++ b/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra S1 Max 0.4 nozzle.json @@ -318,12 +318,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra X 0.4 nozzle.json b/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra X 0.4 nozzle.json index 78925acad9..0f238e5a5b 100644 --- a/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra X 0.4 nozzle.json +++ b/resources/profiles/Anycubic/filament/Generic PETG @Anycubic Kobra X 0.4 nozzle.json @@ -309,12 +309,6 @@ "slow_down_min_speed": [ "20" ], - "supertack_plate_temp": [ - "35" - ], - "supertack_plate_temp_initial_layer": [ - "35" - ], "support_material_interface_fan_speed": [ "-1" ], diff --git a/resources/profiles/Anycubic/filament/fdm_filament_pet.json b/resources/profiles/Anycubic/filament/fdm_filament_pet.json index 62af7c89ed..56671ade0d 100644 --- a/resources/profiles/Anycubic/filament/fdm_filament_pet.json +++ b/resources/profiles/Anycubic/filament/fdm_filament_pet.json @@ -25,6 +25,12 @@ "hot_plate_temp_initial_layer": [ "80" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "slow_down_for_layer_cooling": [ "1" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4 HF.json b/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4 HF.json index 955235b442..3534f1a60b 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4 HF.json +++ b/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4 HF.json @@ -87,6 +87,12 @@ "support_material_interface_fan_speed": [ "90" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "temperature_vitrification": [ "70" ] diff --git a/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4P HF.json b/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4P HF.json index 82a8dc096a..263224a1ea 100644 --- a/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4P HF.json +++ b/resources/profiles/Flashforge/filament/Flashforge HS PETG @FF G4P HF.json @@ -93,6 +93,12 @@ "support_material_interface_fan_speed": [ "90" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "temperature_vitrification": [ "70" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4 HF.json b/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4 HF.json index d1c404cd19..8308064c52 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4 HF.json +++ b/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4 HF.json @@ -90,6 +90,12 @@ "support_material_interface_fan_speed": [ "90" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "temperature_vitrification": [ "70" ] diff --git a/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4P HF.json b/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4P HF.json index 1385ac8454..d8bdc53caa 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4P HF.json +++ b/resources/profiles/Flashforge/filament/Flashforge PETG Pro @FF G4P HF.json @@ -96,6 +96,12 @@ "support_material_interface_fan_speed": [ "90" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "temperature_vitrification": [ "70" ], diff --git a/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4 HF.json b/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4 HF.json index 2736fea042..f38a075dea 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4 HF.json +++ b/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4 HF.json @@ -90,6 +90,12 @@ "support_material_interface_fan_speed": [ "90" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "temperature_vitrification": [ "70" ] diff --git a/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4P HF.json b/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4P HF.json index bc6fbecdb4..c920f757e5 100644 --- a/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4P HF.json +++ b/resources/profiles/Flashforge/filament/Flashforge PETG Transparent @FF G4P HF.json @@ -96,6 +96,12 @@ "support_material_interface_fan_speed": [ "90" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "temperature_vitrification": [ "70" ], diff --git a/resources/profiles/Flashforge/filament/fdm_filament_pet.json b/resources/profiles/Flashforge/filament/fdm_filament_pet.json index 678ab77822..a19e1faece 100644 --- a/resources/profiles/Flashforge/filament/fdm_filament_pet.json +++ b/resources/profiles/Flashforge/filament/fdm_filament_pet.json @@ -28,6 +28,12 @@ "textured_plate_temp_initial_layer": [ "85" ], + "supertack_plate_temp": [ + "60" + ], + "supertack_plate_temp_initial_layer": [ + "60" + ], "slow_down_for_layer_cooling": [ "1" ], From 67f77e16c38519f8854b20dffd7caf38ace1f3c8 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Thu, 10 Sep 2026 18:23:58 +0800 Subject: [PATCH 20/57] ci: cache compiled objects between runs Every CI leg compiled the whole tree from scratch, 42 to 57 minutes of each build job. Objects are now cached with ccache, one entry per leg kept on the branch that built it: a push saves the cache and drops the previous entry, a pull request restores main's and keeps nothing. The precompiled header is turned off whenever the cache is on: Clang stamps it with the build time, so every file including it missed. With it off, a warm run hits 98.5 to 98.9 % of compiles and the compile steps take 1 to 4 minutes; a cold run costs 25 to 60 % more than before, and a change to a widely included header lands in between. --- .github/workflows/build_orca.yml | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 8e1e28db13..f1fb29c46b 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -76,6 +76,56 @@ jobs: if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" } Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin + # Compiler cache. Pushes save it, so main keeps it warm; pull requests + # restore it and discard what they compiled. Objects are keyed on the + # preprocessed source, the compiler and the flags, so a leg only ever + # hits its own entries. A failed install costs the caching, not the build. + - name: Name the compiler cache leg + if: ${{ !inputs.macos-combine-only }} + shell: bash + run: | + leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}" + echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV" + echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV" + + # The action only installs and configures ccache. Restore and save go + # through actions/cache with one path string, since the cache service + # only matches entries saved under the identical path and the action + # spells it differently on Windows. + - name: Compiler cache + id: ccache + if: ${{ !inputs.macos-combine-only }} + continue-on-error: true + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ env.CCACHE_LEG }} + max-size: 3G + restore: false + save: false + + - name: Restore compiler cache + if: ${{ steps.ccache.outcome == 'success' }} + uses: actions/cache/restore@v6 + with: + path: ${{ github.workspace }}/.ccache + key: ${{ env.CCACHE_ENTRY }} + restore-keys: ccache-${{ env.CCACHE_LEG }}- + + - name: Enable compiler cache + if: ${{ steps.ccache.outcome == 'success' }} + shell: bash + run: | + echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV" + echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV" + # Headers a fresh checkout has just written, and the few files that + # use __DATE__ or __TIME__. + echo "CCACHE_SLOPPINESS=time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV" + # Clang rebuilds the precompiled header with a fresh timestamp on + # every run, so everything that includes it would miss. + echo "ORCA_EXTRA_BUILD_ARGS=-DSLIC3R_PCH=OFF" >> "$GITHUB_ENV" + # The restored directory carries the previous run's counters. + ccache -z + - name: Get the version and date on Ubuntu and macOS if: runner.os != 'Windows' run: | @@ -670,3 +720,32 @@ jobs: asset_name: orca_custom_preset_tests.zip asset_content_type: application/octet-stream max_releases: 1 + + - name: Compiler cache statistics + if: ${{ always() && steps.ccache.outcome == 'success' }} + shell: bash + run: ccache -s -v || ccache -s + + # Entries are immutable, so the new one is saved first and the older + # ones for this leg on this ref are dropped afterwards: a failed save + # leaves the previous entry in place. + - name: Save compiler cache + id: ccache_save + if: ${{ steps.ccache.outcome == 'success' && github.event_name != 'pull_request' }} + uses: actions/cache/save@v6 + with: + path: ${{ github.workspace }}/.ccache + key: ${{ env.CCACHE_ENTRY }} + + - name: Drop older compiler cache entries + if: ${{ steps.ccache_save.outcome == 'success' }} + # A read-only token (fork PRs) cannot delete; that only costs storage. + continue-on-error: true + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \ + | jq -r --arg keep "$CCACHE_ENTRY" '.[] | select(.key != $keep) | .id' \ + | tr -d '\r' \ + | while read -r id; do gh cache delete "$id"; done From 6f90ff6e93fb8c00d6f343f4322a570c8dcc0c0b Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 17:29:28 +0800 Subject: [PATCH 21/57] Allow ccache with PCH Clang records the modification time of every input in the precompiled header, so a fresh checkout produces a different header and every file that includes it misses the compiler cache. -fno-pch-timestamp makes the header reproducible, and pch_defines lets ccache cache the header itself. The precompiled header no longer has to be turned off when the cache is on. --- .github/workflows/build_orca.yml | 10 ++++------ cmake/modules/PrecompiledHeader.cmake | 7 +++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index f1fb29c46b..e51cb2e37a 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -117,12 +117,10 @@ jobs: run: | echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV" echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> "$GITHUB_ENV" - # Headers a fresh checkout has just written, and the few files that - # use __DATE__ or __TIME__. - echo "CCACHE_SLOPPINESS=time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV" - # Clang rebuilds the precompiled header with a fresh timestamp on - # every run, so everything that includes it would miss. - echo "ORCA_EXTRA_BUILD_ARGS=-DSLIC3R_PCH=OFF" >> "$GITHUB_ENV" + # Headers a fresh checkout has just written, the few files that + # use __DATE__ or __TIME__, and the precompiled header, whose + # macros ccache cannot see. + echo "CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV" # The restored directory carries the previous run's counters. ccache -z diff --git a/cmake/modules/PrecompiledHeader.cmake b/cmake/modules/PrecompiledHeader.cmake index 7ef80aacff..7d8b3a5603 100644 --- a/cmake/modules/PrecompiledHeader.cmake +++ b/cmake/modules/PrecompiledHeader.cmake @@ -256,6 +256,13 @@ function(add_precompiled_header _target _input) message(STATUS "Adding precompiled header ${_input} to target ${_target}.") target_precompile_headers(${_target} PRIVATE ${_input}) + # Clang records the modification time of every input in the precompiled + # header, which makes it differ between two checkouts of the same source + # and defeats a compiler cache. The build system already rebuilds the + # header when an input changes. + target_compile_options(${_target} PRIVATE + "$<$:SHELL:-Xclang -fno-pch-timestamp>") + get_target_property(_sources ${_target} SOURCES) list(FILTER _sources INCLUDE REGEX ".*\\.mm?") From 6a88f0790edaa79f4e09e25023403a32c20edf98 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 23:02:31 +0800 Subject: [PATCH 22/57] Enable ccache Depend Mode A miss used to cost a preprocessor pass for the hash and then the real compile. With the depend mode ccache hashes the include list the compiler reports, so a miss costs only the compile. Ninja already asks every compiler here for that list. --- .github/workflows/build_orca.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index e51cb2e37a..1b7fd37a0f 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -121,6 +121,9 @@ jobs: # use __DATE__ or __TIME__, and the precompiled header, whose # macros ccache cannot see. echo "CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime,include_file_ctime" >> "$GITHUB_ENV" + # Hash the includes the compiler reports instead of preprocessing + # every miss before compiling it. + echo "CCACHE_DEPEND=1" >> "$GITHUB_ENV" # The restored directory carries the previous run's counters. ccache -z From 1e76e733b7e487db298da922779e09ca03178c7b Mon Sep 17 00:00:00 2001 From: packerlschupfer <83344883+packerlschupfer@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:31:12 +0200 Subject: [PATCH 23/57] CLI: record user overrides in different_settings_to_system for 3MF export (#15595) * CLI: record user overrides in different_settings_to_system for 3MF export Three sites in CLI::run wrote an empty `different_settings_to_system` column and left a //todo: //todo: support user machine preset's different settings different_settings[filament_count+1] = ""; //todo: support system process preset different_settings[0] = ""; //todo: update different settings of filaments different_settings[filament_index] = ""; So a 3MF exported by the CLI does not record which keys the user actually overrode relative to the system parent. Re-opening such a project in the GUI then shows spurious "unsaved changes", and accepting that dialog can revert inherited process/filament/machine values to system defaults. The column could not be filled before because the CLI had no resolved view of the parent preset. It does now: #15438 builds a PresetBundle for inherits resolution, so the parent can be looked up by name and diffed against the resolved leaf. This adds no extra loading -- the bundle is the one already built, and the helper returns "" whenever it is unavailable or the parent cannot be found, which is the previous behaviour. Preset metadata is filtered out of the diff: `inherits`, the three `*_settings_id` keys, and `compatible_printers` / `compatible_prints` and their `_condition` variants, which have their own tracking columns (`inherits_group`, per-slot lists) and would otherwise double-count. A value already carried by the loaded JSON still wins for the process slot, so presets saved with a `different_settings_to_system` field behave as before; the computed value only fills the gap where that field is absent, which is the case for every user preset in my datadir (0 of 47 carry it). System presets keep an empty column: there are no user overrides to record. * CLI: diff the filament slot before load_default_gcodes_to_config The process and machine slots compute their different_settings_to_system column before load_default_gcodes_to_config(); the filament slot did it after. That call materialises absent gcode keys via option(..., true), and DynamicConfig::diff only compares keys present in both configs -- so a gcode key the resolved leaf did not carry would go from 'not compared' to 'compared as empty against the parent' and land in the column as an override the user never made. Hoisted into a local above the call, guarded by load_filament_count > 0 so the work is skipped exactly where it was before, and assigned at the original site. The diff now also runs before config.erase("filament_settings_id"), which is immaterial: cli_different_settings already filters filament_settings_id along with the other *_settings_id keys. This is a consistency fix rather than a demonstrated defect -- resolve_preset merges the parent config, so in practice the gcode keys are already present on both sides and the diff is unaffected. It removes the dependence on that invariant, which the other two slots never had. Reported by HanifKoh in review of #15595. --- src/OrcaSlicer.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 5463f55c20..7c881047e7 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -2046,6 +2046,51 @@ int CLI::run(int argc, char **argv) error, allow_source_manifest); }; + //ORCA: list the keys a user preset overrides relative to its system parent, for the + // `different_settings_to_system` column of an exported 3MF. Without it the CLI + // writes an empty column, so re-opening a CLI-exported project in the GUI shows + // spurious "unsaved changes" and can revert inherited process/filament/machine + // values to system defaults. + // + // The parent comes from the preset bundle that inherits resolution already builds, + // so this adds no extra loading. Returns "" whenever the parent cannot be resolved, + // which is exactly the previous behaviour. + auto cli_different_settings = [&ensure_cli_preset_bundle](const DynamicPrintConfig &resolved, + const std::string &parent_name, + Preset::Type type) -> std::string { + if (parent_name.empty()) + return std::string(); + std::string error; + PresetBundle *bundle = ensure_cli_preset_bundle(error); + if (bundle == nullptr) { + BOOST_LOG_TRIVIAL(warning) << "CLI: no preset bundle for different_settings_to_system: " << error; + return std::string(); + } + const PresetCollection *collection = nullptr; + switch (type) { + case Preset::TYPE_PRINT: collection = &bundle->prints; break; + case Preset::TYPE_FILAMENT: collection = &bundle->filaments; break; + case Preset::TYPE_PRINTER: collection = &bundle->printers; break; + default: return std::string(); + } + const Preset *parent = collection->find_preset2(parent_name, true); + if (parent == nullptr) { + BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: parent preset '%1%' not found; leaving different_settings_to_system empty")%parent_name; + return std::string(); + } + std::vector keys = resolved.diff(parent->config); + //ORCA: preset metadata, not user-tunable settings. compatible_printers / + // compatible_prints have their own tracking columns and would double-count. + keys.erase(std::remove_if(keys.begin(), keys.end(), [](const std::string &k) { + return k == "inherits" || k == "compatible_printers" || k == "compatible_prints" + || k == "compatible_printers_condition" || k == "compatible_prints_condition" + || k == "print_settings_id" || k == "filament_settings_id" || k == "printer_settings_id"; + }), + keys.end()); + BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% overrides vs parent '%2%'")%keys.size()%parent_name; + return Slic3r::escape_strings_cstyle(keys); + }; + auto load_config_file = [&resolve_preset](const std::string& file, DynamicPrintConfig& config, std::string& config_type, std::string& config_name, std::string& filament_id, std::string& config_from) { if (! boost::filesystem::exists(file)) { @@ -2937,8 +2982,10 @@ int CLI::run(int argc, char **argv) } } else { - //todo: support user machine preset's different settings - different_settings[filament_count+1] = ""; + //ORCA: was a //todo — compute the user's overrides instead of writing an empty column. + different_settings[filament_count+1] = new_printer_config_is_system + ? std::string() + : cli_different_settings(load_machine_config, new_printer_system_name, Preset::TYPE_PRINTER); if (new_printer_config_is_system) inherits_group[filament_count+1] = ""; else @@ -3080,8 +3127,14 @@ int CLI::run(int argc, char **argv) print_compatible_printers = std::move(current_print_compatible_printers); } else { - //todo: support system process preset - different_settings[0] = ""; + //ORCA: was a //todo. Prefer a value the loaded JSON already carried, otherwise + // compute the overrides against the system parent. + if (!different_process_setting.empty()) + different_settings[0] = different_process_setting; + else + different_settings[0] = new_process_config_is_system + ? std::string() + : cli_different_settings(load_process_config, new_process_system_name, Preset::TYPE_PRINT); if (new_process_config_is_system) inherits_group[0] = ""; else @@ -3268,6 +3321,16 @@ int CLI::run(int argc, char **argv) int filament_index = load_filaments_index[index]; std::vector different_keys; + //ORCA: diff before load_default_gcodes_to_config, the way the process and machine + // slots above already do. That call materialises absent gcode keys via + // option(..., true), and DynamicConfig::diff only compares keys present in + // both configs -- so a gcode key the leaf did not carry would go from "not + // compared" to "compared as empty against the parent" and land in the column + // as an override the user never made. + std::string filament_different_settings; + if (load_filament_count > 0) + filament_different_settings = cli_different_settings(config, load_filaments_inherit[index], Preset::TYPE_FILAMENT); + load_default_gcodes_to_config(config, Preset::TYPE_FILAMENT); if (load_filament_count > 0) { @@ -3279,8 +3342,8 @@ int CLI::run(int argc, char **argv) opt_filament_settings->set_at(filament_name_setting, filament_index-1, 0); config.erase("filament_settings_id"); - //todo: update different settings of filaments - different_settings[filament_index] = ""; + //ORCA: was a //todo — same treatment as process/machine above. + different_settings[filament_index] = filament_different_settings; inherits_group[filament_index] = load_filaments_inherit[index]; } else { From 74cf1483841b0421282ee5eb9ff5f617d3e1a79d Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 11 Sep 2026 19:22:16 -0500 Subject: [PATCH 24/57] fix: sequential-print arrange settings are ignored and never persisted (#15425) --- src/slic3r/GUI/GLCanvas3D.cpp | 138 ++++++++++--------------------- src/slic3r/GUI/GLCanvas3D.hpp | 19 +---- src/slic3r/GUI/GUI_App.cpp | 2 +- tests/libslic3r/test_arrange.cpp | 98 +++++++++++++++++++++- tests/libslic3r/test_config.cpp | 70 ++++++++++++++++ 5 files changed, 213 insertions(+), 114 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 676d310f7b..e63501eec1 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1078,56 +1078,36 @@ const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25; void GLCanvas3D::load_arrange_settings() { - std::string dist_fff_str = - wxGetApp().app_config->get("arrange", "min_object_distance_fff"); + // Each key must match what _render_arrange_menu writes, which appends a per-mode + // postfix to the base name. + auto load_float = [](const char *key, float &out) { + // The menu writes these with float_to_string_decimal_point, so parse them back + // the same way rather than with anything locale-dependent. + std::string value = wxGetApp().app_config->get("arrange", key); + size_t parsed = 0; + double number = string_to_double_decimal_point(value, &parsed); + if (parsed > 0) + out = float(number); + }; + auto load_bool = [](const char *key, bool &out) { + std::string value = wxGetApp().app_config->get("arrange", key); + if (!value.empty()) + out = (value == "1" || value == "true"); + }; - std::string dist_fff_seq_print_str = - wxGetApp().app_config->get("arrange", "min_object_distance_seq_print_fff"); + load_float("min_object_distance_fff", m_arrange_settings_fff.distance); + load_float("min_object_distance_fff_seq_print", m_arrange_settings_fff_seq_print.distance); + load_float("min_object_distance_sla", m_arrange_settings_sla.distance); - std::string dist_sla_str = - wxGetApp().app_config->get("arrange", "min_object_distance_sla"); + load_bool("enable_rotation_fff", m_arrange_settings_fff.enable_rotation); + load_bool("enable_rotation_fff_seq_print", m_arrange_settings_fff_seq_print.enable_rotation); + load_bool("enable_rotation_sla", m_arrange_settings_sla.enable_rotation); - std::string en_rot_fff_str = - wxGetApp().app_config->get("arrange", "enable_rotation_fff"); - - std::string en_rot_fff_seqp_str = - wxGetApp().app_config->get("arrange", "enable_rotation_seq_print"); - - std::string en_rot_sla_str = - wxGetApp().app_config->get("arrange", "enable_rotation_sla"); - - std::string en_allow_multiple_materials_str = - wxGetApp().app_config->get("arrange", "allow_multi_materials_on_same_plate"); - - std::string en_avoid_region_str = - wxGetApp().app_config->get("arrange", "avoid_extrusion_cali_region"); - - - - if (!dist_fff_str.empty()) - m_arrange_settings_fff.distance = std::stof(dist_fff_str); - - if (!dist_fff_seq_print_str.empty()) - m_arrange_settings_fff_seq_print.distance = std::stof(dist_fff_seq_print_str); - - if (!dist_sla_str.empty()) - m_arrange_settings_sla.distance = std::stof(dist_sla_str); - - if (!en_rot_fff_str.empty()) - m_arrange_settings_fff.enable_rotation = (en_rot_fff_str == "1" || en_rot_fff_str == "true"); - - if (!en_allow_multiple_materials_str.empty()) - m_arrange_settings_fff.allow_multi_materials_on_same_plate = (en_allow_multiple_materials_str == "1" || en_allow_multiple_materials_str == "true"); - - - if (!en_rot_fff_seqp_str.empty()) - m_arrange_settings_fff_seq_print.enable_rotation = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "true"); - - if(!en_avoid_region_str.empty()) - m_arrange_settings_fff.avoid_extrusion_cali_region = (en_avoid_region_str == "1" || en_avoid_region_str == "true"); - - if (!en_rot_sla_str.empty()) - m_arrange_settings_sla.enable_rotation = (en_rot_sla_str == "1" || en_rot_sla_str == "true"); + // These two keys carry no postfix, so the one stored value covers both FFF modes. + load_bool("allow_multi_materials_on_same_plate", m_arrange_settings_fff.allow_multi_materials_on_same_plate); + load_bool("allow_multi_materials_on_same_plate", m_arrange_settings_fff_seq_print.allow_multi_materials_on_same_plate); + load_bool("avoid_extrusion_cali_region", m_arrange_settings_fff.avoid_extrusion_cali_region); + load_bool("avoid_extrusion_cali_region", m_arrange_settings_fff_seq_print.avoid_extrusion_cali_region); //BBS: add specific arrange settings m_arrange_settings_fff_seq_print.is_seq_print = true; @@ -5959,7 +5939,7 @@ bool GLCanvas3D::_render_orient_menu(float left, float right, float bottom, floa } //BBS: GUI refactor: adjust main toolbar position -bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, float top) +void GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, float top) { ImGuiWrapper *imgui = wxGetApp().imgui(); @@ -5984,7 +5964,6 @@ bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, flo imgui->begin(_L("Arrange options"), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar); - ArrangeSettings settings = get_arrange_settings(); ArrangeSettings &settings_out = get_arrange_settings(); const float slider_icon_width = imgui->get_slider_icon_size().x; const float cursor_slider_left = imgui->calc_text_size(_L("Spacing")).x + imgui->scaled(1.5f); @@ -5993,13 +5972,9 @@ bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, flo auto &appcfg = wxGetApp().app_config; PrinterTechnology ptech = current_printer_technology(); - bool settings_changed = false; - float dist_min = 0.f; // 0 means auto std::string dist_key = "min_object_distance", rot_key = "enable_rotation"; - std::string bed_shrink_x_key = "bed_shrink_x", bed_shrink_y_key = "bed_shrink_y"; std::string multi_material_key = "allow_multi_materials_on_same_plate"; std::string avoid_extrusion_key = "avoid_extrusion_cali_region"; - std::string align_to_y_axis_key = "align_to_y_axis"; std::string postfix; //BBS: bool seq_print = false; @@ -6007,59 +5982,41 @@ bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, flo if (ptech == ptSLA) { postfix = "_sla"; } else if (ptech == ptFFF) { - seq_print = &settings == &m_arrange_settings_fff_seq_print; - if (seq_print) { - postfix = "_fff_seq_print"; - } else { - postfix = "_fff"; - } + seq_print = wxGetApp().global_print_sequence() == PrintSequence::ByObject; + postfix = seq_print ? "_fff_seq_print" : "_fff"; } dist_key += postfix; rot_key += postfix; - bed_shrink_x_key += postfix; - bed_shrink_y_key += postfix; ImGui::AlignTextToFramePadding(); imgui->text(_L("Spacing")); ImGui::SameLine(1.2 * cursor_slider_left); ImGui::PushItemWidth(window_width - slider_icon_width); - bool b_Spacing = imgui->bbl_slider_float_style("##Spacing", &settings.distance, dist_min, 100.0f, "%5.2f") || dist_min > settings.distance; + bool b_Spacing = imgui->bbl_slider_float_style("##Spacing", &settings_out.distance, 0.f, 100.0f, "%5.2f", 1.0f, /*clamp=*/false); ImGui::SameLine(window_width - slider_icon_width + 1.3 * cursor_slider_left); ImGui::PushItemWidth(1.5 * slider_icon_width); - bool b_spacing_input = ImGui::BBLDragFloat("##spacing_input", &settings.distance, 0.05f, 0.0f, 0.0f, "%.2f"); - if (b_Spacing || b_spacing_input) - { - settings.distance = std::max(dist_min, settings.distance); - settings_out.distance = settings.distance; + bool b_spacing_input = ImGui::BBLDragFloat("##spacing_input", &settings_out.distance, 0.05f, 0.0f, 0.0f, "%.2f"); + if (b_Spacing || b_spacing_input) { + settings_out.distance = std::max(0.f, settings_out.distance); appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance)); - settings_changed = true; } imgui->text(_L("0 means auto spacing.")); ImGui::Separator(); - if (imgui->bbl_checkbox(_L("Auto rotate for arrangement"), settings.enable_rotation)) { - settings_out.enable_rotation = settings.enable_rotation; + if (imgui->bbl_checkbox(_L("Auto rotate for arrangement"), settings_out.enable_rotation)) appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation); - settings_changed = true; - } - if (imgui->bbl_checkbox(_L("Allow multiple materials on same plate"), settings.allow_multi_materials_on_same_plate)) { - settings_out.allow_multi_materials_on_same_plate = settings.allow_multi_materials_on_same_plate; - appcfg->set("arrange", multi_material_key.c_str(), settings_out.allow_multi_materials_on_same_plate ); - settings_changed = true; - } + if (imgui->bbl_checkbox(_L("Allow multiple materials on same plate"), settings_out.allow_multi_materials_on_same_plate)) + appcfg->set("arrange", multi_material_key.c_str(), settings_out.allow_multi_materials_on_same_plate); // only show this option if the printer has micro Lidar and can do first layer scan DynamicPrintConfig ¤t_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; const bool has_lidar = wxGetApp().preset_bundle->is_bbl_vendor(); auto op = current_config.option("scan_first_layer"); if (has_lidar && op && op->getBool()) { - if (imgui->bbl_checkbox(_L("Avoid extrusion calibration region"), settings.avoid_extrusion_cali_region)) { - settings_out.avoid_extrusion_cali_region = settings.avoid_extrusion_cali_region; - appcfg->set("arrange", avoid_extrusion_key.c_str(), settings_out.avoid_extrusion_cali_region ? "1" : "0"); - settings_changed = true; - } + if (imgui->bbl_checkbox(_L("Avoid extrusion calibration region"), settings_out.avoid_extrusion_cali_region)) + appcfg->set("arrange", avoid_extrusion_key.c_str(), settings_out.avoid_extrusion_cali_region); } else { settings_out.avoid_extrusion_cali_region = false; } @@ -6071,11 +6028,7 @@ bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, flo settings_out.align_to_y_axis = false; } - if (imgui->bbl_checkbox(_L("Align to Y axis"), settings.align_to_y_axis)) { - settings_out.align_to_y_axis = settings.align_to_y_axis; - appcfg->set("arrange", align_to_y_axis_key, settings_out.align_to_y_axis ? "1" : "0"); - settings_changed = true; - } + imgui->bbl_checkbox(_L("Align to Y axis"), settings_out.align_to_y_axis); if (settings_out.enable_rotation == true) { imgui->disabled_end(); } } @@ -6091,7 +6044,6 @@ bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, flo if (imgui->button(_L("Reset"))) { settings_out = ArrangeSettings{}; - settings_out.distance = std::max(dist_min, settings_out.distance); //BBS: add specific arrange settings if (seq_print) settings_out.is_seq_print = true; @@ -6101,18 +6053,16 @@ bool GLCanvas3D::_render_arrange_menu(float left, float right, float bottom, flo else settings_out.align_to_y_axis = false; - appcfg->set("arrange", dist_key, float_to_string_decimal_point(settings_out.distance)); - appcfg->set("arrange", rot_key, settings_out.enable_rotation ? "1" : "0"); - appcfg->set("arrange", align_to_y_axis_key, settings_out.align_to_y_axis ? "1" : "0"); - settings_changed = true; + appcfg->erase("arrange", dist_key); + appcfg->erase("arrange", rot_key); + appcfg->erase("arrange", multi_material_key); + appcfg->erase("arrange", avoid_extrusion_key); } ImGui::PopStyleVar(1); imgui->end(); //BBS ImGuiWrapper::pop_toolbar_style(); - - return settings_changed; } static const float cameraProjection[16] = {1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f}; diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 84dbd5d652..b1dd674d96 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -656,11 +656,7 @@ public: } void load_arrange_settings(); - ArrangeSettings& get_arrange_settings();// { return get_arrange_settings(this); } - ArrangeSettings& get_arrange_settings(PrintSequence print_seq) { - return (print_seq == PrintSequence::ByObject) ? m_arrange_settings_fff_seq_print - : m_arrange_settings_fff; - } + ArrangeSettings& get_arrange_settings(); class SequentialPrintClearance { @@ -1163,17 +1159,6 @@ public: void highlight_toolbar_item(const std::string& item_name); void highlight_gizmo(const std::string& gizmo_name); - ArrangeSettings get_arrange_settings() const { - const ArrangeSettings &settings = get_arrange_settings(); - ArrangeSettings ret = settings; - if (&settings == &m_arrange_settings_fff_seq_print) { - ret.distance = std::max(ret.distance, - float(min_object_distance(*m_config))); - } - - return ret; - } - // Timestamp for FPS calculation and notification fade-outs. static int64_t timestamp_now() { #ifdef _WIN32 @@ -1308,7 +1293,7 @@ private: void _render_selection_sidebar_hints() { m_selection.render_sidebar_hints(m_sidebar_field, m_gizmos.get_uniform_scaling()); } //BBS: GUI refactor: adjust main toolbar position bool _render_orient_menu(float left, float right, float bottom, float top); - bool _render_arrange_menu(float left, float right, float bottom, float top); + void _render_arrange_menu(float left, float right, float bottom, float top); void _render_3d_navigator(); void _update_volumes_hover_state(); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index df2d1fccc0..fee18b4799 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -9197,7 +9197,7 @@ int GUI_App::filaments_cnt() const PrintSequence GUI_App::global_print_sequence() const { PrintSequence global_print_seq = PrintSequence::ByDefault; - auto curr_preset_config = preset_bundle->prints.get_edited_preset().config; + const auto &curr_preset_config = preset_bundle->prints.get_edited_preset().config; if (curr_preset_config.has("print_sequence")) global_print_seq = curr_preset_config.option>("print_sequence")->value; return global_print_seq; diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index a9fb51e352..3906cba8ba 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -4,6 +4,8 @@ #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/PrintConfig.hpp" using namespace Slic3r; using namespace Slic3r::arrangement; @@ -24,11 +26,13 @@ ArrangePolygon make_square(coord_t side) return ap; } -ArrangePolygons squares(int n, double side_mm) +ArrangePolygons squares(int n, double side_mm, double height_mm = 0.) { ArrangePolygons items; - for (int i = 0; i < n; ++i) + for (int i = 0; i < n; ++i) { items.emplace_back(make_square(scaled(side_mm))); + items.back().height = height_mm; + } return items; } @@ -82,6 +86,38 @@ void require_no_overlap(const ArrangePolygons &items) REQUIRE(disjoint(placed_shapes(items))); } +// The sequential-print floor is chosen by comparing object height against the nozzle, +// so the two are defined together and every expectation is derived from them. +constexpr double NOZZLE_HEIGHT_MM = 2.5; +constexpr double CLEARANCE_MM = 30.; +constexpr double NOZZLE_FLOOR_MM = MAX_OUTER_NOZZLE_DIAMETER / 2.; + +ArrangeParams seq_print_params(coord_t min_dist) +{ + ArrangeParams p = quiet_params(min_dist); + p.is_seq_print = true; + p.clearance_radius = float(CLEARANCE_MM); + p.nozzle_height = float(NOZZLE_HEIGHT_MM); + p.object_skirt_offset = 0.f; + return p; +} + +// update_selected_items_inflation reads the bed out of the config to cap inflation. +DynamicPrintConfig bed_config() +{ + DynamicPrintConfig c; + c.set_key_value("printable_area", new ConfigOptionPoints{{0, 0}, {200, 0}, {200, 200}, {0, 200}}); + return c; +} + +ArrangePolygons squares_of_heights(const std::vector &heights_mm) +{ + ArrangePolygons items; + for (double height_mm : heights_mm) + items.push_back(squares(1, 20., height_mm).front()); + return items; +} + } // namespace // Prove the overlap check the other tests rely on actually detects overlap. @@ -222,3 +258,61 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") REQUIRE(ap.bed_idx == 0); require_no_overlap(items); } + +TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") +{ + // The only place sequential-print clearance is enforced. The arrange menu offers + // no floor of its own, so a stored 0 has to be raised here or not at all. + struct Case + { + std::string description; + std::vector heights; + double skirt_offset_mm; + double expected_floor_mm; + }; + + auto c = GENERATE(values({ + {"objects taller than the nozzle need the full clearance", {NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2}, 0., CLEARANCE_MM}, + {"an object exactly at the nozzle height counts as tall", {NOZZLE_HEIGHT_MM, NOZZLE_HEIGHT_MM}, 0., CLEARANCE_MM}, + {"one tall object among short ones is enough", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM * 2}, 0., CLEARANCE_MM}, + {"objects the nozzle clears keep only the nozzle-width floor", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM / 2}, 0., NOZZLE_FLOOR_MM}, + {"a wide skirt raises the floor for short objects", {NOZZLE_HEIGHT_MM / 2, NOZZLE_HEIGHT_MM / 2}, 3., 6.}, + })); + + DYNAMIC_SECTION(c.description) + { + ArrangePolygons items = squares_of_heights(c.heights); + DynamicPrintConfig cfg = bed_config(); + ArrangeParams p = seq_print_params(0); + p.object_skirt_offset = float(c.skirt_offset_mm); + + update_selected_items_inflation(items, &cfg, p); + + CHECK(p.min_obj_distance >= scaled(c.expected_floor_mm)); + CHECK(p.min_obj_distance <= scaled(c.expected_floor_mm + 0.01)); + // Half each, so a pair ends up a full min_obj_distance apart. + CHECK(items.front().inflation == p.min_obj_distance / 2); + } +} + +TEST_CASE("Sequential print keeps an object distance already above the floor", "[Arrange]") +{ + const coord_t stored = scaled(CLEARANCE_MM * 2); + ArrangePolygons items = squares_of_heights({NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2}); + DynamicPrintConfig cfg = bed_config(); + ArrangeParams p = seq_print_params(stored); + + update_selected_items_inflation(items, &cfg, p); + CHECK(p.min_obj_distance == stored); +} + +TEST_CASE("Layered printing does not floor the object distance", "[Arrange]") +{ + ArrangePolygons items = squares_of_heights({NOZZLE_HEIGHT_MM * 2, NOZZLE_HEIGHT_MM * 2}); + DynamicPrintConfig cfg = bed_config(); + ArrangeParams p = seq_print_params(0); + p.is_seq_print = false; + + update_selected_items_inflation(items, &cfg, p); + CHECK(p.min_obj_distance == 0); +} diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index bd147b5881..9a70ecbaeb 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -1091,3 +1091,73 @@ TEST_CASE("get_filament_type treats empty vector options as absent", "[Config][F REQUIRE(displayed == "Sup.PLA"); } } + +namespace { + +// min_object_distance reads exactly these three options. +DynamicPrintConfig spacing_config(PrinterTechnology tech, PrintSequence seq, double clearance_radius) +{ + DynamicPrintConfig c; + c.set_key_value("printer_technology", new ConfigOptionEnum(tech)); + c.set_key_value("print_sequence", new ConfigOptionEnum(seq)); + c.set_key_value("extruder_clearance_radius", new ConfigOptionFloat(clearance_radius)); + return c; +} + +} // namespace + +TEST_CASE("min_object_distance floors object spacing per print sequence", "[Config]") +{ + struct Case + { + std::string description; + PrinterTechnology tech; + PrintSequence sequence; + double clearance_radius; + double expected; + }; + + auto c = GENERATE(values({ + {"sequential FFF takes a clearance radius above the floor", ptFFF, PrintSequence::ByObject, 12., 12.}, + {"sequential FFF holds the floor at the radius", ptFFF, PrintSequence::ByObject, 6., 6.}, + {"sequential FFF holds the floor below the radius", ptFFF, PrintSequence::ByObject, 4., 6.}, + {"layered FFF ignores the clearance radius", ptFFF, PrintSequence::ByLayer, 12., 6.}, + {"SLA is a flat 6mm", ptSLA, PrintSequence::ByObject, 12., 6.}, + {"SLA ignores the print sequence too", ptSLA, PrintSequence::ByLayer, 12., 6.}, + })); + + DYNAMIC_SECTION(c.description) + { + CHECK_THAT(min_object_distance(spacing_config(c.tech, c.sequence, c.clearance_radius)), + Catch::Matchers::WithinAbs(c.expected, 1e-9)); + } +} + +TEST_CASE("min_object_distance yields no floor when an FFF config lacks the options", "[Config]") +{ + // Missing options yield 0 rather than an error, so a caller gets no floor at all. + SECTION("no clearance radius") { + DynamicPrintConfig c; + c.set_key_value("printer_technology", new ConfigOptionEnum(ptFFF)); + c.set_key_value("print_sequence", new ConfigOptionEnum(PrintSequence::ByObject)); + CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(0., 1e-9)); + } + + SECTION("no print sequence") { + DynamicPrintConfig c; + c.set_key_value("printer_technology", new ConfigOptionEnum(ptFFF)); + c.set_key_value("extruder_clearance_radius", new ConfigOptionFloat(12.)); + CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(0., 1e-9)); + } + + SECTION("nothing at all") { + CHECK_THAT(min_object_distance(DynamicPrintConfig{}), Catch::Matchers::WithinAbs(0., 1e-9)); + } + + SECTION("an unset printer technology is treated as FFF") { + DynamicPrintConfig c; + c.set_key_value("print_sequence", new ConfigOptionEnum(PrintSequence::ByObject)); + c.set_key_value("extruder_clearance_radius", new ConfigOptionFloat(12.)); + CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(12., 1e-9)); + } +} From 75f5fe22e8913b686a19849332921341a70ae00c Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 11 Sep 2026 19:24:37 -0500 Subject: [PATCH 25/57] build: clear 12 platform-gated warnings the x64 census could not see (#15633) --- src/libslic3r/PresetBundle.cpp | 12 ++++++------ src/libslic3r/Thread.cpp | 9 +++++++-- src/slic3r/GUI/InstanceCheck.hpp | 1 - src/slic3r/GUI/SelectMachinePop.hpp | 2 ++ src/slic3r/GUI/TextureImportDialog.cpp | 2 ++ src/slic3r/Utils/Serial.cpp | 2 ++ 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index c90ebc756b..4b8fb03a02 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4981,7 +4981,7 @@ static void apply_mixed_config_relocations(DynamicPrintConfig& case coBools: { auto* live = static_cast(opt); const auto* frozen = static_cast(snapshot.get()); - for (const auto [from, to] : moves) { + for (const auto& [from, to] : moves) { const unsigned char cell = from < frozen->values.size() ? frozen->values[from] : 0; if (live->values.size() <= to) live->values.resize(to + 1, 0); @@ -4992,7 +4992,7 @@ static void apply_mixed_config_relocations(DynamicPrintConfig& case coStrings: { auto* live = static_cast(opt); const auto* frozen = static_cast(snapshot.get()); - for (const auto [from, to] : moves) { + for (const auto& [from, to] : moves) { const std::string cell = from < frozen->values.size() ? frozen->values[from] : std::string(); if (live->values.size() <= to) live->values.resize(to + 1, std::string{}); @@ -5028,7 +5028,7 @@ static void apply_receiver_mix_relocations(DynamicPrintConfig& auto* live = static_cast(opt); std::unique_ptr snapshot(opt->clone()); const auto* frozen = static_cast(snapshot.get()); - for (const auto [from, to] : moves) { + for (const auto& [from, to] : moves) { const bool cell = from < frozen->values.size() ? frozen->values[from] : false; if (live->values.size() <= to) live->values.resize(to + 1, false); @@ -5044,7 +5044,7 @@ static void apply_receiver_mix_relocations(DynamicPrintConfig& auto* live = static_cast(opt); std::unique_ptr snapshot(opt->clone()); const auto* frozen = static_cast(snapshot.get()); - for (const auto [from, to] : moves) { + for (const auto& [from, to] : moves) { const std::string cell = from < frozen->values.size() ? frozen->values[from] : std::string(); if (live->values.size() <= to) live->values.resize(to + 1, std::string{}); @@ -5060,7 +5060,7 @@ static void apply_receiver_mix_relocations(DynamicPrintConfig& auto* live = static_cast(opt); std::unique_ptr snapshot(opt->clone()); const auto* frozen = static_cast(snapshot.get()); - for (const auto [from, to] : moves) { + for (const auto& [from, to] : moves) { const int cell = from < frozen->values.size() ? frozen->values[from] : 0; if (live->values.size() <= to) live->values.resize(to + 1, 0); @@ -5087,7 +5087,7 @@ static void apply_receiver_mix_relocations(DynamicPrintConfig& move_ints("filament_volume_map"); { const std::vector> frozen = ams_multi_color_filment; - for (const auto [from, to] : moves) { + for (const auto& [from, to] : moves) { const std::vector cell = from < frozen.size() ? frozen[from] : std::vector(); if (ams_multi_color_filment.size() <= to) ams_multi_color_filment.resize(to + 1, std::vector{}); diff --git a/src/libslic3r/Thread.cpp b/src/libslic3r/Thread.cpp index 3030b6d194..edd7c2a3d0 100644 --- a/src/libslic3r/Thread.cpp +++ b/src/libslic3r/Thread.cpp @@ -30,6 +30,11 @@ static HMODULE s_hKernel32 = nullptr; static SetThreadDescriptionType s_fnSetThreadDescription = nullptr; static GetThreadDescriptionType s_fnGetThreadDescription = nullptr; +// Convert the FARPROC from GetProcAddress to Fn through a generic function pointer. +template static Fn load_proc(HMODULE module, const char* name) { + return reinterpret_cast(reinterpret_cast(::GetProcAddress(module, name))); +} + static bool WindowsGetSetThreadNameAPIInitialize() { if (! s_SetGetThreadDescriptionInitialized) { @@ -37,8 +42,8 @@ static bool WindowsGetSetThreadNameAPIInitialize() // to initialize s_hKernel32 = LoadLibraryW(L"Kernel32.dll"); if (s_hKernel32) { - s_fnSetThreadDescription = (SetThreadDescriptionType)::GetProcAddress(s_hKernel32, "SetThreadDescription"); - s_fnGetThreadDescription = (GetThreadDescriptionType)::GetProcAddress(s_hKernel32, "GetThreadDescription"); + s_fnSetThreadDescription = load_proc(s_hKernel32, "SetThreadDescription"); + s_fnGetThreadDescription = load_proc(s_hKernel32, "GetThreadDescription"); } s_SetGetThreadDescriptionInitialized = true; } diff --git a/src/slic3r/GUI/InstanceCheck.hpp b/src/slic3r/GUI/InstanceCheck.hpp index 5f26f1e48f..9bfb3e2500 100644 --- a/src/slic3r/GUI/InstanceCheck.hpp +++ b/src/slic3r/GUI/InstanceCheck.hpp @@ -87,7 +87,6 @@ private: std::condition_variable m_thread_stop_condition; mutable std::mutex m_thread_stop_mutex; bool m_stop{ false }; - bool m_start{ true }; // background thread method void listen(); diff --git a/src/slic3r/GUI/SelectMachinePop.hpp b/src/slic3r/GUI/SelectMachinePop.hpp index 76d38be522..e34a23708c 100644 --- a/src/slic3r/GUI/SelectMachinePop.hpp +++ b/src/slic3r/GUI/SelectMachinePop.hpp @@ -183,7 +183,9 @@ private: HyperLink* m_hyperlink{nullptr}; // ORCA wxBoxSizer * m_sizer_my_devices{nullptr}; wxBoxSizer * m_sizer_other_devices{nullptr}; +#if defined(__WINDOWS__) wxBoxSizer * m_sizer_search_bar{nullptr}; +#endif wxSearchCtrl* m_search_bar{nullptr}; wxScrolledWindow * m_scrolledWindow{nullptr}; wxTimer * m_refresh_timer{nullptr}; diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp index 1bf52d792c..2e5c1145e1 100644 --- a/src/slic3r/GUI/TextureImportDialog.cpp +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -134,7 +134,9 @@ public: } private: +#if defined(__WXMSW__) || defined(__APPLE__) int m_suspended_count = 0; +#endif }; static bool needs_filament_swatch_border(const wxColour& colour) diff --git a/src/slic3r/Utils/Serial.cpp b/src/slic3r/Utils/Serial.cpp index 4db1acc6b6..f8c03ceb26 100644 --- a/src/slic3r/Utils/Serial.cpp +++ b/src/slic3r/Utils/Serial.cpp @@ -331,7 +331,9 @@ void Serial::set_baud_rate(unsigned baud_rate) speed_t c_ispeed; speed_t c_ospeed; }; +#ifndef BOTHER #define BOTHER CBAUDEX +#endif termios2 ios; handle_errno(::ioctl(handle, TCGETS2, &ios)); From 081bb9a7035795e78d0381f8a67cff898b3b9c33 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 11 Sep 2026 19:27:30 -0500 Subject: [PATCH 26/57] build: clear 41 -Woverloaded-virtual warnings, the last of the category (#15637) Co-authored-by: Raoul Rubien --- src/libslic3r/Config.hpp | 12 ++++++++++++ src/slic3r/GUI/Field.cpp | 10 +++++----- src/slic3r/GUI/Field.hpp | 2 +- src/slic3r/GUI/GUI_ObjectTable.cpp | 4 ++-- src/slic3r/GUI/GUI_ObjectTableSettings.cpp | 2 +- src/slic3r/GUI/GUI_ObjectTableSettings.hpp | 2 +- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 9e4344820d..ea85cda1e7 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -1006,6 +1006,7 @@ public: int getInt() const override { return this->value; } void setInt(int val) override { this->value = val; } ConfigOption* clone() const override { return new ConfigOptionInt(*this); } + using ConfigOptionSingle::operator==; bool operator==(const ConfigOptionInt &rhs) const throw() { return this->value == rhs.value; } std::string serialize() const override @@ -1048,6 +1049,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionIntsTempl(*this); } ConfigOptionIntsTempl& operator= (const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionVector::operator==; bool operator==(const ConfigOptionIntsTempl &rhs) const throw() { return this->values == rhs.values; } bool operator< (const ConfigOptionIntsTempl &rhs) const throw() { return this->values < rhs.values; } // Could a special "nil" value be stored inside the vector, indicating undefined value? @@ -1137,6 +1139,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionString(*this); } ConfigOptionString& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionSingle::operator==; bool operator==(const ConfigOptionString &rhs) const throw() { return this->value == rhs.value; } bool operator< (const ConfigOptionString &rhs) const throw() { return this->value < rhs.value; } bool empty() const { return this->value.empty(); } @@ -1171,6 +1174,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionStrings(*this); } ConfigOptionStrings& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionVector::operator==; bool operator==(const ConfigOptionStrings &rhs) const throw() { return this->values == rhs.values; } bool operator< (const ConfigOptionStrings &rhs) const throw() { return this->values < rhs.values; } bool is_nil(size_t) const override { return false; } @@ -1215,6 +1219,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionPercent(*this); } ConfigOptionPercent& operator= (const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionFloat::operator==; bool operator==(const ConfigOptionPercent &rhs) const throw() { return this->value == rhs.value; } bool operator< (const ConfigOptionPercent &rhs) const throw() { return this->value < rhs.value; } @@ -1257,6 +1262,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionPercentsTempl(*this); } ConfigOptionPercentsTempl& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionFloatsTempl::operator==; bool operator==(const ConfigOptionPercentsTempl &rhs) const throw() { return ConfigOptionFloatsTempl::vectors_equal(this->values, rhs.values); } bool operator< (const ConfigOptionPercentsTempl &rhs) const throw() { return ConfigOptionFloatsTempl::vectors_lower(this->values, rhs.values); } @@ -1502,6 +1508,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionPoint(*this); } ConfigOptionPoint& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionSingle::operator==; bool operator==(const ConfigOptionPoint &rhs) const throw() { return this->value == rhs.value; } bool operator< (const ConfigOptionPoint &rhs) const throw() { return this->value < rhs.value; } @@ -1539,6 +1546,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionPoints(*this); } ConfigOptionPoints& operator= (const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionVector::operator==; bool operator==(const ConfigOptionPoints &rhs) const throw() { return this->values == rhs.values; } bool operator< (const ConfigOptionPoints &rhs) const throw() { return std::lexicographical_compare(this->values.begin(), this->values.end(), rhs.values.begin(), rhs.values.end(), [](const auto &l, const auto &r){ return l < r; }); } @@ -1617,6 +1625,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionPoint3(*this); } ConfigOptionPoint3& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionSingle::operator==; bool operator==(const ConfigOptionPoint3 &rhs) const throw() { return this->value == rhs.value; } bool operator< (const ConfigOptionPoint3 &rhs) const throw() { return this->value.x() < rhs.value.x() || (this->value.x() == rhs.value.x() && (this->value.y() < rhs.value.y() || (this->value.y() == rhs.value.y() && this->value.z() < rhs.value.z()))); } @@ -1860,6 +1869,7 @@ public: bool getBool() const override { return this->value; } ConfigOption* clone() const override { return new ConfigOptionBool(*this); } ConfigOptionBool& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionSingle::operator==; bool operator==(const ConfigOptionBool &rhs) const throw() { return this->value == rhs.value; } bool operator< (const ConfigOptionBool &rhs) const throw() { return int(this->value) < int(rhs.value); } @@ -1911,6 +1921,7 @@ public: ConfigOptionType type() const override { return static_type(); } ConfigOption* clone() const override { return new ConfigOptionBoolsTempl(*this); } ConfigOptionBoolsTempl& operator=(const ConfigOption *opt) { this->set(opt); return *this; } + using ConfigOptionVector::operator==; bool operator==(const ConfigOptionBoolsTempl &rhs) const throw() { return this->values == rhs.values; } bool operator< (const ConfigOptionBoolsTempl &rhs) const throw() { return this->values < rhs.values; } // Could a special "nil" value be stored inside the vector, indicating undefined value? @@ -2163,6 +2174,7 @@ public: ConfigOptionEnumsGenericTempl& operator= (const ConfigOption* opt) { this->set(opt); return *this; } bool operator< (const ConfigOptionInts& rhs) const throw() { return this->values < rhs.values; } + using ConfigOptionInts::operator==; bool operator==(const ConfigOptionInts& rhs) const { if (rhs.type() != this->type()) diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index b8cb698ff9..43ece4e10b 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -2805,11 +2805,11 @@ void PointCtrl::BUILD() //temp->Add(static_text_y, 0, wxALIGN_CENTER_VERTICAL, 0); temp->Add(y_input); - x_textctrl->Bind(wxEVT_TEXT_ENTER, ([this](wxCommandEvent e) { propagate_value(x_textctrl); }), x_textctrl->GetId()); - y_textctrl->Bind(wxEVT_TEXT_ENTER, ([this](wxCommandEvent e) { propagate_value(y_textctrl); }), y_textctrl->GetId()); + x_textctrl->Bind(wxEVT_TEXT_ENTER, ([this](wxCommandEvent e) { propagate_input_value(x_textctrl); }), x_textctrl->GetId()); + y_textctrl->Bind(wxEVT_TEXT_ENTER, ([this](wxCommandEvent e) { propagate_input_value(y_textctrl); }), y_textctrl->GetId()); - x_textctrl->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) { e.Skip(); propagate_value(x_textctrl); }), x_textctrl->GetId()); - y_textctrl->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) { e.Skip(); propagate_value(y_textctrl); }), y_textctrl->GetId()); + x_textctrl->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) { e.Skip(); propagate_input_value(x_textctrl); }), x_textctrl->GetId()); + y_textctrl->Bind(wxEVT_KILL_FOCUS, ([this](wxEvent& e) { e.Skip(); propagate_input_value(y_textctrl); }), y_textctrl->GetId()); // // recast as a wxWindow to fit the calling convention window = dynamic_cast(x_input); @@ -2858,7 +2858,7 @@ bool PointCtrl::value_was_changed(wxTextCtrl* win) return boost::any_cast(m_value) != boost::any_cast(val); } -void PointCtrl::propagate_value(wxTextCtrl* win) +void PointCtrl::propagate_input_value(wxTextCtrl* win) { if (win->GetValue().empty()) on_kill_focus(); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 6219921202..3f55bf5c5a 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -650,7 +650,7 @@ public: void BUILD() override; bool value_was_changed(wxTextCtrl* win); // Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER - void propagate_value(wxTextCtrl* win); + void propagate_input_value(wxTextCtrl* win); void set_value(const Vec2d& value, bool change_event = false); void set_value(const boost::any& value, bool change_event = false) override; boost::any& get_value() override; diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index 35508c6113..a496eca6d3 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -2578,7 +2578,7 @@ void ObjectGridTable::OnSelectCell(int row, int col) return; m_panel->m_side_window->Freeze(); if (row == 0 || col == col_filaments) { - m_panel->m_object_settings->UpdateAndShow(row, false, false, false, nullptr, nullptr, std::string()); + m_panel->m_object_settings->UpdateAndShowRow(row, false, false, false, nullptr, nullptr, std::string()); } else { ObjectGridRow* grid_row = m_grid_data[row - 1]; @@ -2588,7 +2588,7 @@ void ObjectGridTable::OnSelectCell(int row, int col) //m_panel->m_object_settings->get_og()->set_name(GUI::from_u8(grid_row->name.value)); //m_panel->m_page_text->SetLabel(GUI::from_u8(grid_row->name.value)); - m_panel->m_object_settings->UpdateAndShow(row, true, is_object, false, object, grid_row->config, grid_col->category); + m_panel->m_object_settings->UpdateAndShowRow(row, true, is_object, false, object, grid_row->config, grid_col->category); std::vector object_volume_ids; ObjectVolumeID object_volume_id; diff --git a/src/slic3r/GUI/GUI_ObjectTableSettings.cpp b/src/slic3r/GUI/GUI_ObjectTableSettings.cpp index 2290018419..4cd272840f 100644 --- a/src/slic3r/GUI/GUI_ObjectTableSettings.cpp +++ b/src/slic3r/GUI/GUI_ObjectTableSettings.cpp @@ -463,7 +463,7 @@ void ObjectTableSettings::update_config_values(bool is_object, ModelObject* obje m_table->reload_cell_data(m_current_row, category); } -void ObjectTableSettings::UpdateAndShow(int row, const bool show, bool is_object, bool is_multiple_selection, ModelObject* object, ModelConfig* config, const std::string& category) +void ObjectTableSettings::UpdateAndShowRow(int row, const bool show, bool is_object, bool is_multiple_selection, ModelObject* object, ModelConfig* config, const std::string& category) { m_current_row = row; m_current_category = category; diff --git a/src/slic3r/GUI/GUI_ObjectTableSettings.hpp b/src/slic3r/GUI/GUI_ObjectTableSettings.hpp index 39e7e514e2..24e3d427a9 100644 --- a/src/slic3r/GUI/GUI_ObjectTableSettings.hpp +++ b/src/slic3r/GUI/GUI_ObjectTableSettings.hpp @@ -71,7 +71,7 @@ public: //return visible count int update_extra_column_visible_status(ConfigOptionsGroup* option_group, const std::vector& option_keys, ModelConfig* config); void update_config_values(bool is_object, ModelObject* object, ModelConfig* config, const std::string& category, const std::string& changed_opt_key = ""); - void UpdateAndShow(int row, const bool show, bool is_object, bool is_multiple_selection, ModelObject* object, ModelConfig* config, const std::string& category); + void UpdateAndShowRow(int row, const bool show, bool is_object, bool is_multiple_selection, ModelObject* object, ModelConfig* config, const std::string& category); void ValueChanged(int row, bool is_object, ModelObject* object, ModelConfig* config, const std::string& category, const std::string& key); void resetAllValues(int row, bool is_object, ModelObject* object, ModelConfig* config, const std::string& category); void msw_rescale(); From e998ad968aed65ec7e51897ccc15ad84a15978f2 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Fri, 11 Sep 2026 20:43:48 -0500 Subject: [PATCH 27/57] ci: cache the Flatpak job's compiled objects with ccache (#15650) --- .github/workflows/build_all.yml | 88 +++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 0ab9cfe41d..570d3203ed 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -283,21 +283,41 @@ jobs: id: fp_cache_key run: echo "key=flatpak-builder-${{ matrix.variant.arch }}-${{ hashFiles('deps/**', 'scripts/flatpak/com.orcaslicer.OrcaSlicer.yml', 'scripts/flatpak/make_deps_tar.sh') }}" >> "$GITHUB_OUTPUT" shell: bash - # Manage flatpak-builder cache externally so PRs restore but never upload + # Manage flatpak-builder cache externally so PRs restore but never upload. + # The compiler cache under it is keyed per run below, so it is left out. - name: Restore flatpak-builder cache if: github.event_name == 'pull_request' uses: actions/cache/restore@v6 with: - path: .flatpak-builder + path: | + .flatpak-builder/* + !.flatpak-builder/ccache key: ${{ steps.fp_cache_key.outputs.key }} restore-keys: flatpak-builder-${{ matrix.variant.arch }}- - name: Save/restore flatpak-builder cache if: github.event_name != 'pull_request' uses: actions/cache@v6 with: - path: .flatpak-builder + path: | + .flatpak-builder/* + !.flatpak-builder/ccache key: ${{ steps.fp_cache_key.outputs.key }} restore-keys: flatpak-builder-${{ matrix.variant.arch }}- + # Compiler cache for the OrcaSlicer module, as in build_orca.yml. Pull + # requests only restore it; every other run (main, release branches, the + # nightly, a dispatch) saves it. orca_deps stays on the state cache above. + - name: Name the compiler cache leg + run: | + leg="Flatpak-${{ matrix.variant.arch }}" + echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV" + echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV" + shell: bash + - name: Restore compiler cache + uses: actions/cache/restore@v6 + with: + path: .flatpak-builder/ccache + key: ${{ env.CCACHE_ENTRY }} + restore-keys: ccache-${{ env.CCACHE_LEG }}- - name: Disable debug info for faster CI builds run: | sed -i '/^build-options:/a\ no-debuginfo: true\n strip: true' \ @@ -308,6 +328,33 @@ jobs: sed -i "/name: OrcaSlicer/{n;s|buildsystem: simple|buildsystem: simple\n build-options:\n env:\n git_commit_hash: \"$git_commit_hash\"|}" \ scripts/flatpak/com.orcaslicer.OrcaSlicer.yml shell: bash + # flatpak-builder's --ccache only wraps cc and gcc, and the manifest builds + # with clang, so CMake's launcher runs ccache instead; --ccache is still what + # mounts the cache directory into the sandbox. The settings go into that + # directory's own config file, which the sandbox reads too. + - name: Enable compiler cache + run: | + printf ' %s\n' \ + 'CMAKE_C_COMPILER_LAUNCHER: ccache' \ + 'CMAKE_CXX_COMPILER_LAUNCHER: ccache' > "$RUNNER_TEMP/ccache-env.yml" + sed -i "/^ git_commit_hash: /r $RUNNER_TEMP/ccache-env.yml" \ + scripts/flatpak/com.orcaslicer.OrcaSlicer.yml + grep -q '^ CMAKE_CXX_COMPILER_LAUNCHER: ccache$' scripts/flatpak/com.orcaslicer.OrcaSlicer.yml + mkdir -p .flatpak-builder/ccache + export CCACHE_DIR=$PWD/.flatpak-builder/ccache + ccache --set-config=max_size=3G + # The compiler is reinstalled every run, so its mtime means nothing. + ccache --set-config=compiler_check=content + # Headers a fresh checkout has just written, the few files that use + # __DATE__ or __TIME__, and the precompiled header, whose macros ccache + # cannot see. + ccache --set-config=sloppiness=pch_defines,time_macros,include_file_mtime,include_file_ctime + # Hash the includes the compiler reports instead of preprocessing every + # miss before compiling it. + ccache --set-config=depend_mode=true + # The restored directory carries the previous run's counters. + ccache -z + shell: bash - name: Check the manifest keeps orca_deps cacheable run: ./scripts/flatpak/check_manifest_cacheable.sh shell: bash @@ -318,9 +365,42 @@ jobs: with: bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak manifest-path: scripts/flatpak/com.orcaslicer.OrcaSlicer.yml - cache: false + # cache only turns on flatpak-builder --ccache; the caching itself is above. + cache: true + restore-cache: false + save-cache: false arch: ${{ matrix.variant.arch }} upload-artifact: false + - name: Compiler cache statistics + if: always() + run: | + export CCACHE_DIR=$PWD/.flatpak-builder/ccache + ccache -s -v || ccache -s + shell: bash + # Save the new entry first, then drop the older ones for this leg on this + # ref, so a failed save leaves the previous entry in place. + - name: Save compiler cache + id: ccache_save + if: github.event_name != 'pull_request' + uses: actions/cache/save@v6 + with: + path: .flatpak-builder/ccache + key: ${{ env.CCACHE_ENTRY }} + - name: Drop older compiler cache entries + if: ${{ steps.ccache_save.outcome == 'success' }} + # The container has no gh, so this is the list and delete over the REST API. + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches" + curl -sSf -H "Authorization: Bearer $GH_TOKEN" \ + "$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \ + | jq -r --arg keep "$CCACHE_ENTRY" '.actions_caches[] | select(.key != $keep) | .id' \ + | while read -r id; do + curl -sSf -X DELETE -H "Authorization: Bearer $GH_TOKEN" "$api/$id" + done + shell: bash - name: Upload artifacts Flatpak uses: actions/upload-artifact@v7 with: From ccd608678732c801e76cc211d3084ed239989462 Mon Sep 17 00:00:00 2001 From: packerlschupfer <83344883+packerlschupfer@users.noreply.github.com> Date: Sat, 12 Sep 2026 05:17:10 +0200 Subject: [PATCH 28/57] CLI: evaluate compatible_printers_condition in the compat checks (#15449) * CLI: evaluate compatible_printers_condition in the compat checks Slicing from the CLI with --load-settings exits with CLI_PROCESS_NOT_COMPATIBLE (-17), "The selected printer is not compatible with the process preset in the 3mf.", for process/printer pairs the GUI accepts. Reproducible with stock, unmodified Prusa system profiles: orca-slicer --datadir \ --load-settings "/system/Prusa/process/0.20mm SPEED @CORE One HF 0.4.json;/system/Prusa/machine/Prusa CORE One HF 0.4 nozzle.json" \ --load-filaments "/system/Prusa/filament/Prusament PETG @CORE One HF 0.4.json" \ --slice 0 --outputdir /tmp/out model.stl The four compat checks in CLI::run did a literal name match against the `compatible_printers` list only: for (index ...) if (new_print_compatible_printers[index] == new_printer_system_name) process_compatible = true; Process profiles that declare compatibility through `compatible_printers_condition` and leave `compatible_printers` empty are therefore always reported incompatible -- the condition is never consulted. For 0.20mm SPEED @CORE One HF 0.4 that condition is: printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/ The GUI does not have this bug: is_compatible_with_printer() in Preset.cpp treats an empty list as "no explicit constraint" and evaluates the condition in that case. Fix: replace the four loops with a check_compat lambda that calls is_compatible_with_printer() -- the same helper the GUI uses -- wrapping the already-loaded DynamicPrintConfigs in lightweight Preset / PresetWithVendorProfile shells. The 3MF-embedded process/printer full configs are kept in current_process_full_config / current_printer_full_config so the condition can be evaluated for the reprocess paths too; those fall back to the previous literal match when the full config was not preserved. Behaviour is unchanged where an explicit compatible_printers list exists: is_compatible_with_printer() performs the same name match, and returns true when both list and condition are empty, matching the existing "old 3mf, no compatible printers, set to compatible" path. Split out of #13731 (section 1) as a standalone, single-purpose change. Orthogonal to the inherits-chain resolution work in #14718 / #15302 / #15438; those decide which values a preset resolves to, this decides whether the resulting pair is considered compatible. * CLI: translate the 3MF's renamed compatibility keys before the compat check The 3MF fallback fed the project config to is_compatible_with_printer() as-is, but a project config does not carry compatible_printers or compatible_printers_condition. PresetBundle::construct_full_config() erases both and re-emits them as print_compatible_printers and compatible_machine_expression_group; they are renamed back only on the PresetBundle load path, which the CLI does not take. The check therefore saw no list and no condition, read that as 'no constraint' and accepted every printer. That is not just a wrong accept. An early true skips the !process_compatible block that sets machine_switch, so the new printer is never appended to print_compatible_printers and the exported 3MF stays marked compatible only with the printer it came from -- which is exactly what that block exists to prevent. Translate the two keys back before the check. Index 0 of the expression group is the print preset; the group is filled print, filaments, printer. Also note in the comment that profiles/BBL/{process,machine}_full/ are gitignored and generated by nothing in-tree, so current_*_full_config is always empty and this fallback is the only live path -- not the rare non-BBL case the original comment implied. Reported with measurements by HanifKoh in review of #15449. Preset: add a config-level is_compatible_with_printer() overload The CLI holds resolved DynamicPrintConfigs, not Presets, so it wrapped them in throwaway Preset shells at the call site. Moving that into Preset.cpp puts the compatibility policy -- including the documented fail-open on a malformed compatible_printers_condition -- in one place for the GUI and the CLI, rather than leaving a second copy of the plumbing in OrcaSlicer.cpp to drift. Purely additive: neither existing overload changes, so no GUI behaviour moves. Requested by HanifKoh in review of #15449. (cherry picked from commit 14ca1972ef4d3c7d90935d159423013a40a6bd70) * CLI: never overwrite a real compat key with an empty renamed one 7e7f0e3 translated compatible_machine_expression_group[0] into compatible_printers_condition whenever the group vector was non-empty. A project the CLI exported itself carries the real compatible_printers_condition AND an all-empty group, ["", "", ""], so the valid condition was overwritten with "", the check saw no constraint, and every printer was accepted. That fixed GUI-shaped projects and broke CLI-shaped ones. Bisected across six builds re-slicing one CLI-exported CORE One project with an MK4S: every build before 7e7f0e3 gives 'compatible 0' and takes the machine-switch path; with it, 'compatible 1' and no switch. The raw keys now win whenever they carry something; the renamed ones are only a fallback, and an empty value is never written over a real one. Same for the list: print_compatible_printers is used only when compatible_printers is absent or empty and it itself is not. Found by a peer session re-testing the installed build. --- src/OrcaSlicer.cpp | 97 ++++++++++++++++++++++++++++++---------- src/libslic3r/Preset.cpp | 14 ++++++ src/libslic3r/Preset.hpp | 5 +++ 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 7c881047e7..bebd1aad5c 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -53,6 +53,7 @@ using namespace nlohmann; #include "libslic3r/libslic3r.h" #include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/GCode.hpp" #include "libslic3r/Model.hpp" @@ -1466,6 +1467,10 @@ int CLI::run(int argc, char **argv) std::vector upward_compatible_printers, new_print_compatible_printers, current_print_compatible_printers, current_different_settings; std::vector current_filaments_name, current_filaments_system_name, current_inherits_group, current_extruder_variants, new_extruder_variants, current_print_extruder_variants, new_printer_extruder_variants; DynamicPrintConfig load_process_config, load_machine_config; + //ORCA: full configs of the "current" (3MF-embedded) process/printer presets, kept so that + // compatible_printers_condition can be evaluated for them below. Previously only the + // literal compatible_printers list was extracted. + DynamicPrintConfig current_process_full_config, current_printer_full_config; bool new_process_config_is_system = true, new_printer_config_is_system = true; std::string pipe_name, makerlab_name, makerlab_version, different_process_setting; const std::vector &metadata_name = m_config.option("metadata_name", true)->values; @@ -2680,6 +2685,8 @@ int CLI::run(int argc, char **argv) flush_and_exit(ret); } upward_compatible_printers = config.option("upward_compatible_machine", true)->values; + //ORCA: keep the full config so compatible_printers_condition can be evaluated against it below + current_printer_full_config = std::move(config); } } } @@ -2702,6 +2709,8 @@ int CLI::run(int argc, char **argv) flush_and_exit(ret); } current_print_compatible_printers = config.option("compatible_printers", true)->values; + //ORCA: keep the full config so compatible_printers_condition can be evaluated against it below + current_process_full_config = std::move(config); } } } @@ -2720,46 +2729,88 @@ int CLI::run(int argc, char **argv) for (int index = 0; index < upward_compatible_printers.size(); index++) { BOOST_LOG_TRIVIAL(info) << boost::format("index %1%, upward_compatible_printers %2%")%index %upward_compatible_printers[index]; } + //ORCA: Replace the four manual equality-loop checks below with is_compatible_with_printer(), the + // same helper the GUI uses, which also evaluates compatible_printers_condition. Process + // profiles that declare compatibility via condition only -- leaving compatible_printers + // empty -- were always reported incompatible by the literal-name match, so a CLI slice with + // such a preset exited with CLI_PROCESS_NOT_COMPATIBLE (-17) even though the GUI accepts the + // same pair. Behaviour is unchanged where an explicit list exists: is_compatible_with_printer + // does the same name match, and returns true when both list and condition are empty (which + // matches the "old 3mf, no compatible printers" path below). + auto check_compat = [](const DynamicPrintConfig &process_cfg, + const DynamicPrintConfig &printer_cfg, + const std::string &printer_name) -> bool { + return is_compatible_with_printer(process_cfg, Preset::TYPE_PRINT, printer_cfg, printer_name); + }; + + //ORCA: a 3MF's project config does not carry compatible_printers / compatible_printers_condition. + // PresetBundle::construct_full_config() erases both and re-emits them as + // print_compatible_printers and compatible_machine_expression_group; they are renamed back + // only on the PresetBundle load path, which the CLI does not take. Feeding the project config + // to the check as-is therefore presents no list and no condition, and + // is_compatible_with_printer() reads that as "no constraint" and accepts every printer. + // Translate the two keys back. Index 0 of the expression group is the print preset -- the + // group is filled print, filaments, printer (PresetBundle.cpp). + // The raw keys win whenever they carry something. A project the CLI exported itself has the + // real compatible_printers_condition AND an all-empty compatible_machine_expression_group, + // so copying the group's first entry unconditionally would overwrite a valid condition with + // "" and accept every printer. The renamed keys are only a fallback, and an empty value is + // never written over a real one. + auto cli_process_compat_config = [](const DynamicPrintConfig &project_cfg) -> DynamicPrintConfig { + DynamicPrintConfig cfg = project_cfg; + const auto *raw_list = project_cfg.option("compatible_printers"); + const auto *list = project_cfg.option("print_compatible_printers"); + if ((raw_list == nullptr || raw_list->values.empty()) && list != nullptr && !list->values.empty()) + cfg.set_key_value("compatible_printers", new ConfigOptionStrings(list->values)); + const auto *raw_cond = project_cfg.option("compatible_printers_condition"); + const auto *group = project_cfg.option("compatible_machine_expression_group"); + if ((raw_cond == nullptr || raw_cond->value.empty()) && group != nullptr && !group->values.empty() && + !group->values.front().empty()) + cfg.set_key_value("compatible_printers_condition", new ConfigOptionString(group->values.front())); + return cfg; + }; if (!new_printer_name.empty()) { if (!new_process_name.empty()) { - for (int index = 0; index < new_print_compatible_printers.size(); index++) { - if (new_print_compatible_printers[index] == new_printer_system_name) { - process_compatible = true; - break; - } - } + //new process + new printer: both configs came from --load-settings + process_compatible = check_compat(load_process_config, load_machine_config, new_printer_system_name); BOOST_LOG_TRIVIAL(info) << boost::format("new printer %1%, inherited from %2%, new process %3%, inherited from %4% ,compatible %5%") %new_printer_name %new_printer_system_name %new_process_name %new_process_system_name %process_compatible; } else { - for (int index = 0; index < current_print_compatible_printers.size(); index++) { - if (current_print_compatible_printers[index] == new_printer_system_name) { - process_compatible = true; - break; - } + //3MF-embedded process vs new printer. current_process_full_config is only populated from + //profiles/BBL/process_full/, so for every other vendor fall back to the 3MF's own project + //config in m_print_config, with its renamed compatibility keys translated back (see + //cli_process_compat_config above). Without this a 3MF built from a condition-only process + //is rejected when re-sliced with the very printer it was made for. + { + //ORCA: profiles/BBL/{process,machine}_full/ are gitignored and not generated in-tree, + // so current_*_full_config is always empty and this fallback is the only live path. + const DynamicPrintConfig process_cfg = current_process_full_config.empty() + ? cli_process_compat_config(m_print_config) + : current_process_full_config; + process_compatible = check_compat(process_cfg, load_machine_config, new_printer_system_name); } BOOST_LOG_TRIVIAL(info) << boost::format("new printer %1%, inherited from %2%, old process %3%, inherited from %4% ,compatible %5%") %new_printer_name %new_printer_system_name %current_process_name %current_process_system_name %process_compatible; } } else if (!new_process_name.empty()) { - for (int index = 0; index < new_print_compatible_printers.size(); index++) { - if (new_print_compatible_printers[index] == current_printer_system_name) { - process_compatible = true; - break; - } + //new process vs 3MF-embedded printer. As above, current_printer_full_config only resolves for + //BBL profiles; otherwise evaluate against the 3MF's own project config in m_print_config, which + //holds the embedded printer's printer_notes / nozzle_diameter. + { + const DynamicPrintConfig &printer_cfg = current_printer_full_config.empty() ? m_print_config : current_printer_full_config; + process_compatible = check_compat(load_process_config, printer_cfg, current_printer_system_name); } BOOST_LOG_TRIVIAL(info) << boost::format("old printer %1%, inherited from %2%, new process %3%, inherited from %4% ,compatible %5%") %current_printer_name %current_printer_system_name %new_process_name %new_process_system_name %process_compatible; } else { - //check the compatible of old printer&&process - for (int index = 0; index < current_print_compatible_printers.size(); index++) { - if (current_print_compatible_printers[index] == current_printer_system_name) { - process_compatible = true; - break; - } - } + //both sides 3MF-embedded (pure reprocess) + if (!current_process_full_config.empty() && !current_printer_full_config.empty()) + process_compatible = check_compat(current_process_full_config, current_printer_full_config, current_printer_system_name); + else + process_compatible = std::find(current_print_compatible_printers.begin(), current_print_compatible_printers.end(), current_printer_system_name) != current_print_compatible_printers.end(); if (!process_compatible && current_print_compatible_printers.empty()) { BOOST_LOG_TRIVIAL(info) << boost::format("old 3mf, no compatible printers, set to compatible"); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3cf85e8054..e974ffd7f8 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -867,6 +867,20 @@ bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const Pre return is_compatible_with_printer(preset, active_printer, &config); } +// ORCA: see the header. The CLI resolves --load-settings into bare DynamicPrintConfigs and has no +// Preset objects to hand; without this it would have to reimplement the policy or build the shells +// at every call site. +bool is_compatible_with_printer(const DynamicPrintConfig &preset_config, Preset::Type preset_type, + const DynamicPrintConfig &printer_config, const std::string &printer_name) +{ + Preset preset(preset_type, std::string("__compat_check")); + preset.config = preset_config; + Preset printer(Preset::TYPE_PRINTER, printer_name); + printer.config = printer_config; + return is_compatible_with_printer(PresetWithVendorProfile(preset, nullptr), + PresetWithVendorProfile(printer, nullptr)); +} + void Preset::set_visible_from_appconfig(const AppConfig &app_config) { //BBS: add config related log diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 2653628ead..73052678e8 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -459,6 +459,11 @@ protected: bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer); bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config); bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer); +// ORCA: same check for callers that hold raw configs rather than Presets (the CLI). Wraps them in +// throwaway Preset shells and delegates, so the compatibility policy -- including the fail-open on a +// malformed compatible_printers_condition -- lives in one place for the GUI and the CLI alike. +bool is_compatible_with_printer(const DynamicPrintConfig &preset_config, Preset::Type preset_type, + const DynamicPrintConfig &printer_config, const std::string &printer_name); // Where a preset is being loaded from. `Auto` lets load_presets() infer from the directory path. struct PresetOrigin { From 0888e331b51bf17c23f38df3d1361c12b89a1edb Mon Sep 17 00:00:00 2001 From: Valerii Bokhan <80919135+valerii-bokhan@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:59:13 +0200 Subject: [PATCH 29/57] fix: validate float-or-percent input ranges (#15392) --- localization/i18n/OrcaSlicer.pot | 4 +- localization/i18n/ca/OrcaSlicer_ca.po | 8 +- localization/i18n/cs/OrcaSlicer_cs.po | 8 +- localization/i18n/de/OrcaSlicer_de.po | 8 +- localization/i18n/en/OrcaSlicer_en.po | 4 +- localization/i18n/es/OrcaSlicer_es.po | 8 +- localization/i18n/eu/OrcaSlicer_eu.po | 8 +- localization/i18n/fr/OrcaSlicer_fr.po | 8 +- localization/i18n/hu/OrcaSlicer_hu.po | 8 +- localization/i18n/it/OrcaSlicer_it.po | 8 +- localization/i18n/ja/OrcaSlicer_ja.po | 7 +- localization/i18n/ko/OrcaSlicer_ko.po | 8 +- localization/i18n/lt/OrcaSlicer_lt.po | 8 +- localization/i18n/nl/OrcaSlicer_nl.po | 8 +- localization/i18n/pl/OrcaSlicer_pl.po | 17 +-- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 8 +- localization/i18n/ru/OrcaSlicer_ru.po | 8 +- localization/i18n/sv/OrcaSlicer_sv.po | 8 +- localization/i18n/th/OrcaSlicer_th.po | 8 +- localization/i18n/tr/OrcaSlicer_tr.po | 8 +- localization/i18n/uk/OrcaSlicer_uk.po | 8 +- localization/i18n/vi/OrcaSlicer_vi.po | 8 +- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 8 +- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 8 +- src/slic3r/GUI/Field.cpp | 113 ++++++++++++++------ 25 files changed, 125 insertions(+), 180 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index bbdc0e59be..a063ab0484 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -4995,9 +4995,7 @@ msgstr "" #, possible-c-format, possible-boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" #, possible-boost-format diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 373eb6e9fd..4fee47786f 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -5429,13 +5429,9 @@ msgstr "El valor %s està fora de rang. El rang vàlid és de %d a %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"És %s%% or %s %s?\n" -"SÍ per %s%%.\n" -"NO per %s %s." +"És %s%% or %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index b0d64c8005..f79c90bcd1 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -5386,13 +5386,9 @@ msgstr "Hodnota %s je mimo rozsah. Platný rozsah je od %d do %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Je to %s%% nebo %s %s?\n" -"ANO pro %s%%,\n" -"NE pro %s %s." +"Je to %s%% nebo %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index bd25405cc3..2a3e4e19d6 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -5291,13 +5291,9 @@ msgstr "Wert %s ist außerhalb des Bereichs. Der gültige Bereich liegt zwischen #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Heißt es %s%% oder %s %s?\n" -"Ja für %s%%, \n" -"Nein für %s %s." +"Heißt es %s%% oder %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 35c7f8a87a..37485ba662 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -4991,9 +4991,7 @@ msgstr "" #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" #, boost-format diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index efe4c7dbcd..9dfcde53e5 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -5155,13 +5155,9 @@ msgstr "El valor %s está fuera de rango. El rango válido es de %d a %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"¿Es %s%% o %s %s?\n" -"SÍ para %s%%, \n" -"NO para %s %s." +"¿Es %s%% o %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 698941018e..b966a124cb 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -5203,13 +5203,9 @@ msgstr "%s balioa tartetik kanpo dago. Baliozko tartea %d eta %d artekoa da." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"%s%% edo %s %s da?\n" -"BAI %s%%-(r)entzat,\n" -"EZ %s %s-(r)entzat." +"%s%% edo %s %s da?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 6344696234..9d8739babe 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -5241,13 +5241,9 @@ msgstr "La valeur %s est hors plage. La plage valide est comprise entre %d et %d #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Est-ce %s%% ou %s %s ?\n" -"OUI pour %s%%, \n" -"NON pour %s %s." +"Est-ce %s%% ou %s %s ?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 78c06dd4c0..198d3decac 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -5338,13 +5338,9 @@ msgstr "%s érték tartományon kívül van. Az érvényes tartomány: %d - %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"%s%% vagy %s %s?\n" -"IGEN %s%%, \n" -"NEM %s %s." +"%s%% vagy %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index cf5099bca3..bc9a0980f2 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -5339,13 +5339,9 @@ msgstr "Il valore %s è fuori intervallo. L'intervallo valido è da %d a %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"È %s%% o %s %s?\n" -"Sì per %s%%, \n" -"NO per %s %s." +"È %s%% o %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 99cd0d0a83..ed9960bb8a 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -5353,12 +5353,9 @@ msgstr "値%sは範囲外です。有効な範囲は%dから%dです。" #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"%s%% か、それとも %s %sですか?\n" -"%s%% の場合ははい、 %s %s はいいえ。" +"%s%% か、それとも %s %sですか?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 8d12c90228..16dc582437 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -5364,13 +5364,9 @@ msgstr "값 %s이 범위를 벗어났습니다. 유효한 범위는 %d에서 %d #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"%s%% 또는 %s %s입니까?\n" -"%s%%에 대해 예,\n" -"%s %s에 대해 아니요." +"%s%% 또는 %s %s입니까?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 67d02bef6d..0141917d77 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -5325,13 +5325,9 @@ msgstr "Reikšmė %s yra už ribų. Galimas diapazonas yra nuo %d iki %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Ar tai %s%% ar %s %s?\n" -"TAIP %s%%, \n" -"NE %s %s." +"Ar tai %s%% ar %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index eff0fdc6b0..a767093bb8 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -5831,13 +5831,9 @@ msgstr "Waarde %s valt buiten het bereik. Het geldige bereik loopt van %d tot %d #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Is het %s%% or %s %s?\n" -"JA voor %s%%, \n" -"NEE voor %s %s." +"Is het %s%% or %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 6f74f4b602..39b0c71d89 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -5452,13 +5452,9 @@ msgstr "Wartość %s jest spoza zakresu. Poprawny zakres wynosi od %d do %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Czy to %s%% czy %s %s?\n" -"TAK dla %s%%,\n" -"NIE dla %s %s." +"Czy to %s%% czy %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" @@ -25436,15 +25432,6 @@ msgstr "" #~ msgid "Low-temperature filament (PLA/PETG/TPU) is loaded in the extruder. In order to avoid extruder clogging, it is not allowed to set the chamber temperature above 45℃." #~ msgstr "W ekstruzorze jest załadowany filament o niskiej temperaturze (PLA/PETG/TPU). Aby uniknąć zatkania ekstruzora, nie wolno ustawiać temperatury komory powyżej 45℃." -#~ msgid "" -#~ "Is it %s%% or %s %s?\n" -#~ "YES for %s%%,\n" -#~ "NO for %s %s." -#~ msgstr "" -#~ "Czy to %s%% czy %s %s?\n" -#~ "TAK dla %s%%,\n" -#~ "NIE dla %s %s." - #~ msgid "Allow multiple materials on the same plate" #~ msgstr "Pozwól na kilka filamentów na tej samej płycie" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 2a3e9a7f53..ca92c4c09e 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -5169,13 +5169,9 @@ msgstr "Valor %s está fora do intervalo. O intervalo válido é de %d para %d." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"É %s%% ou %s %s?\n" -"SIM para %s%%, \n" -"NÃO para %s %s." +"É %s%% ou %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 2e33546ae8..972f853df4 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -5335,13 +5335,9 @@ msgstr "Значение %s выходит за пределы допустим #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Имелось ввиду %s%%? (введено %s %s)\n" -"Да – изменить на %s%%\n" -"Нет – оставить %s %s." +"Имелось ввиду %s%% или %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index b9aa481d4c..472b105744 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -5905,13 +5905,9 @@ msgstr "Värdet %s ligger utanför intervallet. Giltigt intervall är från %d t #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Det är %s%% eller %s %s?\n" -"JA för %s%%, \n" -"NEJ för %s %s." +"Det är %s%% eller %s %s?" # AI Translated #, boost-format diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index ee7430015e..2865914d23 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -5319,13 +5319,9 @@ msgstr "ค่า %s อยู่นอกช่วง ช่วงที่ถ #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"มันคือ %s%% หรือ %s %s?\n" -"ใช่สำหรับ %s%% \n" -"ไม่ สำหรับ %s %s" +"มันคือ %s%% หรือ %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 0cf9d57412..a5c11f5300 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -5382,13 +5382,9 @@ msgstr "Değer %s aralık dışında. Geçerli aralık %d ile %d arasındadır." #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"%s%% mi yoksa %s %s mi?\n" -"%s%% için EVET,\n" -"%s %s için HAYIR." +"%s%% mi yoksa %s %s mi?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index ec9e97bae0..c3ac201896 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -5330,13 +5330,9 @@ msgstr "Значення %s знаходиться за межами діапа #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Це %s%% або %s %s?\n" -"ТАК для %s%%, \n" -"НІ для %s %s." +"Це %s%% або %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index a80f47cfc1..4504b5c95c 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -5635,13 +5635,9 @@ msgstr "Giá trị %s nằm ngoài phạm vi. Phạm vi hợp lệ từ %d đế #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"Là %s%% hay %s %s?\n" -"YES cho %s%%, \n" -"NO cho %s %s." +"Là %s%% hay %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index faa3419ae5..ff2e0fd940 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -5175,13 +5175,9 @@ msgstr "值 %s 超出了范围,有效的范围是从 %d 到 %d 。" #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"%s%%还是%s %s?\n" -"是:%s%%\n" -"否:%s %s" +"%s%%还是%s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 8f17cfdc5d..e0e468fccf 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -5304,13 +5304,9 @@ msgstr "數值 %s 超出範圍。有效範圍是從 %d 到 %d。" #, c-format, boost-format msgid "" -"Is it %s%% or %s %s?\n" -"YES for %s%%, \n" -"NO for %s %s." +"Is it %s%% or %s %s?" msgstr "" -"是 %s%% 還是 %s %s?\n" -"選『是』代表 %s%%,\n" -"選『否』代表 %s %s。" +"是 %s%% 還是 %s %s?" #, boost-format msgid "Invalid input format. Expected vector of dimensions in the following format: \"%1%\"" diff --git a/src/slic3r/GUI/Field.cpp b/src/slic3r/GUI/Field.cpp index 43ece4e10b..74ef3f87c8 100644 --- a/src/slic3r/GUI/Field.cpp +++ b/src/slic3r/GUI/Field.cpp @@ -11,6 +11,7 @@ #include "libslic3r/PrintConfig.hpp" #include +#include #include #include #include @@ -540,51 +541,95 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true case coStrings: case coFloatOrPercent: case coFloatsOrPercents: { - if ((m_opt.type == coFloatOrPercent || m_opt.type == coFloatsOrPercents) && !str.IsEmpty() && str.Last() != '%') - { + if ((m_opt.type == coFloatOrPercent || m_opt.type == coFloatsOrPercents) && !str.IsEmpty() && + !(m_opt.nullable && str == m_na_value)) { + bool update_control = false; + wxString numeric_str = str; double val = 0.; + const char dec_sep = is_decimal_separator_point() ? '.' : ','; const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - if (str.Replace(dec_sep_alt, dec_sep, false) != 0) - set_value(str, false); + // Orca: normalize the decimal separator and optional unit before + // detecting the percentage suffix and parsing the numeric part. + update_control |= numeric_str.Replace(dec_sep_alt, dec_sep, false) != 0; + update_control |= numeric_str.Replace(" ", "", true) != 0; + const bool has_literal_unit = numeric_str.EndsWith("mm"); + if (has_literal_unit) { + numeric_str.RemoveLast(2); + update_control = true; + } + bool is_percent = !numeric_str.IsEmpty() && numeric_str.Last() == '%'; + if (is_percent) + numeric_str.RemoveLast(); - - // remove space and "mm" substring, if any exists - str.Replace(" ", "", true); - str.Replace("m", "", true); - - if (!str.ToDouble(&val)) - { + if ((has_literal_unit && is_percent) || !numeric_str.ToDouble(&val) || !std::isfinite(val)) { if (!check_value) { m_value.clear(); break; } show_error(m_parent, _L("Invalid numeric.")); - set_value(double_to_string(val), true); - } - else if (((m_opt.sidetext.rfind("mm/s") != std::string::npos && val > m_opt.max) || - (m_opt.sidetext.rfind("mm ") != std::string::npos && val > /*1*/m_opt.max_literal)) && - (m_value.empty() || into_u8(str) != boost::any_cast(m_value))) - { - if (!check_value) { - m_value.clear(); - break; + numeric_str = double_to_string(std::clamp(0., double(m_opt.min), double(m_opt.max))); + is_percent = false; + update_control = true; + } else { + const bool looks_like_missing_percent = !is_percent && !has_literal_unit && + ((m_opt.sidetext.rfind("mm/s") != std::string::npos && val > m_opt.max) || + (m_opt.sidetext.rfind("mm ") != std::string::npos && val > m_opt.max_literal)); + // Orca: validate explicit percentages and literal values before + // asking whether an otherwise valid literal was meant as a percentage. + const bool out_of_range = !m_opt.is_value_valid(val); + if (out_of_range) { + if (!check_value) { + m_value.clear(); + break; + } + show_error(m_parent, _L("Value is out of range.")); + val = std::clamp(val, double(m_opt.min), double(m_opt.max)); + // Orca: retain the inferred percent unit when clamping a + // suspicious unitless value, so 2000 becomes 100%, not 100 mm. + is_percent |= looks_like_missing_percent; + numeric_str = double_to_string(val); + update_control = true; + } else { + const bool value_changed = m_value.empty() || into_u8(str) != boost::any_cast(m_value); + if (looks_like_missing_percent && value_changed) { + if (!check_value) { + m_value.clear(); + break; + } + + const std::string sidetext = m_opt.sidetext.rfind("mm/s") != std::string::npos ? "mm/s" : "mm"; + const wxString stVal = numeric_str; + const wxString msg_text = from_u8((boost::format(_utf8(L("Is it %s%% or %s %s?"))) % + stVal % stVal % sidetext).str()); + WarningDialog dialog(m_parent, msg_text, _L("Parameter validation") + ": " + m_opt_id, wxYES | wxNO); + dialog.SetButtonLabel(wxID_YES, stVal + _L("%")); + dialog.SetButtonLabel(wxID_NO, stVal + " " + _L(sidetext)); + dialog.GetSizer()->SetSizeHints(&dialog); + dialog.Fit(); + dialog.CenterOnParent(); + is_percent = dialog.ShowModal() == wxID_YES; + update_control = true; + } } - const std::string sidetext = m_opt.sidetext.rfind("mm/s") != std::string::npos ? "mm/s" : "mm"; - const wxString stVal = double_to_string(val, 2); - const wxString msg_text = from_u8((boost::format(_utf8(L("Is it %s%% or %s %s?\n" - "YES for %s%%, \n" - "NO for %s %s."))) % - stVal % stVal % sidetext % stVal % stVal % sidetext) - .str()); - WarningDialog dialog(m_parent, msg_text, _L("Parameter validation") + ": " + m_opt_id, wxYES | wxNO); - if ((val > 100) && dialog.ShowModal() == wxID_YES) { - set_value(from_u8((boost::format("%s%%") % stVal).str()), false /*true*/); - str += "%%"; - } else - set_value(stVal, false); // it's no needed but can be helpful, when inputted value contained "," instead of "." + // Orca: also enforce the literal limit after clamping an explicit mm input. + if (!is_percent && m_opt.sidetext.rfind("mm ") != std::string::npos && val > m_opt.max_literal) { + if (!check_value) { + m_value.clear(); + break; + } + if (!out_of_range) + show_error(m_parent, _L("Value is out of range.")); + val = m_opt.max_literal; + numeric_str = double_to_string(val); + update_control = true; + } + } + + if (update_control) { + str = numeric_str + (is_percent ? "%" : ""); + set_value(str, true); } } if (m_opt.opt_key == "thumbnails") { From e7ca4fb87e479e4fa280253e0ad48ee375bdc8f1 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sat, 12 Sep 2026 10:09:39 -0500 Subject: [PATCH 30/57] build: trim GUI_App.hpp includes so edits stop rebuilding the whole GUI (#15644) --- src/OrcaSlicer.cpp | 3 ++- src/slic3r/GUI/AMSDryControl.cpp | 1 + src/slic3r/GUI/AMSDryControl.hpp | 1 + src/slic3r/GUI/AMSMaterialsSetting.cpp | 2 ++ src/slic3r/GUI/BaseTransparentDPIFrame.hpp | 2 ++ src/slic3r/GUI/CalibrationWizard.cpp | 1 + .../GUI/CalibrationWizardPresetPage.cpp | 2 ++ src/slic3r/GUI/CalibrationWizardSavePage.cpp | 1 + src/slic3r/GUI/CapsuleButton.cpp | 1 + src/slic3r/GUI/ColorDecomposeSupport.cpp | 1 + src/slic3r/GUI/ConfigManipulation.cpp | 1 + src/slic3r/GUI/DailyTips.cpp | 1 + src/slic3r/GUI/DeviceCore/DevCalib.cpp | 2 ++ .../GUI/DeviceCore/DevFilaBlackList.cpp | 2 ++ src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 1 + src/slic3r/GUI/DeviceManager.cpp | 2 ++ src/slic3r/GUI/DeviceTab/wgtMsgPanel.cpp | 1 + src/slic3r/GUI/DragCanvas.cpp | 1 + src/slic3r/GUI/EncodedFilament.cpp | 3 +++ src/slic3r/GUI/ExportPresetBundleDialog.cpp | 5 +++++ src/slic3r/GUI/ExtraRenderers.cpp | 1 + src/slic3r/GUI/ExtrusionCalibration.cpp | 1 + src/slic3r/GUI/FilamentMapPanel.cpp | 1 + src/slic3r/GUI/GLTexture.cpp | 2 ++ src/slic3r/GUI/GUI_App.cpp | 8 ++++++++ src/slic3r/GUI/GUI_App.hpp | 19 ++++++++++--------- src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp | 2 ++ src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp | 2 ++ src/slic3r/GUI/HttpServer.cpp | 4 ++++ src/slic3r/GUI/IMSlider.cpp | 1 + src/slic3r/GUI/ImageDPIFrame.hpp | 2 ++ src/slic3r/GUI/ImageGrid.cpp | 2 +- src/slic3r/GUI/Jobs/BindJob.cpp | 4 ++++ src/slic3r/GUI/Jobs/SendJob.cpp | 2 ++ src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp | 1 + src/slic3r/GUI/MainFrame.hpp | 1 + src/slic3r/GUI/MediaFilePanel.cpp | 3 +++ src/slic3r/GUI/MediaPlayCtrl.cpp | 6 ++++++ src/slic3r/GUI/Mouse3DController.cpp | 1 + src/slic3r/GUI/PartPlate.cpp | 1 + src/slic3r/GUI/PartSkipDialog.cpp | 1 + src/slic3r/GUI/Plater.hpp | 1 + src/slic3r/GUI/PluginsConfigDialog.cpp | 1 + src/slic3r/GUI/PluginsDialog.cpp | 1 + src/slic3r/GUI/Preferences.cpp | 1 + src/slic3r/GUI/PrivacyUpdateDialog.cpp | 1 + src/slic3r/GUI/RammingChart.cpp | 1 + src/slic3r/GUI/ReleaseNote.hpp | 1 + src/slic3r/GUI/SendMultiMachinePage.cpp | 1 + src/slic3r/GUI/SendMultiMachinePage.hpp | 3 +++ src/slic3r/GUI/TroubleshootDialog.cpp | 2 ++ src/slic3r/GUI/UserManager.cpp | 2 ++ src/slic3r/GUI/WebGuideDialog.hpp | 2 ++ src/slic3r/GUI/Widgets/CheckList.cpp | 1 + src/slic3r/GUI/Widgets/MultiNozzleSync.cpp | 2 ++ src/slic3r/GUI/Widgets/WebView.cpp | 8 +++++++- src/slic3r/GUI/WipeTowerDialog.cpp | 1 + src/slic3r/Utils/3DPrinterOS.cpp | 3 +++ src/slic3r/Utils/BBLCloudServiceAgent.cpp | 3 +++ src/slic3r/Utils/CalibUtils.cpp | 1 + src/slic3r/Utils/CloudProvider.hpp | 11 +++++++++++ src/slic3r/Utils/CrealityPrintAgent.cpp | 2 ++ src/slic3r/Utils/ICloudServiceAgent.hpp | 4 +--- src/slic3r/Utils/MoonrakerPrinterAgent.cpp | 1 + src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 1 + src/slic3r/Utils/PresetUpdater.cpp | 1 + src/slic3r/Utils/Process.cpp | 1 + src/slic3r/Utils/QidiPrinterAgent.cpp | 2 ++ src/slic3r/Utils/SnapmakerPrinterAgent.cpp | 2 ++ src/slic3r/plugin/PluginResolver.cpp | 1 + 73 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 src/slic3r/Utils/CloudProvider.hpp diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index bebd1aad5c..d3e24437fb 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -78,8 +78,9 @@ using namespace nlohmann; #include "libslic3r/ObjColorUtils.hpp" #include "OrcaSlicer.hpp" -//BBS: add exception handler for win32 +#include #include +//BBS: add exception handler for win32 #ifdef WIN32 #include "dev-utils/BaseException.h" #endif diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index c191e24eac..eb5ddb5d4e 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -1,6 +1,7 @@ #include "AMSDryControl.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" #include "GUI_App.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "I18N.hpp" #include "slic3r/GUI/DeviceCore/DevExtruderSystem.h" diff --git a/src/slic3r/GUI/AMSDryControl.hpp b/src/slic3r/GUI/AMSDryControl.hpp index 223fe137e2..6c5df86849 100644 --- a/src/slic3r/GUI/AMSDryControl.hpp +++ b/src/slic3r/GUI/AMSDryControl.hpp @@ -14,6 +14,7 @@ //Previous defintions class wxGrid; +class ProgressBar; namespace Slic3r { diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 68f1b44212..f0fdf950a0 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -2,6 +2,8 @@ #include "ExtrusionCalibration.hpp" #include "MsgDialog.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "libslic3r/Preset.hpp" #include "I18N.hpp" #include diff --git a/src/slic3r/GUI/BaseTransparentDPIFrame.hpp b/src/slic3r/GUI/BaseTransparentDPIFrame.hpp index 35ed51ddfe..7dc83b4d46 100644 --- a/src/slic3r/GUI/BaseTransparentDPIFrame.hpp +++ b/src/slic3r/GUI/BaseTransparentDPIFrame.hpp @@ -5,8 +5,10 @@ #include #include "GUI_App.hpp" #include "GUI_Utils.hpp" +#include class Button; +class Label; class CheckBox; namespace Slic3r { namespace GUI { class CapsuleButton; diff --git a/src/slic3r/GUI/CalibrationWizard.cpp b/src/slic3r/GUI/CalibrationWizard.cpp index 7496d59a51..f80562578d 100644 --- a/src/slic3r/GUI/CalibrationWizard.cpp +++ b/src/slic3r/GUI/CalibrationWizard.cpp @@ -1,6 +1,7 @@ #include "CalibrationWizard.hpp" #include "I18N.hpp" #include "GUI_App.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "MsgDialog.hpp" #include "CalibrationWizardPage.hpp" #include "../../libslic3r/calib.hpp" diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index c6d491a930..7a83d39dd3 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -1,5 +1,7 @@ #include #include "CalibrationWizardPresetPage.hpp" +#include "GUI.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "I18N.hpp" #include "Widgets/Label.hpp" #include "MsgDialog.hpp" diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.cpp b/src/slic3r/GUI/CalibrationWizardSavePage.cpp index f7699cfab0..427f022d1c 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.cpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.cpp @@ -1,4 +1,5 @@ #include "CalibrationWizardSavePage.hpp" +#include "GUI.hpp" #include "I18N.hpp" #include "Widgets/Label.hpp" #include "MsgDialog.hpp" diff --git a/src/slic3r/GUI/CapsuleButton.cpp b/src/slic3r/GUI/CapsuleButton.cpp index 8afe39889e..8d71f9911e 100644 --- a/src/slic3r/GUI/CapsuleButton.cpp +++ b/src/slic3r/GUI/CapsuleButton.cpp @@ -1,5 +1,6 @@ #include "GUI_App.hpp" #include "CapsuleButton.hpp" +#include "Widgets/StateColor.hpp" #include #include "wx/graphics.h" #include "Widgets/Label.hpp" diff --git a/src/slic3r/GUI/ColorDecomposeSupport.cpp b/src/slic3r/GUI/ColorDecomposeSupport.cpp index 6621b97059..ea67564208 100644 --- a/src/slic3r/GUI/ColorDecomposeSupport.cpp +++ b/src/slic3r/GUI/ColorDecomposeSupport.cpp @@ -1,4 +1,5 @@ #include "ColorDecomposeSupport.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "MixedFilamentDialog.hpp" #include "GUI_App.hpp" #include "MsgDialog.hpp" diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 8eab1c785c..5bd74e107d 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -2,6 +2,7 @@ #include "ConfigManipulation.hpp" #include "I18N.hpp" #include "GUI_App.hpp" +#include "DeviceCore/DevConfigUtil.h" #include "format.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Model.hpp" diff --git a/src/slic3r/GUI/DailyTips.cpp b/src/slic3r/GUI/DailyTips.cpp index d2f758bf5f..894c0316bb 100644 --- a/src/slic3r/GUI/DailyTips.cpp +++ b/src/slic3r/GUI/DailyTips.cpp @@ -1,4 +1,5 @@ #include "DailyTips.hpp" +#include "slic3r/GUI/Widgets/Label.hpp" #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS diff --git a/src/slic3r/GUI/DeviceCore/DevCalib.cpp b/src/slic3r/GUI/DeviceCore/DevCalib.cpp index cf7ee1d90e..ddf71e0c62 100644 --- a/src/slic3r/GUI/DeviceCore/DevCalib.cpp +++ b/src/slic3r/GUI/DeviceCore/DevCalib.cpp @@ -1,5 +1,7 @@ #include #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/UserNotification.hpp" #include "libslic3r/PrintConfig.hpp" diff --git a/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp b/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp index b59499d9d9..7ae990730a 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp @@ -4,6 +4,8 @@ #include #include "DevFilaBlackList.h" +#include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/GUI/DeviceManager.hpp" #include "DevFilaSystem.h" #include "DevManager.h" #include "DevConfigUtil.h" diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index e0a230969b..881aa75d20 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -1,5 +1,6 @@ #include #include "DevFilaSystem.h" +#include "slic3r/Utils/NetworkAgent.hpp" #include "DevNozzleSystem.h" // DevNozzle / DevNozzleSystem for GetNozzleFlowStringByAmsId // TODO: remove this include diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 782574c220..fd60f80d25 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -1,5 +1,7 @@ #include "libslic3r/libslic3r.h" #include "DeviceManager.hpp" +#include "HMS.hpp" +#include "I18N.hpp" #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" diff --git a/src/slic3r/GUI/DeviceTab/wgtMsgPanel.cpp b/src/slic3r/GUI/DeviceTab/wgtMsgPanel.cpp index 64195d97a8..16ae8812be 100644 --- a/src/slic3r/GUI/DeviceTab/wgtMsgPanel.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtMsgPanel.cpp @@ -1,6 +1,7 @@ #include "wgtMsgPanel.h" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/Widgets/Label.hpp" #include "slic3r/GUI/Widgets/StateColor.hpp" #include "slic3r/GUI/wxExtensions.hpp" diff --git a/src/slic3r/GUI/DragCanvas.cpp b/src/slic3r/GUI/DragCanvas.cpp index 04d51c0861..66a9acecbc 100644 --- a/src/slic3r/GUI/DragCanvas.cpp +++ b/src/slic3r/GUI/DragCanvas.cpp @@ -1,6 +1,7 @@ #include "DragCanvas.hpp" #include "wxExtensions.hpp" #include "GUI_App.hpp" +#include "Widgets/StateColor.hpp" namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/EncodedFilament.cpp b/src/slic3r/GUI/EncodedFilament.cpp index f9054e7f6d..0cab200386 100644 --- a/src/slic3r/GUI/EncodedFilament.cpp +++ b/src/slic3r/GUI/EncodedFilament.cpp @@ -1,7 +1,10 @@ #include "EncodedFilament.hpp" +#include #include "GUI_App.hpp" +using json = nlohmann::json; + namespace Slic3r { diff --git a/src/slic3r/GUI/ExportPresetBundleDialog.cpp b/src/slic3r/GUI/ExportPresetBundleDialog.cpp index 6d642ee1c1..9d2f26bf6a 100644 --- a/src/slic3r/GUI/ExportPresetBundleDialog.cpp +++ b/src/slic3r/GUI/ExportPresetBundleDialog.cpp @@ -1,4 +1,5 @@ #include "ExportPresetBundleDialog.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "GUI_App.hpp" #include "ConfigWizard.hpp" #include "I18N.hpp" @@ -12,7 +13,11 @@ #include #include #include +#include #include + +using json = nlohmann::json; + namespace Slic3r { namespace GUI { ExportPresetBundleDialog::ExportPresetBundleDialog( diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp index 18811ef241..3abfdb82ff 100644 --- a/src/slic3r/GUI/ExtraRenderers.cpp +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -1,6 +1,7 @@ #include "ExtraRenderers.hpp" #include "wxExtensions.hpp" #include "GUI.hpp" +#include "I18N.hpp" #include "BitmapComboBox.hpp" #include "Plater.hpp" #include "Widgets/ComboBox.hpp" diff --git a/src/slic3r/GUI/ExtrusionCalibration.cpp b/src/slic3r/GUI/ExtrusionCalibration.cpp index 933e2ac211..1f07823d61 100644 --- a/src/slic3r/GUI/ExtrusionCalibration.cpp +++ b/src/slic3r/GUI/ExtrusionCalibration.cpp @@ -1,5 +1,6 @@ #include "ExtrusionCalibration.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" #include "MsgDialog.hpp" #include "libslic3r/Preset.hpp" #include diff --git a/src/slic3r/GUI/FilamentMapPanel.cpp b/src/slic3r/GUI/FilamentMapPanel.cpp index 81117bc2a6..0f3cc7217d 100644 --- a/src/slic3r/GUI/FilamentMapPanel.cpp +++ b/src/slic3r/GUI/FilamentMapPanel.cpp @@ -1,5 +1,6 @@ #include "FilamentMapPanel.hpp" #include "GUI_App.hpp" +#include "I18N.hpp" #include "Plater.hpp" #include "Widgets/MultiNozzleSync.hpp" // manuallySetNozzleCount producer for extruder_nozzle_stats #include diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index d670181b1b..fbdb308c56 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -9,6 +9,7 @@ #include "3DScene.hpp" #include "OpenGLManager.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" #include "GLModel.hpp" #include @@ -31,6 +32,7 @@ #include "GUI_App.hpp" #include #include +#include namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index fee18b4799..966cf49013 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3,6 +3,14 @@ #include "libslic3r/Technologies.hpp" #include "libslic3r/Platform.hpp" #include "GUI_App.hpp" +#include "BindDialog.hpp" +#include "DeviceManager.hpp" +#include "HMS.hpp" +#include "PresetBundleDialog.hpp" +#include "WebUserLoginDialog.hpp" +#include "WebViewDialog.hpp" +#include "slic3r/Utils/BBLCloudServiceAgent.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "GUI_Init.hpp" #include "GUI_ObjectList.hpp" #include "slic3r/GUI/UserManager.hpp" diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 2569e10271..f6f0b81c92 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -1,23 +1,17 @@ #ifndef slic3r_GUI_App_hpp_ #define slic3r_GUI_App_hpp_ +#include #include #include #include "ActionRegistry.hpp" #include "ImGuiWrapper.hpp" #include "ConfigWizard.hpp" #include "OpenGLManager.hpp" -#include "PresetBundleDialog.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" -#include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/UserNotification.hpp" -#include "slic3r/Utils/NetworkAgent.hpp" -#include "slic3r/Utils/BBLCloudServiceAgent.hpp" -#include "slic3r/GUI/WebViewDialog.hpp" -#include "slic3r/GUI/WebUserLoginDialog.hpp" -#include "slic3r/GUI/BindDialog.hpp" -#include "slic3r/GUI/HMS.hpp" +#include "slic3r/Utils/CloudProvider.hpp" #include "slic3r/GUI/Jobs/UpgradeNetworkJob.hpp" #include "slic3r/GUI/HttpServer.hpp" #include "../Utils/PrintHost.hpp" @@ -64,9 +58,14 @@ class ModelObject; class Model; class UserManager; class DeviceManager; +class MachineObject; class NetworkAgent; +class IPrinterAgent; class TaskManager; +// Same typedef as in bambu_networking.hpp, so this header need not include it. +typedef std::function WasCancelledFn; + namespace GUI{ class RemovableDriveManager; @@ -85,6 +84,8 @@ class ParamsDialog; class HMSQuery; class ModelMallDialog; class PingCodeBindDialog; +class PresetBundleDialog; +class ZUserLogin; class NetworkErrorDialog; class PluginsDialog; class SpeedDialWebDialog; @@ -829,7 +830,7 @@ wxDECLARE_EVENT(EVT_UPDATE_BUNDLE_COMPLETE, wxCommandEvent); bool is_support_filament(int extruder_id, bool strict_check = true); bool is_soluble_filament(int extruder_id); // check if the filament for model is in the list -bool has_filaments(const std::vector& model_filaments); +bool has_filaments(const std::vector& model_filaments); } // namespace GUI } // Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp index 012e62b2bc..99ad00fe55 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAdvancedCut.cpp @@ -1,5 +1,7 @@ // Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. #include "GLGizmoAdvancedCut.hpp" +#include "slic3r/GUI/Widgets/ProgressDialog.hpp" +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp index 904e7d0a07..6d9e810815 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBrimEars.cpp @@ -1,6 +1,7 @@ #include "GLGizmoBrimEars.hpp" #include #include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" #include "slic3r/GUI/GUI_App.hpp" diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp index 6689fcdcea..d807e466c8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFuzzySkin.cpp @@ -4,6 +4,7 @@ #include "libslic3r/Print.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp index e21498163a..6f7d6fed58 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp @@ -1,4 +1,5 @@ #include "GLGizmoMeasure.hpp" +#include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" diff --git a/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp b/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp index 00d608b80a..75dca0855d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoUtils.cpp @@ -32,6 +32,8 @@ */ +using namespace std::string_view_literals; + namespace Slic3r::GUI::GLGizmoUtils { void render_tooltip_button( diff --git a/src/slic3r/GUI/HttpServer.cpp b/src/slic3r/GUI/HttpServer.cpp index afdc46e9f0..ef26f20173 100644 --- a/src/slic3r/GUI/HttpServer.cpp +++ b/src/slic3r/GUI/HttpServer.cpp @@ -4,6 +4,10 @@ #include "slic3r/Utils/Http.hpp" #include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/Utils/BBLNetworkPlugin.hpp" +#include "libslic3r/Thread.hpp" +#include + +using json = nlohmann::json; namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/IMSlider.cpp b/src/slic3r/GUI/IMSlider.cpp index 0d0d6739f8..fa777b6a37 100644 --- a/src/slic3r/GUI/IMSlider.cpp +++ b/src/slic3r/GUI/IMSlider.cpp @@ -1,6 +1,7 @@ #include "IMSlider.hpp" #include "libslic3r/GCode.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" #include "NotificationManager.hpp" #include "Widgets/StateColor.hpp" #ifndef IMGUI_DEFINE_MATH_OPERATORS diff --git a/src/slic3r/GUI/ImageDPIFrame.hpp b/src/slic3r/GUI/ImageDPIFrame.hpp index 817ef6be18..c22d296492 100644 --- a/src/slic3r/GUI/ImageDPIFrame.hpp +++ b/src/slic3r/GUI/ImageDPIFrame.hpp @@ -3,6 +3,8 @@ #include "GUI_App.hpp" #include "GUI_Utils.hpp" +#include +#include class wxStaticBitmap; namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/ImageGrid.cpp b/src/slic3r/GUI/ImageGrid.cpp index abef6f0f12..f6bf25d77c 100644 --- a/src/slic3r/GUI/ImageGrid.cpp +++ b/src/slic3r/GUI/ImageGrid.cpp @@ -521,7 +521,7 @@ void ImageGrid::render(wxDC& dc) if (!m_status_msg.IsEmpty()) { auto si = m_status_icon.GetBmpSize(); auto st = dc.GetMultiLineTextExtent(m_status_msg); - auto rect = wxRect{0, 0, max(st.x, si.x), si.y + 26 + st.y}.CenterIn(wxRect({0, 0}, size)); + auto rect = wxRect{0, 0, std::max(st.x, si.x), si.y + 26 + st.y}.CenterIn(wxRect({0, 0}, size)); dc.DrawBitmap(m_status_icon.bmp(), rect.x + (rect.width - si.x) / 2, rect.y); dc.SetTextForeground(wxColor(0x909090)); dc.DrawText(m_status_msg, rect.x + (rect.width - st.x) / 2, rect.GetBottom() - st.y); diff --git a/src/slic3r/GUI/Jobs/BindJob.cpp b/src/slic3r/GUI/Jobs/BindJob.cpp index 61c430c6e6..76af712f63 100644 --- a/src/slic3r/GUI/Jobs/BindJob.cpp +++ b/src/slic3r/GUI/Jobs/BindJob.cpp @@ -3,6 +3,10 @@ #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceManager.hpp" +#include "slic3r/GUI/HMS.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "slic3r/GUI/DeviceCore/DevManager.h" diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index 67ce02b476..d27b18f24b 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -1,4 +1,6 @@ #include "SendJob.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" diff --git a/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp b/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp index 6cd88ac5d3..7090f0e2e0 100644 --- a/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp +++ b/src/slic3r/GUI/Jobs/UpgradeNetworkJob.cpp @@ -2,6 +2,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" #include "slic3r/Utils/Http.hpp" namespace Slic3r { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 44e0547f50..5340115609 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -66,6 +66,7 @@ class Tab; class PrintHostQueueDialog; class Plater; class MainFrame; +class WebViewPanel; class ParamsDialog; #ifdef __WXGTK__ class ResizeEdgePanel; diff --git a/src/slic3r/GUI/MediaFilePanel.cpp b/src/slic3r/GUI/MediaFilePanel.cpp index 36316f8ff5..e9e1f56b03 100644 --- a/src/slic3r/GUI/MediaFilePanel.cpp +++ b/src/slic3r/GUI/MediaFilePanel.cpp @@ -2,6 +2,9 @@ #include "ImageGrid.h" #include "I18N.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" +#include "DeviceManager.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "Plater.hpp" #include "Widgets/Button.hpp" #include "Widgets/SwitchButton.hpp" diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 29c8c9f664..557d859cf7 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -3,6 +3,11 @@ #include "Widgets/CheckBox.hpp" #include "Widgets/Label.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" +#include "DeviceManager.hpp" +#include "DeviceCore/DevConfigUtil.h" +#include "slic3r/Utils/NetworkAgent.hpp" +#include "libslic3r/Thread.hpp" #include "libslic3r/AppConfig.hpp" #include "I18N.hpp" #include "MsgDialog.hpp" @@ -13,6 +18,7 @@ #include #include #include +#include #include #undef pid_t #include diff --git a/src/slic3r/GUI/Mouse3DController.cpp b/src/slic3r/GUI/Mouse3DController.cpp index 8ed91d461f..0317342412 100644 --- a/src/slic3r/GUI/Mouse3DController.cpp +++ b/src/slic3r/GUI/Mouse3DController.cpp @@ -1,6 +1,7 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" #include "Mouse3DController.hpp" +#include "GUI.hpp" #include "Camera.hpp" #include "GUI_App.hpp" diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index d38ab5e0ac..c9370cc282 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "libslic3r/libslic3r.h" diff --git a/src/slic3r/GUI/PartSkipDialog.cpp b/src/slic3r/GUI/PartSkipDialog.cpp index b9dd7d5007..9bd6687757 100644 --- a/src/slic3r/GUI/PartSkipDialog.cpp +++ b/src/slic3r/GUI/PartSkipDialog.cpp @@ -1,5 +1,6 @@ #include "GUI_Utils.hpp" #include "GUI_App.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include #include #include diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 26cd06978b..84e64acea0 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -42,6 +42,7 @@ class Button; namespace Slic3r { class BuildVolume; +class MachineObject; enum class BuildVolume_Type : char; class Model; class ModelObject; diff --git a/src/slic3r/GUI/PluginsConfigDialog.cpp b/src/slic3r/GUI/PluginsConfigDialog.cpp index 0241b47b8c..9a79588af1 100644 --- a/src/slic3r/GUI/PluginsConfigDialog.cpp +++ b/src/slic3r/GUI/PluginsConfigDialog.cpp @@ -1,6 +1,7 @@ #include "PluginsConfigDialog.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" #include "I18N.hpp" #include "format.hpp" diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp index a39fcbc535..2bac7eddca 100644 --- a/src/slic3r/GUI/PluginsDialog.cpp +++ b/src/slic3r/GUI/PluginsDialog.cpp @@ -2,6 +2,7 @@ #include "GUI.hpp" #include "GUI_App.hpp" +#include "slic3r/Utils/NetworkAgent.hpp" #include "I18N.hpp" #include "OrcaCloudServiceAgent.hpp" #include "slic3r/plugin/PluginConfig.hpp" diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 7d80147efa..73f3a2c90a 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -2,6 +2,7 @@ #include "OptionsGroup.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" +#include "WebViewDialog.hpp" #include "Plater.hpp" #include "GLCanvas3D.hpp" // ORCA: for live preview refresh when toggling "Dim lower layers" #include "MsgDialog.hpp" diff --git a/src/slic3r/GUI/PrivacyUpdateDialog.cpp b/src/slic3r/GUI/PrivacyUpdateDialog.cpp index 92d6d6c8c7..c417767a42 100644 --- a/src/slic3r/GUI/PrivacyUpdateDialog.cpp +++ b/src/slic3r/GUI/PrivacyUpdateDialog.cpp @@ -1,5 +1,6 @@ #include "PrivacyUpdateDialog.hpp" #include "GUI_App.hpp" +#include "GUI.hpp" #include "BitmapCache.hpp" #include #include diff --git a/src/slic3r/GUI/RammingChart.cpp b/src/slic3r/GUI/RammingChart.cpp index 96cd3b65a7..29116b12cb 100644 --- a/src/slic3r/GUI/RammingChart.cpp +++ b/src/slic3r/GUI/RammingChart.cpp @@ -7,6 +7,7 @@ #include "GUI.hpp" #include "GUI_App.hpp" #include "I18N.hpp" +#include "Widgets/StateColor.hpp" wxDEFINE_EVENT(EVT_WIPE_TOWER_CHART_CHANGED, wxCommandEvent); diff --git a/src/slic3r/GUI/ReleaseNote.hpp b/src/slic3r/GUI/ReleaseNote.hpp index 0c11dc2f58..cfd372bc97 100644 --- a/src/slic3r/GUI/ReleaseNote.hpp +++ b/src/slic3r/GUI/ReleaseNote.hpp @@ -35,6 +35,7 @@ #include "Widgets/CheckBox.hpp" #include "Widgets/ComboBox.hpp" #include "Widgets/ScrolledWindow.hpp" +#include "Widgets/HyperLink.hpp" #include #include diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index 2d1b713264..cafbb360e6 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -3,6 +3,7 @@ #include "I18N.hpp" #include "GUI_App.hpp" +#include "slic3r/Utils/bambu_networking.hpp" #include "MainFrame.hpp" #include "Widgets/RadioBox.hpp" #include diff --git a/src/slic3r/GUI/SendMultiMachinePage.hpp b/src/slic3r/GUI/SendMultiMachinePage.hpp index a63bc51bb0..eadd78eb0a 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.hpp +++ b/src/slic3r/GUI/SendMultiMachinePage.hpp @@ -15,6 +15,9 @@ #include "SelectMachine.hpp" namespace Slic3r { + +struct PrintParams; + namespace GUI { #define SEND_LEFT_PADDING_LEFT 15 #define SEND_LEFT_PRINTABLE 40 diff --git a/src/slic3r/GUI/TroubleshootDialog.cpp b/src/slic3r/GUI/TroubleshootDialog.cpp index 5acb75c6ea..7cbb00b5d8 100644 --- a/src/slic3r/GUI/TroubleshootDialog.cpp +++ b/src/slic3r/GUI/TroubleshootDialog.cpp @@ -6,6 +6,8 @@ #include "GUI_App.hpp" #include "MainFrame.hpp" +#include +#include #include #include #include "wx/clipbrd.h" diff --git a/src/slic3r/GUI/UserManager.cpp b/src/slic3r/GUI/UserManager.cpp index 456582e896..e874c2158d 100644 --- a/src/slic3r/GUI/UserManager.cpp +++ b/src/slic3r/GUI/UserManager.cpp @@ -1,9 +1,11 @@ #include "libslic3r/libslic3r.h" #include "UserManager.hpp" #include "DeviceManager.hpp" +#include "BindDialog.hpp" #include "NetworkAgent.hpp" #include "GUI.hpp" #include "GUI_App.hpp" +#include "I18N.hpp" #include "MsgDialog.hpp" #include "DeviceCore/DevManager.h" diff --git a/src/slic3r/GUI/WebGuideDialog.hpp b/src/slic3r/GUI/WebGuideDialog.hpp index c4cfc8bf6d..1ad60175ae 100644 --- a/src/slic3r/GUI/WebGuideDialog.hpp +++ b/src/slic3r/GUI/WebGuideDialog.hpp @@ -43,6 +43,8 @@ namespace Slic3r { namespace GUI { class GuideFrame : public DPIDialog { public: + using json = nlohmann::json; + GuideFrame(GUI_App *pGUI, long style = wxCAPTION | wxCLOSE_BOX | wxSYSTEM_MENU); virtual ~GuideFrame(); diff --git a/src/slic3r/GUI/Widgets/CheckList.cpp b/src/slic3r/GUI/Widgets/CheckList.cpp index cd0dcebff3..c0101441ff 100644 --- a/src/slic3r/GUI/Widgets/CheckList.cpp +++ b/src/slic3r/GUI/Widgets/CheckList.cpp @@ -1,6 +1,7 @@ #include "CheckList.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" CheckList::CheckList( wxWindow* parent, diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 20944df553..9278565c47 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -20,6 +20,8 @@ #include #include +#include +#include #include #include diff --git a/src/slic3r/GUI/Widgets/WebView.cpp b/src/slic3r/GUI/Widgets/WebView.cpp index e281d97407..a29a3cb725 100644 --- a/src/slic3r/GUI/Widgets/WebView.cpp +++ b/src/slic3r/GUI/Widgets/WebView.cpp @@ -1,9 +1,13 @@ #include "WebView.hpp" +#include "slic3r/GUI/Widgets/StateColor.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/Utils/MacDarkMode.hpp" #include +#include +#include + #include #include #if wxUSE_WEBVIEW_EDGE @@ -12,6 +16,8 @@ #include #endif #include +#include +#include #if defined(__WIN32__) || defined(__WXMAC__) #include "wx/private/jsscriptwrapper.h" #endif @@ -73,7 +79,7 @@ DWORD DownloadAndInstallWV2RT() { }) .perform_sync(); // Sleep for 1 second to wait for the buffer writen into disk - std::this_thread::sleep_for(1000ms); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); if (downloaded) { // Either Package the WebView2 Bootstrapper with your app or download it using fwlink // Then invoke install at Runtime. diff --git a/src/slic3r/GUI/WipeTowerDialog.cpp b/src/slic3r/GUI/WipeTowerDialog.cpp index d4fbcc6fe3..9a473c7c8a 100644 --- a/src/slic3r/GUI/WipeTowerDialog.cpp +++ b/src/slic3r/GUI/WipeTowerDialog.cpp @@ -6,6 +6,7 @@ #include "GUI.hpp" #include "I18N.hpp" #include "GUI_App.hpp" +#include "WebViewDialog.hpp" #include "MsgDialog.hpp" #include "format.hpp" #include "libslic3r/Color.hpp" diff --git a/src/slic3r/Utils/3DPrinterOS.cpp b/src/slic3r/Utils/3DPrinterOS.cpp index 61fcc80d5b..503dbe63e3 100755 --- a/src/slic3r/Utils/3DPrinterOS.cpp +++ b/src/slic3r/Utils/3DPrinterOS.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,8 @@ #include +using json = nlohmann::json; + namespace fs = boost::filesystem; namespace pt = boost::property_tree; diff --git a/src/slic3r/Utils/BBLCloudServiceAgent.cpp b/src/slic3r/Utils/BBLCloudServiceAgent.cpp index 846e4ce509..801ad1cf57 100644 --- a/src/slic3r/Utils/BBLCloudServiceAgent.cpp +++ b/src/slic3r/Utils/BBLCloudServiceAgent.cpp @@ -8,6 +8,9 @@ #include #include #include + +using json = nlohmann::json; + namespace Slic3r { diff --git a/src/slic3r/Utils/CalibUtils.cpp b/src/slic3r/Utils/CalibUtils.cpp index 499228d13c..25aad85d2f 100644 --- a/src/slic3r/Utils/CalibUtils.cpp +++ b/src/slic3r/Utils/CalibUtils.cpp @@ -3,6 +3,7 @@ #include "../GUI/GUI_App.hpp" #include "../GUI/DeviceCore/DevStorage.h" #include "../GUI/DeviceManager.hpp" +#include "NetworkAgent.hpp" #include "../GUI/Jobs/ProgressIndicator.hpp" #include "../GUI/PartPlate.hpp" #include "libslic3r/CutUtils.hpp" diff --git a/src/slic3r/Utils/CloudProvider.hpp b/src/slic3r/Utils/CloudProvider.hpp new file mode 100644 index 0000000000..0f03683222 --- /dev/null +++ b/src/slic3r/Utils/CloudProvider.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace Slic3r { + +// Identifiers of the cloud services an ICloudServiceAgent can stand for. +static const std::string ORCA_CLOUD_PROVIDER("orca"); +static const std::string BBL_CLOUD_PROVIDER("bbl"); + +} // namespace Slic3r diff --git a/src/slic3r/Utils/CrealityPrintAgent.cpp b/src/slic3r/Utils/CrealityPrintAgent.cpp index 9b3bd5843e..f340a61267 100644 --- a/src/slic3r/Utils/CrealityPrintAgent.cpp +++ b/src/slic3r/Utils/CrealityPrintAgent.cpp @@ -12,6 +12,8 @@ #include #include +using json = nlohmann::json; + namespace Slic3r { namespace { diff --git a/src/slic3r/Utils/ICloudServiceAgent.hpp b/src/slic3r/Utils/ICloudServiceAgent.hpp index 556c253641..7d326eb4c6 100644 --- a/src/slic3r/Utils/ICloudServiceAgent.hpp +++ b/src/slic3r/Utils/ICloudServiceAgent.hpp @@ -2,6 +2,7 @@ #define __I_CLOUD_SERVICE_AGENT_HPP__ #include "bambu_networking.hpp" +#include "CloudProvider.hpp" #include "../../libslic3r/ProjectTask.hpp" #include #include @@ -37,9 +38,6 @@ namespace Slic3r { * implementation. */ -static const std::string ORCA_CLOUD_PROVIDER("orca"); -static const std::string BBL_CLOUD_PROVIDER("bbl"); - struct CloudEvent { std::string provider; // ORCA_CLOUD_PROVIDER or BBL_CLOUD_PROVIDER }; diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index 571d707a9f..384a69d51e 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/DeviceCore/DevFilaSystem.h" #include "slic3r/GUI/DeviceCore/DevManager.h" #include "../GUI/DeviceCore/DevStorage.h" diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index 4bfe429cd3..5d129a490e 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 032f9dbf7a..69bfa4217e 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -42,6 +42,7 @@ #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/Utils/Http.hpp" +#include "slic3r/Utils/bambu_networking.hpp" #include "slic3r/Config/Version.hpp" #include "slic3r/Config/Snapshot.hpp" #include "slic3r/GUI/MarkdownTip.hpp" diff --git a/src/slic3r/Utils/Process.cpp b/src/slic3r/Utils/Process.cpp index 518462bbc9..96da521114 100644 --- a/src/slic3r/Utils/Process.cpp +++ b/src/slic3r/Utils/Process.cpp @@ -21,6 +21,7 @@ #include #endif +#include #include namespace Slic3r { diff --git a/src/slic3r/Utils/QidiPrinterAgent.cpp b/src/slic3r/Utils/QidiPrinterAgent.cpp index 6b05480194..1f437853ba 100644 --- a/src/slic3r/Utils/QidiPrinterAgent.cpp +++ b/src/slic3r/Utils/QidiPrinterAgent.cpp @@ -9,6 +9,8 @@ #include #include +using json = nlohmann::json; + namespace Slic3r { namespace { diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index ab7aa9bd52..5783738af7 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -6,6 +6,8 @@ #include "nlohmann/json.hpp" #include +using json = nlohmann::json; + namespace Slic3r { namespace { diff --git a/src/slic3r/plugin/PluginResolver.cpp b/src/slic3r/plugin/PluginResolver.cpp index 4ba58ee550..6e72929c63 100644 --- a/src/slic3r/plugin/PluginResolver.cpp +++ b/src/slic3r/plugin/PluginResolver.cpp @@ -3,6 +3,7 @@ #include "PluginManager.hpp" #include "../Utils/Http.hpp" #include "../Utils/OrcaCloudServiceAgent.hpp" +#include "../Utils/NetworkAgent.hpp" #include "../GUI/GUI.hpp" #include "../GUI/GUI_App.hpp" #include "../GUI/I18N.hpp" From db9163ec34cdd4080f64ff8bcde24eff0d9c9072 Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:26:11 -0300 Subject: [PATCH 31/57] Set the CMake policy CMP0177 (#15657) Update CMakeLists.txt --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5620875d4f..85ee9c4232 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,10 @@ endif() cmake_minimum_required(VERSION 3.13) +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) +endif() + # The following line used to be in tests/CMakeLists.txt # Having it there causes rebuilds of all targets on any CMakeLists.txt change under tests/ From c5965fa4d9edc9cfebdaba4dfbdfb3a551f3a888 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:31:28 +0300 Subject: [PATCH 32/57] Fix: clear stale paths when merging perimeter regions (#15662) --- src/libslic3r/Layer.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 1d6c2b0703..b7ec08f856 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -187,6 +187,12 @@ void Layer::make_perimeters() { BOOST_LOG_TRIVIAL(trace) << "Generating perimeters for layer " << this->id(); + const auto clear_generated_extrusions = [](LayerRegion *layer_region) { + layer_region->perimeters.clear(); + layer_region->fills.clear(); + layer_region->thin_fills.clear(); + }; + // keep track of regions whose perimeters we have already generated std::vector done(m_regions.size(), false); @@ -217,13 +223,11 @@ void Layer::make_perimeters() if (this_region.gradient_volume_id() != other_region.gradient_volume_id()) continue; if (is_perimeter_compatible(*m_object->print(), this_region, other_region)) - { - other_layerm->perimeters.clear(); - other_layerm->fills.clear(); - other_layerm->thin_fills.clear(); - layerms.push_back(other_layerm); - done[it - m_regions.begin()] = true; - } + { + clear_generated_extrusions(other_layerm); + layerms.push_back(other_layerm); + done[it - m_regions.begin()] = true; + } } if (layerms.size() == 1) { // optimization @@ -231,6 +235,10 @@ void Layer::make_perimeters() (*layerm)->make_perimeters((*layerm)->slices, {*layerm}, &(*layerm)->fill_surfaces, &(*layerm)->fill_no_overlap_expolygons); (*layerm)->fill_expolygons = to_expolygons((*layerm)->fill_surfaces.surfaces); } else { + // Orca: Unlike the compatible regions above, the initiating region has not + // been cleared yet and may contain paths from a previous incompatible run. + clear_generated_extrusions(*layerm); + SurfaceCollection new_slices; // Use the region with highest infill rate, as the make_perimeters() function below decides on the gap fill based on the infill existence. LayerRegion *layerm_config = layerms.front(); From fe0d47c7a340362d57ed90911cb732d55b50bce0 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sat, 12 Sep 2026 13:04:44 -0500 Subject: [PATCH 33/57] feat(issues): add a crash report template (#15524) --- .github/ISSUE_TEMPLATE/bug_report.yml | 39 +++-- .github/ISSUE_TEMPLATE/crash_report.yml | 183 ++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/crash_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 63f74a069e..6019c2bc8b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: 🐞 Bug Report -description: File a bug report +description: Something behaves incorrectly while Orca Slicer keeps running labels: ["bug"] body: - type: markdown @@ -10,6 +10,8 @@ body: Please note that this is not the place to make feature requests or ask for help. For this, please use the [Feature request](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=feature_request.yml) issue type or you can discuss your idea on our [Discord server](https://discord.gg/P4VE9UY9gJ) with others. + If Orca Slicer closes on its own, freezes or stops responding, please use the [Crash report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=crash_report.yml) form instead. It asks for the logs a crash needs. + Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment. - type: checkboxes attributes: @@ -47,7 +49,7 @@ body: id: os_type attributes: label: "Operating System (OS)" - description: "What OSes are you are experiencing issues on?" + description: "What OSes are you experiencing issues on?" multiple: true options: - Linux @@ -86,7 +88,7 @@ body: id: reproduce_steps attributes: label: How to reproduce - description: Please described the detailed steps to reproduce this issue + description: Please describe the detailed steps to reproduce this issue placeholder: | 1. Go to '...' 2. Click on '...' @@ -108,28 +110,23 @@ body: description: What should happen after the above steps? validations: required: true - - type: markdown - id: file_required - attributes: - value: | - Please be sure to add the following files: - * Please upload a ZIP archive containing the **project file** used when the problem arise. Please export it just before or after the problem occurs. Even if you did nothing and/or there is no object, export it! (We need the configurations in project file). - You can export the project file from the application menu in `File`->`Save project as...`, then zip it - * A **log file** for crashes and similar issues. - You can find your log file here: - Windows: `%APPDATA%\OrcaSlicer\log` or usually `C:\Users\\AppData\Roaming\OrcaSlicer\log` - MacOS: `$HOME/Library/Application Support/OrcaSlicer/log` - Linux: `$HOME/.config/OrcaSlicer/log` - If Orca Slicer still starts, you can also reach this directory from the application menu in `Help` -> `Show Configuration Folder` - You can zip the log directory, or just select the newest logs when this issue happens, and zip them - type: textarea id: file_uploads attributes: label: Project file & Debug log uploads - description: Drop the project file and debug log here + description: | + Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB. + + * The **project file** used when the problem happened, zipped. Export it just before or after the problem occurs. Even if you did nothing and there is no object on the plate, export it, since we need the configuration it carries. `File` -> `Save project as...` + * The **log folder**, zipped. `Help` -> `Show Configuration Folder` opens it, or find it at: + * Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\\AppData\Roaming\OrcaSlicer\log` + * macOS: `$HOME/Library/Application Support/OrcaSlicer/log` + * Linux: `$HOME/.config/OrcaSlicer/log` + * Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log` + * If the zip comes out over 25 MB, attach the newest logs from that folder on their own instead. placeholder: | - Project File: `File` -> `Save project as...` then zip it & drop it here - Log File: `Help` -> `Show Configuration Folder`, then zip the log directory, or just select the newest logs in `log` when this issue happens and zip them, then drop the zip file here + Zipped project file + Zipped log folder validations: required: true - type: checkboxes @@ -144,7 +141,5 @@ body: label: Anything else? description: | Screenshots? References? Anything that will give us more context about the issue you are encountering! - - Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/crash_report.yml b/.github/ISSUE_TEMPLATE/crash_report.yml new file mode 100644 index 0000000000..bcbee11d36 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash_report.yml @@ -0,0 +1,183 @@ +name: 💥 Crash Report +description: Orca Slicer closes on its own, freezes or stops responding +labels: ["crash"] +body: + - type: markdown + attributes: + value: | + **Thank you for taking the time to report a crash.** + + Use this form when Orca Slicer closes on its own, freezes, or stops responding. + If the application stays open and only produces a wrong result, please use the [Bug report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=bug_report.yml) form instead. + A printer whose toolhead collides with the print is also a bug report rather than a crash, since the application itself did not stop. + + Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment. + - type: checkboxes + attributes: + label: Is this crash reproducible in the latest nightly build? + description: > + Please verify this crash still happens in the latest nightly build first. It may already be fixed there: + [Nightly builds](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds). + options: + - label: I have checked the latest nightly build and the crash is still reproducible + required: true + - type: checkboxes + attributes: + label: Is there an existing issue for this crash? + description: Please search to see if an issue already exists for the crash you encountered. + options: + - label: I have searched the existing issues + required: true + - type: input + id: version + attributes: + label: OrcaSlicer Version + description: Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`. + placeholder: e.g. 2.5.0 + validations: + required: true + - type: input + id: working_version + attributes: + label: Regression compared to a previous version + description: Did it work in a previous version? + placeholder: e.g. 2.3.2 + validations: + required: false + - type: dropdown + id: os_type + attributes: + label: "Operating System (OS)" + description: "What OSes are you seeing the crash on?" + multiple: true + options: + - Linux + - macOS + - Windows + validations: + required: true + - type: input + id: os_version + attributes: + label: "OS Version" + description: "What OS version does this relate to?" + placeholder: "i.e. OS: Windows 7/8/10/11 ..., Ubuntu 22.04/Fedora 36 ..., macOS 10.15/11.1/12.3 ..." + validations: + required: true + - type: input + id: printer + attributes: + label: Printer + description: Which printer was selected + placeholder: Voron 2.4/VzBot/Prusa MK4/Bambu Lab X1 series/Bambu Lab P1P/... + validations: + required: true + - type: dropdown + id: crash_moment + attributes: + label: When does the crash happen? + description: Pick the point where Orca Slicer stops working. + options: + - Not sure + - On startup, before the main window appears + - When opening or importing a project or model + - While changing printer, filament or process settings + - While slicing + - In the 3D view, Preview or Assembly view + - When exporting G-code or sending a print to the printer + - On the Device tab, or connecting to a printer (camera, sync, login) + - While using a specific tool, dialog or calibration + - After resuming from sleep or changing monitors + - When closing the application + - No clear pattern + validations: + required: true + - type: dropdown + id: crash_frequency + attributes: + label: How often does it happen? + options: + - Not sure + - Every time + - Often, but not every time + - Rarely + - It only happened once + validations: + required: true + - type: dropdown + id: fresh_config + attributes: + label: Does it still crash with a fresh configuration? + description: > + Close Orca Slicer and rename your configuration folder (`%APPDATA%\OrcaSlicer` on Windows, + `$HOME/Library/Application Support/OrcaSlicer` on macOS, `$HOME/.config/OrcaSlicer` on Linux), + then start it again. Renaming keeps your settings, so you can put the folder back afterwards. + options: + - I have not tried this + - Yes, it still crashes + - No, the crash goes away + validations: + required: true + - type: textarea + id: reproduce_steps + attributes: + label: How to reproduce + description: Please describe the detailed steps that lead to the crash. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. Orca Slicer closes + validations: + required: true + - type: textarea + id: system_info + attributes: + label: Additional system information + description: > + Display card and driver version are worth adding for crashes on startup or in the 3D view. + CPU and memory are worth adding for crashes while slicing. + placeholder: | + CPU: 11th gen Intel r core tm i7-1185g7/AMD Ryzen 7 6800h/... + Memory: 32/16 GB... + Display Card: NVIDIA Quadro P400/... + validations: + required: false + - type: textarea + id: file_uploads + attributes: + label: Project file, logs and crash report uploads + description: | + A crash report without logs usually cannot be acted on. Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB. + + * The **project file** used when the crash happened, zipped. Export it just before or after the crash, even if the plate is empty, since we need the configuration it carries. `File` -> `Save project as...` + * The whole **log folder**, zipped rather than single files picked out of it. `Help` -> `Show Configuration Folder` opens it, or find it at: + * Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\\AppData\Roaming\OrcaSlicer\log` + * macOS: `$HOME/Library/Application Support/OrcaSlicer/log` + * Linux: `$HOME/.config/OrcaSlicer/log` + * Flatpak: `$HOME/.var/app/com.orcaslicer.OrcaSlicer/config/OrcaSlicer/log` + * On Windows the crash itself is written to a separate `crash_*.log` in there, and that is the file we need most. If the zip comes out over 25 MB GitHub will refuse it, so attach the newest log and any `crash_*.log` on their own instead. + * The **operating system crash report**, on macOS and Linux, where Orca Slicer cannot write its own crash log. It is often the only record of where it died: + * macOS: Console.app -> Crash Reports, or `$HOME/Library/Logs/DiagnosticReports/`. The file starts with `OrcaSlicer` and ends in `.ips`. Zip it before attaching, GitHub does not accept `.ips` files. + * Linux: run `orca-slicer` from a terminal (Flatpak: `flatpak run com.orcaslicer.OrcaSlicer`) and paste everything it prints when it dies. On systemd systems `coredumpctl info orca-slicer` gives a backtrace. + placeholder: | + Zipped project file + Zipped log folder + Zipped macOS .ips crash report, or the terminal output on Linux + validations: + required: true + - type: checkboxes + id: file_checklist + attributes: + label: Checklist of files to include + options: + - label: Log folder + - label: Project file + - label: Operating system crash report (macOS and Linux) + - type: textarea + attributes: + label: Anything else? + description: | + Screenshots? References? Anything that will give us more context about the crash you are encountering! + validations: + required: false From bb8c2ae5ce9db94a6262455a89112442e92991cb Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sat, 12 Sep 2026 13:52:05 -0500 Subject: [PATCH 34/57] build: enable -Werror with a documented exception list (#15660) --- CMakeLists.txt | 143 +++++++++++++++++++++++++++++++------------------ 1 file changed, 91 insertions(+), 52 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85ee9c4232..d2880a7d4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -557,59 +557,101 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR elseif (NOT MINGW) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" ) endif () - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder" ) - # On GCC and Clang, no return from a non-void function is a warning only. Here, we make it an error. - add_compile_options(-Werror=return-type) + # Every warning is an error unless it appears in one of the two lists below. + # disabled - never wanted. Off everywhere, so it never warns or errors. + # demoted - wanted, not cleared yet. Still warns, does not error. - # Since some portions of code are just commented out or put under conditional compilation, there are - # a bunch of warning related to unused functions and variables. Suppress those warnings to not pollute - # compilers diagnostics output with warnings we not going to look at - add_compile_options(-Wno-unused-function -Wno-unused-variable -Wno-unused-but-set-variable -Wno-unused-label -Wno-unused-local-typedefs) + # Disabled. + set(warnings_disabled + reorder # members initialised in an order we chose + sign-compare # signed/unsigned comparisons throughout + misleading-indentation # false positives on mixed tabs and spaces + switch # unhandled enum value in a switch + unused-function # commented-out or conditionally compiled code + unused-variable # commented-out or conditionally compiled code + unused-but-set-variable # commented-out or conditionally compiled code + unused-label # commented-out or conditionally compiled code + unused-local-typedefs # commented-out or conditionally compiled code + ) + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + list(APPEND warnings_disabled deprecated-declarations) # legacy OpenGL calls + endif () + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.0) + list(APPEND warnings_disabled ignored-attributes) # from Eigen headers marked SYSTEM + endif () + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND warnings_disabled unknown-pragmas) # igl pragmas, GCC bug 66943 + endif () + foreach (w IN LISTS warnings_disabled) + add_compile_options(-Wno-${w}) + endforeach () - # Ignore signed/unsigned comparison warnings - add_compile_options(-Wno-sign-compare) + # Turn everything else into an error. Dependency headers are exempt because the SYSTEM + # include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out, + # apart from GCC's maybe-uninitialized, demoted below. + add_compile_options(-Werror) - # The mismatch of tabs and spaces throughout the project can sometimes - # cause this warning to appear even though the indentation is fine. - # Some includes also cause the warning - add_compile_options(-Wno-misleading-indentation) + # Demoted. Remove a name once its category is cleared on every compiler. + set(warnings_demoted) + if (APPLE) + list(APPEND warnings_demoted + # MacDarkMode.mm makes two calls to AppKit's private titlebarViewController + # and one to a wxWidgets category on NSTableColumn whose header is not + # imported. Clearing it means declaring the private selectors ourselves, which + # needs a macOS build to verify. + objc-method-access + ) + endif () + if (WIN32 AND CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64") + list(APPEND warnings_demoted + # About two dozen GetProcAddress casts, most in the vendored dark_mode.hpp, + # retype FARPROC to a real signature. The __stdcall typedefs are identical to + # FARPROC on x64, so only arm64 reports them. Clearing them is a separate + # sweep. + cast-function-type-mismatch + ) + endif () + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND warnings_demoted + # maybe-uninitialized runs after inlining and reports inside boost/variant, + # boost/tuple and the bundled clipper header even with -isystem. + maybe-uninitialized - # Disable warning if enum value does not have a corresponding case in switch statement - add_compile_options(-Wno-switch) + # array-bounds is reported once, where ConfigOptionVector::set_at inlines + # into OrcaSlicer.cpp on a branch the preceding type test rules out. + array-bounds - # removes LOTS of extraneous Eigen warnings (GCC only supports it since 6.1) - # https://eigen.tuxfamily.org/bz/show_bug.cgi?id=1221 - if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 6.0) - add_compile_options(-Wno-ignored-attributes) # Tamas: Eigen include dirs are marked as SYSTEM - endif() + # template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers. + template-id-cdtor + ) + endif () + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + list(APPEND warnings_demoted + # enum-constexpr-conversion is a Clang warning that defaults to an error, + # present through clang 20 and gone in clang 21. + enum-constexpr-conversion + ) + endif () - # Clang reports legacy OpenGL calls as deprecated. Turn off the warning for now - # to reduce the clutter, we know about this one. It should be reenabled after - # we finally get rid of the deprecated code. - if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - add_compile_options(-Wno-deprecated-declarations) - endif() - - if((${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang") AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_GREATER 15) - include(CheckCXXCompilerFlag) - check_cxx_compiler_flag(-Wno-error=enum-constexpr-conversion HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV) - if(HAS_WNO_ERROR_ENUM_CONSTEXPR_CONV) - add_compile_options(-Wno-error=enum-constexpr-conversion) - endif() - endif() - - #GCC generates loads of -Wunknown-pragmas when compiling igl. The fix is not easy due to a bug in gcc, see - # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66943 or - # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431 - # We will turn the warning of for GCC for now: - if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - # GCC generates loads of -Wunknown-pragmas when compiling igl. The fix is not easy due to a bug in gcc, see - # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66943 or - # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53431 - # We will turn the warning of for GCC for now: - add_compile_options(-Wno-unknown-pragmas) - endif() + # The list mixes names not every compiler has, so add each exception only where the + # compiler knows the warning. Probe with the positive -W, which an unknown + # warning fails on both compilers (GCC errors, Clang reports unknown-warning-option). + # An option that takes a =N argument rejects the bare -W, so fall back to + # -W=1 and demote with the trailing =. + include(CheckCXXCompilerFlag) + foreach (category IN LISTS warnings_demoted) + string(MAKE_C_IDENTIFIER "ORCA_HAS_W_${category}" _orca_has_w) + check_cxx_compiler_flag("-W${category}" ${_orca_has_w}) + if (${_orca_has_w}) + add_compile_options(-Wno-error=${category}) + else () + check_cxx_compiler_flag("-W${category}=1" ${_orca_has_w}_arg) + if (${${_orca_has_w}_arg}) + add_compile_options(-Wno-error=${category}=) + endif () + endif () + endforeach () # Compress the debug info with zstd to save space in Flatpak CI builds if(FLATPAK) @@ -619,10 +661,6 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR endif() endif() - if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 14) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=template-id-cdtor" ) - endif() - endif() if (SLIC3R_ASAN) @@ -1212,8 +1250,9 @@ endif () if (NOT SLIC3R_WARNINGS) add_compile_options(-w) elseif (MSVC AND NOT IS_CLANG_CL) - # /we4715 is C4715, no return from a non-void function, matching the - # -Werror=return-type the GNU/Clang builds apply. + # /we4715 is C4715, no return from a non-void function, an error on the GNU/Clang + # builds under -Werror. MSVC is not in that model, so this stays a single promoted + # warning. add_compile_options(/W3 /we4715) endif () From c21e48450c44fbf9b08d4ed2647d7921899f47dd Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:21:14 +0300 Subject: [PATCH 35/57] Fix single-instance activation maximizing OrcaSlicer (#15665) --- src/slic3r/GUI/InstanceCheck.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/InstanceCheck.cpp b/src/slic3r/GUI/InstanceCheck.cpp index bc68a3f788..28c5176cb1 100644 --- a/src/slic3r/GUI/InstanceCheck.cpp +++ b/src/slic3r/GUI/InstanceCheck.cpp @@ -114,7 +114,10 @@ namespace instance_check_internal if (my_instance_hash == other_instance_hash) { BOOST_LOG_TRIVIAL(debug) << "win enum - found correct instance"; orca_slicer_hwnd = hwnd; - ShowWindow(hwnd, SW_SHOWMAXIMIZED); + // Do not alter the window state when opening a file in the existing instance. + // A minimized window still needs restoring before it can receive focus. + if (IsIconic(hwnd)) + ShowWindow(hwnd, SW_RESTORE); SetForegroundWindow(hwnd); return false; } From a7775296b0861a6755f023db4cd550d5095bddeb Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Sun, 13 Sep 2026 05:29:35 -0500 Subject: [PATCH 36/57] Fix macOS custom color accuracy (#15283) * Fix macOS custom color accuracy * Fix wxWidgets dependency patch command * Apply macOS color patch to current wxWidgets branch --- ...001-macos-use-srgb-colour-components.patch | 29 +++++++++++++++++++ deps/wxWidgets/wxWidgets.cmake | 10 +++++++ 2 files changed, 39 insertions(+) create mode 100644 deps/wxWidgets/0001-macos-use-srgb-colour-components.patch diff --git a/deps/wxWidgets/0001-macos-use-srgb-colour-components.patch b/deps/wxWidgets/0001-macos-use-srgb-colour-components.patch new file mode 100644 index 0000000000..decbee0ad9 --- /dev/null +++ b/deps/wxWidgets/0001-macos-use-srgb-colour-components.patch @@ -0,0 +1,29 @@ +diff --git a/src/osx/cocoa/colour.mm b/src/osx/cocoa/colour.mm +index 31515d146f..86b33e94a2 100644 +--- a/src/osx/cocoa/colour.mm ++++ b/src/osx/cocoa/colour.mm +@@ -125,3 +125,3 @@ + wxOSXEffectiveAppearanceSetter helper; +- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] ) ++ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] ) + return [colRGBA redComponent]; +@@ -134,3 +134,3 @@ + wxOSXEffectiveAppearanceSetter helper; +- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] ) ++ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] ) + return [colRGBA greenComponent]; +@@ -143,3 +143,3 @@ + wxOSXEffectiveAppearanceSetter helper; +- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] ) ++ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] ) + return [colRGBA blueComponent]; +@@ -152,3 +152,3 @@ + wxOSXEffectiveAppearanceSetter helper; +- if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] ) ++ if ( NSColor* colRGBA = [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] ) + return [colRGBA alphaComponent]; +@@ -160,3 +160,3 @@ + { +- return [m_nsColour colorUsingColorSpaceName:NSCalibratedRGBColorSpace] != nil; ++ return [m_nsColour colorUsingColorSpace:[NSColorSpace sRGBColorSpace]] != nil; + } diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake index 1e2cc85f78..e57e82f3e9 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/wxWidgets/wxWidgets.cmake @@ -21,12 +21,22 @@ else () set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF") endif () +set(_wx_patch_command "") +if (APPLE) + set(_wx_patch_command + ${GIT_EXECUTABLE} checkout -f -- src/osx/cocoa/colour.mm + COMMAND ${GIT_EXECUTABLE} apply --verbose + ${CMAKE_CURRENT_LIST_DIR}/0001-macos-use-srgb-colour-components.patch + ) +endif () + orcaslicer_add_cmake_project( wxWidgets GIT_REPOSITORY "https://github.com/SoftFever/Orca-deps-wxWidgets" GIT_TAG v3.3.2 GIT_SHALLOW ON GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp + PATCH_COMMAND ${_wx_patch_command} DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG} CMAKE_ARGS -DwxBUILD_PRECOMP=ON From 15ebdc379918be1e6302920ebbb24565ed5f11de Mon Sep 17 00:00:00 2001 From: yw4z Date: Sun, 13 Sep 2026 14:41:27 +0300 Subject: [PATCH 37/57] enable menu icons on macOS and Linux for plate / background menus (#15620) Update GUI_Factories.cpp --- src/slic3r/GUI/GUI_Factories.cpp | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index d8e54978fa..05248c38df 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -1392,7 +1392,7 @@ void MenuFactory::create_default_menu() { wxMenu* sub_menu_primitives = append_submenu_add_generic(&m_default_menu, ModelVolumeType::INVALID); wxMenu* sub_menu_handy = append_submenu_add_handy_model(&m_default_menu, ModelVolumeType::INVALID); -#ifdef __WINDOWS__ + append_submenu(&m_default_menu, sub_menu_primitives, wxID_ANY, _L("Add Primitive"), "", "menu_add_part", []() {return true; }, m_parent); append_submenu(&m_default_menu, sub_menu_handy, wxID_ANY, _L("Add Handy models"), "", "menu_add_part", @@ -1400,15 +1400,6 @@ void MenuFactory::create_default_menu() append_menu_item(&m_default_menu, wxID_ANY, _L("Add Models"), "", // ORCA: Add Models [](wxCommandEvent&) { plater()->add_file(); }, "menu_add_part", &m_default_menu, []() {return wxGetApp().plater()->can_add_model(); }, m_parent); -#else - append_submenu(&m_default_menu, sub_menu_primitives, wxID_ANY, _L("Add Primitive"), "", "", - []() {return true; }, m_parent); - append_submenu(&m_default_menu, sub_menu_handy, wxID_ANY, _L("Add Handy models"), "", "", - []() {return true; }, m_parent); - append_menu_item(&m_default_menu, wxID_ANY, _L("Add Models"), "", // ORCA: Add Models - [](wxCommandEvent&) { plater()->add_file(); }, "", &m_default_menu, - []() {return wxGetApp().plater()->can_add_model(); }, m_parent); -#endif m_default_menu.AppendSeparator(); @@ -1789,7 +1780,6 @@ void MenuFactory::create_plate_menu() wxMenu* sub_menu_primitives = append_submenu_add_generic(menu, ModelVolumeType::INVALID); wxMenu* sub_menu_handy = append_submenu_add_handy_model(menu, ModelVolumeType::INVALID); -#ifdef __WINDOWS__ append_submenu(menu, sub_menu_primitives, wxID_ANY, _L("Add Primitive"), "", "menu_add_part", []() {return true; }, m_parent); append_submenu(menu, sub_menu_handy, wxID_ANY, _L("Add Handy models"), "", "menu_add_part", @@ -1797,15 +1787,7 @@ void MenuFactory::create_plate_menu() append_menu_item(menu, wxID_ANY, _L("Add Models"), "", // ORCA: Add Models [](wxCommandEvent&) { plater()->add_file(); }, "menu_add_part", menu, []() {return wxGetApp().plater()->can_add_model(); }, m_parent); -#else - append_submenu(menu, sub_menu_primitives, wxID_ANY, _L("Add Primitive"), "", "", - []() {return true; }, m_parent); - append_submenu(menu, sub_menu_handy, wxID_ANY, _L("Add Handy models"), "", "", - []() {return true; }, m_parent); - append_menu_item(menu, wxID_ANY, _L("Add Models"), "", // ORCA: Add Models - [](wxCommandEvent&) { plater()->add_file(); }, "", menu, - []() {return wxGetApp().plater()->can_add_model(); }, m_parent); -#endif + append_menu_item_replace_all_with_stl(menu); From 9e8fbc17dde6699650d9fd88e48bd53419b2d2bd Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 13 Sep 2026 14:39:55 -0500 Subject: [PATCH 38/57] ci: clear the per-run annotations and revive the weekly doxygen job (#15659) --- .github/workflows/build_all.yml | 4 ++-- .github/workflows/build_orca.yml | 4 +++- .github/workflows/doxygen-docs.yml | 17 ++++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 570d3203ed..ae5231e784 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -100,7 +100,7 @@ jobs: matrix: include: ${{ fromJSON(vars.SELF_HOSTED && '[{"arch":"x64","os":"orca-win-server","compiler":"clang"}]' - || '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-arm","compiler":"clang"}]') }} + || '[{"arch":"x64","os":"windows-latest","compiler":"clang"},{"arch":"arm64","os":"windows-11-vs2026-arm","compiler":"clang"}]') }} needs: check_build_script # Don't run scheduled builds on forks: if: ${{ !cancelled() && needs.check_build_script.result == 'success' && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }} @@ -169,7 +169,7 @@ jobs: if: ${{ !cancelled() && success() && !vars.SELF_HOSTED }} uses: ./.github/workflows/unit_tests.yml with: - os: windows-11-arm + os: windows-11-vs2026-arm artifact: ${{ github.sha }}-tests-windows-arm64 test-dir: build-arm64/tests unit_tests_macos_arm64: diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 1b7fd37a0f..4b53767d4f 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -96,12 +96,14 @@ jobs: id: ccache if: ${{ !inputs.macos-combine-only }} continue-on-error: true - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1.2.24 with: key: ${{ env.CCACHE_LEG }} max-size: 3G restore: false save: false + # ccache -s runs as its own step; no summary table per job. + job-summary: '' - name: Restore compiler cache if: ${{ steps.ccache.outcome == 'success' }} diff --git a/.github/workflows/doxygen-docs.yml b/.github/workflows/doxygen-docs.yml index 6af7255fa3..d7d2f982e7 100644 --- a/.github/workflows/doxygen-docs.yml +++ b/.github/workflows/doxygen-docs.yml @@ -19,11 +19,18 @@ jobs: permissions: contents: read steps: - - uses: thejerrybao/setup-swap-space@v1 - with: - swap-space-path: /swapfile - swap-size-gb: 8 - remove-existing-swap-files: true + # Doxygen with call graphs over all of src/ outgrows the runner's RAM; + # replace the runner's swapfile with an 8 GB one. + - name: Grow swap space + run: | + set -euo pipefail + sudo swapoff -a + sudo rm -f /swapfile + sudo fallocate -l 8G /swapfile + sudo chmod 600 /swapfile + sudo mkswap /swapfile + sudo swapon /swapfile + free -h - name: Checkout repository uses: actions/checkout@v7 From d643b10ac4495e81192136dbe69f55a80949ce23 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 13 Sep 2026 15:46:03 -0500 Subject: [PATCH 39/57] build: expand PrintConfig.hpp option lists twice per class instead of five times (#15658) --- src/libslic3r/PrintConfig.hpp | 84 +++++++++++++++++---------------- tests/libslic3r/test_config.cpp | 55 +++++++++++++++++++++ 2 files changed, 98 insertions(+), 41 deletions(-) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f7cbe8b2e5..18e66adb34 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1011,41 +1011,46 @@ public: \ { PrintConfigDef::handle_legacy(opt_key, value); } #define PRINT_CONFIG_CLASS_ELEMENT_DEFINITION(r, data, elem) BOOST_PP_TUPLE_ELEM(0, elem) BOOST_PP_TUPLE_ELEM(1, elem); -#define PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2(KEY) cache.opt_add(BOOST_PP_STRINGIZE(KEY), base_ptr, this->KEY); -#define PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION(r, data, elem) PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2(BOOST_PP_TUPLE_ELEM(1, elem)) -#define PRINT_CONFIG_CLASS_ELEMENT_HASH(r, data, elem) boost::hash_combine(seed, BOOST_PP_TUPLE_ELEM(1, elem).hash()); -#define PRINT_CONFIG_CLASS_ELEMENT_EQUAL(r, data, elem) if (! (BOOST_PP_TUPLE_ELEM(1, elem) == rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return false; -#define PRINT_CONFIG_CLASS_ELEMENT_LOWER(r, data, elem) \ - if (BOOST_PP_TUPLE_ELEM(1, elem) < rhs.BOOST_PP_TUPLE_ELEM(1, elem)) return true; \ - if (! (BOOST_PP_TUPLE_ELEM(1, elem) == rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return false; +#define PRINT_CONFIG_CLASS_ELEMENT_VISIT(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), this->BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return; +// Each option list is expanded into the members and again into for_each_option_pair(), which calls +// f(key, this->option, rhs.option) in declaration order and stops when f returns false. hash(), +// operator==, operator< and initialize() iterate the options through that visitor. +#define PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \ + size_t hash() const throw() \ + { \ + size_t seed = 0; \ + this->for_each_option_pair(*this, [&seed](const char*, const auto &a, const auto&) { boost::hash_combine(seed, a.hash()); return true; }); \ + return seed; \ + } \ + bool operator==(const CLASS_NAME &rhs) const throw() \ + { \ + bool eq = true; \ + this->for_each_option_pair(rhs, [&eq](const char*, const auto &a, const auto &b) { eq = (a == b); return eq; }); \ + return eq; \ + } \ + bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \ + bool operator<(const CLASS_NAME &rhs) const throw() \ + { \ + int c = 0; \ + this->for_each_option_pair(rhs, [&c](const char*, const auto &a, const auto &b) { if (a < b) c = -1; else if (! (a == b)) c = 1; return c == 0; }); \ + return c < 0; \ + } \ +protected: \ + void initialize(StaticCacheBase &cache, const char *base_ptr) \ + { \ + this->for_each_option_pair(*this, [&cache, base_ptr](const char *key, const auto &a, const auto&) { cache.opt_add(key, base_ptr, a); return true; }); \ + } #define PRINT_CONFIG_CLASS_DEFINE(CLASS_NAME, PARAMETER_DEFINITION_SEQ) \ class CLASS_NAME : public StaticPrintConfig { \ STATIC_PRINT_CONFIG_CACHE(CLASS_NAME) \ public: \ BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ) \ - size_t hash() const throw() \ + template void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const \ { \ - size_t seed = 0; \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ) \ - return seed; \ - } \ - bool operator==(const CLASS_NAME &rhs) const throw() \ - { \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ) \ - return true; \ - } \ - bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \ - bool operator<(const CLASS_NAME &rhs) const throw() \ - { \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_LOWER, _, PARAMETER_DEFINITION_SEQ) \ - return false; \ - } \ -protected: \ - void initialize(StaticCacheBase &cache, const char *base_ptr) \ - { \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION, _, PARAMETER_DEFINITION_SEQ) \ + BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ) \ } \ + PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \ }; #define PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM(r, data, i, elem) BOOST_PP_COMMA_IF(i) public elem @@ -1059,43 +1064,43 @@ protected: \ if (! (*static_cast(this) == static_cast(rhs))) return false; // Generic version, with or without new parameters. Don't use this directly. -#define PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION, PARAMETER_REGISTRATION, PARAMETER_HASHES, PARAMETER_EQUALS) \ +#define PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION, PARAMETER_VISIT) \ class CLASS_NAME : PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST(CLASSES_PARENTS_TUPLE) { \ STATIC_PRINT_CONFIG_CACHE_DERIVED(CLASS_NAME) \ CLASS_NAME() : PRINT_CONFIG_CLASS_DERIVED_INITIALIZER(CLASSES_PARENTS_TUPLE, 0) { assert(s_cache_##CLASS_NAME.initialized()); *this = s_cache_##CLASS_NAME.defaults(); } \ public: \ PARAMETER_DEFINITION \ + template void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { PARAMETER_VISIT } \ size_t hash() const throw() \ { \ size_t seed = 0; \ BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_DERIVED_HASH, _, BOOST_PP_TUPLE_TO_SEQ(CLASSES_PARENTS_TUPLE)) \ - PARAMETER_HASHES \ + this->for_each_option_pair(*this, [&seed](const char*, const auto &a, const auto&) { boost::hash_combine(seed, a.hash()); return true; }); \ return seed; \ } \ bool operator==(const CLASS_NAME &rhs) const throw() \ { \ BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_DERIVED_EQUAL, _, BOOST_PP_TUPLE_TO_SEQ(CLASSES_PARENTS_TUPLE)) \ - PARAMETER_EQUALS \ - return true; \ + bool eq = true; \ + this->for_each_option_pair(rhs, [&eq](const char*, const auto &a, const auto &b) { eq = (a == b); return eq; }); \ + return eq; \ } \ bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \ protected: \ CLASS_NAME(int) : PRINT_CONFIG_CLASS_DERIVED_INITIALIZER(CLASSES_PARENTS_TUPLE, 1) {} \ void initialize(StaticCacheBase &cache, const char* base_ptr) { \ PRINT_CONFIG_CLASS_DERIVED_INITCACHE(CLASSES_PARENTS_TUPLE) \ - PARAMETER_REGISTRATION \ + this->for_each_option_pair(*this, [&cache, base_ptr](const char *key, const auto &a, const auto&) { cache.opt_add(key, base_ptr, a); return true; }); \ } \ }; // Variant without adding new parameters. #define PRINT_CONFIG_CLASS_DERIVED_DEFINE0(CLASS_NAME, CLASSES_PARENTS_TUPLE) \ - PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, BOOST_PP_EMPTY(), BOOST_PP_EMPTY(), BOOST_PP_EMPTY(), BOOST_PP_EMPTY()) + PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, BOOST_PP_EMPTY(), BOOST_PP_EMPTY()) // Variant with adding new parameters. #define PRINT_CONFIG_CLASS_DERIVED_DEFINE(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION_SEQ) \ PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, \ BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ), \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION, _, PARAMETER_DEFINITION_SEQ), \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ), \ - BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ)) + BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ)) // This object is mapped to Perl as Slic3r::Config::PrintObject. PRINT_CONFIG_CLASS_DEFINE( @@ -2148,11 +2153,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE0( #undef STATIC_PRINT_CONFIG_CACHE_BASE #undef STATIC_PRINT_CONFIG_CACHE_DERIVED #undef PRINT_CONFIG_CLASS_ELEMENT_DEFINITION -#undef PRINT_CONFIG_CLASS_ELEMENT_EQUAL -#undef PRINT_CONFIG_CLASS_ELEMENT_LOWER -#undef PRINT_CONFIG_CLASS_ELEMENT_HASH -#undef PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION -#undef PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2 +#undef PRINT_CONFIG_CLASS_ELEMENT_VISIT +#undef PRINT_CONFIG_CLASS_COMMON_BODY #undef PRINT_CONFIG_CLASS_DEFINE #undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST #undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 9a70ecbaeb..3813e2df3f 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -1161,3 +1161,58 @@ TEST_CASE("min_object_distance yields no floor when an FFF config lacks the opti CHECK_THAT(min_object_distance(c), Catch::Matchers::WithinAbs(12., 1e-9)); } } + +TEST_CASE("Static print configs compare, order and hash by their option values", "[Config]") +{ + // PrintObjectConfig comes from PRINT_CONFIG_CLASS_DEFINE; PrintConfig combines MachineEnvelopeConfig + // and GCodeConfig through PRINT_CONFIG_CLASS_DERIVED_DEFINE. Both generate hash(), operator==, + // operator< and the option registration from the same option list. The hash inequalities use fixed + // inputs, so they are deterministic; they check that hash() covers the changed option. + SECTION("default-constructed configs are equal and find their options by key") + { + PrintObjectConfig a, b; + REQUIRE(a == b); + REQUIRE(a.hash() == b.hash()); + REQUIRE_FALSE(a < b); + REQUIRE_FALSE(b < a); + REQUIRE(a.optptr("layer_height") == &a.layer_height); + REQUIRE(a.optptr("brim_object_gap") == &a.brim_object_gap); + } + + SECTION("one differing option makes the configs unequal and orders them") + { + PrintObjectConfig a, b; + b.layer_height.value = a.layer_height.value + 0.05; + REQUIRE(a != b); + REQUIRE(a.hash() != b.hash()); + REQUIRE(a < b); + REQUIRE_FALSE(b < a); + } + + SECTION("ordering is decided by the first option in declaration order that differs") + { + PrintObjectConfig a, b; + a.brim_object_gap.value = b.brim_object_gap.value + 1.0; // declared first + a.layer_height.value = b.layer_height.value - 0.05; // declared later, points the other way + REQUIRE(b < a); + REQUIRE_FALSE(a < b); + } + + SECTION("a derived config sees differences in its parents and in its own options") + { + PrintConfig a, b; + REQUIRE(a == b); + REQUIRE(a.hash() == b.hash()); + + b.gcode_flavor.value = b.gcode_flavor.value == gcfMarlinLegacy ? gcfKlipper : gcfMarlinLegacy; // GCodeConfig parent + REQUIRE(a != b); + REQUIRE(a.hash() != b.hash()); + + PrintConfig c, d; + d.skirt_distance.value = c.skirt_distance.value + 1.0; // PrintConfig's own list + REQUIRE(c != d); + REQUIRE(c.hash() != d.hash()); + REQUIRE(c.optptr("skirt_distance") == &c.skirt_distance); + REQUIRE(c.optptr("gcode_flavor") == &c.gcode_flavor); + } +} From 636b623cb7a9cefe6194e367354876531d1cb581 Mon Sep 17 00:00:00 2001 From: Daniel Williams <35799546+danielwoz@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:55:42 +0800 Subject: [PATCH 40/57] tests: regression test that every PrintRegion/Object field is in a preset key list (#13466) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_preset_options.cpp | 70 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/libslic3r/test_preset_options.cpp diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 5c10ab1496..2f859f46fe 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(${_TEST_NAME}_tests test_preset_setting_id.cpp test_preset_diff.cpp test_vendor_cache.cpp + test_preset_options.cpp test_elephant_foot_compensation.cpp test_fill_corner_smoothing.cpp test_filament_mixer.cpp diff --git a/tests/libslic3r/test_preset_options.cpp b/tests/libslic3r/test_preset_options.cpp new file mode 100644 index 0000000000..1763f5fbc7 --- /dev/null +++ b/tests/libslic3r/test_preset_options.cpp @@ -0,0 +1,70 @@ +// Regression test for the "option in def + UI but missing from preset key list" +// crash class. +// +// The print preset's DynamicPrintConfig is seeded with only the keys returned by +// Preset::print_options() (PresetBundle.cpp). A field added to PrintRegionConfig +// or PrintObjectConfig and registered via print_config_def plus a TabPrint +// optgroup, but left out of print_options(), still gets its control built; on tab +// activation reload_config -> get_config_value dispatches to opt_bool/opt_int on a +// DynamicPrintConfig with no entry for the key, and the accessor null-derefs the +// result of option(key). +// +// The invariant asserted here is the inverse: every key declared on +// PrintRegionConfig and PrintObjectConfig appears in Preset::print_options() or +// Preset::filament_options(), the two preset key lists that seed a print preset's +// DynamicConfig. + +#include + +#include "libslic3r/Preset.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include + +using namespace Slic3r; + +namespace { + +// Deprecated keys renamed in handle_legacy() (ironing_direction -> +// ironing_angle, wall_infill_order -> wall_sequence); neither is in a +// preset list. Register new options in a preset list, not here. +const std::set kDeprecatedRegionFields = { + "ironing_direction", + "wall_infill_order", +}; + +void check_keys_are_in_a_preset(const t_config_option_keys& keys, const std::string& class_name) +{ + REQUIRE_FALSE(keys.empty()); + const auto& print_options = Preset::print_options(); + const auto& filament_options = Preset::filament_options(); + const std::set in_print(print_options.begin(), print_options.end()); + const std::set in_filament(filament_options.begin(), filament_options.end()); + for (const std::string& key : keys) { + DYNAMIC_SECTION(class_name << "::" << key) + { + INFO("'" << key << "' on " << class_name + << " is missing from " + "Preset::print_options()/filament_options(); add it to " + "s_Preset_print_options (or s_Preset_filament_options) in Preset.cpp."); + const bool registered = in_print.count(key) || in_filament.count(key) || kDeprecatedRegionFields.count(key); + REQUIRE(registered); + } + } +} + +} // namespace + +// Bodies are laid out like the rest of the test suite rather than collapsed +// onto the brace line. +// clang-format off +TEST_CASE("Every PrintRegionConfig field is registered in a preset key list", "[Preset][Config]") +{ + check_keys_are_in_a_preset(PrintRegionConfig::defaults().keys(), "PrintRegionConfig"); +} + +TEST_CASE("Every PrintObjectConfig field is registered in a preset key list", "[Preset][Config]") +{ + check_keys_are_in_a_preset(PrintObjectConfig::defaults().keys(), "PrintObjectConfig"); +} +// clang-format on From 26fa1694d962a557d6681c9b74466069e279be4d Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 13 Sep 2026 17:08:08 -0500 Subject: [PATCH 41/57] ci: save the compiler cache from cancelled and failed builds too (#15668) --- .github/workflows/build_all.yml | 17 +++++++++++++---- .github/workflows/build_orca.yml | 20 +++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index ae5231e784..10edec5aaa 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -313,6 +313,7 @@ jobs: echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV" shell: bash - name: Restore compiler cache + id: ccache_restore uses: actions/cache/restore@v6 with: path: .flatpak-builder/ccache @@ -371,24 +372,31 @@ jobs: save-cache: false arch: ${{ matrix.variant.arch }} upload-artifact: false + # The build has just touched everything it can use, so an object untouched + # for a week is dead, usually orphaned by a flag change. - name: Compiler cache statistics if: always() run: | export CCACHE_DIR=$PWD/.flatpak-builder/ccache + ccache --evict-older-than 7d ccache -s -v || ccache -s shell: bash # Save the new entry first, then drop the older ones for this leg on this - # ref, so a failed save leaves the previous entry in place. + # ref, so a failed save leaves the previous entry in place. A cancelled or + # failed build saves too, since what it compiled is still valid; a restore + # that did not finish does not, since the directory may be a truncated copy. - name: Save compiler cache id: ccache_save - if: github.event_name != 'pull_request' + if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }} uses: actions/cache/save@v6 with: path: .flatpak-builder/ccache key: ${{ env.CCACHE_ENTRY }} - name: Drop older compiler cache entries - if: ${{ steps.ccache_save.outcome == 'success' }} + if: ${{ always() && steps.ccache_save.outcome == 'success' }} # The container has no gh, so this is the list and delete over the REST API. + # Older means a lower run id, so two runs finishing close together keep + # the newer entry whichever of them cleans up last. continue-on-error: true env: GH_TOKEN: ${{ github.token }} @@ -396,7 +404,8 @@ jobs: api="$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/caches" curl -sSf -H "Authorization: Bearer $GH_TOKEN" \ "$api?ref=$GITHUB_REF&key=ccache-$CCACHE_LEG-&per_page=100" \ - | jq -r --arg keep "$CCACHE_ENTRY" '.actions_caches[] | select(.key != $keep) | .id' \ + | jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \ + '.actions_caches[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \ | while read -r id; do curl -sSf -X DELETE -H "Authorization: Bearer $GH_TOKEN" "$api/$id" done diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 4b53767d4f..95ec52a65d 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -106,6 +106,7 @@ jobs: job-summary: '' - name: Restore compiler cache + id: ccache_restore if: ${{ steps.ccache.outcome == 'success' }} uses: actions/cache/restore@v6 with: @@ -724,31 +725,40 @@ jobs: asset_content_type: application/octet-stream max_releases: 1 + # The build has just touched everything it can use, so an object + # untouched for a week is dead, usually orphaned by a flag change. - name: Compiler cache statistics if: ${{ always() && steps.ccache.outcome == 'success' }} shell: bash - run: ccache -s -v || ccache -s + run: | + ccache --evict-older-than 7d + ccache -s -v || ccache -s # Entries are immutable, so the new one is saved first and the older # ones for this leg on this ref are dropped afterwards: a failed save - # leaves the previous entry in place. + # leaves the previous entry in place. A cancelled or failed build saves + # too, since what it compiled is still valid; a restore that did not + # finish does not, since the directory may be a truncated copy. - name: Save compiler cache id: ccache_save - if: ${{ steps.ccache.outcome == 'success' && github.event_name != 'pull_request' }} + if: ${{ always() && steps.ccache_restore.outcome == 'success' && github.event_name != 'pull_request' }} uses: actions/cache/save@v6 with: path: ${{ github.workspace }}/.ccache key: ${{ env.CCACHE_ENTRY }} - name: Drop older compiler cache entries - if: ${{ steps.ccache_save.outcome == 'success' }} + if: ${{ always() && steps.ccache_save.outcome == 'success' }} # A read-only token (fork PRs) cannot delete; that only costs storage. + # Older means a lower run id, so two runs finishing close together keep + # the newer entry whichever of them cleans up last. continue-on-error: true shell: bash env: GH_TOKEN: ${{ github.token }} run: | gh cache list --ref "$GITHUB_REF" --key "ccache-$CCACHE_LEG-" --limit 100 --json id,key \ - | jq -r --arg keep "$CCACHE_ENTRY" '.[] | select(.key != $keep) | .id' \ + | jq -r --arg prefix "ccache-$CCACHE_LEG-" --argjson run "$GITHUB_RUN_ID" \ + '.[] | select((.key | ltrimstr($prefix) | split("-")[0] | tonumber?) < $run) | .id' \ | tr -d '\r' \ | while read -r id; do gh cache delete "$id"; done From aef9ca2efb54df9a8ae020fc53d5a7bec359c229 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:42:49 +0300 Subject: [PATCH 42/57] Fix label object error for toolchanges without object instances (#15666) --- src/libslic3r/GCode.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index e9cdb620e0..4aa45a60ed 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6308,8 +6308,13 @@ LayerResult GCode::process_layer( all_label_ids.insert(inst.label_object_id); break; } - std::vector filament_instances_id(all_label_ids.begin(), all_label_ids.end()); - m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id); + // Orca: A scheduled extruder may have no object instances on this layer. + // Clear any pending mask so it cannot be emitted for the wrong toolchange. + m_filament_instances_code.clear(); + if (!all_label_ids.empty()) { + std::vector filament_instances_id(all_label_ids.begin(), all_label_ids.end()); + m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id); + } } // The inline _extrude hook may already have taken the snapshot mid-extrusion on a From fd63164268bf6835612ee719cc77e124c687c974 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:03:45 +0300 Subject: [PATCH 43/57] Fix Printer Agent preset undo (#15645) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- src/slic3r/GUI/OptionsGroup.cpp | 8 +++--- src/slic3r/GUI/Tab.cpp | 44 ++++----------------------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 7d63556eff..99151ca599 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -798,11 +798,9 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config, #endif else if (opt_key == "printer_agent") { - // why: printer_agent is a coString kept out of m_opt_map. The generic non-opt_map revert - // below restores the edited config from get_value(), but a deregistered/"(missing)" saved - // id has no selectable row, so the field yields no value and the edited config keeps the - // user's interim pick -> stuck dirty. Restore the SAVED id straight into the edited config - // (displayable or not; config is the saved or system baseline), then repaint and notify. + // A deregistered/"(missing)" saved id has no selectable row, so the field yields no + // value. Restore the saved id directly instead of letting the generic revert path read + // the field value back into the edited config. const std::string saved_id = config.opt_string("printer_agent"); set_value(opt_key, saved_id); this->change_opt_value(opt_key, saved_id); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 2dedb23365..914e4cc7bb 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5029,28 +5029,12 @@ void TabPrinter::build_fff() auto registered_printer_agents = NetworkAgentFactory::get_registered_printer_agents(); if (!registered_printer_agents.empty()) { - ConfigOptionDef def; - def.type = coString; - def.gui_type = ConfigOptionDef::GUIType::printer_agent_select; - def.width = 3 * Field::def_width_wider() / 2; - def.label = L("Printer Agent"); - def.tooltip = L("Select the network agent implementation for printer communication. " + option = optgroup->get_option("printer_agent"); + option.opt.gui_type = ConfigOptionDef::GUIType::printer_agent_select; + option.opt.width = 3 * Field::def_width_wider() / 2; + option.opt.tooltip = L("Select the network agent implementation for printer communication. " "Available agents are registered at startup."); - def.mode = comAdvanced; - - // Create the field without get_option() so it is not registered in m_opt_map. - // ConfigOptionsGroup handles printer_agent before the generic mapped write path. - Line agent_line = optgroup->create_single_option_line(Option(def, "printer_agent")); - optgroup->append_line(agent_line); - if (Field* agent_field = get_field("printer_agent")) - { - if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) - choice->set_value(m_config->opt_string("printer_agent"), false); - } - - // Register by hand so the UnsavedChanges dialog can render a row for it. - wxGetApp().sidebar().get_searcher().add_key("printer_agent", m_type, optgroup->title, - optgroup->config_category()); + optgroup->append_single_option_line(option); } } @@ -5912,15 +5896,6 @@ void TabPrinter::reload_config() if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); - // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. - if (Field* agent_field = get_field("printer_agent")) - { - if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) - { - const std::string selected_agent = m_config->opt_string("printer_agent"); - choice->set_value(selected_agent, false); - } - } } void TabPrinter::activate_selected_page(std::function throw_if_canceled) @@ -5932,15 +5907,6 @@ void TabPrinter::activate_selected_page(std::function throw_if_canceled) if (m_active_page && m_active_page->title() == "Multimaterial") m_active_page->set_value("extruders_count", int(m_extruders_count)); - // m_opt_map-driven reload does not cover printer_agent, so sync this custom field explicitly. - if (Field* agent_field = get_field("printer_agent")) - { - if (auto* choice = dynamic_cast(agent_field); choice && choice->getWindow()) - { - const std::string selected_agent = m_config->opt_string("printer_agent"); - choice->set_value(selected_agent, false); - } - } } void TabPrinter::clear_pages() From c5b152b722245f96d6baad88830b38f4c4a3e167 Mon Sep 17 00:00:00 2001 From: packerlschupfer <83344883+packerlschupfer@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:37:05 +0200 Subject: [PATCH 44/57] CLI: record command-line overrides in different_settings_to_system (#15642) * CLI: record command-line overrides in different_settings_to_system Settings passed on the command line (--sparse-infill-density 25% ...) override the loaded presets when m_extra_config is applied to m_print_config, but nothing recorded them in different_settings_to_system. The exported project therefore carried the new value with no mark that it was modified, and re-opening it in the GUI reverted it to the system preset's value -- the same failure the preset-leaf diff fixes for user presets, via a different source of override. The key set comes from m_config, not m_extra_config. read_cli() puts only what the user typed into m_config and setup() adds nothing but CLI-own defaults (none of the keys run() materialises there is a preset option), whereas the CLI writes its own values into m_extra_config (has_filament_switcher, filament_colour, filament_map ...), which must not be reported as user overrides. Values are snapshotted just before the apply and only keys the override actually changed are recorded: a typed value equal to the loaded one modifies nothing, and listing it would read as a spurious difference against what the GUI writes. Each key lands in the column(s) whose preset type owns it -- process, every filament, printer -- and a key already present is not duplicated. Keys no preset owns (curr_bed_type, a project setting) land nowhere, as in the GUI. Follow-up to #15595, split out at review. * CLI: judge command-line overrides the way the value is read Review follow-ups on the override recording: - Lists were compared as whole serialized strings. read_cli() builds a fresh one-entry list, so --nozzle-temperature 245 against 245,245,245 on a three-filament project was recorded in every filament column although nothing changed. Lists are now compared entry by entry with a missing entry read as the first, as get_at() reads it (and as resize() pads). - The log line fired for every changed key, including ones no preset owns (curr_bed_type) and which therefore land in no column. It now fires only when a column took the key. - m_print_config.has(key) straight after apply(m_extra_config, true) was always true, both configs sharing print_config_def; removed. columns.size() >= 2 also always holds after the resize to filament_count + 2 -- different_settings_to_system is not a CLI option, so nothing in between can shrink it -- but that rests on code far away, so it stays a plain check rather than an assert: release builds compile asserts out, and a _GLIBCXX_ASSERTIONS build would abort on columns[0]. Deliberately NOT done: comparing a key the loaded config lacks against its built-in default. On reopen the GUI restores an unlisted key from the SYSTEM preset, not the default. A 3MF written before an option existed leaves it absent here, so --sparse-infill-density 20% (the default) against a Prusa system 15% would go unrecorded and be reverted to 15%. Absent keys stay always-recorded: over-recording is cosmetic, under-recording loses the value. Verified that such a key really is absent at this point, rather than filled from the system preset. Reported by HanifKoh and raistlin7447 in review of #15642. --- src/OrcaSlicer.cpp | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index d3e24437fb..499e73d073 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -3846,9 +3846,94 @@ int CLI::run(int argc, char **argv) } } + //ORCA: settings passed on the command line (--sparse-infill-density 25% ...) override the loaded + // presets right here, so they belong in different_settings_to_system just as a preset + // override does. Without them re-opening the exported project in the GUI shows nothing + // modified and reverts those values to the system presets'. + // + // The keys come from m_config, not m_extra_config: read_cli() puts only what the user typed + // into m_config (setup() adds nothing but CLI-own defaults), whereas the CLI writes its own + // values into m_extra_config. Only keys whose value the override actually changed are + // recorded -- a typed value equal to the loaded one modifies nothing -- and each lands in + // the column(s) whose preset type owns it: [0] process, [1..n-2] filaments, [n-1] printer. + // + // "Changed" is judged the way the value is read: a list is compared entry by entry with a + // missing entry read as the first, as get_at() does -- so --nozzle-temperature 245 against + // 245,245,245 is no change, although the two serialize differently. + // + // A key the loaded config does not carry at all is always recorded, even if the typed value + // equals the built-in default. On reopen the GUI restores an unlisted key from the SYSTEM + // preset, which need not match that default: a 3MF written before an option existed leaves + // it absent here, and --sparse-infill-density 20% (the default) against a Prusa system 15% + // would otherwise go unrecorded and be reverted. Over-recording is cosmetic; under-recording + // loses the value. + std::map> cli_override_before; + for (const std::string &key : m_config.keys()) { + if (!m_extra_config.has(key)) + continue; + const ConfigOption *loaded = m_print_config.option(key); + cli_override_before[key].reset(loaded != nullptr ? loaded->clone() : nullptr); // null: always recorded + } + // Apply command line options to a more specific DynamicPrintConfig which provides normalize() // (command line options override --load files) m_print_config.apply(m_extra_config, true); + + if (!cli_override_before.empty()) { + std::vector &columns = m_print_config.option("different_settings_to_system", true)->values; + auto owned_by = [](const std::vector &options, const std::string &key) { + return std::find(options.begin(), options.end(), key) != options.end(); + }; + auto add_to_column = [&columns](size_t index, const std::string &key) { + std::vector keys; + Slic3r::unescape_strings_cstyle(columns[index], keys); + if (std::find(keys.begin(), keys.end(), key) == keys.end()) { + keys.push_back(key); + columns[index] = Slic3r::escape_strings_cstyle(keys); + } + }; + auto same_value = [](const ConfigOption *a, const ConfigOption *b) { + if (a == nullptr || b == nullptr) + return false; + const auto *va = dynamic_cast(a); + const auto *vb = dynamic_cast(b); + if (va == nullptr || vb == nullptr) + return va == vb && a->serialize() == b->serialize(); + const std::vector ea = va->vserialize(), eb = vb->vserialize(); + if (ea.empty() || eb.empty()) + return ea.empty() && eb.empty(); + for (size_t i = 0; i < std::max(ea.size(), eb.size()); ++i) + if (ea[i < ea.size() ? i : 0] != eb[i < eb.size() ? i : 0]) + return false; + return true; + }; + //ORCA: always true after the resize to filament_count + 2 above, and nothing in between can + // shrink the column vector -- different_settings_to_system is not a CLI option. Kept as + // a check rather than an assert: release builds compile asserts out, so an assert would + // protect nothing, while a build with _GLIBCXX_ASSERTIONS would abort on columns[0]. + if (columns.size() >= 2) { + for (const auto &[key, before] : cli_override_before) { + if (same_value(before.get(), m_print_config.option(key))) + continue; + bool recorded = false; + if (owned_by(Preset::print_options(), key)) { + add_to_column(0, key); + recorded = true; + } + if (owned_by(Preset::filament_options(), key)) { + for (size_t i = 1; i + 1 < columns.size(); ++i) + add_to_column(i, key); + recorded = true; + } + if (owned_by(Preset::printer_options(), key)) { + add_to_column(columns.size() - 1, key); + recorded = true; + } + if (recorded) + BOOST_LOG_TRIVIAL(info) << boost::format("CLI: override %1% recorded in different_settings_to_system") % key; + } + } + } // Normalizing after importing the 3MFs / AMFs m_print_config.normalize_fdm(); From 4373bc36978d1ea58829360d7fc85fb89418482a Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 14 Sep 2026 13:38:51 +0800 Subject: [PATCH 45/57] Add a Nightly Parity Workflow Runs orca-test-repo's full override-sweep effect stage (two shards) and the GUI-vs-CLI parity harness every night against the latest successful build_all Linux AppImage, with sources checked out at that build's commit. Kept out of the per-build regression step, whose time budget it would exceed, and never gates a build. --- .github/workflows/parity_nightly.yml | 219 +++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 .github/workflows/parity_nightly.yml diff --git a/.github/workflows/parity_nightly.yml b/.github/workflows/parity_nightly.yml new file mode 100644 index 0000000000..79f5c9b514 --- /dev/null +++ b/.github/workflows/parity_nightly.yml @@ -0,0 +1,219 @@ +# Nightly parity checks from OrcaSlicer/orca-test-repo, kept out of the +# per-build "Run external slicer regression tests" step because they take far +# longer than that step's budget: +# effect - the CLI override sweep's full effect stage: every landed option +# re-sliced on its own to see whether it changes the G-code +# harness - the GUI-vs-CLI parity harness (metrics only, never fails) +# Both test the latest successful build_all.yml Linux AppImage from main, with +# sources checked out at the commit that build was made from. Nothing here +# gates a build or a PR. +name: Parity Nightly + +on: + schedule: + # build_all.yml starts at 17:00 UTC and has finished by ~20:00 + - cron: "0 21 * * *" + workflow_dispatch: + inputs: + test_repo_ref: + description: "orca-test-repo ref to run" + required: false + default: "main" + build_branch: + description: "branch whose latest successful build_all artifact to test" + required: false + default: "main" + fixtures: + description: "harness fixture ids, space-separated (empty = all)" + required: false + default: "" + cli_presets: + description: "harness lane C presets: flat = flatten inherits first, raw = leaf profile as-is" + required: false + default: "flat" + +permissions: + contents: read + actions: read + +jobs: + build: + name: Find the build to test + # Don't run scheduled checks on forks + if: github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer' + runs-on: ubuntu-24.04 + outputs: + run_id: ${{ steps.find.outputs.run_id }} + head_sha: ${{ steps.find.outputs.head_sha }} + steps: + - id: find + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + gh run list --workflow build_all.yml \ + --branch "${{ inputs.build_branch || 'main' }}" \ + --status success --limit 1 --json databaseId,headSha \ + --jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \ + >> "$GITHUB_OUTPUT" + cat "$GITHUB_OUTPUT" + + effect: + name: Override sweep effect stage (shard ${{ matrix.shard }}) + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + # orca-test-repo's parity/effect_routing.json holds a 2-way split, + # ~12.5 min a shard on this runner + shard: [0, 1] + steps: + - &checkout-suite + name: Check out the test suite + uses: actions/checkout@v7 + with: + repository: OrcaSlicer/orca-test-repo + ref: ${{ inputs.test_repo_ref || 'main' }} + path: orca-test-repo + + # The AppImage ships only packed preset caches, so profiles and the CLI + # option surface come from the sources the build was made from + - &checkout-slicer + name: Check out OrcaSlicer at the build's commit + uses: actions/checkout@v7 + with: + ref: ${{ needs.build.outputs.head_sha }} + path: slicer + lfs: 'false' + + - &extract-appimage + name: Download and extract the Linux AppImage + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + set -euo pipefail + gh run download "${{ needs.build.outputs.run_id }}" --dir appimage \ + --pattern "OrcaSlicer_Linux_ubuntu_2404*" + appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1) + [ -n "$appimage" ] || { echo "no x86_64 AppImage in run ${{ needs.build.outputs.run_id }}"; exit 1; } + chmod +x "$appimage" + "$appimage" --appimage-extract > /dev/null + # The bare binary cannot find the AppImage's bundled libraries; AppRun + # sets them up and execs it, so exit codes and signals pass through + [ -x squashfs-root/AppRun ] || { echo "no AppRun in the AppImage"; exit 1; } + echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV" + echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV" + + - name: Install the AppImage's host runtime dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0 + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install suite dependencies + run: pip install -r orca-test-repo/requirements.txt + + - name: Run the override sweep with the full effect stage + id: run + continue-on-error: true + working-directory: orca-test-repo + run: | + set -o pipefail + # -rA keeps the per-stage summaries, which pytest otherwise swallows + # for passing tests + python -m pytest test_cli_overrides.py -c pytest.ini -v -rA \ + --effect-full --effect-shard ${{ matrix.shard }}/2 \ + --orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \ + 2>&1 | tee ../sweep.log + + - name: Publish job summary + if: always() + run: | + { + echo "## Override sweep effect stage, shard ${{ matrix.shard }}/2" + echo "Build ${{ needs.build.outputs.head_sha }} (run ${{ needs.build.outputs.run_id }})" + echo '```' + grep -E "\[override sweep" sweep.log || echo "no stage summaries, see the log" + grep -E "^=+ .*(passed|failed)" sweep.log | tail -1 || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload the override report + if: always() + uses: actions/upload-artifact@v7 + with: + name: override-report-shard${{ matrix.shard }} + path: | + orca-test-repo/.pytest_cache/override_report.json + sweep.log + if-no-files-found: warn + retention-days: 30 + + # The sweep step continues on error so the summary and report still get + # published; this puts the failure back on the job + - name: Fail the job if the sweep failed + if: steps.run.outcome == 'failure' + run: | + echo "the override sweep failed, see the job summary and the uploaded report" >&2 + exit 1 + + harness: + name: GUI-vs-CLI parity harness + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 180 + steps: + - *checkout-suite + - *checkout-slicer + - *extract-appimage + + - name: Install display tooling and the AppImage's host runtime + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + xvfb xdotool imagemagick openbox mesa-utils \ + libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0 + + - name: Run the parity harness + run: | + set -euo pipefail + fixtures=() + for f in ${{ inputs.fixtures || '' }}; do + fixtures+=(--fixture "$f") + done + # 2 GUI displays: ~1.5 cores peak / ~1.9 GB on this 4-vCPU runner, + # and each fixture is fully isolated, so results match a serial run + python3 orca-test-repo/parity/run_parity.py \ + --slicer-root "$ORCA_SOURCE" --bin "$ORCA_BIN" \ + --cli-presets "${{ inputs.cli_presets || 'flat' }}" \ + --gui-workers 2 --out "$PWD/parity-out" "${fixtures[@]}" + + - name: Publish job summary + if: always() + run: | + if [ -f parity-out/report.md ]; then + cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY" + else + echo "the harness produced no report, see the log" >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Drop per-lane datadirs before upload + if: always() + run: rm -rf parity-out/*/seed parity-out/*/datadir-* || true + + - name: Upload the scorecard and evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: parity-scorecard + path: parity-out/ + if-no-files-found: warn + retention-days: 30 From ffb4f192c1bcab178e1eb25f74afbb630f0a9c61 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 14 Sep 2026 14:19:25 +0800 Subject: [PATCH 46/57] Fix macOS UI issue in publish dialog. Remove item_size helper in TabCtrl and its relevant setter --- src/slic3r/GUI/PublishSettingsDialog.cpp | 3 --- src/slic3r/GUI/Widgets/Button.cpp | 9 ++++++--- src/slic3r/GUI/Widgets/TabCtrl.cpp | 23 +++++++---------------- src/slic3r/GUI/Widgets/TabCtrl.hpp | 5 ----- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 16b0b6201c..e63cec7615 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -1037,9 +1037,6 @@ size_t PublishSettingsDialog::section_group_for(Section kind) section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); - // The mixed tabs carry full swatch compositions: give them a touch more room than the - // filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem). - section.mixed_tabs->SetItemSpace(FromDIP(3)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); section.mixed_tabs->Hide(); } diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 22b1c34cab..5f03636f18 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -311,8 +311,11 @@ void Button::render(wxDC& dc) } } auto szContent = textSize; + // Whether the measured content reserved the text/icon gap. macOS measures an empty label + // as 0-high, so the gap is skipped there; the dot must not advance past it in that case. + const bool gap_reserved = szContent.y > 0; if (icon.bmp().IsOk()) { - if (szContent.y > 0) { + if (gap_reserved) { //BBS norrow size between text and icon if (vertical) szContent.y += spacing; @@ -357,10 +360,10 @@ void Button::render(wxDC& dc) dc.DrawBitmap(icon.bmp(), pt); //BBS norrow size between text and icon if (vertical) { - pt.y += szIcon.y + spacing; + pt.y += szIcon.y + (gap_reserved ? spacing : 0); pt.x = rcContent.x; } else { - pt.x += szIcon.x + spacing; + pt.x += szIcon.x + (gap_reserved ? spacing : 0); pt.y = rcContent.y; } } diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 34de109b8f..ef23e2c5e4 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -99,7 +99,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli btns.push_back(btn); if (btns.size() > 1) sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); - sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space); + sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL); sizer->AddStretchSpacer(1); relayout(); return btns.size() - 1; @@ -256,12 +256,12 @@ void TabCtrl::relayout() int item = sel + 1; int first = 0; for (int i = 0; i < item; ++i) - offset += btns[i]->GetMinSize().x + item_space * 2; + offset += btns[i]->GetMinSize().x; if (item < btns.size()) - offset += btns[item]->GetMinSize().x + item_space * 2; + offset += btns[item]->GetMinSize().x; int width = GetSize().x; for (int i = 0; i < btns.size(); ++i) { - auto size = btns[i]->GetMinSize().x + item_space * 2; + auto size = btns[i]->GetMinSize().x; if (i < sel && offset > width) { sizer->Show(i * 2 + 1, false); sizer->Show(i * 2 + 2, false); @@ -284,26 +284,17 @@ void TabCtrl::relayout() if (item >= btns.size()) --item; // Keep spacing 2 ~ 10 TAB_BUTTON_SPACE - int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8; + int b = GetSize().x - offset - 10 - (item + 1 - first) * 16; sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0}); Layout(); } -void TabCtrl::SetItemSpace(int space) -{ - if (space < 0 || space == item_space) - return; - item_space = space; - relayout(); - Refresh(); -} - int TabCtrl::GetFullSize() const { - // Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing. + // Mirrors relayout(): a 10px leading spacer plus every button's min width. int width = 10; for (const Button* btn : btns) - width += btn->GetMinSize().x + item_space * 2; + width += btn->GetMinSize().x; return width; } diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index 493c4edee5..d89da145af 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -14,7 +14,6 @@ class TabCtrl : public StaticBox int sel = -1; wxFont bold; - int item_space = 2; // space around each button, both sides (SetItemSpace) public: TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0); @@ -64,10 +63,6 @@ public: int GetNextVisible(int item) const; bool IsVisible(unsigned int item) const; - // Extra space around each tab button (in px on both sides). Defaults to the control-wide - // standard; call before appending items so every button picks it up. - void SetItemSpace(int space); - int GetFullSize() const; private: From 31f6eb2718491ba34272786c826ba577a4410777 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:26:32 +0800 Subject: [PATCH 47/57] Keep the First Value When a Per-Filament Variant Option Is Too Short (#15639) update_values_to_printer_extruders_for_multiple_filaments picks each filament's value from the flattened (filament x variant) columns of every per-filament variant option. When a column index fell past the end of the option's values, it skipped that filament and left the zero the output vector was created with. The GUI always hands this function full columns, but the CLI does not: - a CLI override of a single value, such as --nozzle-temperature=211 on a four-filament project, came out as 211,0,0,0, so three filaments would print at 0 C; - loading fewer filament presets than the project has filaments left the remaining filaments' columns missing, so filament_cooling_before_tower came out as 10,10,0,0 and filament_ramming_volumetric_speed as -1,-1,0,0. An out-of-range column now keeps the option's first value, the fallback get_at() and the sibling gather step already use. The seven per-type copies of the loop are replaced by that same gather_option_values helper, moved above the function; it now takes its caller's name for its log lines. An empty option, which has no first value, is given one registered default per filament first; it used to be replaced with zeros. On a partial load a filament whose preset was not loaded takes the first filament's value rather than its own preset's, which the CLI does not load; for the options seen in practice those agree. --- src/libslic3r/PrintConfig.cpp | 215 ++++-------------- .../test_config_variant_expansion.cpp | 28 +++ 2 files changed, 67 insertions(+), 176 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 4c27995ba0..e8ac749bd3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10936,6 +10936,28 @@ std::vector DynamicPrintConfig::update_values_to_printer_extruders(DynamicP return variant_index; } +// Regathers a vector option's values through per-slot source indices (one input index per +// output slot). Out-of-range indices keep the first value, matching get_at's fallback. +template +static void gather_option_values(const char *caller, const std::string &key, OptType *opt, const std::vector &slot_param_indices) +{ + if (!opt || opt->values.empty()) { + BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key; + return; + } + std::vector new_values; + new_values.reserve(slot_param_indices.size()); + for (int idx : slot_param_indices) { + if (idx < 0 || static_cast(idx) >= opt->values.size()) { + BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx; + new_values.emplace_back(opt->values.front()); + } + else + new_values.emplace_back(opt->values[idx]); + } + opt->values = std::move(new_values); +} + void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set& key_set, std::string id_name, std::string variant_name) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count; @@ -11013,155 +11035,18 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key; continue; } + // An empty option has no first value to fall back on; give it one registered default per filament. + if (auto *vec = dynamic_cast(this->option(key)); vec && vec->empty() && optdef->default_value) + vec->resize(filament_count, optdef->default_value.get()); switch (optdef->type) { - case coStrings: - { - ConfigOptionStrings * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } - case coInts: - { - ConfigOptionInts * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } - case coFloats: - { - ConfigOptionFloats * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } - case coPercents: - { - ConfigOptionPercents * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } - case coFloatsOrPercents: - { - ConfigOptionFloatsOrPercents * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } - case coBools: - { - ConfigOptionBools * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } - case coEnums: - { - ConfigOptionEnumsGeneric * opt = this->option(key); - if (!opt) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key; - break; - } - std::vector new_values; - - new_values.resize(filament_count); - for (int f_index = 0; f_index < filament_count; f_index++) - { - if (variant_index[f_index] < 0 || static_cast(variant_index[f_index]) >= opt->size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index]; - continue; - } - new_values[f_index] = opt->get_at(variant_index[f_index]); - } - opt->values = new_values; - break; - } + case coStrings: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; + case coInts: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; + case coFloats: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; + case coPercents: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; + case coFloatsOrPercents: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; + case coBools: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; + case coEnums: gather_option_values(__FUNCTION__, key, this->option(key), variant_index); break; default: BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key; break; @@ -11180,28 +11065,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen } } -// Regathers a vector option's values through per-slot source indices (one input index per -// output slot). Out-of-range indices keep the first value, matching get_at's fallback. -template -static void gather_option_values(const std::string &key, OptType *opt, const std::vector &slot_param_indices) -{ - if (!opt || opt->values.empty()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key; - return; - } - std::vector new_values; - new_values.reserve(slot_param_indices.size()); - for (int idx : slot_param_indices) { - if (idx < 0 || static_cast(idx) >= opt->values.size()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx; - new_values.emplace_back(opt->values.front()); - } - else - new_values.emplace_back(opt->values[idx]); - } - opt->values = std::move(new_values); -} - void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config, const std::unordered_map>& filament_variant_uses, int extruder_count, int extruder_nozzle_volume_count, @@ -11296,13 +11159,13 @@ void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(Dy continue; } switch (optdef->type) { - case coStrings: gather_option_values(key, this->option(key), slot_param_indices); break; - case coInts: gather_option_values(key, this->option(key), slot_param_indices); break; - case coFloats: gather_option_values(key, this->option(key), slot_param_indices); break; - case coPercents: gather_option_values(key, this->option(key), slot_param_indices); break; - case coFloatsOrPercents: gather_option_values(key, this->option(key), slot_param_indices); break; - case coBools: gather_option_values(key, this->option(key), slot_param_indices); break; - case coEnums: gather_option_values(key, this->option(key), slot_param_indices); break; + case coStrings: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; + case coInts: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; + case coFloats: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; + case coPercents: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; + case coFloatsOrPercents: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; + case coBools: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; + case coEnums: gather_option_values(__FUNCTION__, key, this->option(key), slot_param_indices); break; default: BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key; break; diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index 2469789d5b..d8e09539bb 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -484,6 +484,34 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 21.})); REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } + + SECTION("a variant option shorter than the filament slots keeps its first value instead of zero") { + DynamicPrintConfig config; + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHighFlow}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow"}; + make_filament_arrays(config); + config.option("filament_map", true)->values = {1, 2}; + // no loaded preset carries the key, so only its single registered default is present + config.option("filament_cooling_before_tower", true)->values = {10.}; + // only the first filament's two variant columns were loaded + config.option("filament_ramming_volumetric_speed", true)->values = {-1., -2.}; + + std::vector> nozzle_volume_types; + int extruder_count = 2; + int count = config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); + + config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, + "filament_self_index", "filament_extruder_variant"); + + // filament 2 resolves to column 3 (its extruder's High Flow column), past the end of both vectors + REQUIRE_THAT(config.option("filament_cooling_before_tower")->values, + Catch::Matchers::Approx(std::vector({10., 10.}))); + REQUIRE_THAT(config.option("filament_ramming_volumetric_speed")->values, + Catch::Matchers::Approx(std::vector({-1., -1.}))); + REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12., 21.})); + } } // update_values_from_multi_to_multi_2 walks the DESTINATION PRINTER's variant list while writing From 00429da73928550a88c5dc73c683a1d8078d61f5 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:28:08 +0800 Subject: [PATCH 48/57] Apply the GUI's Mixed Filament Rules on the CLI (#15636) A valid mixed filament already slices the same on the CLI as in the GUI; these are the places where the CLI still skipped a rule the GUI applies. - Keep the prime tower when a mixed filament is used, even if every --load-filaments preset is the same. A mixed filament swaps between its components every layer, so turning the tower off left the swaps with nothing to purge on. - Leave a mixed slot's row and column of the flush matrix at zero when --filament-colour triggers a recompute, as the GUI does; a mixed slot never reaches a nozzle. - Refuse a mixed slot that has no filament of its own. Feature filament ids aimed at it were past the filament count, got reset to filament 1 and the model silently printed in one colour. - Refuse a plate that uses a mixed filament whose components are different filament types, the type half of the GUI's Sidebar::has_broken_mixed_filament. Missing or out-of-range components are already rejected for the whole project by validate(). get_extruders_under_cli gains an expand_mixed_slots flag so the gate can see mixed slots rather than their components; existing callers keep the expanded list. Both refusals exit with the new CLI_MIXED_FILAMENT_INVALID (-69). --- src/OrcaSlicer.cpp | 68 +++++++++++++++++++++++++++++++++++- src/libslic3r/Utils.hpp | 1 + src/slic3r/GUI/PartPlate.cpp | 4 +-- src/slic3r/GUI/PartPlate.hpp | 3 +- 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 499e73d073..b75c653eda 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -53,6 +53,7 @@ using namespace nlohmann; #include "libslic3r/libslic3r.h" #include "libslic3r/Config.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/GCode.hpp" @@ -162,6 +163,7 @@ std::map cli_errors = { {CLI_FILAMENT_CAN_NOT_MAP, "Some filaments cannot be mapped to correct extruders for multi-extruder Printer."}, {CLI_ONLY_ONE_TPU_SUPPORTED, "Not support printing 2 or more TPU filaments."}, {CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER, "Some filaments cannot be printed on the extruder mapped to."}, + {CLI_MIXED_FILAMENT_INVALID, "A mixed filament is invalid: its components are different filament types, or it has no filament of its own."}, {CLI_SLICING_ERROR, "Failed slicing the model. Please verify the slicing of all plates on Orca Slicer before uploading."}, {CLI_GCODE_PATH_CONFLICTS, " G-code conflicts detected after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer. If the file slices normally in Orca Slicer, try moving the wipe tower further from other models, as we use more conservative parameters for it during upload."}, {CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, "Found G-code in unprintable area of multi-extruder printers after slicing. Please make sure the 3mf file can be successfully sliced in the latest Orca Slicer."} @@ -3700,6 +3702,15 @@ int CLI::run(int argc, char **argv) } } + // A mixed slot never reaches a nozzle, so its row and column stay empty, as in the GUI. + // Command line options are not merged into m_print_config yet, so they win here. + const ConfigOptionBools *is_mixed_opt = m_extra_config.option("filament_is_mixed"); + if (!is_mixed_opt) + is_mixed_opt = m_print_config.option("filament_is_mixed"); + auto is_mixed_slot = [is_mixed_opt](int idx) { + return is_mixed_opt && idx < static_cast(is_mixed_opt->values.size()) && is_mixed_opt->values[idx]; + }; + for (size_t nozzle_id = 0; nozzle_id < new_extruder_count; ++nozzle_id) { std::vector flush_vol_mtx = get_flush_volumes_matrix(flush_vol_matrix, nozzle_id, new_extruder_count); for (int from_idx = 0; from_idx < project_filament_count; from_idx++) { @@ -3709,7 +3720,7 @@ int CLI::run(int argc, char **argv) bool is_from_support = filament_is_support->get_at(from_idx); for (int to_idx = 0; to_idx < project_filament_count; to_idx++) { bool is_to_support = filament_is_support->get_at(to_idx); - if (from_idx == to_idx) { + if (from_idx == to_idx || is_mixed_slot(from_idx) || is_mixed_slot(to_idx)) { flush_vol_mtx[project_filament_count * from_idx + to_idx] = 0.f; } else { int flushing_volume = 0; @@ -3937,6 +3948,22 @@ int CLI::run(int argc, char **argv) // Normalizing after importing the 3MFs / AMFs m_print_config.normalize_fdm(); + // A mixed slot is virtual but still needs a filament entry of its own. Without one, feature + // filament ids aimed at it fall outside the filament count, are reset to the first filament + // and the model silently prints in a single colour. + if (const auto *is_mixed_opt = m_print_config.option("filament_is_mixed")) { + const auto &is_mixed = is_mixed_opt->values; + for (size_t slot = static_cast(std::max(filament_count, 0)); slot < is_mixed.size(); ++slot) { + if (!is_mixed[slot]) + continue; + BOOST_LOG_TRIVIAL(error) << boost::format("mixed filament slot %1% has no filament of its own, only %2% filaments are loaded; " + "load one filament per slot, including each mixed one") + % (slot + 1) % filament_count; + record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, 0, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info); + flush_and_exit(CLI_MIXED_FILAMENT_INVALID); + } + } + m_print_config.option>("printer_technology", true)->value = printer_technology; bool has_wipe_tower_position = m_print_config.option("wipe_tower_x") && m_print_config.option("wipe_tower_y"); @@ -3991,6 +4018,15 @@ int CLI::run(int argc, char **argv) bool is_smooth_timelapse = false; if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth)) is_smooth_timelapse = true; + // A mixed filament swaps between its components every layer, so it needs the tower even when + // every loaded preset is the same. + if (disable_wipe_tower_after_mapping) { + if (const auto *is_mixed_opt = m_print_config.option("filament_is_mixed"); + is_mixed_opt && has_any_mixed_filament(is_mixed_opt->values)) { + disable_wipe_tower_after_mapping = false; + BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to a mixed filament")%__LINE__; + } + } if (disable_wipe_tower_after_mapping) { if (is_smooth_timelapse) { @@ -6197,6 +6233,36 @@ int CLI::run(int argc, char **argv) flush_and_exit(CLI_ONLY_ONE_TPU_SUPPORTED); } + // Same type gate as the GUI's Sidebar::has_broken_mixed_filament: refuse a plate that uses a + // mixed slot whose components are different filament types. Missing or out-of-range + // components never get here, validate() already rejects them for the whole project. + const auto *is_mixed_opt = m_print_config.option("filament_is_mixed"); + const auto *components_opt = m_print_config.option("filament_mixed_components"); + if (is_mixed_opt && components_opt && has_any_mixed_filament(is_mixed_opt->values)) { + const auto &is_mixed = is_mixed_opt->values; + const auto &components = components_opt->values; + const size_t num_physical = static_cast(filament_count) - static_cast(std::count(is_mixed.begin(), is_mixed.end(), true)); + std::vector physical_types(num_physical); + for (size_t f_index = 0; f_index < num_physical; ++f_index) { + std::string displayed_type; + physical_types[f_index] = m_print_config.get_filament_type(displayed_type, static_cast(f_index)); + if (physical_types[f_index].empty()) + physical_types[f_index] = "PLA"; + } + const std::vector mismatched_slots = check_mixed_filament_type_consistency(is_mixed, components, physical_types); + // plate_filaments has mixed slots expanded to their components; the gate needs the slots. + const std::vector plate_slots = mismatched_slots.empty() ? std::vector() : + part_plate->get_extruders_under_cli(true, m_print_config, false); + for (size_t slot : mismatched_slots) { + if (std::find(plate_slots.begin(), plate_slots.end(), static_cast(slot) + 1) == plate_slots.end()) + continue; + BOOST_LOG_TRIVIAL(error) << boost::format("plate %1%: mixed filament %2% mixes components of different filament types") + % (index + 1) % (slot + 1); + record_exit_reson(outfile_dir, CLI_MIXED_FILAMENT_INVALID, index + 1, cli_errors[CLI_MIXED_FILAMENT_INVALID], sliced_info); + flush_and_exit(CLI_MIXED_FILAMENT_INVALID); + } + } + if (new_extruder_count > 1) { std::vector> unprintable_filament_vec; for (const std::set& filamnt_ids : unprintable_filament_ids) { diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 797894442a..c364860531 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -70,6 +70,7 @@ #define CLI_FILAMENT_CAN_NOT_MAP -66 #define CLI_ONLY_ONE_TPU_SUPPORTED -67 #define CLI_FILAMENTS_NOT_SUPPORTED_BY_EXTRUDER -68 +#define CLI_MIXED_FILAMENT_INVALID -69 #define CLI_SLICING_ERROR -100 #define CLI_GCODE_PATH_CONFLICTS -101 diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index c9370cc282..893260f934 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1717,7 +1717,7 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode, const Dynam return plate_extruders; } -std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const +std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config, bool expand_mixed_slots) const { std::vector plate_extruders; @@ -1878,7 +1878,7 @@ std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D // Expand mixed filament slots to their physical components. A mixed slot is virtual and // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the // physical filaments it resolves to instead. - { + if (expand_mixed_slots) { auto* is_mixed_opt = full_config.option("filament_is_mixed"); auto* comp_strs_opt = full_config.option("filament_mixed_components"); if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 6d7eb18beb..e913ebaabf 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -350,7 +350,8 @@ public: // get used filaments from config, 1 based idx std::vector get_extruders(bool conside_custom_gcode = false) const; std::vector get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const; - std::vector get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const; + // expand_mixed_slots = false keeps mixed filament slots as slots instead of their components. + std::vector get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config, bool expand_mixed_slots = true) const; std::vector get_extruders_without_support(bool conside_custom_gcode = false) const; // get used filaments from gcode result, 1 based idx std::vector get_used_filaments(); From 5f01f21661d5bd4002a6b261464ec4cd13cb3c7d Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 14 Sep 2026 17:44:17 +0800 Subject: [PATCH 49/57] Load Each Vendor Tree Once When the CLI Resolves System Presets Resolving a system preset through its vendor manifest loaded the whole vendor tree and the filament library from JSON, and the CLI did that separately for every --load-settings and --load-filaments file. A run with machine, process and filament presets parsed BBL's 2,879 profile files and the library's 512 three times over, about a second each. Keep the library and vendor bundles loaded by the manifest path on the PresetBundle that resolved them, keyed by source root, vendor and substitution rule, and have the CLI resolve every system preset through one bundle for the whole run. A failed load is not kept, so errors are reported as before. On a cube slice with X1C machine, process and PLA presets: 2.42 s -> 0.93 s, BBL.json opened once instead of three times, identical G-code. --- src/OrcaSlicer.cpp | 10 ++-- src/libslic3r/PresetBundle.cpp | 58 ++++++++++++------- src/libslic3r/PresetBundle.hpp | 14 +++++ .../libslic3r/test_preset_bundle_loading.cpp | 44 ++++++++++++++ 4 files changed, 100 insertions(+), 26 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b75c653eda..f2ce73e1f4 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -2010,19 +2010,21 @@ int CLI::run(int argc, char **argv) } }; - auto resolve_preset = [&ensure_cli_preset_bundle](const std::string &file, DynamicPrintConfig &config, + // One resolver for the whole run, so presets from the same vendor tree share its load. + std::unique_ptr system_preset_resolver; + auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config, std::string &config_type, const std::string &config_from, bool probe_type, std::string &error) { const auto *inherits = config.option(BBL_JSON_KEY_INHERITS); if (!probe_type && (inherits == nullptr || inherits->value.empty())) return true; - std::unique_ptr source_bundle; PresetBundle *bundle = nullptr; bool allow_source_manifest = false; if (config_from == "system") { - source_bundle = std::make_unique(); - bundle = source_bundle.get(); + if (!system_preset_resolver) + system_preset_resolver = std::make_unique(); + bundle = system_preset_resolver.get(); allow_source_manifest = true; } else { bundle = ensure_cli_preset_bundle(error); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 4b8fb03a02..9cef965490 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -549,30 +549,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ continue; try { - PresetBundle library_bundle; - const PresetBundle *base_bundle = nullptr; - if (vendor_id != ORCA_FILAMENT_LIBRARY && - boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) { - library_bundle.m_preserve_vendor_source_paths = true; - library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem, - compatibility_rule, nullptr, false); - if (library_bundle.error_count() != 0) { - error = "OrcaFilamentLibrary contains invalid presets"; - return false; - } - base_bundle = &library_bundle; - } - - PresetBundle source_bundle; - source_bundle.m_preserve_vendor_source_paths = true; - source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, - compatibility_rule, base_bundle, false); - if (source_bundle.error_count() != 0) { - error = "Vendor bundle contains invalid presets"; + const SourceManifestBundles *loaded = load_source_manifest(root_dir, vendor_id, compatibility_rule, error); + if (loaded == nullptr) return false; - } - const Preset *resolved = find_loaded(source_bundle); + const Preset *resolved = find_loaded(*loaded->vendor); if (resolved == nullptr) { if (error.empty()) error = "Source file is not an instantiated preset in its vendor manifest"; @@ -591,6 +572,39 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ return false; } +const PresetBundle::SourceManifestBundles *PresetBundle::load_source_manifest(const boost::filesystem::path &root_dir, + const std::string &vendor_id, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error) +{ + auto key = std::make_tuple(root_dir.string(), vendor_id, static_cast(compatibility_rule)); + if (auto it = m_source_manifest_bundles.find(key); it != m_source_manifest_bundles.end()) + return &it->second; + + SourceManifestBundles loaded; + if (vendor_id != ORCA_FILAMENT_LIBRARY && + boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) { + loaded.library = std::make_unique(); + loaded.library->m_preserve_vendor_source_paths = true; + loaded.library->load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem, + compatibility_rule, nullptr, false); + if (loaded.library->error_count() != 0) { + error = "OrcaFilamentLibrary contains invalid presets"; + return nullptr; + } + } + + loaded.vendor = std::make_unique(); + loaded.vendor->m_preserve_vendor_source_paths = true; + loaded.vendor->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, + compatibility_rule, loaded.library.get(), false); + if (loaded.vendor->error_count() != 0) { + error = "Vendor bundle contains invalid presets"; + return nullptr; + } + return &m_source_manifest_bundles.emplace(std::move(key), std::move(loaded)).first->second; +} + bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, const std::string &source_file, ForwardCompatibilitySubstitutionRule compatibility_rule, diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 353b6dc07d..a0fceb332b 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -652,6 +653,19 @@ private: bool m_generate_vendor_caches { false }; bool m_preserve_vendor_source_paths { false }; + // Vendor trees loaded by resolve_preset_config's manifest path, so every preset + // resolved through this bundle shares one load per source root and vendor. + struct SourceManifestBundles { + std::unique_ptr library; + std::unique_ptr vendor; + }; + std::map, SourceManifestBundles> m_source_manifest_bundles; + + const SourceManifestBundles *load_source_manifest(const boost::filesystem::path &root_dir, + const std::string &vendor_id, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error); + // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). bool check_duplicate_filament_subtypes() const; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index ecdede7053..5341e6c621 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -987,6 +987,50 @@ TEST_CASE("Resolution terminates when no vendor manifest exists", "[Preset][Bund CHECK(error == "Preset was not found in the loaded bundle"); } +TEST_CASE("Manifest-backed resolution reuses the vendor tree it already loaded", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path process_dir = dir.path() / "Acme" / "process"; + fs::create_directories(process_dir); + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" + << R"({"name":"fdm_process_common","sub_path":"process/base.json"},)" + << R"({"name":"Acme First","sub_path":"process/first.json"},)" + << R"({"name":"Acme Second","sub_path":"process/second.json"}]})"; + auto write_base = [&](double travel_speed) { + std::ofstream((process_dir / "base.json").string()) + << R"({"type":"process","name":"fdm_process_common","from":"system",)" + << R"("instantiation":"false","travel_speed":[")" << travel_speed << R"("]})"; + }; + auto write_child = [&](const std::string &file, const std::string &name) { + std::ofstream((process_dir / file).string()) + << R"({"type":"process","name":")" << name << R"(","from":"system",)" + << R"("instantiation":"true","inherits":"fdm_process_common"})"; + }; + write_base(111.0); + write_child("first.json", "Acme First"); + write_child("second.json", "Acme Second"); + + auto travel_speed = [&](PresetBundle &bundle, const std::string &file) { + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, (process_dir / file).string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + return raw.option("travel_speed")->values.front(); + }; + + PresetBundle bundle; + CHECK_THAT(travel_speed(bundle, "first.json"), Catch::Matchers::WithinAbs(111.0, 1e-6)); + + // Only a reload would see this change. + write_base(222.0); + CHECK_THAT(travel_speed(bundle, "second.json"), Catch::Matchers::WithinAbs(111.0, 1e-6)); + + PresetBundle fresh; + CHECK_THAT(travel_speed(fresh, "second.json"), Catch::Matchers::WithinAbs(222.0, 1e-6)); +} + // Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic // library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible // with that printer and the plater combo box lists the shared alias twice. From d4840901fc2476e6d141ab46da51a8e361705516 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 14 Sep 2026 18:51:48 +0800 Subject: [PATCH 50/57] Test That Failed Vendor Loads Are Not Kept and the Library Base Is Reused Cover the two cache paths the first test left open: a vendor tree that fails to load is retried on the next resolution instead of being served from the cache, and a type-probed filament resolved through resolve_preset_config_type reuses the OrcaFilamentLibrary base already loaded for a sibling. --- .../libslic3r/test_preset_bundle_loading.cpp | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 5341e6c621..29c38395ac 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1031,6 +1031,93 @@ TEST_CASE("Manifest-backed resolution reuses the vendor tree it already loaded", CHECK_THAT(travel_speed(fresh, "second.json"), Catch::Matchers::WithinAbs(222.0, 1e-6)); } +TEST_CASE("Manifest-backed resolution does not keep a vendor tree that failed to load", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path child_file = dir.path() / "Acme" / "process" / "child.json"; + auto write_manifest = [&](const std::string &leading_entry) { + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","process_list":[)" << leading_entry + << R"({"name":"Acme Process","sub_path":"process/child.json"}]})"; + }; + write_manifest("123,"); + fs::create_directories(child_file.parent_path()); + std::ofstream(child_file.string()) + << R"({"type":"process","name":"Acme Process","from":"system",)" + << R"("instantiation":"true","layer_height":"0.2"})"; + + PresetBundle bundle; + auto resolve = [&](std::string &error) { + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_process_common"; + return bundle.resolve_preset_config(raw, Preset::TYPE_PRINT, child_file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error); + }; + + std::string error; + CHECK_FALSE(resolve(error)); + CHECK_FALSE(error.empty()); + + write_manifest(""); + error.clear(); + CHECK(resolve(error)); + CHECK(error.empty()); +} + +TEST_CASE("Manifest-backed resolution reuses the library base for type-probed files", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path library_pet = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament" / "pet.json"; + const fs::path filament_dir = dir.path() / "Acme" / "filament"; + + std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string()) + << R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)" + << R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"}]})"; + fs::create_directories(library_pet.parent_path()); + auto write_library_pet = [&](double density) { + std::ofstream(library_pet.string()) + << R"({"type":"filament","name":"fdm_filament_pet","from":"system",)" + << R"("filament_id":"GFL99","instantiation":"false",)" + << R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})"; + }; + write_library_pet(1.27); + + std::ofstream((dir.path() / "Acme.json").string()) + << R"({"version":"1.0.0","name":"Acme","filament_list":[)" + << R"({"name":"Acme PETG","sub_path":"filament/petg.json","filament_id":"GFA00"},)" + << R"({"name":"Acme PETG Matte","sub_path":"filament/petg_matte.json","filament_id":"GFA01"}]})"; + fs::create_directories(filament_dir); + auto write_child = [&](const std::string &file, const std::string &name, const std::string &filament_id) { + std::ofstream((filament_dir / file).string()) + << R"({"type":"filament","name":")" << name << R"(","from":"system",)" + << R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})"; + }; + write_child("petg.json", "Acme PETG", "GFA00"); + write_child("petg_matte.json", "Acme PETG Matte", "GFA01"); + + auto density = [](const DynamicPrintConfig &config) { + return config.option("filament_density")->values.front(); + }; + + PresetBundle bundle; + DynamicPrintConfig first; + first.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet"; + std::string error; + REQUIRE(bundle.resolve_preset_config(first, Preset::TYPE_FILAMENT, (filament_dir / "petg.json").string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK_THAT(density(first), Catch::Matchers::WithinAbs(1.27, 1e-6)); + + // Only a reload would see this change. + write_library_pet(1.5); + + DynamicPrintConfig second; + Preset::Type type = Preset::TYPE_INVALID; + REQUIRE(bundle.resolve_preset_config_type(second, type, (filament_dir / "petg_matte.json").string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + CHECK(type == Preset::TYPE_FILAMENT); + CHECK_THAT(density(second), Catch::Matchers::WithinAbs(1.27, 1e-6)); +} + // Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic // library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible // with that printer and the plater combo box lists the shared alias twice. From 70247ad298a1087c5d507b27a9f0e95f6c236b09 Mon Sep 17 00:00:00 2001 From: Daniel Williams <35799546+danielwoz@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:37:04 +0800 Subject: [PATCH 51/57] Extract Layer::choose_ironing_extruder for unit-testable ironing routing (#13467) * Extract Layer::choose_ironing_extruder for unit-testable ironing routing The ironing extruder selection in make_ironing() was a 5-line nested conditional inlined at the top of the loop, with no isolated test coverage. Pull the gating into a static helper so the routing decision is unit-testable without spinning up the slicing pipeline. Pure refactor: the helper preserves the original logic bit-for-bit (NoIroning -> -1; AllSolid always enabled; TopSurfaces and TopmostOnly require some top shells or, in spiral mode, more than one bottom shell; TopmostOnly additionally requires being on the topmost layer; enabled ironing routes to solid_infill_filament). Add tests/fff_print/test_choose_ironing_extruder.cpp covering: - AllSolid regardless of layer position - TopSurfaces with top_shell_layers > 0 - TopSurfaces with top_shell_layers=0 + spiral mode + bottom_shell_layers>1 - TopmostOnly + topmost layer - NoIroning short-circuit - TopSurfaces with top_shell_layers=0 (and not spiral) -> disabled - TopSurfaces, spiral, but bottom_shell_layers=1 -> disabled - TopmostOnly on a non-topmost layer -> disabled * Move ironing routing test into the Fill subsystem file Rename the test to tests/libslic3r/test_fill.cpp and tag it [Fill] to match the subsystem it covers, use flat behavioral test cases with GENERATE for the parameterized ones, and drop the history narration from the code comments. * tests: move ironing routing tests into fff_print/test_fill.cpp Keeps the Fill tests in one file, alongside the existing ironing rotation-template test. --- src/libslic3r/Fill/Fill.cpp | 36 ++++++++++++------- src/libslic3r/Layer.hpp | 6 ++++ tests/fff_print/test_fill.cpp | 68 +++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index f5386b085c..28fabed8af 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -1595,6 +1595,25 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc return sparse_infill_polylines; } +// Returns the filament id (1-based) the region is ironed with, or -1 when the +// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need +// either some top shells or, in spiral mode, more than one bottom shell, and +// TopmostOnly additionally needs the layer to be the topmost one. +int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg, + bool spiral_mode, + bool is_topmost_layer) +{ + if (cfg.ironing_type == IroningType::NoIroning) + return -1; + const bool gate = (cfg.ironing_type == IroningType::AllSolid) + || ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1)) + && (cfg.ironing_type == IroningType::TopSurfaces + || (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer))); + if (!gate) + return -1; + return cfg.top_surface_filament_id; +} + // Create ironing extrusions over top surfaces. void Layer::make_ironing() { @@ -1664,19 +1683,10 @@ void Layer::make_ironing() if (! layerm->slices.empty()) { IroningParams ironing_params; const PrintRegionConfig &config = layerm->region().config(); - if (config.ironing_type != IroningType::NoIroning && - (config.ironing_type == IroningType::AllSolid || - ((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) && - (config.ironing_type == IroningType::TopSurfaces || - (config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) { - if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) { - // Iron the whole face. - ironing_params.extruder = config.top_surface_filament_id; - } else { - // Iron just the infill. - ironing_params.extruder = config.top_surface_filament_id; - } - } + ironing_params.extruder = Layer::choose_ironing_extruder( + config, + /*spiral_mode=*/this->object()->print()->config().spiral_mode, + /*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr); if (ironing_params.extruder != -1) { //TODO just_infill is currently not used. ironing_params.just_infill = false; diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 8a5aa78036..9be6b86139 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -16,6 +16,7 @@ using LayerPtrs = std::vector; class LayerRegion; using LayerRegionPtrs = std::vector; class PrintRegion; +class PrintRegionConfig; class PrintObject; class Print; @@ -200,6 +201,11 @@ public: FillAdaptive::Octree *support_fill_octree, FillLightning::Generator* lightning_generator) const; void make_ironing(); + // Returns the filament id (1-based) the region is ironed with, or -1 when the + // region is not ironed. + static int choose_ironing_extruder(const PrintRegionConfig &cfg, + bool spiral_mode, + bool is_topmost_layer); void make_contour_z(const sla::IndexedMesh &mesh); void export_region_slices_to_svg(const char *path) const; diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index aa81570e56..a3696c47ad 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -15,6 +15,7 @@ #include "libslic3r/Geometry.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/PrintConfig.hpp" #include "libslic3r/SVG.hpp" #include "libslic3r/libslic3r.h" @@ -676,6 +677,73 @@ TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]") REQUIRE(compared > int(ironing.size()) / 2); } + +namespace { + +PrintRegionConfig ironing_config(IroningType type, + int top_surface_filament_id = 1, + int top_shell_layers = 3, + int bottom_shell_layers = 1) +{ + PrintRegionConfig cfg; + cfg.ironing_type.value = type; + cfg.top_surface_filament_id.value = top_surface_filament_id; + cfg.top_shell_layers.value = top_shell_layers; + cfg.bottom_shell_layers.value = bottom_shell_layers; + cfg.outer_wall_filament_id.value = 1; + cfg.wall_loops.value = 2; + return cfg; +} + +} // namespace + +TEST_CASE("Ironing an all-solid region uses the top surface filament on every layer", "[Fill]") +{ + const PrintRegionConfig cfg = ironing_config(IroningType::AllSolid, /*top_surface_filament_id=*/2); + const bool is_topmost_layer = GENERATE(false, true); + CAPTURE(is_topmost_layer); + REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, is_topmost_layer) == 2); +} + +TEST_CASE("Ironing top surfaces uses the top surface filament when the region has top shells", "[Fill]") +{ + const PrintRegionConfig cfg = ironing_config(IroningType::TopSurfaces, + /*top_surface_filament_id=*/3, + /*top_shell_layers=*/2); + REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == 3); +} + +TEST_CASE("Ironing top surfaces without top shells needs spiral mode and more than one bottom shell", "[Fill]") +{ + const PrintRegionConfig one_bottom_shell = ironing_config(IroningType::TopSurfaces, + /*top_surface_filament_id=*/1, + /*top_shell_layers=*/0, + /*bottom_shell_layers=*/1); + const PrintRegionConfig two_bottom_shells = ironing_config(IroningType::TopSurfaces, + /*top_surface_filament_id=*/1, + /*top_shell_layers=*/0, + /*bottom_shell_layers=*/2); + + REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == 1); + REQUIRE(Layer::choose_ironing_extruder(one_bottom_shell, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == -1); + REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1); +} + +TEST_CASE("Ironing the topmost surface only applies to the topmost layer", "[Fill]") +{ + const PrintRegionConfig cfg = ironing_config(IroningType::TopmostOnly, /*top_surface_filament_id=*/4); + REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/true) == 4); + REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1); +} + +TEST_CASE("A region with ironing turned off is never ironed", "[Fill]") +{ + const PrintRegionConfig cfg = ironing_config(IroningType::NoIroning); + const bool spiral_mode = GENERATE(false, true); + CAPTURE(spiral_mode); + REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1); +} + TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]") { auto angles_for = [](int direction) { From 31eb8a2bd1f402da52b4b81af82ef436b2a83705 Mon Sep 17 00:00:00 2001 From: packerlschupfer <83344883+packerlschupfer@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:35:29 +0200 Subject: [PATCH 52/57] CLI: let --export-settings - write the merged config to stdout --export-settings already writes the merged config as JSON at the right point in the CLI flow. Passing - writes the same document to stdout. - ConfigBase::save_to_json gains a stream overload. The file overload serializes through it before opening the file, so the format is unchanged and a config that cannot be serialized leaves an existing file untouched instead of truncating it. - On stdout, invalid UTF-8 in string values is written as U+FFFD instead of ending the process with an uncaught type_error; files keep the strict behaviour. - - is rejected up front when combined with an action or transform that can write to stdout or does real work, so stdout carries only the JSON. - The unconditional "skip locked instance" stdout write during arrange now goes to the log. - Tests in tests/libslic3r/test_config.cpp. --- src/OrcaSlicer.cpp | 27 ++++++++++++++-- src/libslic3r/Config.cpp | 21 +++++++++---- src/libslic3r/Config.hpp | 3 ++ src/libslic3r/PrintConfig.cpp | 2 +- tests/libslic3r/test_config.cpp | 55 +++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b75c653eda..07009ef46a 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1387,6 +1387,25 @@ int CLI::run(int argc, char **argv) if (downward_check_option) downward_check = downward_check_option->value; + // --export-settings - writes its JSON to stdout, so reject every action or transform that may write there + // too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is + // sliced or exported. + if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") { + static const std::set stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save", + "mtcpp", "mstpp", "no_check", "normative_check", "pipe" }; + for (const std::vector *opt_keys : { &m_actions, &m_transforms }) { + for (const std::string &opt_key : *opt_keys) { + if (stdout_compatible.count(opt_key) == 0) { + std::string flag = opt_key; + std::replace(flag.begin(), flag.end(), '_', '-'); + boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl; + record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info); + flush_and_exit(CLI_INVALID_PARAMS); + } + } + } + } + bool start_gui = m_actions.empty() && !downward_check; if (start_gui) { BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl; @@ -5348,7 +5367,7 @@ int CLI::run(int argc, char **argv) //skip this object due to be locked in plate ap.itemid = locked_aps.size(); locked_aps.emplace_back(ap); - boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx; } } } @@ -5937,7 +5956,11 @@ int CLI::run(int argc, char **argv) //FIXME check for mixing the FFF / SLA parameters. // or better save fff_print_config vs. sla_print_config //m_print_config.save(m_config.opt_string("save")); - m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION)); + const std::string &settings_file = m_config.opt_string(opt_key); + if (settings_file == "-") + m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true); + else + m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION)); } else if (opt_key == "info") { // --info works on unrepaired model for (Model &model : m_models) { diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 394cfb5b74..52a46dcacf 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -1515,6 +1516,19 @@ std::optional parse_capability_ref(const std::string& value //BBS: add json support void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const +{ + // Serialize first: if that throws (invalid UTF-8), the existing file stays untouched. + std::ostringstream ss; + this->save_to_json(ss, name, from, version); + boost::nowide::ofstream c; + c.open(file, std::ios::out | std::ios::trunc); + c << ss.str(); + c.close(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file; +} + +void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const { json j; //record the headers @@ -1561,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name, j["plugins"] = unique_refs; } - boost::nowide::ofstream c; - c.open(file, std::ios::out | std::ios::trunc); - c << j.dump(1, '\t') << std::endl; - c.close(); - - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file; + os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl; } void ConfigBase::save(const std::string &file) const diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index ea85cda1e7..6d23ec3770 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2825,6 +2825,9 @@ public: //BBS: add json support void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const; + // Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless + // replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler). + void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const; // Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin // dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json() diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index e8ac749bd3..0b0fe71dc0 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -11916,7 +11916,7 @@ CLIActionsConfigDef::CLIActionsConfigDef() def = this->add("export_settings", coString); def->label = L("Export Settings"); - def->tooltip = L("This exports settings to a file."); + def->tooltip = L("This exports settings to a file. Use - to write them to stdout."); def->cli_params = "settings.json"; def->set_default_value(new ConfigOptionString("output.json")); diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 3813e2df3f..208bbc6cf0 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -15,6 +15,8 @@ #include #include +#include + using namespace Slic3r; SCENARIO("Generic config validation performs as expected.", "[Config]") { @@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[ CHECK(reloaded.option("slicing_pipeline_plugin")->values == refs); } +TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") { + DynamicPrintConfig config; + config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); + config.set_key_value("wall_loops", new ConfigOptionInt(3)); + config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" })); + config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5")); + + ScopedTemporaryFile tmp(".json"); + config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"); + std::string file_contents; + { + boost::nowide::ifstream ifs(tmp.string()); + file_contents.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + // The file format: one tab per nesting level and a trailing newline. + REQUIRE_FALSE(file_contents.empty()); + CHECK(file_contents.rfind("{\n\t\"", 0) == 0); + CHECK(file_contents.back() == '\n'); + + std::ostringstream strict, replaced; + config.save_to_json(strict, "test_preset", "User", "1.0.0.0"); + config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true); + CHECK(strict.str() == file_contents); + CHECK(replaced.str() == file_contents); + CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5"); +} + +TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") { + DynamicPrintConfig config; + config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff")); + + std::ostringstream strict, replaced; + CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error); + REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true)); + CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD"); +} + +TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") { + DynamicPrintConfig config; + config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff")); + + ScopedTemporaryFile tmp(".json"); + { + boost::nowide::ofstream ofs(tmp.string()); + ofs << "previous"; + } + CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error); + + boost::nowide::ifstream ifs(tmp.string()); + const std::string contents((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + CHECK(contents == "previous"); +} + TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") { const std::vector refs = { "master_plugin;;header-stamp", From 54968834932950f67596e70f64845c1a72ed252c Mon Sep 17 00:00:00 2001 From: Nopraz <12595433+Nopraz@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:44:15 +0200 Subject: [PATCH 53/57] =?UTF-8?q?fix(profiles):=20Snapmaker=20U1=20?= =?UTF-8?q?=E2=80=94=20cap=20ABS/ASA/PPS=20bed=20temps=20at=20100=20=C2=B0?= =?UTF-8?q?C=20(#15483)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The U1's heated bed tops out at 100 °C, but these profiles requested 105-110 °C, which leads to print errors unless the user modifies the printer's firmware configuration. Affected profiles: - Snapmaker ABS @U1 base (110/105 → 100) - Snapmaker ASA @U1 base (110 → 100) - Fiberon ASA-CF08 @Snapmaker U1 base (105 → 100) - Fiberon PPS-GF20 @Snapmaker U1 base (105 → 100) Bumps Snapmaker.json to 02.04.00.10. Co-authored-by: yw4z --- resources/profiles/Snapmaker.json | 2 +- .../Fiberon ASA-CF08 @Snapmaker U1 base.json | 10 +++++----- .../Fiberon PPS-GF20 @Snapmaker U1 base.json | 16 ++++++++-------- .../filament/Snapmaker ABS @U1 base.json | 4 ++-- .../filament/Snapmaker ASA @U1 base.json | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index 0407dca2ec..393271d0e5 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.04.00.12", + "version": "02.04.00.13", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1 base.json b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1 base.json index 693553bdcb..86648d6096 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1 base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon ASA-CF08 @Snapmaker U1 base.json @@ -15,13 +15,13 @@ "1" ], "cool_plate_temp": [ - "105" + "100" ], "cool_plate_temp_initial_layer": [ - "105" + "100" ], "eng_plate_temp": [ - "105" + "100" ], "eng_plate_temp_initial_layer": [ "100" @@ -48,7 +48,7 @@ "Polymaker" ], "hot_plate_temp": [ - "105" + "100" ], "hot_plate_temp_initial_layer": [ "100" @@ -72,7 +72,7 @@ "110.8" ], "textured_plate_temp": [ - "105" + "100" ], "textured_plate_temp_initial_layer": [ "100" diff --git a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PPS-GF20 @Snapmaker U1 base.json b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PPS-GF20 @Snapmaker U1 base.json index 5e1cfa7c61..ee28c2b059 100644 --- a/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PPS-GF20 @Snapmaker U1 base.json +++ b/resources/profiles/Snapmaker/filament/Polymaker/Fiberon PPS-GF20 @Snapmaker U1 base.json @@ -15,16 +15,16 @@ "1" ], "cool_plate_temp": [ - "105" + "100" ], "cool_plate_temp_initial_layer": [ - "105" + "100" ], "eng_plate_temp": [ - "105" + "100" ], "eng_plate_temp_initial_layer": [ - "105" + "100" ], "fan_cooling_layer_time": [ "12" @@ -51,10 +51,10 @@ "Polymaker" ], "hot_plate_temp": [ - "105" + "100" ], "hot_plate_temp_initial_layer": [ - "105" + "100" ], "nozzle_temperature": [ "300" @@ -81,10 +81,10 @@ "110" ], "textured_plate_temp": [ - "105" + "100" ], "textured_plate_temp_initial_layer": [ - "105" + "100" ], "filament_type": [ "ABS" diff --git a/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1 base.json index 67754ade09..48740f94bc 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker ABS @U1 base.json @@ -9,10 +9,10 @@ "" ], "hot_plate_temp": [ - "110" + "100" ], "hot_plate_temp_initial_layer": [ - "105" + "100" ], "overhang_fan_speed": [ "20" diff --git a/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1 base.json b/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1 base.json index 413c14cebb..b75f8d84d3 100644 --- a/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1 base.json +++ b/resources/profiles/Snapmaker/filament/Snapmaker ASA @U1 base.json @@ -9,7 +9,7 @@ "" ], "hot_plate_temp": [ - "110" + "100" ], "hot_plate_temp_initial_layer": [ "100" From 5c635d5e504c5f88d45ff7f0d66b63a83382d0bc Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Mon, 14 Sep 2026 15:04:03 -0500 Subject: [PATCH 54/57] build: scope -Werror to the Clang family so GCC builds again (#15701) --- CMakeLists.txt | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d2880a7d4b..6e713d8c88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -587,10 +587,15 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR add_compile_options(-Wno-${w}) endforeach () - # Turn everything else into an error. Dependency headers are exempt because the SYSTEM - # include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out, - # apart from GCC's maybe-uninitialized, demoted below. - add_compile_options(-Werror) + # GCC is not built in CI, so don't throw errors CI won't catch. + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Werror=return-type) + else () + # Turn everything else into an error. Dependency headers are exempt because the + # SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their + # diagnostics out. + add_compile_options(-Werror) + endif () # Demoted. Remove a name once its category is cleared on every compiler. set(warnings_demoted) @@ -612,20 +617,6 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR cast-function-type-mismatch ) endif () - if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - list(APPEND warnings_demoted - # maybe-uninitialized runs after inlining and reports inside boost/variant, - # boost/tuple and the bundled clipper header even with -isystem. - maybe-uninitialized - - # array-bounds is reported once, where ConfigOptionVector::set_at inlines - # into OrcaSlicer.cpp on a branch the preceding type test rules out. - array-bounds - - # template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers. - template-id-cdtor - ) - endif () if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") list(APPEND warnings_demoted # enum-constexpr-conversion is a Clang warning that defaults to an error, From 292cf0095e698a6e0f96041bd142fd41afd6ccfb Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Mon, 14 Sep 2026 16:31:23 -0500 Subject: [PATCH 55/57] drop the per-frame mouse raycast that only a drag start reads (#15664) --- src/slic3r/GUI/GLCanvas3D.cpp | 11 +++-------- src/slic3r/GUI/GLCanvas3D.hpp | 1 - 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index e63501eec1..76192491bf 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2098,12 +2098,6 @@ void GLCanvas3D::render(bool only_init) _render_selection_center(); #endif // ENABLE_RENDER_SELECTION_CENTER - // we need to set the mouse's scene position here because the depth buffer - // could be invalidated by the following gizmo render methods - // this position is used later into on_mouse() to drag the objects - if (m_picking_enabled) - m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast()); - // sidebar hints need to be rendered before the gizmos because the depth buffer // could be invalidated by the following gizmo render methods _render_selection_sidebar_hints(); @@ -4491,12 +4485,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); volume_bbox.offset(1.0); const bool is_cut_connector_selected = m_selection.is_any_connector(); - if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) { + const Vec3d scene_position = _mouse_to_3d(pos); + if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(scene_position) && !is_cut_connector_selected) { m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; // The dragging operation is initiated. m_mouse.drag.move_volume_idx = volume_idx; m_selection.setup_cache(); - m_mouse.drag.start_position_3D = m_mouse.scene_position; + m_mouse.drag.start_position_3D = scene_position; m_sequential_print_clearance_first_displacement = true; m_moving = true; diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index b1dd674d96..c2962c3858 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -337,7 +337,6 @@ class GLCanvas3D bool dragging{ false }; Vec2d position{ DBL_MAX, DBL_MAX }; - Vec3d scene_position{ DBL_MAX, DBL_MAX, DBL_MAX }; bool ignore_left_up{ false }; Drag drag; bool ignore_right_up; From efc9f253ee2d3e16cfb95331ea5234d2b237dca1 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Mon, 14 Sep 2026 23:47:01 -0500 Subject: [PATCH 56/57] fix: resolve relative input paths given on the command line (#14803) Opening a model with a relative path, for example `orca-slicer ./some.3mf`, failed with "Loading of a model file failed." and "The file does not contain any geometry data.", while the same file opened by an absolute path or by drag and drop worked. GUI_App::init_app_config() changes the working directory to /log, and it runs from the GUI_App constructor because the app config is needed early for instance checking. The input files are opened much later, in post_init(), so a path still relative at that point resolved against the log directory instead of the directory OrcaSlicer was started from, and the 3MF reader failed to open it. Resolve the input paths in CLI::setup(), which runs before GUI_App is constructed and therefore before the working directory moves. Absolute paths are returned unchanged, so the forms that open today are unaffected, and custom open protocol URLs are passed through since post_init() hands those to the downloader rather than the file loader. The working directory change is left alone. It was added in #3248 so the TUTK logs land in the data directory instead of the working directory (#3209). --- src/OrcaSlicer.cpp | 7 ++++ src/libslic3r/Utils.hpp | 3 ++ src/libslic3r/utils.cpp | 13 +++++++ tests/libslic3r/test_utils.cpp | 64 ++++++++++++++++++++++++++++++++++ tests/test_utils.hpp | 18 ++++++++++ 5 files changed, 105 insertions(+) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b75c653eda..24f218caa5 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -7715,6 +7715,13 @@ bool CLI::setup(int argc, char **argv) this->print_help(); return false; } + + // Orca: resolve here, while the process is still in the directory the user invoked it from. + // GUI_App's constructor moves the working directory to /log, long before the GUI + // opens these files in post_init(), and a relative path would then resolve against that. + for (std::string &input_file : m_input_files) + input_file = resolve_cli_input_path(input_file); + // Parse actions and transform options. for (auto const &opt_key : opt_order) { if (cli_actions_config_def.has(opt_key)) diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index c364860531..b21da72fc8 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -314,6 +314,9 @@ extern unsigned get_current_pid(); std::string per_user_temp_id(); // Per-user temp root under `base`; an empty `user_id` returns `base` unchanged. std::string per_user_temp_dir(const std::string &base, const std::string &user_id); +// Completes a relative command line input path against the current working directory. Absolute +// paths and custom open protocol URLs are returned unchanged. +std::string resolve_cli_input_path(const std::string &path); // BBS: backup & restore std::string get_process_name(int pid); diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 58323b29ce..9def5dad17 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1339,6 +1339,19 @@ std::string per_user_temp_dir(const std::string &base, const std::string &user_i return base + "/orcaslicer_" + user_id; } +std::string resolve_cli_input_path(const std::string &path) +{ + const boost::filesystem::path input(path); + if (path.empty() || is_supported_open_protocol(path) || input.is_absolute()) + return path; + + boost::system::error_code ec; + const boost::filesystem::path resolved = boost::filesystem::system_complete(input, ec); + if (ec) + return path; + return resolved.lexically_normal().make_preferred().string(); +} + // BBS: backup & restore std::string get_process_name(int pid) { diff --git a/tests/libslic3r/test_utils.cpp b/tests/libslic3r/test_utils.cpp index 484438127c..7880b783f1 100644 --- a/tests/libslic3r/test_utils.cpp +++ b/tests/libslic3r/test_utils.cpp @@ -4,6 +4,8 @@ #include "test_utils.hpp" +#include + #include #include #include @@ -88,3 +90,65 @@ TEST_CASE("copy_file reports the OS error when the destination cannot be written REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; })); #endif // _WIN32 } + +TEST_CASE("A resolved input path still names the same file after the working directory changes", "[utils]") { + ScopedTemporaryFile model(".3mf"); + { std::ofstream out(model.string()); out << "3mf"; } + const std::string name = model.path().filename().string(); + + // Resolve the bare name from the directory holding the file, then move away from it. The guard + // restores the directory the test started in, wherever this leaves it. + ScopedWorkingDirectory cwd(model.path().parent_path()); + const std::string resolved = resolve_cli_input_path(name); + boost::filesystem::current_path(boost::filesystem::path(TEST_DATA_DIR)); + + REQUIRE(boost::filesystem::exists(resolved)); + REQUIRE(boost::filesystem::equivalent(resolved, model.path())); + // Control: the bare name finds nothing from here, so resolving it this late would have failed. + REQUIRE_FALSE(boost::filesystem::exists(name)); +} + +TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[utils]") { + ScopedWorkingDirectory cwd(boost::filesystem::temp_directory_path()); + // Read back rather than reusing temp_directory_path(): changing to it resolves any symlink. + const boost::filesystem::path here = boost::filesystem::current_path(); + + SECTION("a bare name") { + REQUIRE(resolve_cli_input_path("model.3mf") == (here / "model.3mf").make_preferred().string()); + } + SECTION("a ./ prefix is dropped") { + REQUIRE(resolve_cli_input_path("./model.3mf") == (here / "model.3mf").make_preferred().string()); + } + SECTION("a ../ traversal is collapsed") { + REQUIRE(resolve_cli_input_path("../model.3mf") == (here.parent_path() / "model.3mf").make_preferred().string()); + } +} + +TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[utils]") { + SECTION("an absolute path") { + const boost::filesystem::path absolute = (boost::filesystem::temp_directory_path() / "model.3mf").make_preferred(); + REQUIRE(resolve_cli_input_path(absolute.string()) == absolute.string()); + } +#ifdef _WIN32 + // Every absolute form Windows accepts opens today, so each must come back byte for byte: + // normalizing them would rewrite the forward slashes and rebuild the \\?\ and UNC prefixes. + SECTION("an absolute Windows path of any form") { + for (const std::string absolute : {R"(C:\models\model.3mf)", + R"(C:/models/model.3mf)", + R"(\\server\share\model.3mf)", + R"(\\?\C:\models\model.3mf)"}) + REQUIRE(resolve_cli_input_path(absolute) == absolute); + } +#endif + // These are downloaded rather than opened, and completing one would produce a path, not a URL. + SECTION("a custom open protocol URL") { + for (const std::string url : {"orcaslicer://open/?file=https://example.com/model.3mf", + "prusaslicer://open/?file=https://example.com/model.3mf", + "bambustudio://open/?file=https://example.com/model.3mf", + "cura://open/?file=https://example.com/model.3mf"}) + REQUIRE(resolve_cli_input_path(url) == url); + } + SECTION("an empty argument") { + REQUIRE(resolve_cli_input_path("").empty()); + } +} diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index e3fbbe8fab..0b04e6ad11 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -176,4 +176,22 @@ inline void write_debug_stream([[maybe_unused]] const std::string &name, [[maybe #endif } +// Changes the working directory and restores the previous one on scope exit, including when an +// assertion throws. It is process wide state shared with every other test. +class ScopedWorkingDirectory +{ +public: + explicit ScopedWorkingDirectory(const boost::filesystem::path &dir) + : m_previous(boost::filesystem::current_path()) + { + boost::filesystem::current_path(dir); + } + ~ScopedWorkingDirectory() { boost::system::error_code ec; boost::filesystem::current_path(m_previous, ec); } + ScopedWorkingDirectory(const ScopedWorkingDirectory &) = delete; + ScopedWorkingDirectory &operator=(const ScopedWorkingDirectory &) = delete; + +private: + boost::filesystem::path m_previous; +}; + #endif // SLIC3R_TEST_UTILS From d5cf1502c442b0b4860dedfa0b6d791d243f4299 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Tue, 15 Sep 2026 13:31:30 +0800 Subject: [PATCH 57/57] Share One Library Load Between Vendors in the CLI Preset Resolver The manifest resolver loaded OrcaFilamentLibrary once per vendor it resolved through, so a run that mixes vendors parsed the library tree again for each of them. The library is now cached like any other vendor tree, keyed on its root and substitution rule, and doubles as the base every vendor under that root loads against. A vendor bundle only reads from its base while loading, so sharing the instance is safe. The cache key carries the substitution rule as its enum, and the lookup lambdas take a const bundle since they only read. --- src/libslic3r/PresetBundle.cpp | 48 ++++++++-------- src/libslic3r/PresetBundle.hpp | 18 +++--- .../libslic3r/test_preset_bundle_loading.cpp | 56 +++++++++++++++++++ 3 files changed, 87 insertions(+), 35 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 9cef965490..54e5db27e4 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -484,7 +484,7 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem) compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; - auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * { + auto collection_for_type = [](const PresetBundle &bundle, Preset::Type preset_type) -> const PresetCollection * { switch (preset_type) { case Preset::TYPE_PRINT: return &bundle.prints; case Preset::TYPE_FILAMENT: return &bundle.filaments; @@ -493,15 +493,15 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ } }; - PresetCollection *collection = collection_for_type(*this, type); + const PresetCollection *collection = collection_for_type(*this, type); if (collection == nullptr) { error = "Unsupported preset type"; return false; } const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal(); - auto find_loaded = [&](PresetBundle &bundle) -> const Preset * { - PresetCollection *loaded_collection = collection_for_type(bundle, type); + auto find_loaded = [&](const PresetBundle &bundle) -> const Preset * { + const PresetCollection *loaded_collection = collection_for_type(bundle, type); const Preset *resolved = nullptr; for (const Preset &preset : loaded_collection->get_presets()) { if (preset.file.empty()) @@ -549,11 +549,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ continue; try { - const SourceManifestBundles *loaded = load_source_manifest(root_dir, vendor_id, compatibility_rule, error); + const PresetBundle *loaded = load_source_vendor(root_dir, vendor_id, compatibility_rule, error); if (loaded == nullptr) return false; - const Preset *resolved = find_loaded(*loaded->vendor); + const Preset *resolved = find_loaded(*loaded); if (resolved == nullptr) { if (error.empty()) error = "Source file is not an instantiated preset in its vendor manifest"; @@ -572,37 +572,35 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ return false; } -const PresetBundle::SourceManifestBundles *PresetBundle::load_source_manifest(const boost::filesystem::path &root_dir, - const std::string &vendor_id, - ForwardCompatibilitySubstitutionRule compatibility_rule, - std::string &error) +const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir, + const std::string &vendor_id, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error) { - auto key = std::make_tuple(root_dir.string(), vendor_id, static_cast(compatibility_rule)); - if (auto it = m_source_manifest_bundles.find(key); it != m_source_manifest_bundles.end()) - return &it->second; + auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule); + if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end()) + return it->second.get(); - SourceManifestBundles loaded; + // The library loads with no base of its own, so the tree a vendor inherits from + // is the same one that resolves the library's own presets. + const PresetBundle *library = nullptr; if (vendor_id != ORCA_FILAMENT_LIBRARY && boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) { - loaded.library = std::make_unique(); - loaded.library->m_preserve_vendor_source_paths = true; - loaded.library->load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem, - compatibility_rule, nullptr, false); - if (loaded.library->error_count() != 0) { + library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error); + if (library == nullptr) { error = "OrcaFilamentLibrary contains invalid presets"; return nullptr; } } - loaded.vendor = std::make_unique(); - loaded.vendor->m_preserve_vendor_source_paths = true; - loaded.vendor->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, - compatibility_rule, loaded.library.get(), false); - if (loaded.vendor->error_count() != 0) { + auto bundle = std::make_unique(); + bundle->m_preserve_vendor_source_paths = true; + bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false); + if (bundle->error_count() != 0) { error = "Vendor bundle contains invalid presets"; return nullptr; } - return &m_source_manifest_bundles.emplace(std::move(key), std::move(loaded)).first->second; + return m_source_vendor_bundles.emplace(std::move(key), std::move(bundle)).first->second.get(); } bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type, diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index a0fceb332b..88455fabf3 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -654,17 +654,15 @@ private: bool m_preserve_vendor_source_paths { false }; // Vendor trees loaded by resolve_preset_config's manifest path, so every preset - // resolved through this bundle shares one load per source root and vendor. - struct SourceManifestBundles { - std::unique_ptr library; - std::unique_ptr vendor; - }; - std::map, SourceManifestBundles> m_source_manifest_bundles; + // resolved through this bundle shares one load per source root and vendor. The + // filament library is one such tree, shared by every vendor under its root. + std::map, std::unique_ptr> + m_source_vendor_bundles; - const SourceManifestBundles *load_source_manifest(const boost::filesystem::path &root_dir, - const std::string &vendor_id, - ForwardCompatibilitySubstitutionRule compatibility_rule, - std::string &error); + const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir, + const std::string &vendor_id, + ForwardCompatibilitySubstitutionRule compatibility_rule, + std::string &error); // Orca: validation only - flag any printer with two or more compatible // filament presets sharing one filament_id (ambiguous AMS subtype match). diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 29c38395ac..73d244cf42 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1118,6 +1118,62 @@ TEST_CASE("Manifest-backed resolution reuses the library base for type-probed fi CHECK_THAT(density(second), Catch::Matchers::WithinAbs(1.27, 1e-6)); } +TEST_CASE("Manifest-backed resolution shares the library between vendors under one root", "[Preset][Bundle][Regression]") +{ + ScopedTemporaryDir dir; + const fs::path library_dir = dir.path() / PresetBundle::ORCA_FILAMENT_LIBRARY / "filament"; + + std::ofstream((dir.path() / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json")).string()) + << R"({"version":"1.0.0","name":"OrcaFilamentLibrary","filament_list":[)" + << R"({"name":"fdm_filament_pet","sub_path":"filament/pet.json","filament_id":"GFL99"},)" + << R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json","filament_id":"GFL98"}]})"; + fs::create_directories(library_dir); + auto write_library_pet = [&](double density) { + std::ofstream((library_dir / "pet.json").string()) + << R"({"type":"filament","name":"fdm_filament_pet","from":"system",)" + << R"("filament_id":"GFL99","instantiation":"false",)" + << R"("filament_type":["PETG"],"filament_density":[")" << density << R"("]})"; + }; + write_library_pet(1.27); + std::ofstream((library_dir / "generic_petg.json").string()) + << R"({"type":"filament","name":"Generic PETG","from":"system",)" + << R"("filament_id":"GFL98","instantiation":"true","inherits":"fdm_filament_pet"})"; + + auto write_vendor = [&](const std::string &vendor, const std::string &filament_id) { + const fs::path filament_dir = dir.path() / vendor / "filament"; + fs::create_directories(filament_dir); + std::ofstream((dir.path() / (vendor + ".json")).string()) + << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)" + << R"({"name":")" << vendor << R"( PETG","sub_path":"filament/petg.json","filament_id":")" << filament_id << R"("}]})"; + std::ofstream((filament_dir / "petg.json").string()) + << R"({"type":"filament","name":")" << vendor << R"( PETG","from":"system",)" + << R"("filament_id":")" << filament_id << R"(","instantiation":"true","inherits":"fdm_filament_pet"})"; + return filament_dir / "petg.json"; + }; + const fs::path acme_petg = write_vendor("Acme", "GFA00"); + const fs::path beta_petg = write_vendor("Beta", "GFB00"); + + auto density = [&](PresetBundle &bundle, const fs::path &file) { + DynamicPrintConfig raw; + raw.option(BBL_JSON_KEY_INHERITS, true)->value = "fdm_filament_pet"; + std::string error; + REQUIRE(bundle.resolve_preset_config(raw, Preset::TYPE_FILAMENT, file.string(), + ForwardCompatibilitySubstitutionRule::EnableSilent, error)); + return raw.option("filament_density")->values.front(); + }; + + PresetBundle bundle; + CHECK_THAT(density(bundle, acme_petg), Catch::Matchers::WithinAbs(1.27, 1e-6)); + + // Only a reload would see this change. + write_library_pet(1.5); + CHECK_THAT(density(bundle, beta_petg), Catch::Matchers::WithinAbs(1.27, 1e-6)); + CHECK_THAT(density(bundle, library_dir / "generic_petg.json"), Catch::Matchers::WithinAbs(1.27, 1e-6)); + + PresetBundle fresh; + CHECK_THAT(density(fresh, beta_petg), Catch::Matchers::WithinAbs(1.5, 1e-6)); +} + // Orca: a filament in the Orca Filament Library that names its compatible printers has to hide the generic // library filament sharing its alias, the same way a vendor owned filament does. Otherwise both are compatible // with that printer and the plater combo box lists the shared alias twice.