From 575a8748325396c2018360a0216e1a70d55c2e0c Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Sun, 23 Aug 2026 19:37:35 -0500 Subject: [PATCH 01/13] fix: keep CRLF when patching CPython on Windows git apply inherits the caller's configuration, so under core.autocrlf=input it rewrites the patched PCbuild/find_python.bat to LF. cmd.exe cannot resolve goto labels in an LF batch file, so CPython's build fails with "The system cannot find the batch label specified - begin_search" and then "Cannot locate python.exe on PATH or as PYTHON variable". git init already runs in the extracted source, so setting core.autocrlf on the repository it creates is enough, without touching the shared PATCH_CMD. --- deps/python3/python3.cmake | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake index e93045fe45..936d75f488 100644 --- a/deps/python3/python3.cmake +++ b/deps/python3/python3.cmake @@ -15,7 +15,12 @@ if(WIN32) # See https://github.com/python/cpython/issues/153438 # Patch from https://github.com/python/cpython/pull/153608 # This patch has not been merged to 3.12 yet so we need to apply it manually - set(_patch_cmd git init && ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch) + # + # The config lands on the CPython repo git init just made, not OrcaSlicer. Without + # it the patched find_python.bat comes out LF and cmd.exe cannot find its goto labels. + set(_patch_cmd git init + && git config core.autocrlf false + && ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch) if(MSVC_VERSION EQUAL 1800) set(_python_platform_toolset v120) From 4d67159ea2463dc662e00555b0203b9210c9e6bb Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Mon, 31 Aug 2026 08:20:26 -0500 Subject: [PATCH 02/13] fix: scope the CRLF override to the git apply invocation git config wrote core.autocrlf into the repository git init creates in the extracted CPython source. Nothing outside deps/build reads that repository, so the setting was already contained. -c applies the override to the one invocation instead, so no repository config is written at all. It cannot reuse PATCH_CMD, so the shared flags are spelled out here. --- deps/python3/python3.cmake | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake index 936d75f488..78eb6e72b2 100644 --- a/deps/python3/python3.cmake +++ b/deps/python3/python3.cmake @@ -16,11 +16,12 @@ if(WIN32) # Patch from https://github.com/python/cpython/pull/153608 # This patch has not been merged to 3.12 yet so we need to apply it manually # - # The config lands on the CPython repo git init just made, not OrcaSlicer. Without - # it the patched find_python.bat comes out LF and cmd.exe cannot find its goto labels. + # Without core.autocrlf=false the patched find_python.bat comes out LF and + # cmd.exe cannot find its goto labels. set(_patch_cmd git init - && git config core.autocrlf false - && ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch) + && ${GIT_EXECUTABLE} -c core.autocrlf=false apply --verbose + --ignore-space-change --whitespace=fix + ${CMAKE_CURRENT_LIST_DIR}/01-windows-nuget.patch) if(MSVC_VERSION EQUAL 1800) set(_python_platform_toolset v120) From 6014f3fb43502ce9dcbfd2abb6f087f6eef76033 Mon Sep 17 00:00:00 2001 From: ocidburn <79639602+ocidburn@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:05:11 +0200 Subject: [PATCH 03/13] Fix: guard the fifth null deref of the same kind, on the project-settings path (#15451) --- src/libslic3r/Config.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index a2756a9f8e..394cfb5b74 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -1049,7 +1049,8 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex std::vector& different_settings = this->option("different_settings_to_system", true)->values; size_t size = different_settings.size(); if (size == 0) { - size = this->option("filament_settings_id")->values.size() + 2; + const auto *filament_ids = this->option("filament_settings_id"); + size = (filament_ids ? filament_ids->values.size() : 0) + 2; different_settings.resize(size); } From 71e4e191917d70a7a508a1a3f322730b6a79d86d Mon Sep 17 00:00:00 2001 From: yw4z Date: Sun, 6 Sep 2026 00:17:42 +0300 Subject: [PATCH 04/13] Fix crash on startup if mixed filament has invalid component (#15432) --- src/slic3r/GUI/Plater.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7f1e6ab400..90206a438c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3988,16 +3988,16 @@ void Sidebar::update_mixed_filament_list() p->m_panel_mixed_warning->Show(false); // Show/dismiss 3D canvas notification for broken mixed filaments - if (has_mixed && !broken_set.empty()) { - auto* notify = wxGetApp().plater()->get_notification_manager(); - if (notify) + auto* notify = plater->get_notification_manager(); + GLCanvas3D* view3d_canvas = plater->get_view3D_canvas3D(); + if(view3d_canvas && view3d_canvas->is_initialized() && notify){ + if (has_mixed && !broken_set.empty()) { notify->push_notification(NotificationType::BBLMixedFilamentBroken, NotificationManager::NotificationLevel::ErrorNotificationLevel, _u8L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); - } else { - auto* notify = wxGetApp().plater()->get_notification_manager(); - if (notify) + } else { notify->close_notification_of_type(NotificationType::BBLMixedFilamentBroken); + } } if (has_mixed) { From c0c2cc5068bff9ef5e5f4e4138fa25fa1f4e65d2 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sat, 5 Sep 2026 20:11:40 -0500 Subject: [PATCH 05/13] fix: Virtual Camera Tools install downloads the network plugin (#15540) --- src/slic3r/GUI/MediaPlayCtrl.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 71fbcb054b..0d75c3770d 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -494,13 +494,13 @@ void MediaPlayCtrl::ToggleStream() DownloadProgressDialog2(MediaPlayCtrl *ctrl) : DownloadProgressDialog(_L("Downloading Virtual Camera Tools")), ctrl(ctrl) {} struct UpgradeNetworkJob2 : UpgradeNetworkJob { - UpgradeNetworkJob2(std::shared_ptr pri) : UpgradeNetworkJob() { + UpgradeNetworkJob2() { name = "cameratools"; package_name = "camera_tools.zip"; } }; - std::shared_ptr make_job(std::shared_ptr pri) - { return std::make_shared(pri); } + std::unique_ptr make_job() override + { return std::make_unique(); } void on_finish() override { ctrl->CallAfter([ctrl = this->ctrl] { ctrl->ToggleStream(); }); From 0b6bbd85bd269277e940f09b906481ae9b16ac0c Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 6 Sep 2026 09:08:57 -0500 Subject: [PATCH 06/13] build: clear 75 warnings - hidden base overloads (#15542) --- src/libslic3r/Int128.hpp | 1 - src/slic3r/GUI/AMSMaterialsSetting.cpp | 5 --- src/slic3r/GUI/AMSMaterialsSetting.hpp | 1 - src/slic3r/GUI/AMSSetting.cpp | 4 +- src/slic3r/GUI/AMSSetting.hpp | 2 +- .../GUI/DeviceTab/uiAMSBestPositionPopup.cpp | 2 +- .../GUI/DeviceTab/uiAMSBestPositionPopup.hpp | 2 +- .../GUI/DeviceTab/uiAmsHumidityPopup.cpp | 2 +- src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h | 4 +- .../GUI/DeviceTab/wgtDeviceNozzleRack.cpp | 4 +- .../GUI/DeviceTab/wgtDeviceNozzleRack.h | 2 +- .../GUI/DeviceTab/wgtDeviceNozzleSelect.cpp | 2 +- src/slic3r/GUI/FilamentMapPanel.cpp | 44 +++++-------------- src/slic3r/GUI/FilamentMapPanel.hpp | 11 ++--- src/slic3r/GUI/SelectMachine.cpp | 4 +- src/slic3r/GUI/SendToPrinter.cpp | 2 +- .../GUI/SlicingProgressNotification.cpp | 12 ++--- .../GUI/SlicingProgressNotification.hpp | 4 +- src/slic3r/GUI/Widgets/AMSControl.cpp | 4 +- src/slic3r/GUI/Widgets/AnimaController.cpp | 2 +- src/slic3r/GUI/Widgets/AnimaController.hpp | 2 +- src/slic3r/GUI/Widgets/ErrorMsgStaticText.hpp | 2 +- src/slic3r/GUI/Widgets/MultiNozzleSync.cpp | 2 +- src/slic3r/GUI/Widgets/RadioBox.cpp | 5 --- src/slic3r/GUI/Widgets/RadioBox.hpp | 4 -- 25 files changed, 45 insertions(+), 84 deletions(-) diff --git a/src/libslic3r/Int128.hpp b/src/libslic3r/Int128.hpp index f8951f20f9..e7238ca745 100644 --- a/src/libslic3r/Int128.hpp +++ b/src/libslic3r/Int128.hpp @@ -125,7 +125,6 @@ public: /******************************************** Splitting the 128bit number into two 64bit words *********************************************/ Int128(int64_t lo = 0) : m_lo((uint64_t)lo), m_hi((lo < 0) ? -1 : 0) {} - Int128(const Int128 &val) : m_lo(val.m_lo), m_hi(val.m_hi) {} Int128(const int64_t& hi, const uint64_t& lo) : m_lo(lo), m_hi(hi) {} Int128& operator = (const int64_t &val) diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index 2e436e8462..e0b7f1ddeb 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -1938,11 +1938,6 @@ void ColorPickerPopup::paintEvent(wxPaintEvent& evt) void ColorPickerPopup::OnDismiss() {} -void ColorPickerPopup::Popup() -{ - PopupWindow::Popup(); -} - bool ColorPickerPopup::ProcessLeftDown(wxMouseEvent& event) { return PopupWindow::ProcessLeftDown(event); } diff --git a/src/slic3r/GUI/AMSMaterialsSetting.hpp b/src/slic3r/GUI/AMSMaterialsSetting.hpp index 81167884a7..2cbd58e9a9 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.hpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.hpp @@ -85,7 +85,6 @@ public: void set_ams_colours(std::vector ams); void set_def_colour(wxColour col); void paintEvent(wxPaintEvent& evt); - void Popup(); virtual void OnDismiss() wxOVERRIDE; virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE; diff --git a/src/slic3r/GUI/AMSSetting.cpp b/src/slic3r/GUI/AMSSetting.cpp index d8760cfcf6..9ba9031f19 100644 --- a/src/slic3r/GUI/AMSSetting.cpp +++ b/src/slic3r/GUI/AMSSetting.cpp @@ -292,7 +292,7 @@ void AMSSetting::UpdateByObj(MachineObject* obj) update_ams_img(obj); - m_ams_type->Update(obj); + m_ams_type->UpdateInfo(obj); //m_ams_arrange_order->Update(obj); update_insert_material_read_mode(obj); m_sizer_remain_block->Show(obj->is_support_update_remain); @@ -624,7 +624,7 @@ void AMSSettingTypePanel::CreateGui() Fit(); } -void AMSSettingTypePanel::Update(const MachineObject* obj) +void AMSSettingTypePanel::UpdateInfo(const MachineObject* obj) { if (!obj) { Show(false); diff --git a/src/slic3r/GUI/AMSSetting.hpp b/src/slic3r/GUI/AMSSetting.hpp index 69630c59d2..e157c947ae 100644 --- a/src/slic3r/GUI/AMSSetting.hpp +++ b/src/slic3r/GUI/AMSSetting.hpp @@ -110,7 +110,7 @@ public: ~AMSSettingTypePanel(); public: - void Update(const MachineObject* obj); + void UpdateInfo(const MachineObject* obj); private: void CreateGui(); diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp index a258a2178a..8505a559d6 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.cpp @@ -705,7 +705,7 @@ ReselectMachineDialog::ReselectMachineDialog(wxWindow* parent) Centre(); } -void ReselectMachineDialog::Update(MachineObject* obj, const std::map& best_pos_map, const std::vector& ams_mapping, wxString save_time) +void ReselectMachineDialog::UpdateInfo(MachineObject* obj, const std::map& best_pos_map, const std::vector& ams_mapping, wxString save_time) { if (suggestText) diff --git a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp index be8f957c94..265666bb12 100644 --- a/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp +++ b/src/slic3r/GUI/DeviceTab/uiAMSBestPositionPopup.hpp @@ -188,7 +188,7 @@ class ReselectMachineDialog : public wxDialog public: ReselectMachineDialog(wxWindow* parent); ~ReselectMachineDialog(); - void Update(MachineObject* obj, + void UpdateInfo(MachineObject* obj, const std::map& best_pos_map, const std::vector& ams_mapping, wxString save_time); diff --git a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp index 259506612d..e33987af33 100644 --- a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp +++ b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.cpp @@ -98,7 +98,7 @@ void uiAmsPercentHumidityDryPopup::Create() Refresh(); } -void uiAmsPercentHumidityDryPopup::Update(int humidiy_level, int humidity_percent, int left_dry_time, float current_temperature) +void uiAmsPercentHumidityDryPopup::UpdateInfo(int humidiy_level, int humidity_percent, int left_dry_time, float current_temperature) { if (m_humidity_level != humidiy_level || m_humidity_percent != humidity_percent || m_left_dry_time != left_dry_time || m_current_temperature != current_temperature) diff --git a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h index a109381c86..867e6ac118 100644 --- a/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h +++ b/src/slic3r/GUI/DeviceTab/uiAmsHumidityPopup.h @@ -38,14 +38,14 @@ public: ~uiAmsPercentHumidityDryPopup() = default; public: - void Update(uiAmsHumidityInfo *info) { m_ams_id = info->ams_id; Update(info->humidity_display_idx, info->humidity_percent, info->left_dry_time, info->current_temperature); }; + void UpdateInfo(uiAmsHumidityInfo *info) { m_ams_id = info->ams_id; UpdateInfo(info->humidity_display_idx, info->humidity_percent, info->left_dry_time, info->current_temperature); }; std::string get_owner_ams_id() const { return m_ams_id; } void msw_rescale(); private: - void Update(int humidiy_level, int humidity_percent, int left_dry_time, float current_temperature); + void UpdateInfo(int humidiy_level, int humidity_percent, int left_dry_time, float current_temperature); void UpdateContents(); void Create(); diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp index 1e40a71150..44394866c8 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.cpp @@ -399,7 +399,7 @@ void wgtDeviceNozzleRackArea::UpdateNozzleItems(const std::unordered_mapUpdate(nozzle_rack); + iter.second->UpdateInfo(nozzle_rack); } /*update nozzle possition and background*/ @@ -837,7 +837,7 @@ void wgtDeviceNozzleRackNozzleItem::SetSelected(bool selected) } } -void wgtDeviceNozzleRackNozzleItem::Update(const std::shared_ptr rack, bool on_rack /*= true*/) +void wgtDeviceNozzleRackNozzleItem::UpdateInfo(const std::shared_ptr rack, bool on_rack /*= true*/) { m_rack = rack; diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h index 385fa6be48..16cd311120 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleRack.h @@ -200,7 +200,7 @@ public: wgtDeviceNozzleRackNozzleItem(wxWindow* parent, int nozzle_id); public: - void Update(const std::shared_ptr rack, bool on_rack = true); // on_rack is false means extruder nozzle + void UpdateInfo(const std::shared_ptr rack, bool on_rack = true); // on_rack is false means extruder nozzle int GetNozzleId() const { return m_nozzle_id; } void SetDisplayIdText(const wxString& text) { m_nozzle_label_id->SetLabel(text);}; diff --git a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp index 4dc072d721..f351e7af2b 100644 --- a/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp +++ b/src/slic3r/GUI/DeviceTab/wgtDeviceNozzleSelect.cpp @@ -114,7 +114,7 @@ static void s_update_nozzle_info(wgtDeviceNozzleRackNozzleItem* item, std::shared_ptr rack, const DevNozzle& nozzle_info) { - item->Update(rack, nozzle_info.IsOnRack()); + item->UpdateInfo(rack, nozzle_info.IsOnRack()); if (nozzle_info.IsUnknown()) { if (item->GetToolTipText() != _L("Nozzle information needs to be read")) { item->SetToolTip(_L("Nozzle information needs to be read")); diff --git a/src/slic3r/GUI/FilamentMapPanel.cpp b/src/slic3r/GUI/FilamentMapPanel.cpp index ed63016438..81117bc2a6 100644 --- a/src/slic3r/GUI/FilamentMapPanel.cpp +++ b/src/slic3r/GUI/FilamentMapPanel.cpp @@ -642,19 +642,12 @@ void FilamentMapBtnPanel::Select(bool selected) Refresh(); } -void GUI::FilamentMapBtnPanel::Hide() +bool GUI::FilamentMapBtnPanel::Show(bool show) { - m_btn->Hide(); - m_label->Hide(); - m_detail->Hide(); - wxPanel::Hide(); -} -void GUI::FilamentMapBtnPanel::Show() -{ - m_btn->Show(); - m_label->Show(); - m_detail->Show(); - wxPanel::Show(); + m_btn->Show(show); + m_label->Show(show); + m_detail->Show(show); + return wxPanel::Show(show); } FilamentMapAutoPanel::FilamentMapAutoPanel(wxWindow *parent, FilamentMapMode mode, bool machine_synced) : wxPanel(parent) @@ -694,18 +687,11 @@ FilamentMapAutoPanel::FilamentMapAutoPanel(wxWindow *parent, FilamentMapMode mod Layout(); GUI::wxGetApp().UpdateDarkUIWin(this); } -void FilamentMapAutoPanel::Hide() +bool FilamentMapAutoPanel::Show(bool show) { - m_flush_panel->Hide(); - m_match_panel->Hide(); - wxPanel::Hide(); -} - -void FilamentMapAutoPanel::Show() -{ - m_flush_panel->Show(); - m_match_panel->Show(); - wxPanel::Show(); + m_flush_panel->Show(show); + m_match_panel->Show(show); + return wxPanel::Show(show); } void FilamentMapAutoPanel::UpdateStatus() @@ -743,16 +729,10 @@ FilamentMapDefaultPanel::FilamentMapDefaultPanel(wxWindow *parent) : wxPanel(par GUI::wxGetApp().UpdateDarkUIWin(this); } -void FilamentMapDefaultPanel::Hide() +bool FilamentMapDefaultPanel::Show(bool show) { - m_label->Hide(); - wxPanel::Hide(); -} - -void FilamentMapDefaultPanel::Show() -{ - m_label->Show(); - wxPanel::Show(); + m_label->Show(show); + return wxPanel::Show(show); } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/FilamentMapPanel.hpp b/src/slic3r/GUI/FilamentMapPanel.hpp index a0caf8e78b..ce7c8010a3 100644 --- a/src/slic3r/GUI/FilamentMapPanel.hpp +++ b/src/slic3r/GUI/FilamentMapPanel.hpp @@ -69,10 +69,9 @@ class FilamentMapBtnPanel : public wxPanel { public: FilamentMapBtnPanel(wxWindow *parent, const wxString &label, const wxString &detail, const std::string &icon_path); - void Hide(); - void Show(); + bool Show(bool show = true) override; void Select(bool selected); - bool Enable(bool enable); + bool Enable(bool enable) override; bool IsEnabled() const { return m_enabled; } protected: void OnPaint(wxPaintEvent &event); @@ -99,8 +98,7 @@ class FilamentMapAutoPanel : public wxPanel { public: FilamentMapAutoPanel(wxWindow *parent, FilamentMapMode mode, bool machine_synced); - void Hide(); - void Show(); + bool Show(bool show = true) override; FilamentMapMode GetMode() const { return m_mode; } private: @@ -116,8 +114,7 @@ class FilamentMapDefaultPanel : public wxPanel { public: FilamentMapDefaultPanel(wxWindow *parent); - void Hide(); - void Show(); + bool Show(bool show = true) override; private: Label *m_label; diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 199de9236e..ddd352a582 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -2176,7 +2176,7 @@ void SelectMachineDialog::on_reselect_dialog_btn_clicked(wxMouseEvent&) best_pos_map[slot.id] = pos.value(); } } - m_best_pos_dialog->Update(obj, best_pos_map, m_ams_mapping_result, text); + m_best_pos_dialog->UpdateInfo(obj, best_pos_map, m_ams_mapping_result, text); m_best_pos_dialog->ShowModal(); } @@ -2201,7 +2201,7 @@ void SelectMachineDialog::update_best_pos_dialog(wxCommandEvent& evt) best_pos_map[slot.id] = pos.value(); } } - m_best_pos_dialog->Update(obj_, best_pos_map, m_ams_mapping_result, text); + m_best_pos_dialog->UpdateInfo(obj_, best_pos_map, m_ams_mapping_result, text); } void SelectMachineDialog::show_status(PrintDialogStatus status, std::vector params, wxString wiki_url) diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index a63d96ffce..c4db0423ab 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -1391,7 +1391,7 @@ void SendToPrinterDialog::show_status(PrintDialogStatus status, std::vectorShow(); m_animaicon->Stop(); - m_animaicon->Enable(); + m_animaicon->ShowEnabledIcon(); Layout(); Enable_Send_Button(false); diff --git a/src/slic3r/GUI/SlicingProgressNotification.cpp b/src/slic3r/GUI/SlicingProgressNotification.cpp index 6d7c6d7ecd..539da41024 100644 --- a/src/slic3r/GUI/SlicingProgressNotification.cpp +++ b/src/slic3r/GUI/SlicingProgressNotification.cpp @@ -259,8 +259,8 @@ void NotificationManager::SlicingProgressNotification::render(GLCanvas3D& canvas // ImVec2 view_dailytips_text_pos = m_window_pos + ImVec2(text_left_margin_x, m_window_height / 2.0f + m_line_height * 0.2f); // bbl_render_left_sign(imgui, m_window_width, m_window_height, m_window_pos.x + m_window_width, m_window_pos.y); - // render_text(text_pos); - // render_close_button(button_pos, button_size); + // render_progress_text(text_pos); + // render_progress_close_button(button_pos, button_size); // render_show_dailytips(view_dailytips_text_pos); //} @@ -278,8 +278,8 @@ void NotificationManager::SlicingProgressNotification::render(GLCanvas3D& canvas ImVec2 button_pos = child_window_pos + ImVec2(progress_panel_width - button_size.x, progress_panel_height - text_bottom - button_size.y / 2.0f); ImVec2 text_pos = ImVec2(progress_bar_pos.x, progress_bar_pos.y - m_line_height * (1.2f + m_lines_count - 1)); - render_text(text_pos); - render_close_button(button_pos, button_size); + render_progress_text(text_pos); + render_progress_close_button(button_pos, button_size); if (m_sp_state == SlicingProgressState::SP_PROGRESS) { render_bar(progress_bar_pos, progress_bar_size); render_cancel_button(button_pos, button_size); @@ -319,7 +319,7 @@ void NotificationManager::SlicingProgressNotification::render(GLCanvas3D& canvas ImGui::PopStyleColor(3); } -void Slic3r::GUI::NotificationManager::SlicingProgressNotification::render_text(const ImVec2& pos) +void Slic3r::GUI::NotificationManager::SlicingProgressNotification::render_progress_text(const ImVec2& pos) { ImGuiWrapper& imgui = *wxGetApp().imgui(); float scale = imgui.get_font_size() / 15.0f; @@ -462,7 +462,7 @@ void Slic3r::GUI::NotificationManager::SlicingProgressNotification::render_cance } } -void NotificationManager::SlicingProgressNotification::render_close_button(const ImVec2& pos, const ImVec2& size) +void NotificationManager::SlicingProgressNotification::render_progress_close_button(const ImVec2& pos, const ImVec2& size) { if (m_sp_state == SlicingProgressState::SP_CANCELLED || m_sp_state == SlicingProgressState::SP_COMPLETED) { ImGuiWrapper& imgui = *wxGetApp().imgui(); diff --git a/src/slic3r/GUI/SlicingProgressNotification.hpp b/src/slic3r/GUI/SlicingProgressNotification.hpp index 44f2bbd32b..6a7ede1e2e 100644 --- a/src/slic3r/GUI/SlicingProgressNotification.hpp +++ b/src/slic3r/GUI/SlicingProgressNotification.hpp @@ -53,10 +53,10 @@ protected: void init() override; void render(GLCanvas3D& canvas, float initial_y, bool move_from_overlay, float overlay_width, float right_margin) override; /* PARAMS: pos is relative to screen */ - void render_text(const ImVec2& pos); + void render_progress_text(const ImVec2& pos); void render_bar(const ImVec2& pos, const ImVec2& size); void render_cancel_button(const ImVec2& pos, const ImVec2& size); - void render_close_button(const ImVec2& pos, const ImVec2& size); + void render_progress_close_button(const ImVec2& pos, const ImVec2& size); void render_dailytips_panel(const ImVec2& pos, const ImVec2& size); void render_show_dailytips(const ImVec2& pos); diff --git a/src/slic3r/GUI/Widgets/AMSControl.cpp b/src/slic3r/GUI/Widgets/AMSControl.cpp index 41f14b6a11..7ba24fd991 100644 --- a/src/slic3r/GUI/Widgets/AMSControl.cpp +++ b/src/slic3r/GUI/Widgets/AMSControl.cpp @@ -278,7 +278,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons m_ams_dry_ctr_win->Move(popup_pos); m_ams_dry_ctr_win->ShowModal(); } else { - m_percent_humidity_dry_popup->Update(info); + m_percent_humidity_dry_popup->UpdateInfo(info); wxPoint img_pos = ClientToScreen(wxPoint(0, 0)); wxPoint popup_pos(img_pos.x - m_percent_humidity_dry_popup->GetSize().GetWidth() + FromDIP(150), img_pos.y - FromDIP(80)); @@ -1034,7 +1034,7 @@ void AMSControl::UpdateAms(const std::string &series_name, humidity_info.humidity_percent = the_info.humidity_raw; humidity_info.left_dry_time = the_info.left_dray_time; humidity_info.current_temperature = the_info.current_temperature; - m_percent_humidity_dry_popup->Update(&humidity_info); + m_percent_humidity_dry_popup->UpdateInfo(&humidity_info); break; } } diff --git a/src/slic3r/GUI/Widgets/AnimaController.cpp b/src/slic3r/GUI/Widgets/AnimaController.cpp index acf78c01f5..d5f665cde1 100644 --- a/src/slic3r/GUI/Widgets/AnimaController.cpp +++ b/src/slic3r/GUI/Widgets/AnimaController.cpp @@ -78,7 +78,7 @@ void AnimaIcon::Stop() m_timer->Stop(); } -void AnimaIcon::Enable() +void AnimaIcon::ShowEnabledIcon() { if (m_bitmap) { m_bitmap->SetBitmap(m_image_enable); } } diff --git a/src/slic3r/GUI/Widgets/AnimaController.hpp b/src/slic3r/GUI/Widgets/AnimaController.hpp index e2f67ff8a6..daf8f95314 100644 --- a/src/slic3r/GUI/Widgets/AnimaController.hpp +++ b/src/slic3r/GUI/Widgets/AnimaController.hpp @@ -13,7 +13,7 @@ public: void Play(); void Stop(); - void Enable(); + void ShowEnabledIcon(); bool IsPlaying() const { return IsRunning(); }; bool IsRunning() const; diff --git a/src/slic3r/GUI/Widgets/ErrorMsgStaticText.hpp b/src/slic3r/GUI/Widgets/ErrorMsgStaticText.hpp index 735a118a20..63968a57c5 100644 --- a/src/slic3r/GUI/Widgets/ErrorMsgStaticText.hpp +++ b/src/slic3r/GUI/Widgets/ErrorMsgStaticText.hpp @@ -17,7 +17,7 @@ public: void paintEvent(wxPaintEvent &evt); - void SetLabel(wxString msg){m_msg = msg;}; + void SetLabel(const wxString &msg) override { m_msg = msg; } }; #endif diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 518bda1d19..20944df553 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -600,7 +600,7 @@ StaticBox* HotEndTable::CreateNozzleBox(const std::vector& nozzle_indices) void HotEndTable::UpdateNozzleItems(const std::unordered_map& nozzle_items, std::shared_ptr nozzle_rack) { for (auto& item : nozzle_items) - item.second->Update(nozzle_rack); + item.second->UpdateInfo(nozzle_rack); } void HotEndTable::OnPaint(wxPaintEvent& evt) diff --git a/src/slic3r/GUI/Widgets/RadioBox.cpp b/src/slic3r/GUI/Widgets/RadioBox.cpp index 7f20b8724c..f00447e0ec 100644 --- a/src/slic3r/GUI/Widgets/RadioBox.cpp +++ b/src/slic3r/GUI/Widgets/RadioBox.cpp @@ -36,11 +36,6 @@ void RadioBox::SetValue(bool value) update(); } -bool RadioBox::GetValue() -{ - return wxBitmapToggleButton::GetValue(); -} - void RadioBox::Rescale() { diff --git a/src/slic3r/GUI/Widgets/RadioBox.hpp b/src/slic3r/GUI/Widgets/RadioBox.hpp index b2c03aae15..a34bd594dd 100644 --- a/src/slic3r/GUI/Widgets/RadioBox.hpp +++ b/src/slic3r/GUI/Widgets/RadioBox.hpp @@ -15,14 +15,10 @@ public: public: void SetValue(bool value) override; - bool GetValue(); void Rescale(); bool Disable() { return wxBitmapToggleButton::Disable(); } - bool Enable() { - return wxBitmapToggleButton::Enable(); - } private: void update(); From 85dc86642588eb5c872ad6e0278b16ad70027fef Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:36:23 -0300 Subject: [PATCH 07/13] Spiral Inset infill (spiral-concentric infill) (#15085) Co-authored-by: Ian Bassi --- resources/images/param_spiralinset.svg | 1 + src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Fill/Fill.cpp | 6 +- src/libslic3r/Fill/FillBase.cpp | 2 + src/libslic3r/Fill/FillSpiralInset.cpp | 426 ++++++++++++++++++ src/libslic3r/Fill/FillSpiralInset.hpp | 37 ++ src/libslic3r/Layer.cpp | 1 + src/libslic3r/PrintConfig.cpp | 10 +- src/libslic3r/PrintConfig.hpp | 4 +- src/libslic3r/Support/SupportParameters.hpp | 2 + src/slic3r/GUI/ConfigManipulation.cpp | 13 +- tests/fff_print/test_fill.cpp | 207 +++++++++ .../test_config_variant_expansion.cpp | 6 + 13 files changed, 708 insertions(+), 9 deletions(-) create mode 100644 resources/images/param_spiralinset.svg create mode 100644 src/libslic3r/Fill/FillSpiralInset.cpp create mode 100644 src/libslic3r/Fill/FillSpiralInset.hpp diff --git a/resources/images/param_spiralinset.svg b/resources/images/param_spiralinset.svg new file mode 100644 index 0000000000..6ec1ab36aa --- /dev/null +++ b/resources/images/param_spiralinset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 2880a3cc6b..c33b7be429 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -147,6 +147,8 @@ set(lisbslic3r_sources Fill/FillBase.hpp Fill/FillConcentric.cpp Fill/FillConcentric.hpp + Fill/FillSpiralInset.cpp + Fill/FillSpiralInset.hpp Fill/FillConcentricInternal.cpp Fill/FillConcentricInternal.hpp Fill/FillCornerSmoothing.cpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 0888e1bb55..dc772580ca 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -950,7 +950,7 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p params.extruder = region_config.internal_solid_filament_id; // Orca: forced fill order applies only to top/bottom surfaces filled with a // center-based pattern; everything else stays at Default to keep batching together. - if (params.pattern == ipConcentric || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) { + if (params.pattern == ipConcentric || params.pattern == ipSpiralInset || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) { if (params.extrusion_role == erTopSolidInfill) params.fill_order = region_config.top_surface_fill_order.value; else if (params.extrusion_role == erBottomSurface) @@ -1332,7 +1332,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.anchor_length = surface_fill.params.anchor_length; params.anchor_length_max = surface_fill.params.anchor_length_max; params.resolution = resolution; - params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipConcentricInternal; + params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipSpiralInset || + surface_fill.params.pattern == ipConcentricInternal; params.layer_height = layerm->layer()->height; params.lateral_lattice_angle_1 = surface_fill.params.lateral_lattice_angle_1; params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2; @@ -1515,6 +1516,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc case ipCubic: case ipLine: case ipConcentric: + case ipSpiralInset: case ipHoneycomb: case ipLateralHoneycomb: case ip3DHoneycomb: diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index 45157ec42d..2b307007b8 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -15,6 +15,7 @@ #include "FillBase.hpp" #include "FillConcentric.hpp" +#include "FillSpiralInset.hpp" #include "FillHoneycomb.hpp" #include "Fill3DHoneycomb.hpp" #include "FillGyroid.hpp" @@ -41,6 +42,7 @@ Fill* Fill::new_from_type(const InfillPattern type) { switch (type) { case ipConcentric: return new FillConcentric(); + case ipSpiralInset: return new FillSpiralInset(); case ipHoneycomb: return new FillHoneycomb(); case ipLateralHoneycomb: return new FillLateralHoneycomb(); case ip3DHoneycomb: return new Fill3DHoneycomb(); diff --git a/src/libslic3r/Fill/FillSpiralInset.cpp b/src/libslic3r/Fill/FillSpiralInset.cpp new file mode 100644 index 0000000000..e6323c2572 --- /dev/null +++ b/src/libslic3r/Fill/FillSpiralInset.cpp @@ -0,0 +1,426 @@ +#include "../ClipperUtils.hpp" +#include "../ExPolygon.hpp" +#include "../Surface.hpp" +#include "../VariableWidth.hpp" +#include "Arachne/WallToolPaths.hpp" + +#include "FillSpiralInset.hpp" + +#include +#include +#include + +namespace Slic3r { + +// Index of the corner the spiral should start at. Every following loop is split at the point nearest +// the end of the one before it, so this choice propagates inwards and decides where the whole spiral +// hands over from ring to ring. A tight corner is the worst place for it: there the next ring +// retreats along the bisector by spacing/sin(angle), so the spiral has to strike out several spacings +// to reach it instead of stepping across to a ring running parallel one spacing away. +// +// A right angle is taken first when the loop has one. It clips cleanly, since the trimming below +// scales with 1/sin(angle) and so is at its shortest and least sensitive there, and it holds its +// shape as the loop is offset inwards, which keeps the handover in the same place ring after ring. +// Failing that the widest corner is the flattest stretch on offer, which is the next best handover. +// A straight point is no corner at all and only turns up as an artefact of the offsetting, so it is +// skipped. +static int find_spiral_start_corner(const Polygon& loop) +{ + const size_t n = loop.points.size(); + if (n < 3) + return 0; + + // cos(85 deg): a corner within five degrees of square counts as a right angle. + static const double right_angle_cos = 0.08716; + // cos(179 deg): anything flatter than this counts as a straight point rather than a corner. + static const double straight_cos = -0.99985; + + // Only convex corners qualify. A reflex corner spans the same angle between its two edges but + // bulges the other way, so the next ring in steps away from it along the bisector instead of + // hugging it, and starting there hands over across a long diagonal on every single ring. Loops + // arrive counter-clockwise, in which case a convex corner turns left, but check the winding + // rather than trust it. A closed loop always has at least one convex corner. + const double convex_turn = loop.is_counter_clockwise() ? 1.0 : -1.0; + + double best_right_cos = right_angle_cos; + int best_right = -1; + double best_wide_cos = 1.0; + int best_wide = -1; + for (size_t i = 0; i < n; ++i) { + const Point& p_prev = loop.points[(i - 1 + n) % n]; + const Point& p = loop.points[i]; + const Point& p_next = loop.points[(i + 1) % n]; + + Vec2d e_in = (p - p_prev).cast(); + Vec2d e_out = (p_next - p).cast(); + double len1 = e_in.norm(); + double len2 = e_out.norm(); + if (len1 < 1e-6 || len2 < 1e-6) + continue; + if (convex_turn * (e_in.x() * e_out.y() - e_in.y() * e_out.x()) <= 0.0) + continue; + + // Cosine of the angle the two edges span at the corner: 1 at a spike, 0 square, -1 straight. + double cos_val = -e_in.dot(e_out) / (len1 * len2); + if (std::abs(cos_val) < best_right_cos) { + best_right_cos = std::abs(cos_val); + best_right = int(i); + } + if (cos_val > straight_cos && cos_val < best_wide_cos) { + best_wide_cos = cos_val; + best_wide = int(i); + } + } + if (best_right >= 0) + return best_right; + // A loop smooth enough to have no corner at all, a circle say, hands over equally well anywhere. + return best_wide < 0 ? 0 : best_wide; +} + +// Length to trim off the end of a loop so that it does not overlap the start of the next one. +// The theoretical gap is distance/sin(alpha), alpha being the angle between the last segment of the +// loop and the first segment of the next one. +static double loop_clip_length(const Polyline& loop_path, const double gap) +{ + const Point& p_prev = loop_path.points[loop_path.points.size() - 2]; + const Point& p_last = loop_path.points.back(); + const Point& p_next = loop_path.points[1]; + Vec2d v1 = (p_last - p_prev).cast(); + Vec2d v2 = (p_next - p_last).cast(); + if (v1.norm() < 1e-6 || v2.norm() < 1e-6) + return gap; + + double alpha = std::atan2(std::abs(v1.x() * v2.y() - v1.y() * v2.x()), v1.dot(v2)); + // Outside 45deg < alpha < 120deg the 1/sin(alpha) term would clip far too much, so fall back to the plain gap. + return (alpha > M_PI / 4 && alpha < 2 * M_PI / 3) ? gap / std::sin(alpha) : gap; +} + +// The chaining below drives two kinds of loop: the plain offset polygons of the classic path, and +// Arachne's variable width walls. These are the only four steps that differ between them. Widths run +// two per segment, so every point added or removed takes a pair with it. +static Polyline open_loop(const Polygon& loop, int start_index) { return loop.split_at_index(start_index); } + +static ThickPolyline open_loop(const Arachne::ExtrusionLine& loop, int start_index) +{ + ThickPolyline path = Arachne::to_thick_polyline(loop); + // start_at_index() rotates a closed path, and wants it closed with a matching width at both ends. + if (path.points.front() != path.points.back()) { + const coordf_t w_first = path.width.front(), w_last = path.width.back(); + path.points.emplace_back(path.points.front()); + path.width.emplace_back(w_last); + path.width.emplace_back(w_first); + } + path.start_at_index(start_index); + return path; +} + +static void clip_path_end(Polyline& path, double distance) { path.clip_end(distance); } + +static void clip_path_end(ThickPolyline& path, double distance) +{ + // Polyline::clip_end() knows nothing about the widths, so walk back trimming the two together. + while (distance > 0 && path.points.size() >= 2) { + const Point last = path.points.back(); + const coordf_t w_end = path.width.back(); + path.points.pop_back(); + path.width.pop_back(); + const coordf_t w_start = path.width.back(); + path.width.pop_back(); + + const Vec2d v = (path.points.back() - last).cast(); + const double len = v.norm(); + if (len > distance) { + const double t = distance / len; + path.points.emplace_back((last.cast() + v * t).cast()); + path.width.emplace_back(w_start); + path.width.emplace_back(w_start + (w_end - w_start) * (1.0 - t)); + return; + } + distance -= len; + } + path.clear(); +} + +static void append_path(Polyline& dst, Polyline&& src) { dst.append(std::move(src)); } + +static void append_path(ThickPolyline& dst, ThickPolyline&& src) +{ + if (dst.empty()) { + dst = std::move(src); + return; + } + if (dst.points.back() == src.points.front()) { + // Carrying straight on from the same point, so there is no run across to give a width to. + src.points.erase(src.points.begin()); + src.width.erase(src.width.begin(), src.width.begin() + 2); + } else { + // The run across to the next loop tapers between the two ends it joins. + const coordf_t w_from = dst.width.back(), w_to = src.width.front(); + dst.width.emplace_back(w_from); + dst.width.emplace_back(w_to); + } + append(dst.points, std::move(src.points)); + append(dst.width, std::move(src.width)); +} + +// The classic loops all carry the same width, so the innermost one of an island can still ring an +// unfilled pin hole, which the spiral plugs by running into the middle. Arachne's walls widen to take +// up whatever is left over, so there is nothing there to plug and the stub would only double back +// over the wall that just filled it. +static bool leaves_a_centre_hole(const Polygon&) { return true; } +static bool leaves_a_centre_hole(const Arachne::ExtrusionLine&) { return false; } + +static void append_path_point(Polyline& path, const Point& point) { path.points.emplace_back(point); } + +static void append_path_point(ThickPolyline& path, const Point& point) +{ + const coordf_t w = path.width.back(); + path.points.emplace_back(point); + path.width.emplace_back(w); + path.width.emplace_back(w); +} + +// Chain the loops of one surface into as few continuous spirals as its shape allows. The loops arrive +// ordered outside in, depth first, each paired with its outline in loop_outlines; every decision here +// is made on those outlines, so the two kinds of loop take exactly the same route. +template +static std::vector generate_spiral_insets(const FillParams& params, + const std::vector& loops, + const Polygons& loop_outlines, + const coord_t distance, + const ExPolygon& original_expoly) +{ + std::vector output; + PathType spiral; + Point current_pos(0, 0); + // Index into loops of the innermost loop appended to the spiral currently being built. + int innermost_loop = -1; + + // Whether the spiral can run straight from one point to the other. The run across is extruded, + // not travelled, so it has to be a genuine step over to the ring alongside: + // - up to a ring spacing and a half it cannot leave the material, and needs no check at all, + // which covers all but a few of the loops; + // - beyond that it is tested against the surface, which catches the points that are close in a + // straight line but separated by a hole or a notch; + // - past four spacings it is refused outright. A handover does stretch at a corner, where the + // next ring retreats along the bisector by spacing/sin(angle), but four spacings is already a + // fifteen degree wedge, and down a wedge that tight the run across would trace the bisector, + // which is where the tail is filled from anyway. Anything longer is a traverse across the + // surface that prints over what it crosses. Breaking the spiral leaves the G-code to travel it. + const double free_hop = 1.5 * double(distance); + const double max_hop = 4.0 * double(distance); + auto reachable = [&](const Point& from, const Point& to) { + const double hop = from.distance_to(to); + if (hop > max_hop) + return false; + return hop <= free_hop || original_expoly.contains(Line(from, to)); + }; + + // The centre point plugs the pin hole left in the middle of an island, it is not meant to + // traverse it, so it is only worth adding when the innermost loop has shrunk to about a ring. + const double max_center_stub = 2.0 * double(distance); + + // Emit the spiral built so far as one path and start over on a fresh island. + auto flush_spiral = [&]() { + if (spiral.empty()) + return; + // Run into the middle of the innermost loop so the island's centre is filled instead of being + // left as a pin hole. Only where there is a hole to fill: the loop has to still enclose open + // space once its own bead is accounted for, or the stub just runs back over that bead. And + // the point has to sit inside the loop and be reachable, or it runs off across the surface. + if (innermost_loop >= 0 && leaves_a_centre_hole(*loops[innermost_loop])) { + const Polygon& innermost = loop_outlines[innermost_loop]; + const Point centroid = innermost.centroid(); + if (!offset(innermost, -float(0.5 * double(distance))).empty() && centroid != spiral.last_point() && + spiral.last_point().distance_to(centroid) <= max_center_stub && innermost.contains(centroid) && + reachable(spiral.last_point(), centroid)) + append_path_point(spiral, centroid); + } + output.emplace_back(std::move(spiral)); + spiral.clear(); + innermost_loop = -1; + current_pos = Point(0, 0); + }; + + for (size_t i = 0; i < loops.size(); ++i) { + const Polygon& outline = loop_outlines[i]; + if (outline.points.empty()) + continue; + + // The loop is opened into a path with the split point repeated at both ends, so a usable one + // has at least 3 points. Both kinds of loop share the outline's indices, hence its start point. + PathType loop_path = open_loop(*loops[i], spiral.empty() ? find_spiral_start_corner(outline) : + current_pos.nearest_point_index(outline.points)); + if (loop_path.size() < 3) + continue; + + // Island jumping: the loops are ordered by their nesting, depth first, so the next one + // continues the current spiral exactly when it lies inside the one just laid down. Distance + // cannot stand in for that test: at a sharp corner the next ring retreats along the bisector + // by spacing/sin(angle), which leaves it several spacings away while still being the very + // next ring in, and the spiral would break off at every spike. + const bool same_island = innermost_loop >= 0 && loop_outlines[innermost_loop].contains(loop_path.points.front()); + if (!spiral.empty() && (!same_island || !reachable(spiral.last_point(), loop_path.points.front()))) { + flush_spiral(); + loop_path = open_loop(*loops[i], find_spiral_start_corner(outline)); + if (loop_path.size() < 3) + continue; + } + + // Clip the end of the loop to leave room for the run into the next one. The last loop of the + // surface has no successor, so it only gives up half of the gap. + clip_path_end(loop_path, loop_clip_length(loop_path, (i + 1 == loops.size() ? 0.5 : 1.0) * double(distance))); + // Clipping empties the path when the loop is shorter than the clipping length, which happens + // on the degenerate slivers that offsetting leaves behind. Such a loop carries no extrusion. + if (loop_path.size() < 2) + continue; + + append_path(spiral, std::move(loop_path)); + innermost_loop = int(i); + current_pos = spiral.last_point(); + } + + flush_spiral(); + + // An outward fill order runs every spiral from its centre to its outer edge, innermost island first. + if (params.fill_order != SurfaceFillOrder::Inward) { + for (PathType& path : output) + path.reverse(); + std::reverse(output.begin(), output.end()); + } + + return output; +} + +void FillSpiralInset::_fill_surface_single(const FillParams& params, + unsigned int thickness_layers, + const std::pair& direction, + ExPolygon expolygon, + Polylines& polylines_out) +{ + BoundingBox bounding_box = expolygon.contour.bounding_box(); + + coord_t min_spacing = scale_(this->spacing); + coord_t distance = coord_t(min_spacing / params.density); + + if (params.density > 0.9999f && !params.dont_adjust) { + distance = this->_adjust_solid_spacing(bounding_box.size()(0), distance); + this->spacing = unscale(distance); + } + + Polygons loops = to_polygons(expolygon); + + ExPolygons last{std::move(expolygon)}; + while (!last.empty()) { + last = offset2_ex(last, -(distance + min_spacing / 2), +min_spacing / 2); + append(loops, to_polygons(last)); + } + + // Orders the loops outside in, depth first, which is the order the chaining below expects. + loops = union_pt_chained_outside_in(loops); + + std::vector loop_refs; + loop_refs.reserve(loops.size()); + for (const Polygon& loop : loops) + loop_refs.emplace_back(&loop); + + Polylines spiral_result = generate_spiral_insets(params, loop_refs, loops, distance, expolygon); + + append(polylines_out, spiral_result); +} + +void FillSpiralInset::_fill_surface_single(const FillParams& params, + unsigned int thickness_layers, + const std::pair& direction, + ExPolygon expolygon, + ThickPolylines& thick_polylines_out) +{ + assert(params.use_arachne); + assert(this->print_config != nullptr && this->print_object_config != nullptr); + + // Only a solid surface is worth the variable width walls; a sparse one falls back to plain loops. + if (params.density <= 0.9999f || params.dont_adjust) { + Polylines polylines; + this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines); + append(thick_polylines_out, to_thick_polylines(std::move(polylines), scaled(this->spacing))); + return; + } + + // no rotation is supported for this infill pattern + Point bbox_size = expolygon.contour.bounding_box().size(); + coord_t min_spacing = scaled(this->spacing); + + coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1; + Polygons polygons = offset(expolygon, float(min_spacing) / 2.f); + + double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end()); + Arachne::WallToolPathsParams input_params; + input_params.min_bead_width = 0.85 * min_nozzle_diameter; + input_params.min_feature_size = 0.25 * min_nozzle_diameter; + input_params.wall_transition_length = 1.0 * min_nozzle_diameter; + input_params.wall_transition_angle = 10; + input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter; + input_params.wall_distribution_count = 1; + + Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, params.layer_height, input_params); + std::vector walls_by_inset = wallToolPaths.getToolPaths(); + + // Open walls are the thin features Arachne fits between the closed ones. They cannot join a + // spiral, so they go out as they are; leaving them behind is what would put the gaps back. + std::vector walls; + Polygons wall_outlines; + ThickPolylines open_walls; + for (const Arachne::VariableWidthLines& inset : walls_by_inset) + for (const Arachne::ExtrusionLine& wall : inset) { + if (wall.empty()) + continue; + if (wall.is_closed) { + walls.emplace_back(&wall); + wall_outlines.emplace_back(wall.toPolygon()); + } else { + open_walls.emplace_back(Arachne::to_thick_polyline(wall)); + } + } + + // Arachne hands the walls back grouped by inset, which is not their nesting: around a hole the + // wall of a given inset lies inside the wall of that same inset around the contour. Nest them by + // containment instead, so the spiral follows one island all the way in before starting the next, + // the same order union_pt_chained_outside_in gives the classic path above. + const size_t wall_count = walls.size(); + std::vector nesting_depth(wall_count, 0), parent(wall_count, -1); + std::vector inside(wall_count * wall_count, 0); + for (size_t i = 0; i < wall_count; ++i) + for (size_t j = 0; j < wall_count; ++j) + if (i != j && wall_outlines[j].contains(walls[i]->junctions.front().p)) { + inside[i * wall_count + j] = 1; + ++nesting_depth[i]; + } + // The innermost of the walls containing this one, which is the deepest of them, is its parent. + for (size_t i = 0; i < wall_count; ++i) + for (size_t j = 0; j < wall_count; ++j) + if (inside[i * wall_count + j] && (parent[i] < 0 || nesting_depth[parent[i]] < nesting_depth[j])) + parent[i] = int(j); + + std::vector ordered; + Polygons outlines; + ordered.reserve(wall_count); + outlines.reserve(wall_count); + std::function descend = [&](int idx) { + ordered.emplace_back(walls[idx]); + outlines.emplace_back(wall_outlines[idx]); + for (size_t k = 0; k < wall_count; ++k) + if (parent[k] == idx) + descend(int(k)); + }; + for (size_t i = 0; i < wall_count; ++i) + if (parent[i] < 0) + descend(int(i)); + + ThickPolylines spiral_result = + generate_spiral_insets(params, ordered, outlines, min_spacing, expolygon); + + append(thick_polylines_out, std::move(spiral_result)); + append(thick_polylines_out, std::move(open_walls)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/Fill/FillSpiralInset.hpp b/src/libslic3r/Fill/FillSpiralInset.hpp new file mode 100644 index 0000000000..eb799ec2f3 --- /dev/null +++ b/src/libslic3r/Fill/FillSpiralInset.hpp @@ -0,0 +1,37 @@ +#ifndef slic3r_FillSpiralInset_hpp_ +#define slic3r_FillSpiralInset_hpp_ + +#include "FillBase.hpp" + +namespace Slic3r { + +class FillSpiralInset : public Fill +{ +public: + ~FillSpiralInset() override = default; + bool is_self_crossing() override { return false; } + +protected: + Fill* clone() const override { return new FillSpiralInset(*this); }; + void _fill_surface_single( + const FillParams ¶ms, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon expolygon, + Polylines &polylines_out) override; + + // Orca: solid surfaces are filled with Arachne's variable width walls, which widen to take up + // whatever the fixed width loops above would have left over as gaps. + void _fill_surface_single( + const FillParams ¶ms, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon expolygon, + ThickPolylines &thick_polylines_out) override; + + bool no_sort() const override { return true; } +}; + +} // namespace Slic3r + +#endif // slic3r_FillSpiralInset_hpp_ diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 87ad11bcf8..1d6c2b0703 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -419,6 +419,7 @@ coordf_t Layer::get_sparse_infill_max_void_area() double spacing = flow.scaled_spacing() * (100 - density) / density; switch (pattern) { case ipConcentric: + case ipSpiralInset: case ipRectilinear: case ipLine: case ipGyroid: diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 100089fd24..1db476eede 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -275,6 +275,7 @@ static t_config_enum_values s_keys_map_InfillPattern { { "tpmsfk", ipTpmsFK }, { "gyroid", ipGyroid }, { "concentric", ipConcentric }, + { "spiralinset", ipSpiralInset }, { "hilbertcurve", ipHilbertCurve }, { "archimedeanchords", ipArchimedeanChords }, { "octagramspiral", ipOctagramSpiral } @@ -371,6 +372,7 @@ static t_config_enum_values s_keys_map_SupportMaterialInterfacePattern { { "auto", smipAuto }, { "rectilinear", smipRectilinear }, { "concentric", smipConcentric }, + { "spiralinset", smipSpiralInset }, { "rectilinear_interlaced", smipRectilinearInterlaced}, { "grid", smipGrid } }; @@ -2292,6 +2294,7 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("rectilinear"); def->enum_values.push_back("alignedrectilinear"); def->enum_values.push_back("concentric"); + def->enum_values.push_back("spiralinset"); def->enum_values.push_back("hilbertcurve"); def->enum_values.push_back("archimedeanchords"); def->enum_values.push_back("octagramspiral"); @@ -2300,6 +2303,7 @@ void PrintConfigDef::init_fff_params() def->enum_labels.push_back(L("Rectilinear")); def->enum_labels.push_back(L("Aligned Rectilinear")); def->enum_labels.push_back(L("Concentric")); + def->enum_labels.push_back(L("Spiral Inset")); def->enum_labels.push_back(L("Hilbert Curve")); def->enum_labels.push_back(L("Archimedean Chords")); def->enum_labels.push_back(L("Octagram Spiral")); @@ -2382,7 +2386,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Top surface fill order"); def->category = L("Strength"); def->tooltip = L("Direction in which top surfaces are filled when using a center-based pattern " - "(Concentric, Archimedean Chords, Octagram Spiral).\n" + "(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Outward starts at the center of the surface, so any excess material is pushed " "towards the edge where it is least visible. Inward starts at the edge and ends " "with the tight curves at the center.\n" @@ -2401,7 +2405,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Bottom surface fill order"); def->category = L("Strength"); def->tooltip = L("Direction in which bottom surfaces are filled when using a center-based pattern " - "(Concentric, Archimedean Chords, Octagram Spiral).\n" + "(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n" "Inward starts each surface with the wider outer curves, which improves first layer " "adhesion on build plates where the tight curves at the center may not stick. " "Outward starts at the center, pushing any excess material towards the edge.\n" @@ -6963,11 +6967,13 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("auto"); def->enum_values.push_back("rectilinear"); def->enum_values.push_back("concentric"); + def->enum_values.push_back("spiralinset"); def->enum_values.push_back("rectilinear_interlaced"); def->enum_values.push_back("grid"); def->enum_labels.push_back(L("Default")); def->enum_labels.push_back(L("Rectilinear")); def->enum_labels.push_back(L("Concentric")); + def->enum_labels.push_back(L("Spiral Inset")); def->enum_labels.push_back(L("Rectilinear Interlaced")); def->enum_labels.push_back(L("Grid")); def->mode = comAdvanced; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 330151c4d3..f7cbe8b2e5 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -113,7 +113,7 @@ enum InfillPattern : int { ipCubic, ipAdaptiveCubic, ipQuarterCubic, ipSupportCubic, ipLightning, ipHoneycomb, ip3DHoneycomb, ipLateralHoneycomb, ipLateralLattice, ipCrossHatch, ipTpmsD, ipTpmsFK, ipGyroid, - ipConcentric, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, + ipConcentric, ipSpiralInset, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipSupportBase, ipConcentricInternal, ipCount, }; @@ -271,7 +271,7 @@ enum LongRectrationLevel }; enum SupportMaterialInterfacePattern { - smipAuto, smipRectilinear, smipConcentric, smipRectilinearInterlaced, smipGrid + smipAuto, smipRectilinear, smipConcentric, smipSpiralInset, smipRectilinearInterlaced, smipGrid }; // BBS diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp index dc0e94ed03..8d43edb444 100644 --- a/src/libslic3r/Support/SupportParameters.hpp +++ b/src/libslic3r/Support/SupportParameters.hpp @@ -141,6 +141,8 @@ struct SupportParameters { this->contact_fill_pattern = ipGrid; else if (object_config.support_interface_pattern == smipRectilinearInterlaced) this->contact_fill_pattern = ipRectilinear; + else if (object_config.support_interface_pattern == smipSpiralInset) + this->contact_fill_pattern = ipSpiralInset; else this->contact_fill_pattern = (object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) || diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 09b7fcfad4..8eab1c785c 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -741,14 +741,21 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool have_infill = config->option("sparse_infill_density")->value > 0; // sparse_infill_filament_id uses the same logic as in Print::extruders() for (auto el : { "sparse_infill_pattern", "infill_combination", "fill_multiline","infill_direction", - "minimum_sparse_infill_area", "sparse_infill_filament_id", "infill_anchor", "infill_anchor_max","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"}) + "minimum_sparse_infill_area", "sparse_infill_filament_id","infill_shift_step","sparse_infill_rotate_template","symmetric_infill_y_axis"}) toggle_line(el, have_infill); + InfillPattern pattern = config->opt_enum("sparse_infill_pattern"); + + // Orca: the concentric patterns follow the surface outline instead of crossing it, so there is + // nothing for an infill anchor to attach to. Hide the anchor settings for them. + bool have_infill_anchor = have_infill && pattern != ipConcentric && pattern != ipSpiralInset; + toggle_line("infill_anchor", have_infill_anchor); + toggle_line("infill_anchor_max", have_infill_anchor); + bool have_combined_infill = config->opt_bool("infill_combination") && have_infill; toggle_line("infill_combination_max_layer_height", have_combined_infill); // Infill patterns that support multiline infill. - InfillPattern pattern = config->opt_enum("sparse_infill_pattern"); bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric || pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral; @@ -831,7 +838,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("separated_infills", is_internal_infill_separable); // Fill order is only meaningful for the center-based surface fill patterns; hide it otherwise. - auto is_centered_fill = [](InfillPattern p) { return p == ipConcentric || p == ipArchimedeanChords || p == ipOctagramSpiral; }; + auto is_centered_fill = [](InfillPattern p) { return p == ipConcentric || p == ipSpiralInset || p == ipArchimedeanChords || p == ipOctagramSpiral; }; toggle_line("top_surface_fill_order", has_top_shell && is_centered_fill(config->opt_enum("top_surface_pattern"))); toggle_line("bottom_surface_fill_order", has_bottom_shell && is_centered_fill(config->opt_enum("bottom_surface_pattern"))); diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index d26639c659..04e5b61831 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -699,6 +699,213 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set", } } +// Orca: the spiral inset pattern chains the concentric loops into a single continuous path per +// island, so it has to cope with the degenerate loops offsetting leaves behind and it must not join +// loops that only look adjacent. +namespace { + +Slic3r::Polylines spiral_inset_fill(const Slic3r::ExPolygon &surface_shape, double spacing) +{ + std::unique_ptr filler(Slic3r::Fill::new_from_type("spiralinset")); + filler->spacing = spacing; + // Cancel the half-spacing contraction fill_surface() applies, so the filler sees the shape as given. + filler->overlap = 0.5 * spacing; + + Slic3r::FillParams fill_params; + fill_params.density = 1.f; + fill_params.dont_adjust = true; + + Slic3r::Surface surface(Slic3r::stBottom, surface_shape); + return filler->fill_surface(&surface, fill_params); +} + +Slic3r::ExPolygon rectangle(double x, double y, double w, double h) +{ + return Slic3r::ExPolygon({Slic3r::Point::new_scale(x, y), Slic3r::Point::new_scale(x + w, y), + Slic3r::Point::new_scale(x + w, y + h), Slic3r::Point::new_scale(x, y + h)}); +} + +// Area of the surface the toolpaths fail to cover, and the largest single patch of it, in mm2. Each +// bead is measured at its own width so the variable width walls are not sold short. +std::pair uncovered_area(const Slic3r::ExPolygon &surface_shape, const Slic3r::Polygons &covered) +{ + double total = 0, biggest = 0; + for (const Slic3r::ExPolygon &gap : Slic3r::diff_ex(Slic3r::ExPolygons{surface_shape}, Slic3r::union_(covered))) { + const double area = unscale(unscale(gap.area())); + total += area; + biggest = std::max(biggest, area); + } + return {total, biggest}; +} + +Slic3r::Polygons beads_of(const Slic3r::Polylines &paths, double width) +{ + return Slic3r::offset(paths, float(scale_(0.5 * width))); +} + +Slic3r::Polygons beads_of(const Slic3r::ThickPolylines &paths) +{ + Slic3r::Polygons covered; + for (const Slic3r::ThickPolyline &path : paths) + for (size_t i = 0; i + 1 < path.points.size(); ++i) { + Slic3r::Polyline segment; + segment.points = {path.points[i], path.points[i + 1]}; + Slic3r::append(covered, Slic3r::offset(Slic3r::Polylines{segment}, + float(0.5 * std::max(path.width[2 * i], path.width[2 * i + 1])))); + } + return covered; +} + +} // namespace + +TEST_CASE("Spiral inset fill drops loops shorter than the end clipping", "[Fill][Regression]") +{ + // A sliver whose whole perimeter is shorter than the length clipped off the end of a loop, so the + // clipping consumes the path entirely. Such a loop carries no extrusion and must be dropped + // rather than kept as an empty path and read back from. + const double spacing = 0.45; + + Slic3r::Polylines paths; + REQUIRE_NOTHROW(paths = spiral_inset_fill(rectangle(0, 0, 0.05, 0.05), spacing)); + for (const Slic3r::Polyline &path : paths) + CHECK(path.size() >= 2); + + // The same surface at a size the clipping cannot swallow still gets filled. + REQUIRE_NOTHROW(paths = spiral_inset_fill(rectangle(0, 0, 5, 5), spacing)); + REQUIRE(paths.size() == 1); + CHECK(paths.front().size() >= 2); +} + +TEST_CASE("Spiral inset fill keeps separate islands on separate paths", "[Fill]") +{ + // Two lobes joined by a neck narrower than the loop spacing: the inward offsets break the surface + // into two islands, which cannot share one spiral, and no path may leave the surface. + const double spacing = 0.45; + Slic3r::ExPolygon dumbbell = rectangle(0, 0, 6, 6); + dumbbell = Slic3r::union_ex(Slic3r::ExPolygons{dumbbell, rectangle(6, 2.9, 4, 0.2), rectangle(10, 0, 6, 6)}).front(); + + const Slic3r::Polylines paths = spiral_inset_fill(dumbbell, spacing); + REQUIRE(paths.size() >= 2); + + // Inflate by a hair so that loops sitting exactly on the outline still count as contained. + const Slic3r::ExPolygons within = Slic3r::offset_ex(dumbbell, float(SCALED_EPSILON)); + REQUIRE(within.size() == 1); + for (const Slic3r::Polyline &path : paths) { + CHECK(path.size() >= 2); + CHECK(within.front().contains(path)); + } +} + + +TEST_CASE("Spiral inset fill stays connected across sharp corners", "[Fill][Regression]") +{ + // At a corner of half-angle a, the next ring inward retreats along the bisector by spacing/sin(a), + // which leaves it several spacings from the end of the ring it continues. Judging the break by + // distance broke the spiral into loose rings at every spike; nesting is what decides the island. + const double spacing = 0.45; + const Slic3r::ExPolygon spike({Slic3r::Point::new_scale(0, 0), Slic3r::Point::new_scale(30, 0), + Slic3r::Point::new_scale(15, 4)}); + + const Slic3r::Polylines paths = spiral_inset_fill(spike, spacing); + CHECK(paths.size() == 1); + + const Slic3r::ExPolygons within = Slic3r::offset_ex(spike, float(SCALED_EPSILON)); + REQUIRE(within.size() == 1); + for (const Slic3r::Polyline &path : paths) + CHECK(within.front().contains(path)); +} + +TEST_CASE("Spiral inset fill starts on a convex corner", "[Fill][Regression]") +{ + // The only right angle on this outline is the reflex one: the two edges meeting at the origin + // span 90 degrees exactly as a square corner would, but the material lies outside them. The next + // ring in steps away from a reflex corner along the bisector instead of hugging it, so starting + // the spiral there sent it across a long diagonal on every single ring. + const double spacing = 0.45; + const Slic3r::ExPolygon notched({Slic3r::Point::new_scale(0, 0), Slic3r::Point::new_scale(0, 10), + Slic3r::Point::new_scale(-16, 18), Slic3r::Point::new_scale(-16, -2), + Slic3r::Point::new_scale(-8, -16), Slic3r::Point::new_scale(18, -16), + Slic3r::Point::new_scale(10, 0)}); + + const Slic3r::Polylines paths = spiral_inset_fill(notched, spacing); + REQUIRE(paths.size() >= 1); + + // Every edge of the outline is at least 45 degrees off the bisector of that reflex corner, and + // so is every ring offset from it. A long segment running along the bisector can therefore only + // be the spiral striking out across the rings to reach the next one. + for (const Slic3r::Polyline &path : paths) + for (const Slic3r::Line &segment : path.lines()) { + const Vec2d v = (segment.b - segment.a).cast(); + const double direction = std::fmod(std::atan2(v.y(), v.x()) * 180.0 / M_PI + 180.0, 180.0); + if (std::abs(direction - 45.0) > 25.0) + continue; + CAPTURE(direction, unscale(segment.length())); + CHECK(segment.length() <= scale_(1.5 * spacing)); + } +} + +TEST_CASE("Spiral inset fill closes the gaps with variable width walls", "[Fill]") +{ + // Fixed width loops cannot fill a region that is not a whole number of lines across and leave the + // remainder open, which on a ring shows up as a wedge several lines wide. Plain concentric avoids + // that by building solid surfaces out of Arachne's variable width walls, and so must this pattern. + const double spacing = 0.45; + Slic3r::ExPolygon ring = rectangle(0, 0, 24, 24); + Slic3r::Polygon hole; + for (int i = 0; i < 64; ++i) { + const double angle = -2.0 * PI * i / 64.0; // clockwise, so it reads as a hole + hole.points.emplace_back(Slic3r::Point::new_scale(12 + 7.3 * std::cos(angle), 12 + 7.3 * std::sin(angle))); + } + ring.holes.emplace_back(hole); + + Slic3r::PrintConfig print_config; + Slic3r::PrintObjectConfig object_config; + auto make_filler = [&]() { + std::unique_ptr filler(Slic3r::Fill::new_from_type("spiralinset")); + filler->spacing = spacing; + filler->overlap = 0.5 * spacing; // cancel the contraction, so both see the same surface + filler->print_config = &print_config; + filler->print_object_config = &object_config; + return filler; + }; + + Slic3r::FillParams params; + params.density = 1.f; + params.dont_adjust = false; + params.layer_height = 0.2; + + const Slic3r::Surface surface(Slic3r::stTop, ring); + + std::unique_ptr fixed = make_filler(); + const Slic3r::Polylines fixed_width = fixed->fill_surface(&surface, params); + REQUIRE(!fixed_width.empty()); + const auto fixed_gaps = uncovered_area(ring, beads_of(fixed_width, fixed->spacing)); + + params.use_arachne = true; + std::unique_ptr variable = make_filler(); + const Slic3r::ThickPolylines variable_width = variable->fill_surface_arachne(&surface, params); + REQUIRE(!variable_width.empty()); + const auto variable_gaps = uncovered_area(ring, beads_of(variable_width)); + + CAPTURE(fixed_gaps.first, fixed_gaps.second, variable_gaps.first, variable_gaps.second); + // The wedges the fixed width loops leave behind are what the variable width walls take up. + CHECK(variable_gaps.second < 0.5 * fixed_gaps.second); + CHECK(variable_gaps.first < fixed_gaps.first); + + // And it is still a spiral: far fewer paths than the ring has loops. + // And the walls are still chained into spirals rather than printed one path per wall. The ring is + // at its narrowest (12 - 7.3) mm across and is filled from both sides, so it is at least this many + // walls thick there and thicker elsewhere. Arachne's short thin feature walls cannot join a spiral, + // so only the substantial paths count towards this. + const size_t walls_across = size_t(2.0 * (12.0 - 7.3) / spacing); + size_t spirals = 0; + for (const Slic3r::ThickPolyline &path : variable_width) + if (path.length() > scale_(10.0 * spacing)) + ++spirals; + CAPTURE(spirals, walls_across, variable_width.size(), fixed_width.size()); + CHECK(2 * spirals < walls_across); +} + TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]") { // A cell whose sides are several times the line width, so that the corners have room to be rounded. diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index 28901702cc..2469789d5b 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -130,6 +130,12 @@ TEST_CASE("get_config_index_base resolves (volume type, extruder type, id) to a } } +TEST_CASE("support interface pattern registry includes spiral inset", "[Config]") +{ + const auto &values = ConfigOptionEnum::get_enum_values(); + REQUIRE(values.at("spiralinset") == SupportMaterialInterfacePattern::smipSpiralInset); +} + TEST_CASE("get_extruder_nozzle_volume_count reads the per-extruder volume-type layout", "[Config]") { std::vector> nozzle_volume_types; From 0365304ae03b537eaca6c7445947a9116bfcb496 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 6 Sep 2026 13:42:57 -0500 Subject: [PATCH 08/13] fix: G-code preview drops comment text and cuts non-ASCII lines short (#15448) --- src/slic3r/GUI/GCodeViewer.cpp | 37 +++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index e5a88e4ea4..49477ac7dd 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -785,6 +785,19 @@ void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& fil } } +// Byte offset just past the first count characters of str, or its length if it is shorter. +static size_t utf8_offset(const std::string& str, size_t count) +{ + const char* const begin = str.c_str(); + const char* const end = begin + str.size(); + const char* pos = begin; + for (size_t i = 0; i < count && pos < end; ++i) { + unsigned int codepoint = 0; + pos += ImTextCharFromUtf8(&codepoint, pos, end); + } + return pos - begin; +} + //BBS: GUI refactor: move to right void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, float right, uint64_t curr_line_id) const { @@ -796,23 +809,27 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f // read line from file const size_t start = id == 1 ? 0 : m_lines_ends[id - 2]; const size_t original_len = m_lines_ends[id - 1] - start; - const size_t len = std::min(original_len, (size_t) 55); + // A character is four bytes at most, so 55 of them always fit in 220. + const size_t len = std::min(original_len, (size_t) 55 * 4); std::string gline(m_file.data() + start, len); - // If original line is longer than 55 characters, truncate and append "..." - if (original_len > 55) - gline = gline.substr(0, 52) + "..."; + // If original line is longer than 55 characters, truncate and append "...". + // The cut must land on a character boundary or it leaves half a character behind. + if (len < original_len || utf8_offset(gline, 55) < gline.size()) + gline = gline.substr(0, utf8_offset(gline, 52)) + "..."; std::string command, parameters, comment; - // extract comment - std::vector tokens; - boost::split(tokens, gline, boost::is_any_of(";"), boost::token_compress_on); - command = tokens.front(); - if (tokens.size() > 1) - comment = ";" + tokens.back(); + const size_t comment_start = gline.find(';'); + if (comment_start == std::string::npos) + command = gline; + else { + command = gline.substr(0, comment_start); + comment = gline.substr(comment_start); + } // extract gcode command and parameters if (!command.empty()) { + std::vector tokens; boost::split(tokens, command, boost::is_any_of(" "), boost::token_compress_on); command = tokens.front(); if (tokens.size() > 1) { From 43ce8c5e46c2e82b937f48aa96ce2ea1ac215383 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 6 Sep 2026 16:06:18 -0500 Subject: [PATCH 09/13] fix: SLIC3R_PCH=OFF now builds on Windows, allowing compiler caching (#15552) --- build_win.bat | 66 ++++++++++++++++++++++- scripts/test_build_win.ps1 | 52 +++++++++++++++++- src/libslic3r/BlacklistedLibraryCheck.cpp | 1 + src/libslic3r/CMakeLists.txt | 7 +++ src/libslic3r/MultiPoint.hpp | 1 + src/libslic3r/Print.cpp | 5 ++ src/libslic3r/SLA/Hollowing.cpp | 1 + src/slic3r/CMakeLists.txt | 3 ++ src/slic3r/GUI/DragCanvas.hpp | 1 + src/slic3r/GUI/GUI_App.cpp | 1 + src/slic3r/GUI/OptionsGroup.hpp | 2 +- src/slic3r/GUI/wxMediaCtrl3.h | 3 ++ src/slic3r/Utils/Bonjour.hpp | 1 + src/slic3r/win_platform.hpp | 19 +++++++ 14 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/slic3r/win_platform.hpp diff --git a/build_win.bat b/build_win.bat index c447d8a71f..840279e4f3 100644 --- a/build_win.bat +++ b/build_win.bat @@ -52,6 +52,7 @@ call :add_section "Build configuration" call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)" call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)" call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled" +call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache" call :add_section "Toolchain" call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler" @@ -60,6 +61,7 @@ call :add_arg use_ninja bool x ninja "Use the Ninja Multi-Config generator" call :add_arg use_msbuild bool "" msbuild "Use the Visual Studio generator (default)" call :add_arg vs_version string "" vs "Visual Studio release: 2019, 2022 or 2026 (default: autodetect)" call :add_arg clang_path string "" clang-path "Path to clang-cl.exe, requires -x (default: the one from Visual Studio)" +call :add_arg cache string "" cache "Compiler cache: ccache, sccache or off, requires -l -x (default: off)" call :add_section "How much gets rebuilt" call :add_arg slicer_target string "" slicer-target "Build one slicer target instead of all, e.g. libslic3r" @@ -391,6 +393,49 @@ if not "%clang_path%" == "" if not "%using_ninja%" == "ON" ( exit /b 1 ) +set "cache_args=" +if "%cache%" == "" goto :cache_ready +if /I "%cache%" == "off" goto :cache_ready +if /I "%cache%" == "ccache" goto :cache_named +if /I "%cache%" == "sccache" goto :cache_named +echo Unknown --cache value "%cache%". Expected ccache, sccache or off. +exit /b 1 + +REM CMake accepts a compiler launcher under any generator but only runs it +REM under Makefile and Ninja. ccache also refuses cl.exe because the build +REM passes /Zi. +:cache_named +REM Only a configure records the launcher, so --no-configure needs none. +if "%no_configure%" == "ON" goto :cache_ready +if "%build_deps%%build_slicer%" == "" goto :cache_ready +if not "%using_ninja%" == "ON" goto :cache_needs_clang +if not "%use_clang_cl%" == "ON" goto :cache_needs_clang +goto :cache_tool + +:cache_needs_clang +echo --cache needs clang-cl and Ninja; add -l -x. +exit /b 1 + +REM Name a full path, so the recorded launcher does not depend on PATH. +:cache_tool +set "cache_exe=" +for /f "tokens=*" %%i in ('where %cache% 2^>nul') do ( + if not defined cache_exe set "cache_exe=%%i" +) +if not defined cache_exe ( + echo %cache% is not on PATH. Install it, or leave --cache off. + exit /b 1 +) +REM Forward slashes, so CMake does not read a backslash as an escape. +set "cache_exe=%cache_exe:\=/%" +set "cache_args=-DCMAKE_C_COMPILER_LAUNCHER="%cache_exe%" -DCMAKE_CXX_COMPILER_LAUNCHER="%cache_exe%"" +REM Neither cache stores a compile that uses a precompiled header. sccache +REM refuses /Fp outright. ccache does too unless its sloppiness is loosened, +REM and even then most hits fall back to the slower preprocessed mode. +set "no_pch=ON" + +:cache_ready + REM Ninja prints [123/456] by default, which says nothing about how far REM along it is or how long is left. %p is a percentage, %w and %W are REM elapsed and remaining, but those two arrived in ninja 1.12 and an @@ -474,6 +519,8 @@ REM real path rather than one glued onto the repository root. for %%p in ("!build_dir!") do set "build_full=%%~fp" echo Configuration: %build_type%, %arch% if not "%clang_exe%" == "" echo Compiler: %clang_exe% +if "%no_pch%" == "ON" echo Precompiled header: off +if not "%cache_args%" == "" echo Compiler cache: %cache_exe% set "SIG_FLAG=" if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%" @@ -570,6 +617,10 @@ if "%build_deps%" == "ON" ( %error_check% ) + if not "!cache_args!" == "" ( + set "deps_args=!deps_args! !cache_args!" + ) + if not "%no_configure%" == "ON" ( call :print_and_run cmake -S deps -B "!DEP_TREE!" -G "%generator%" %gen_args% -DCMAKE_BUILD_TYPE=%build_type% !deps_args! %ORCA_DEPS_CMAKE_ARGS% %error_check% @@ -641,6 +692,16 @@ if "%build_slicer%" == "ON" ( set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON" ) + REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because + REM CMake takes the last definition on the command line. + if "%no_pch%" == "ON" ( + set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF" + ) + + if not "!cache_args!" == "" ( + set "slicer_args=!slicer_args! !cache_args!" + ) + REM Configuring against a tree that was never built fails deep inside REM package resolution. Name it here instead. Skipped when -d is about to REM build it in this same run, and under a dry run, which configures @@ -870,6 +931,7 @@ REM get_str_len -> length in %ret% echo %script_name% -s --no-configure -j 8 Rebuild quickly while iterating echo %script_name% -s --slicer-target glad Compile one target to check the toolchain echo %script_name% -l -x --run-tests Test that toolchain's build, not the default one + echo %script_name% -s -l -x --cache ccache Rebuild through a compiler cache echo. echo Environment: echo ORCA_DEPS_CMAKE_ARGS Extra arguments for the deps configure @@ -879,8 +941,8 @@ REM get_str_len -> length in %ret% echo NINJA_STATUS Ninja progress format, if you want your own echo debugscript Set to ON to trace this script echo. - echo set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_PCH=OFF -DSLIC3R_MSVC_PDB=OFF - echo $env:ORCA_SLICER_CMAKE_ARGS = '-DSLIC3R_PCH=OFF' (PowerShell) + echo set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_BUILD_SANDBOXES=ON -DSLIC3R_WARNINGS=OFF + echo $env:ORCA_SLICER_CMAKE_ARGS = '-DSLIC3R_BUILD_SANDBOXES=ON' (PowerShell) echo. echo --deps-args and --slicer-args cannot carry a value with spaces; use echo these instead. Neither form supports a value containing an ampersand. diff --git a/scripts/test_build_win.ps1 b/scripts/test_build_win.ps1 index 563430559f..34f64117af 100644 --- a/scripts/test_build_win.ps1 +++ b/scripts/test_build_win.ps1 @@ -92,6 +92,12 @@ New-Item -ItemType Directory -Force -Path $clangDir | Out-Null Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $clangDir 'clang-cl.exe') -Force $clangOnPath = "$clangDir;$env:PATH" +# A ccache that only has to exist. Nothing runs it; the script only locates it. +$cacheDir = Join-Path $fixtures 'cache' +New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null +Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $cacheDir 'ccache.exe') -Force +$ccacheOnPath = "$cacheDir;$env:PATH" + # ProgramFiles(x86) is where the script looks for vswhere, so an empty one # stands in for a machine whose Visual Studio has no clang toolset. $noVs = Join-Path $fixtures 'no-vs' @@ -121,7 +127,7 @@ $cases = @( 'Examples:', 'Environment:') } @{ Name = 'the environment section shows what to set'; Args = @('--help'); DryRun = $false Contains = @('ORCA_DEPS_CMAKE_ARGS', 'ORCA_SLICER_CMAKE_ARGS', 'ORCA_UPDATER_SIG_KEY', 'NINJA_STATUS', - 'set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_PCH=OFF', '(PowerShell)', 'debugscript') } + 'set ORCA_SLICER_CMAKE_ARGS=-DSLIC3R_BUILD_SANDBOXES=ON', '(PowerShell)', 'debugscript') } @{ Name = 'section headers do not widen the flag column'; Args = @('--help'); DryRun = $false Match = @('^ -d, --deps +Download') } # Windows Terminal opens at 120 columns and wraps at 120, so 119 is the @@ -294,6 +300,14 @@ $cases = @( Contains = @('-DBUILD_TESTS=ON') } @{ Name = '-a enables ASAN for the slicer'; Args = @('-s', '-a') Contains = @('-DSLIC3R_ASAN=ON') } + @{ Name = '--no-pch turns the precompiled header off'; Args = @('-s', '--no-pch') + Contains = @('-DSLIC3R_PCH=OFF') } + @{ Name = '--no-pch says so in the banner'; Args = @('-s', '--no-pch') + Contains = @('Precompiled header: off') } + @{ Name = 'the precompiled header is on unless asked'; Args = @('-s') + NotContains = @('SLIC3R_PCH') } + @{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch') + NotContains = @('COMPILER_LAUNCHER') } @{ Name = 'the slicer build runs gettext'; Args = @('-s') Contains = @('run_gettext.bat') } # tools\7z.exe needs a 7z.dll beside it, which the repo does not carry, @@ -324,6 +338,42 @@ $cases = @( @{ Name = 'deps and slicer build in one invocation'; Args = @('-d', '-s', '-x', '-l') Contains = @('cmake -S deps', 'cmake -B "build-clang" ') } + 'the compiler cache' + @{ Name = '--cache needs clang-cl and Ninja'; Args = @('-s', '--cache', 'ccache'); ExpectExit = 1 + Contains = @('needs clang-cl and Ninja') } + # cl.exe is out of scope, since ccache refuses every compile under /Zi. + @{ Name = '--cache under Ninja still needs clang-cl'; Args = @('-s', '-x', '--cache', 'ccache'); ExpectExit = 1 + Contains = @('needs clang-cl and Ninja') } + @{ Name = 'an unknown --cache value is rejected'; Args = @('-s', '-x', '--cache', 'nope'); ExpectExit = 1 + Contains = @('Expected ccache, sccache or off') } + # A bare PATH, since the machine running the tests may have sccache installed. + @{ Name = 'a --cache tool that is not there is caught early'; Args = @('-s', '-l', '-x', '--cache', 'sccache'); ExpectExit = 1 + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + Contains = @('is not on PATH') } + @{ Name = '--cache takes any casing'; Args = @('-s', '-l', '-x', '--cache', 'CCACHE') + Env = @{ PATH = $ccacheOnPath } + Contains = @('ccache.exe') } + @{ Name = '--cache off asks for no launcher'; Args = @('-s', '-x', '--cache', 'off') + NotContains = @('COMPILER_LAUNCHER') } + @{ Name = 'no --cache asks for no launcher'; Args = @('-s', '-x') + NotContains = @('COMPILER_LAUNCHER') } + @{ Name = '--cache turns the precompiled header off'; Args = @('-s', '-l', '-x', '--cache', 'ccache') + Env = @{ PATH = $ccacheOnPath } + Contains = @('-DSLIC3R_PCH=OFF', 'COMPILER_LAUNCHER') } + # The resolved path, not the bare name, so PATH cannot change it later. + @{ Name = '--cache names the resolved path in the banner'; Args = @('-s', '-l', '-x', '--cache', 'ccache') + Env = @{ PATH = $ccacheOnPath } + Match = @('^Compiler cache: .*/ccache\.exe$') } + @{ Name = '--cache reaches the dependency configure too'; Args = @('-d', '-l', '-x', '--cache', 'ccache') + Env = @{ PATH = $ccacheOnPath } + Contains = @('-DCMAKE_C_COMPILER_LAUNCHER=') } + # Nothing records a launcher without a configure, so the tool is not needed. + # Reaching the cmake check on a bare PATH is what proves it was skipped. + @{ Name = '--no-configure asks for no cache tool'; Args = @('-s', '-l', '-x', '--no-configure', '--cache', 'ccache'); ExpectExit = 1 + Env = @{ PATH = 'C:\Windows\system32;C:\Windows' } + Contains = @('CMake was not found') + NotContains = @('is not on PATH') } + 'the developer loop' @{ Name = '--slicer-target builds one target'; Args = @('-s', '--slicer-target', 'libslic3r') Contains = @('--config Release --target libslic3r') } diff --git a/src/libslic3r/BlacklistedLibraryCheck.cpp b/src/libslic3r/BlacklistedLibraryCheck.cpp index 938f542497..cf7e6da2aa 100644 --- a/src/libslic3r/BlacklistedLibraryCheck.cpp +++ b/src/libslic3r/BlacklistedLibraryCheck.cpp @@ -1,6 +1,7 @@ #include "BlacklistedLibraryCheck.hpp" #include +#include #include #ifdef WIN32 diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c33b7be429..d07c42d8f6 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -665,6 +665,13 @@ if(SLIC3R_PROFILE) target_link_libraries(libslic3r PRIVATE Shiny) endif() +if (WIN32) + # Public, since BlacklistedLibraryCheck.hpp includes windows.h. Empty + # WIN32_LEAN_AND_MEAN matches the sources that define it themselves; bare + # NOMINMAX matches the one libigl already passes. + target_compile_definitions(libslic3r PUBLIC "WIN32_LEAN_AND_MEAN=" "NOMINMAX") +endif () + if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) add_precompiled_header(libslic3r pchheader.hpp FORCEINCLUDE) endif () diff --git a/src/libslic3r/MultiPoint.hpp b/src/libslic3r/MultiPoint.hpp index de386b501c..096230d22c 100644 --- a/src/libslic3r/MultiPoint.hpp +++ b/src/libslic3r/MultiPoint.hpp @@ -24,6 +24,7 @@ public: explicit MultiPoint(const Points &_points) : points(_points) {} MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; } MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; } + virtual ~MultiPoint() = default; void scale(double factor); void scale(double factor_x, double factor_y); void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index e96304e874..aee50db534 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1,3 +1,8 @@ +#ifdef _WIN32 +// Keep this first. A header below reaches boost/regex, whose w32_regex_traits +// needs the Win32 types declared already. +#include +#endif #include "Config.hpp" #include "Exception.hpp" #include "Print.hpp" diff --git a/src/libslic3r/SLA/Hollowing.cpp b/src/libslic3r/SLA/Hollowing.cpp index df62127770..f50cdbced2 100644 --- a/src/libslic3r/SLA/Hollowing.cpp +++ b/src/libslic3r/SLA/Hollowing.cpp @@ -1,4 +1,5 @@ #include +#include #include #include diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 1e39e30b0d..83ad94e563 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -916,6 +916,9 @@ endif () if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE) +elseif (MSVC) + # Puts the Windows headers first when the PCH is off. + target_compile_options(libslic3r_gui PRIVATE "/FIslic3r/win_platform.hpp") endif () if (APPLE) diff --git a/src/slic3r/GUI/DragCanvas.hpp b/src/slic3r/GUI/DragCanvas.hpp index aa8d8ddfa8..f39e836673 100644 --- a/src/slic3r/GUI/DragCanvas.hpp +++ b/src/slic3r/GUI/DragCanvas.hpp @@ -3,6 +3,7 @@ #include "wx/bitmap.h" #include "wx/dragimag.h" +#include "wx/panel.h" namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 5b80a866ca..3500493329 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -12,6 +12,7 @@ #include "BuildCommit.hpp" #include "Downloader.hpp" #include +#include #include #include #include diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index c808545145..b20d16aca9 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -199,7 +199,7 @@ public: OptionsGroup(wxWindow *_parent, const wxString &title, const wxString &icon, bool is_tab_opt = false, column_t extra_clmn = nullptr); - ~OptionsGroup() { clear(true); } + virtual ~OptionsGroup() { clear(true); } wxGridSizer* get_grid_sizer() { return m_grid_sizer; } const std::vector& get_lines() { return m_lines; } diff --git a/src/slic3r/GUI/wxMediaCtrl3.h b/src/slic3r/GUI/wxMediaCtrl3.h index 1d64955ffd..ff4595ad32 100644 --- a/src/slic3r/GUI/wxMediaCtrl3.h +++ b/src/slic3r/GUI/wxMediaCtrl3.h @@ -8,6 +8,9 @@ #ifndef wxMediaCtrl3_h #define wxMediaCtrl3_h +#include +#include "wx/window.h" +#include "wx/bitmap.h" #include "wx/uri.h" #include "wx/mediactrl.h" diff --git a/src/slic3r/Utils/Bonjour.hpp b/src/slic3r/Utils/Bonjour.hpp index 50b71791fa..3ef8f0e1a2 100644 --- a/src/slic3r/Utils/Bonjour.hpp +++ b/src/slic3r/Utils/Bonjour.hpp @@ -115,6 +115,7 @@ class UdpSession { public: UdpSession(Bonjour::ReplyFn rfn); + virtual ~UdpSession() = default; virtual void handle_receive(const boost::system::error_code& error, size_t bytes) = 0; std::vector buffer; boost::asio::ip::udp::endpoint remote_endpoint; diff --git a/src/slic3r/win_platform.hpp b/src/slic3r/win_platform.hpp new file mode 100644 index 0000000000..0c5a8e81b2 --- /dev/null +++ b/src/slic3r/win_platform.hpp @@ -0,0 +1,19 @@ +#pragma once + +// Force-included into libslic3r_gui when SLIC3R_PCH is OFF, standing in for +// pchheader.hpp, which includes before anything else. Arriving +// late and transitively, rpcndr.h defines a global `byte` that collides with +// std::byte under `using namespace std`, and the control and URL moniker +// types the GUI uses are never declared. +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include + #include + #include +#endif // _WIN32 + From 8d44b680bdc174f3da4a7adfeba62b33fbc7e084 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 6 Sep 2026 18:23:47 -0500 Subject: [PATCH 10/13] fix: bumping the bundled uv version rebuilds the whole GUI library (#15553) --- src/slic3r/CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 83ad94e563..7c25792bb3 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -984,10 +984,6 @@ endif () # Add a definition so that we can tell we are compiling slic3r. target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_CURRENTLY_COMPILING_GUI_MODULE) -if(ORCA_BUNDLED_UV_EXECUTABLE_CONFIG) - target_compile_definitions(libslic3r_gui PRIVATE "ORCA_BUNDLED_UV_EXECUTABLE=\"${ORCA_BUNDLED_UV_EXECUTABLE_CONFIG}\"") -endif() - if (ORCA_BUILD_PYTHON_STUBGEN_MODULE) add_library(orca_stubgen MODULE plugin/PythonPluginBridge.cpp From 494d50bd55dd30f9bfeda2125bae6bdc3fb6e5a0 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Mon, 7 Sep 2026 01:16:45 -0500 Subject: [PATCH 11/13] fix: restore Ctrl+drag panning on macOS (#15312) On macOS wxWidgets reports Ctrl+left as a synthetic right button, which is what made Ctrl+drag pan the canvas. #14999 added an unconditional correction of the event's button state from wxGetMouseState(), which reports the physical buttons and knows nothing about that synthesis, so the synthetic right button was overwritten with a plain left button on every event. Ctrl+drag then matched the left button mapping and rotated instead of panning. Apply the correction only when the event carries no button state at all. On macOS wx populates button state only for the mouse-down and mouse-dragged event types, which are also the only ones the Ctrl+left translation touches, so the ImGui capture fix keeps every event it was added for. Fixes #15214 Co-authored-by: Noisyfox --- src/slic3r/GUI/GLCanvas3D.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 7db7e01e9c..6cdc27ed65 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4168,7 +4168,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) // https://github.com/OrcaSlicer/OrcaSlicer/pull/14999#issuecomment-5151344759 // We solve this by correcting the state of the event from the actual mouse state querying with `wxGetMouseState()` // so it works like on other platforms. - { + // Only fill in state the event does not carry, to preserve wx's synthetic right button for Ctrl+left. + if (!evt.ButtonIsDown(wxMOUSE_BTN_ANY)) { const auto state = wxGetMouseState(); evt.SetLeftDown(state.LeftIsDown()); evt.SetMiddleDown(state.MiddleIsDown()); From 886d43d37a7eb98db7d090fe8cb8399089800b69 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:35:36 +0800 Subject: [PATCH 12/13] Fix Support Fill Cost Thresholds Being Frozen By The First Call (#15564) Fix support fill cost thresholds being frozen by the first call --- src/libslic3r/Fill/FillBase.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index 2b307007b8..f4615dc640 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -2467,9 +2467,11 @@ void Fill::connect_base_support(Polylines &&infill_ordered, const std::vector arches = evaluate_support_arches(infill_ordered, graph, spacing, params); - static const double cost_low = line_spacing * 1.3; - static const double cost_high = line_spacing * 2.; - static const double cost_veryhigh = line_spacing * 3.; + // Must not be static: line_spacing varies per call (base vs interface fills differ), + // and a static here would fix these to whichever call ran first, order depending on thread count. + const double cost_low = line_spacing * 1.3; + const double cost_high = line_spacing * 2.; + const double cost_veryhigh = line_spacing * 3.; { std::vector selected; From fcb128c51b424976ca262fb6ed9af91129107171 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Mon, 7 Sep 2026 05:57:09 -0500 Subject: [PATCH 13/13] Fix Traditional Chinese language selection (#15567) Use the zh_TW-specific wxWidgets language enum so the packaged Traditional Chinese catalog passes the Preferences language filter.\n\nFixes #15561 --- src/slic3r/GUI/Preferences.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 355c4a26a8..08483f3100 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -347,7 +347,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS wxLanguage supported_languages[]{ wxLANGUAGE_ENGLISH, wxLANGUAGE_CHINESE_SIMPLIFIED, - wxLANGUAGE_CHINESE, + wxLANGUAGE_CHINESE_TRADITIONAL, wxLANGUAGE_GERMAN, wxLANGUAGE_CZECH, wxLANGUAGE_FRENCH, @@ -407,7 +407,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS if (vlist[i] == wxLocale::GetLanguageInfo(wxLANGUAGE_CHINESE_SIMPLIFIED)) { language_name = wxString::FromUTF8("\xe4\xb8\xad\xe6\x96\x87\x28\xe7\xae\x80\xe4\xbd\x93\x29"); } - else if (vlist[i] == wxLocale::GetLanguageInfo(wxLANGUAGE_CHINESE)) { + else if (vlist[i] == wxLocale::GetLanguageInfo(wxLANGUAGE_CHINESE_TRADITIONAL)) { language_name = wxString::FromUTF8("\xe4\xb8\xad\xe6\x96\x87\x28\xe7\xb9\x81\xe9\xab\x94\x29"); } else if (vlist[i] == wxLocale::GetLanguageInfo(wxLANGUAGE_SPANISH)) {