From 9ed459e132baee9d82a926bb4b47b930aa4eacb1 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 25 Sep 2026 13:25:45 +0800 Subject: [PATCH 01/38] fix: slicing event name and ID --- src/libslic3r/GCode.cpp | 15 ++++++++++----- src/libslic3r/Print.cpp | 15 ++++++++++----- src/slic3r/GUI/Plater.cpp | 8 ++++---- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 1a1d1bcbff..8bbd7a0344 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2479,7 +2479,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu { LifecycleEventContext ctx; - ctx.name = std::to_string(print->model().id().id); + ctx.id = std::to_string(print->model().id().id); + ctx.name = print->get_model_name(); ctx.code = LifecycleEvtCode::Ok; ctx.msg = path; ctx.cancellation_check = [print]() { return print->canceled(); }; @@ -2531,7 +2532,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu } { LifecycleEventContext ctx; - ctx.name = std::to_string(print->model().id().id); + ctx.id = std::to_string(print->model().id().id); + ctx.name = print->get_model_name(); ctx.code = LifecycleEvtCode::Error; ctx.msg = std::string(path) + "\n" + err_msg; ctx.cancellation_check = [print]() { return print->canceled(); }; @@ -2555,7 +2557,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu boost::nowide::remove(path_tmp.c_str()); { LifecycleEventContext ctx; - ctx.name = std::to_string(print->model().id().id); + ctx.id = std::to_string(print->model().id().id); + ctx.name = print->get_model_name(); ctx.code = LifecycleEvtCode::Error; ctx.msg = std::string(path) + "\n" + ex.what(); ctx.cancellation_check = [print]() { return print->canceled(); }; @@ -2668,7 +2671,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu if (ret) { { LifecycleEventContext ctx; - ctx.name = std::to_string(print->model().id().id); + ctx.id = std::to_string(print->model().id().id); + ctx.name = print->get_model_name(); ctx.code = LifecycleEvtCode::Error; ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message(); ctx.cancellation_check = [print]() { return print->canceled(); }; @@ -2687,7 +2691,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu { LifecycleEventContext ctx; - ctx.name = std::to_string(print->model().id().id); + ctx.id = std::to_string(print->model().id().id); + ctx.name = print->get_model_name(); ctx.code = LifecycleEvtCode::Ok; ctx.msg = path; ctx.cancellation_check = [print]() { return print->canceled(); }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 477c2c74ca..850761339f 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2716,7 +2716,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) { LifecycleEventContext ctx; - ctx.name = std::to_string(m_model.id().id); + ctx.id = std::to_string(m_model.id().id); + ctx.name = get_model_name(); ctx.code = LifecycleEvtCode::Ok; ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx); @@ -3343,7 +3344,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) { LifecycleEventContext ctx; - ctx.name = std::to_string(m_model.id().id); + ctx.id = std::to_string(m_model.id().id); + ctx.name = get_model_name(); ctx.code = LifecycleEvtCode::Ok; ctx.cancellation_check = [this]() { return canceled(); }; fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx); @@ -4949,7 +4951,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces { { LifecycleEventContext ctx; - ctx.name = std::to_string(m_model.id().id); + ctx.id = std::to_string(m_model.id().id); + ctx.name = get_model_name(); ctx.code = LifecycleEvtCode::Ok; ctx.msg = file; ctx.cancellation_check = [this]() { return canceled(); }; @@ -4979,7 +4982,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str(); { LifecycleEventContext ctx; - ctx.name = std::to_string(m_model.id().id); + ctx.id = std::to_string(m_model.id().id); + ctx.name = get_model_name(); ctx.code = LifecycleEvtCode::Error; ctx.msg = file + "\n" + ex.what(); ctx.cancellation_check = [this]() { return canceled(); }; @@ -4993,7 +4997,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces { LifecycleEventContext ctx; - ctx.name = std::to_string(m_model.id().id); + ctx.id = std::to_string(m_model.id().id); + ctx.name = get_model_name(); ctx.code = LifecycleEvtCode::Ok; ctx.msg = file; ctx.cancellation_check = [this]() { return canceled(); }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5eb36f20f9..04318dcbb5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12801,9 +12801,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) this->background_process.stop(); notification_manager->set_slicing_progress_export_possible(); - // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. - const std::string lifecycle_job_name = this->background_process.fff_print() ? - this->background_process.fff_print()->output_filename() : std::string(); this->background_process.reset_export(); // This bool stops showing export finished notification even when process_completed_with_error is false bool has_error = false; @@ -12850,7 +12847,10 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) { Slic3r::LifecycleEventContext ctx; - ctx.name = lifecycle_job_name; + if (const Print* print = this->background_process.fff_print()) { + ctx.id = std::to_string(print->model().id().id); + ctx.name = print->get_model_name(); + } ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok); ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string()); Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx); From 04204ec0d3e740f80ecc1e526ad2f03e1943500a Mon Sep 17 00:00:00 2001 From: peachismomo Date: Sun, 27 Sep 2026 17:25:56 +0800 Subject: [PATCH 02/38] test: cover slicing lifecycle event context --- tests/fff_print/test_print.cpp | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 383a7165d5..83dbfa0945 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -15,6 +15,9 @@ #include "libslic3r/Layer.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/Exception.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "test_helpers.hpp" #include "test_utils.hpp" @@ -22,7 +25,10 @@ #include #include #include +#include #include +#include +#include using namespace Slic3r; using namespace Slic3r::Test; @@ -224,8 +230,68 @@ std::string resolved_output_name(Model& model, const std::string& format, const return print.output_filename(filename_base); } +struct ScopedLifecycleHook +{ + explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); } + ~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); } +}; + } // namespace +TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvents]") +{ + struct ObservedEvent { + LifecycleEvent event; + std::string id; + std::string name; + }; + std::vector events; + ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) { + events.push_back({ event, ctx.id, ctx.name }); + }); + + Print print; + Model model; + ModelInfo info; + info.model_name = "Lifecycle test model"; + model.model_info = std::make_shared(std::move(info)); + init_print({cube(20)}, print, model); + + print.process(); + ScopedTemporaryFile temp(".gcode"); + print.export_gcode(temp.string(), nullptr, nullptr); + GCodeProcessorResult result; + print.export_gcode_from_previous_file(temp.string(), &result); + + const std::string expected_id = std::to_string(print.model().id().id); + const std::vector expected_events = { + LifecycleEvent::SliceStarted, + LifecycleEvent::SliceGeometryFinished, + LifecycleEvent::GCodeExportStarted, + LifecycleEvent::GCodeExportFinished, + LifecycleEvent::GCodeExportStarted, + LifecycleEvent::GCodeExportFinished, + }; + REQUIRE(events.size() == expected_events.size()); + for (size_t i = 0; i < expected_events.size(); ++i) { + CHECK(events[i].event == expected_events[i]); + CHECK(events[i].id == expected_id); + CHECK(events[i].name == "Lifecycle test model"); + } +} + +TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("filename_format", new ConfigOptionString("{int(total_weight*10) / 10.0}")); + + Print print; + Model model; + init_print({cube(20)}, print, model, config); + + CHECK_THROWS_AS(print.output_filename(), PlaceholderParserError); +} + TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]") { Model model; From 58842bab05d0ce9208239da97f886a27233e4fe2 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Sun, 27 Sep 2026 17:29:01 +0800 Subject: [PATCH 03/38] test: test for SliceStarted ensuring stable model ID is presetn when model name is absent --- tests/fff_print/test_print.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 83dbfa0945..32cd608b43 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -280,6 +280,26 @@ TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvent } } +TEST_CASE("Slicing lifecycle event name is empty without model metadata", "[Print][LifecycleEvents]") +{ + std::string event_id; + std::string event_name = "unset"; + ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) { + if (event == LifecycleEvent::SliceStarted) { + event_id = ctx.id; + event_name = ctx.name; + } + }); + + Print print; + Model model; + init_print({cube(20)}, print, model); + print.process(); + + CHECK(event_id == std::to_string(print.model().id().id)); + CHECK(event_name.empty()); +} + TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); From 9e16cdb23b710d533a0abc186a84ee830f6b5d28 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Sun, 27 Sep 2026 17:29:25 +0800 Subject: [PATCH 04/38] fix: comment above reset_export --- src/slic3r/GUI/Plater.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 04318dcbb5..0555a43776 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12801,6 +12801,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) this->background_process.stop(); notification_manager->set_slicing_progress_export_possible(); + // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. this->background_process.reset_export(); // This bool stops showing export finished notification even when process_completed_with_error is false bool has_error = false; From e340a13c186e024ece6b3b4873c34137836aabe8 Mon Sep 17 00:00:00 2001 From: peachismomo Date: Sun, 27 Sep 2026 17:53:41 +0800 Subject: [PATCH 05/38] fix: use current print for slicing lifecycle identity --- src/slic3r/GUI/Plater.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0555a43776..a7023c6966 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -12848,11 +12848,18 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) { Slic3r::LifecycleEventContext ctx; - if (const Print* print = this->background_process.fff_print()) { - ctx.id = std::to_string(print->model().id().id); - ctx.name = print->get_model_name(); + if (const PrintBase* print = this->background_process.current_print()) { + const Model& model = print->model(); + ctx.id = std::to_string(model.id().id); + if (model.model_info) + ctx.name = model.model_info->model_name; + } else { + // Realistically Printbase* print will never be null because select_technology already asserts an active print + // and the worker thread asserts it before processing. + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": slicing completed without an active print; lifecycle event has no model ID"; } - ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok); + ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : + (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok); ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string()); Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx); } From bd65cce3f42bb705526a59c22c6331ce8e849e75 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:58:10 +0300 Subject: [PATCH 06/38] Unify GUI scroll rates (#15902) Use DPI-aware 20 DIP vertical scrolling for general-purpose GUI scroll areas. Keep list-based views aligned to their item height so one wheel increment follows the visible row rhythm. This makes Preferences and other dialogs scroll consistently across Windows DPI settings. --- src/slic3r/GUI/AmsMappingPopup.cpp | 8 ++++---- src/slic3r/GUI/Auxiliary.cpp | 2 +- src/slic3r/GUI/BindDialog.cpp | 2 +- src/slic3r/GUI/CAD/DesignPanel.cpp | 2 +- src/slic3r/GUI/CaliHistoryDialog.cpp | 2 +- src/slic3r/GUI/CalibrationPanel.cpp | 2 +- src/slic3r/GUI/CalibrationWizard.cpp | 2 +- src/slic3r/GUI/CalibrationWizardPage.cpp | 2 +- src/slic3r/GUI/ConfigWizard.cpp | 2 +- src/slic3r/GUI/CreatePresetsDialog.cpp | 10 +++++----- src/slic3r/GUI/DeviceErrorDialog.cpp | 2 +- src/slic3r/GUI/GUI_ObjectTable.cpp | 4 ++-- src/slic3r/GUI/HMSPanel.cpp | 2 +- src/slic3r/GUI/KBShortcutsDialog.cpp | 2 +- src/slic3r/GUI/MixedFilamentDialog.cpp | 2 +- src/slic3r/GUI/MsgDialog.cpp | 2 +- src/slic3r/GUI/MultiMachineManagerPage.cpp | 2 +- src/slic3r/GUI/MultiMachinePage.cpp | 2 +- src/slic3r/GUI/MultiTaskManagerPage.cpp | 4 ++-- src/slic3r/GUI/ObjColorDialog.cpp | 2 +- src/slic3r/GUI/ParamsPanel.cpp | 2 +- src/slic3r/GUI/PartSkipDialog.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 6 ++---- src/slic3r/GUI/Preferences.cpp | 2 +- src/slic3r/GUI/PrintOptionsDialog.cpp | 2 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 2 +- src/slic3r/GUI/ReleaseNote.cpp | 12 ++++++------ src/slic3r/GUI/SafetyOptionsDialog.cpp | 4 ++-- src/slic3r/GUI/SelectMachine.cpp | 4 ++-- src/slic3r/GUI/SelectMachinePop.cpp | 2 +- src/slic3r/GUI/SendMultiMachinePage.cpp | 4 ++-- src/slic3r/GUI/SendToPrinter.cpp | 2 +- src/slic3r/GUI/StatusPanel.cpp | 2 +- src/slic3r/GUI/SyncAmsInfoDialog.cpp | 2 +- src/slic3r/GUI/TextureImportDialog.cpp | 6 +++--- src/slic3r/GUI/UnsavedChangesDialog.cpp | 2 +- src/slic3r/GUI/UpdateDialogs.cpp | 2 +- src/slic3r/GUI/UpgradePanel.cpp | 2 +- src/slic3r/GUI/Widgets/CheckList.cpp | 4 ++-- src/slic3r/GUI/Widgets/ProgressDialog.cpp | 2 +- src/slic3r/GUI/wxExtensions.cpp | 2 +- 41 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/slic3r/GUI/AmsMappingPopup.cpp b/src/slic3r/GUI/AmsMappingPopup.cpp index 22ffaef034..32a580845d 100644 --- a/src/slic3r/GUI/AmsMappingPopup.cpp +++ b/src/slic3r/GUI/AmsMappingPopup.cpp @@ -819,7 +819,7 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) : m_scrolled_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL); m_scrolled_window->SetBackgroundColour(*wxWHITE); - m_scrolled_window->SetScrollRate(0, FromDIP(10)); + m_scrolled_window->SetScrollRate(0, FromDIP(20)); wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL); @@ -1000,13 +1000,13 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) : show_pos.x = screen_size.GetLeft(); m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height)); m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height)); - m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(10)); + m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(20)); } else { m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height)); m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height)); - m_scrolled_window->SetScrollRate(0, FromDIP(10)); + m_scrolled_window->SetScrollRate(0, FromDIP(20)); } //Vertical Direction Processing @@ -2117,7 +2117,7 @@ void AmsReplaceMaterialDialog::create() identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green) m_scrollview_groups = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - m_scrollview_groups->SetScrollRate(5, 5); + m_scrollview_groups->SetScrollRate(5, FromDIP(20)); //m_scrollview_groups->SetMinSize(wxSize(400, 400)); //m_scrollview_groups->SetMaxSize(wxSize(400, 400)); m_scrollview_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 14b8d238a2..444daa4f91 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -606,7 +606,7 @@ AuFolderPanel::AuFolderPanel(wxWindow *parent, AuxiliaryFolderType type, wxWindo wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - m_scrolledWindow->SetScrollRate(5, 5); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL); wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL); diff --git a/src/slic3r/GUI/BindDialog.cpp b/src/slic3r/GUI/BindDialog.cpp index 4c6d3c249d..883765751b 100644 --- a/src/slic3r/GUI/BindDialog.cpp +++ b/src/slic3r/GUI/BindDialog.cpp @@ -558,7 +558,7 @@ PingCodeBindDialog::~PingCodeBindDialog() { //show bind failed info m_sw_bind_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL); m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_bind_failed_info->SetScrollRate(5, 5); + m_sw_bind_failed_info->SetScrollRate(5, FromDIP(20)); m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90))); m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90))); diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index d6e0329ef8..0b4f86a618 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -3521,7 +3521,7 @@ DesignPanel::DesignPanel(wxWindow* parent) cards->Show(m_box_mate, false, true); m_form->FitInside(); - m_form->SetScrollRate(0, 10); // vertical only, like Prepare's sidebar: never scroll labels out + m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out m_form->SetMinSize(wxSize(264, -1)); // Right column: a small view toolbar over the live 3D viewport that mirrors diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 3ce8c1bb81..7055f441ac 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -115,7 +115,7 @@ HistoryWindow::HistoryWindow(wxWindow* parent, const std::vector& auto main_sizer = new wxBoxSizer(wxVERTICAL); auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - scroll_window->SetScrollRate(5, 5); + scroll_window->SetScrollRate(5, FromDIP(20)); scroll_window->SetBackgroundColour(*wxWHITE); scroll_window->SetMinSize(HISTORY_WINDOW_SIZE); scroll_window->SetSize(HISTORY_WINDOW_SIZE); diff --git a/src/slic3r/GUI/CalibrationPanel.cpp b/src/slic3r/GUI/CalibrationPanel.cpp index fca9fa2be9..d906a8211c 100644 --- a/src/slic3r/GUI/CalibrationPanel.cpp +++ b/src/slic3r/GUI/CalibrationPanel.cpp @@ -207,7 +207,7 @@ SelectMObjectPopup::SelectMObjectPopup(wxWindow* parent) m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE); - m_scrolledWindow->SetScrollRate(0, 5); + m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y); auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL); m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow); m_scrolledWindow->Layout(); diff --git a/src/slic3r/GUI/CalibrationWizard.cpp b/src/slic3r/GUI/CalibrationWizard.cpp index fa42143f06..4d6e694380 100644 --- a/src/slic3r/GUI/CalibrationWizard.cpp +++ b/src/slic3r/GUI/CalibrationWizard.cpp @@ -81,7 +81,7 @@ CalibrationWizard::CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowI wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); - m_scrolledWindow->SetScrollRate(5, 5); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); m_scrolledWindow->SetBackgroundColour(*wxWHITE); wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL); diff --git a/src/slic3r/GUI/CalibrationWizardPage.cpp b/src/slic3r/GUI/CalibrationWizardPage.cpp index c117ec8857..ff02feab68 100644 --- a/src/slic3r/GUI/CalibrationWizardPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPage.cpp @@ -834,7 +834,7 @@ void CaliPageSendingPanel::create(wxWindow* parent) m_sw_print_failed_info = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_print_failed_info->SetScrollRate(0, 5); + m_sw_print_failed_info->SetScrollRate(0, FromDIP(20)); m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125))); diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 412df19091..7aaa0e7865 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -2710,7 +2710,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) SetSizerAndFit(vsizer); // We can now enable scrolling on hscroll - p->hscroll->SetScrollRate(30, 30); + p->hscroll->SetScrollRate(30, FromDIP(20)); on_window_geometry(this, [this]() { p->init_dialog_size(); diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index e6fb6e0782..16b9a5765d 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -699,7 +699,7 @@ CreateFilamentPresetDialog::CreateFilamentPresetDialog(wxWindow *parent) m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY); m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350))); m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE); - m_scrolled_preset_panel->SetScrollRate(5, 5); + m_scrolled_preset_panel->SetScrollRate(5, FromDIP(20)); m_scrolled_sizer = new wxBoxSizer(wxVERTICAL); m_scrolled_sizer->Add(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); m_scrolled_sizer->Add(0, 0, 0, wxTOP, FromDIP(5)); @@ -1593,7 +1593,7 @@ CreatePrinterPresetDialog::CreatePrinterPresetDialog(wxWindow *parent) m_page1 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page1->SetBackgroundColour(*wxWHITE); - m_page1->SetScrollRate(5, 5); + m_page1->SetScrollRate(5, FromDIP(20)); m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE); create_printer_page1(m_page1); @@ -2652,7 +2652,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_presets_template_item(wxWindow *pa wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL); m_scrolled_preset_window = new wxScrolledWindow(parent); - m_scrolled_preset_window->SetScrollRate(5, 5); + m_scrolled_preset_window->SetScrollRate(5, FromDIP(20)); m_scrolled_preset_window->SetBackgroundColour(*wxWHITE); //m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1))); m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(1500), FromDIP(-1))); @@ -4297,7 +4297,7 @@ wxBoxSizer *ExportConfigsDialog::create_select_printer(wxWindow *parent) optionSizer->SetMinSize(OPTION_SIZE); horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10)); m_scrolled_preset_window = new wxScrolledWindow(parent); - m_scrolled_preset_window->SetScrollRate(5, 5); + m_scrolled_preset_window->SetScrollRate(5, FromDIP(20)); m_scrolled_preset_window->SetBackgroundColour(*wxWHITE); m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400))); m_scrolled_preset_window->SetSize(wxSize(FromDIP(660), FromDIP(400))); @@ -4736,7 +4736,7 @@ wxBoxSizer *EditFilamentPresetDialog::create_preset_tree_sizer() { wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL); m_preset_tree_window = new wxScrolledWindow(this); - m_preset_tree_window->SetScrollRate(5, 5); + m_preset_tree_window->SetScrollRate(5, FromDIP(20)); m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR); m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400))); m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300))); diff --git a/src/slic3r/GUI/DeviceErrorDialog.cpp b/src/slic3r/GUI/DeviceErrorDialog.cpp index 9790db67bd..4e81135a75 100644 --- a/src/slic3r/GUI/DeviceErrorDialog.cpp +++ b/src/slic3r/GUI/DeviceErrorDialog.cpp @@ -39,7 +39,7 @@ DeviceErrorDialog::DeviceErrorDialog(MachineObject* obj, wxWindow* parent, wxWin m_line_top->SetBackgroundColour(wxColour(166, 169, 170)); m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_scroll_area->SetScrollRate(0, 5); + m_scroll_area->SetScrollRate(0, FromDIP(20)); m_scroll_area->SetBackgroundColour(*wxWHITE); m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250))); diff --git a/src/slic3r/GUI/GUI_ObjectTable.cpp b/src/slic3r/GUI/GUI_ObjectTable.cpp index a496eca6d3..c3c3295800 100644 --- a/src/slic3r/GUI/GUI_ObjectTable.cpp +++ b/src/slic3r/GUI/GUI_ObjectTable.cpp @@ -2764,12 +2764,12 @@ ObjectTablePanel::ObjectTablePanel( wxWindow* parent, wxWindowID id, const wxPoi //m_object_grid->AssignTable(m_object_grid_table); m_side_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(440),FromDIP(480)), wxVSCROLL); - m_side_window->SetScrollRate( 0, 5 ); + m_side_window->SetScrollRate(0, FromDIP(20)); m_page_sizer = new wxBoxSizer(wxVERTICAL); //m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL); m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff)); m_side_window->SetSizer(m_page_sizer); - m_side_window->SetScrollbars(1, 20, 1, 2); + m_side_window->SetScrollbars(1, FromDIP(20), 1, 2); //m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER); //m_side_window->EnableScrolling(false, true); diff --git a/src/slic3r/GUI/HMSPanel.cpp b/src/slic3r/GUI/HMSPanel.cpp index c2a8314bd5..1268cb8f7b 100644 --- a/src/slic3r/GUI/HMSPanel.cpp +++ b/src/slic3r/GUI/HMSPanel.cpp @@ -171,7 +171,7 @@ HMSPanel::HMSPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wx m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); - m_scrolledWindow->SetScrollRate(5, 5); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); m_top_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index c9fdfe4ab4..63580387a7 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -211,7 +211,7 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page) wxGetApp().UpdateDarkUI(scrollable_panel); const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE); scrollable_panel->SetBackgroundColour(page_colour); - scrollable_panel->SetScrollRate(0, 20); + scrollable_panel->SetScrollRate(0, FromDIP(20)); const int page_width = FromDIP(PAGE_WIDTH); scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450))); diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index 8176a76202..246c6548c6 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -1125,7 +1125,7 @@ wxBoxSizer* MixedFilamentDialog::create_recommendation_grid() outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116))); - m_recommendation_scroll->SetScrollRate(0, 5); + m_recommendation_scroll->SetScrollRate(0, FromDIP(20)); m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES); diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 7f1effd162..0f1d3f62d5 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -398,7 +398,7 @@ static void add_msg_content(wxWindow *parent, } wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); scrolledWindow->SetBackgroundColour(*wxWHITE); - scrolledWindow->SetScrollRate(0, 20); + scrolledWindow->SetScrollRate(0, scrolledWindow->FromDIP(20)); scrolledWindow->EnableScrolling(false, true); wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL); Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1)); diff --git a/src/slic3r/GUI/MultiMachineManagerPage.cpp b/src/slic3r/GUI/MultiMachineManagerPage.cpp index d5f9cbce8a..f68188a9f1 100644 --- a/src/slic3r/GUI/MultiMachineManagerPage.cpp +++ b/src/slic3r/GUI/MultiMachineManagerPage.cpp @@ -398,7 +398,7 @@ MultiMachineManagerPage::MultiMachineManagerPage(wxWindow* parent) m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_machine_list->SetBackgroundColour(*wxWHITE); - m_machine_list->SetScrollRate(0, 5); + m_machine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); m_machine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_machine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); diff --git a/src/slic3r/GUI/MultiMachinePage.cpp b/src/slic3r/GUI/MultiMachinePage.cpp index e48e402405..2b6fe75f34 100644 --- a/src/slic3r/GUI/MultiMachinePage.cpp +++ b/src/slic3r/GUI/MultiMachinePage.cpp @@ -323,7 +323,7 @@ MultiMachinePickPage::MultiMachinePickPage(Plater* plater /*= nullptr*/) scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30))); scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30))); scroll_macine_list->SetBackgroundColour(*wxWHITE); - scroll_macine_list->SetScrollRate(0, 5); + scroll_macine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); sizer_machine_list = new wxBoxSizer(wxVERTICAL); scroll_macine_list->SetSizer(sizer_machine_list); diff --git a/src/slic3r/GUI/MultiTaskManagerPage.cpp b/src/slic3r/GUI/MultiTaskManagerPage.cpp index 48a7fbe43e..d063b34743 100644 --- a/src/slic3r/GUI/MultiTaskManagerPage.cpp +++ b/src/slic3r/GUI/MultiTaskManagerPage.cpp @@ -675,7 +675,7 @@ LocalTaskManagerPage::LocalTaskManagerPage(wxWindow* parent) m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_task_list->SetBackgroundColour(*wxWHITE); - m_task_list->SetScrollRate(0, 5); + m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); @@ -1056,7 +1056,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent) m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_task_list->SetBackgroundColour(*wxWHITE); - m_task_list->SetScrollRate(0, 5); + m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT)); m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); diff --git a/src/slic3r/GUI/ObjColorDialog.cpp b/src/slic3r/GUI/ObjColorDialog.cpp index ef5d4ba9d2..4388449bb5 100644 --- a/src/slic3r/GUI/ObjColorDialog.cpp +++ b/src/slic3r/GUI/ObjColorDialog.cpp @@ -355,7 +355,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c //new color table m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); - m_scrolledWindow->SetScrollRate(0, 20); + m_scrolledWindow->SetScrollRate(0, FromDIP(20)); m_scrolledWindow->EnableScrolling(false, true); m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT); draw_new_table(); diff --git a/src/slic3r/GUI/ParamsPanel.cpp b/src/slic3r/GUI/ParamsPanel.cpp index b4fd81706a..9e16fcf8df 100644 --- a/src/slic3r/GUI/ParamsPanel.cpp +++ b/src/slic3r/GUI/ParamsPanel.cpp @@ -391,7 +391,7 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c m_page_sizer = new wxBoxSizer(wxVERTICAL); m_page_view->SetSizer(m_page_sizer); - m_page_view->SetScrollbars(1, 20, 1, 2); + m_page_view->SetScrollbars(1, FromDIP(20), 1, 2); //m_page_view->SetScrollRate( 5, 5 ); if (m_mode_region) diff --git a/src/slic3r/GUI/PartSkipDialog.cpp b/src/slic3r/GUI/PartSkipDialog.cpp index 9bd6687757..860779ead4 100644 --- a/src/slic3r/GUI/PartSkipDialog.cpp +++ b/src/slic3r/GUI/PartSkipDialog.cpp @@ -148,7 +148,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _ m_line->SetBackgroundColour(wxColor(238, 238, 238)); m_list_view = new wxScrolledWindow(m_book_third_panel, wxID_ANY, wxDefaultPosition, wxSize(267, -1), wxHSCROLL | wxVSCROLL); - m_list_view->SetScrollRate(5, 5); + m_list_view->SetScrollRate(5, FromDIP(30)); m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378))); m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378))); m_list_view->SetBackgroundColour(*wxWHITE); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 607ec64174..bdb8340edc 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3049,8 +3049,7 @@ Sidebar::Sidebar(Plater *parent) // add filament content p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); - p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); - p->m_panel_filament_content->SetScrollRate(0, 5); + p->m_panel_filament_content->SetScrollRate(0, FromDIP(20)); //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); @@ -3142,8 +3141,7 @@ Sidebar::Sidebar(Plater *parent) // 3) Mixed filament rows, in their own scroll area so a long mixed list does not // push the physical filament list off screen. p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); - p->m_mixed_scroll_area->SetScrollRate(0, 5); + p->m_mixed_scroll_area->SetScrollRate(0, FromDIP(20)); p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); { auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index fd5012b5ef..055449c35d 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1475,7 +1475,7 @@ void PreferencesDialog::create() app_config = get_app_config(); m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_parent->SetScrollRate(5, 5); + m_parent->SetScrollRate(0, FromDIP(20)); m_parent->SetBackgroundColour(*wxWHITE); m_sizer_body = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/PrintOptionsDialog.cpp b/src/slic3r/GUI/PrintOptionsDialog.cpp index 349e75fa06..d68bd24ccc 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.cpp +++ b/src/slic3r/GUI/PrintOptionsDialog.cpp @@ -32,7 +32,7 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent) m_scrollwindow = new wxScrolledWindow(this, wxID_ANY); - m_scrollwindow->SetScrollRate(0, FromDIP(10)); + m_scrollwindow->SetScrollRate(0, FromDIP(20)); m_scrollwindow->SetBackgroundColour(*wxWHITE); m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord)); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index bd5c045053..d231857f2c 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -1225,7 +1225,7 @@ size_t PublishSettingsDialog::category_index_for( } category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - category.scroll->SetScrollRate(0, 10); + category.scroll->SetScrollRate(0, FromDIP(20)); category.scroll->SetBackgroundColour(GetBackgroundColour()); category.list_sizer = new wxBoxSizer(wxVERTICAL); category.scroll->SetSizer(category.list_sizer); diff --git a/src/slic3r/GUI/ReleaseNote.cpp b/src/slic3r/GUI/ReleaseNote.cpp index 029310faf9..c8338dc79a 100644 --- a/src/slic3r/GUI/ReleaseNote.cpp +++ b/src/slic3r/GUI/ReleaseNote.cpp @@ -76,7 +76,7 @@ ReleaseNoteDialog::ReleaseNoteDialog(Plater *plater /*= nullptr*/) m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL); - m_vebview_release_note->SetScrollRate(5, 5); + m_vebview_release_note->SetScrollRate(5, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_vebview_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); @@ -142,7 +142,7 @@ UpdatePluginDialog::UpdatePluginDialog(wxWindow* parent /*= nullptr*/) operation_tips->SetMaxSize(wxSize(FromDIP(260), -1)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(5, 5); + m_vebview_release_note->SetScrollRate(5, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150))); m_vebview_release_note->SetMaxSize(wxSize(FromDIP(260), FromDIP(150))); @@ -277,7 +277,7 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent) m_simplebook_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_scrollwindows_release_note = new wxScrolledWindow(m_simplebook_release_note, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL); - m_scrollwindows_release_note->SetScrollRate(5, 5); + m_scrollwindows_release_note->SetScrollRate(5, FromDIP(20)); m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); //webview @@ -538,7 +538,7 @@ SecondaryCheckDialog::SecondaryCheckDialog(wxWindow* parent, wxWindowID id, cons m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(0, 5); + m_vebview_release_note->SetScrollRate(0, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380))); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); @@ -823,7 +823,7 @@ PrintErrorDialog::PrintErrorDialog(wxWindow* parent, wxWindowID id, const wxStri m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(5)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(0, 5); + m_vebview_release_note->SetScrollRate(0, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250))); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); @@ -1122,7 +1122,7 @@ ConfirmBeforeSendDialog::ConfirmBeforeSendDialog(wxWindow* parent, wxWindowID id m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_vebview_release_note->SetScrollRate(0, 5); + m_vebview_release_note->SetScrollRate(0, FromDIP(20)); m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380))); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); diff --git a/src/slic3r/GUI/SafetyOptionsDialog.cpp b/src/slic3r/GUI/SafetyOptionsDialog.cpp index 6536666bb1..c633d2778e 100644 --- a/src/slic3r/GUI/SafetyOptionsDialog.cpp +++ b/src/slic3r/GUI/SafetyOptionsDialog.cpp @@ -31,7 +31,7 @@ SafetyOptionsDialog::SafetyOptionsDialog(wxWindow* parent) SetSize(FromDIP(480),FromDIP(320)); m_scrollwindow = new wxScrolledWindow(this, wxID_ANY); - m_scrollwindow->SetScrollRate(0, FromDIP(10)); + m_scrollwindow->SetScrollRate(0, FromDIP(20)); m_scrollwindow->SetBackgroundColour(*wxWHITE); m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord)); @@ -272,4 +272,4 @@ void SafetyOptionsDialog::show_idel_heating_toast(const wxString &text) m_idel_heating_toast_timer.StartOnce(3000); } -}} // namespace Slic3r::GUI \ No newline at end of file +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index a9767d6c72..a3b83bd643 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -148,7 +148,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL); m_scroll_area = new wxScrolledWindow(this); - m_scroll_area->SetScrollRate(0, 20); + m_scroll_area->SetScrollRate(0, FromDIP(20)); m_scroll_area->SetBackgroundColour(m_colour_def_color); m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600))); m_scroll_area->SetMaxSize(wxSize(FromDIP(700), FromDIP(600))); @@ -707,7 +707,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater) //show bind failed info m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)), wxVSCROLL); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_print_failed_info->SetScrollRate(0, 5); + m_sw_print_failed_info->SetScrollRate(0, FromDIP(20)); m_sw_print_failed_info->SetMinSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125))); diff --git a/src/slic3r/GUI/SelectMachinePop.cpp b/src/slic3r/GUI/SelectMachinePop.cpp index 9b34e74f23..0046fd5f7f 100644 --- a/src/slic3r/GUI/SelectMachinePop.cpp +++ b/src/slic3r/GUI/SelectMachinePop.cpp @@ -327,7 +327,7 @@ SelectMachinePopup::SelectMachinePopup(wxWindow *parent) m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE); - m_scrolledWindow->SetScrollRate(0, 5); + m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y); auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL); m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow); m_scrolledWindow->Layout(); diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index cafbb360e6..b386d1f451 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -278,7 +278,7 @@ SendMultiMachinePage::SendMultiMachinePage(Plater* plater) m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_main_scroll->SetBackgroundColour(*wxWHITE); - m_main_scroll->SetScrollRate(5, 5); + m_main_scroll->SetScrollRate(5, FromDIP(20)); m_sizer_body = new wxBoxSizer(wxVERTICAL); m_main_page = create_page(); @@ -1353,7 +1353,7 @@ wxPanel* SendMultiMachinePage::create_page() scroll_macine_list = new wxScrolledWindow(main_page, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(800), FromDIP(300)), wxHSCROLL | wxVSCROLL); scroll_macine_list->SetBackgroundColour(*wxWHITE); - scroll_macine_list->SetScrollRate(5, 5); + scroll_macine_list->SetScrollRate(5, FromDIP(SEND_ITEM_MAX_HEIGHT)); scroll_macine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT))); scroll_macine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT))); diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index c4db0423ab..9071c0affd 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -384,7 +384,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater) //show bind failed info m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); - m_sw_print_failed_info->SetScrollRate(0, 5); + m_sw_print_failed_info->SetScrollRate(0, FromDIP(20)); m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125))); diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 218371f0b5..0b6e4b6577 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -1302,7 +1302,7 @@ void PrintingTaskPanel::set_star_count(int star_count) StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) : wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL) { - this->SetScrollRate(25, 25); + SetScrollRate(25, FromDIP(20)); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return; obj = dev->get_selected_machine(); diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 7f075dbbfa..19b1ddc568 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -700,7 +700,7 @@ SyncAmsInfoDialog::SyncAmsInfoDialog(wxWindow *parent, SyncInfo &info) : //wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow->SetBackgroundColour(*wxWHITE); - m_scrolledWindow->SetScrollRate(0, 20); + m_scrolledWindow->SetScrollRate(0, FromDIP(20)); m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX)); m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX)); m_scrolledWindow->EnableScrolling(false,true); diff --git a/src/slic3r/GUI/TextureImportDialog.cpp b/src/slic3r/GUI/TextureImportDialog.cpp index 2e5c1145e1..7296a1ebeb 100644 --- a/src/slic3r/GUI/TextureImportDialog.cpp +++ b/src/slic3r/GUI/TextureImportDialog.cpp @@ -534,7 +534,7 @@ public: m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_content->SetBackgroundColour(pop_bg); - m_content->SetScrollRate(0, FromDIP(5)); + m_content->SetScrollRate(0, FromDIP(20)); auto* outer = new wxBoxSizer(wxVERTICAL); const int pop_w = std::max(FromDIP(213), popup_width); @@ -2274,7 +2274,7 @@ void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer) m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(300))); - m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + m_mapping_scroll->SetScrollRate(0, FromDIP(20)); m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); @@ -4335,7 +4335,7 @@ void TextureImportDialog::on_dpi_changed(const wxRect&) if (m_mapping_scroll) { m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300))); - m_mapping_scroll->SetScrollRate(0, FromDIP(10)); + m_mapping_scroll->SetScrollRate(0, FromDIP(20)); } if (m_btn_skip) { diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index db64bdcc99..df809396f5 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -942,7 +942,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_ m_sizer_tab->Add(m_table_top, 1, 0, 0); m_scrolledWindow = new wxScrolledWindow(m_panel_tab, wxID_ANY, wxDefaultPosition, UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE, wxNO_BORDER|wxVSCROLL); - m_scrolledWindow->SetScrollRate(0, 5); + m_scrolledWindow->SetScrollRate(0, FromDIP(20)); m_scrolledWindow->SetBackgroundColour(GREY200); m_sizer_bottom = new wxBoxSizer(wxVERTICAL); m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0); diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index 0710e054f7..42383cd177 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -130,7 +130,7 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_ m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); auto m_scrollwindw_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)),wxVSCROLL); - m_scrollwindw_release_note->SetScrollRate(0, 5); + m_scrollwindw_release_note->SetScrollRate(0, FromDIP(20)); m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL); diff --git a/src/slic3r/GUI/UpgradePanel.cpp b/src/slic3r/GUI/UpgradePanel.cpp index d31fde8613..5f168025ed 100644 --- a/src/slic3r/GUI/UpgradePanel.cpp +++ b/src/slic3r/GUI/UpgradePanel.cpp @@ -1629,7 +1629,7 @@ UpgradePanel::UpgradePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, auto m_main_sizer = new wxBoxSizer(wxVERTICAL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_scrolledWindow->SetScrollRate(5, 25); + m_scrolledWindow->SetScrollRate(5, FromDIP(20)); m_machine_list_sizer = new wxBoxSizer(wxVERTICAL); diff --git a/src/slic3r/GUI/Widgets/CheckList.cpp b/src/slic3r/GUI/Widgets/CheckList.cpp index c0101441ff..ed404c62fe 100644 --- a/src/slic3r/GUI/Widgets/CheckList.cpp +++ b/src/slic3r/GUI/Widgets/CheckList.cpp @@ -62,7 +62,7 @@ CheckList::CheckList( s_sizer = new wxBoxSizer(wxVERTICAL); m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style); - m_scroll_area->SetScrollRate(0, 10); + m_scroll_area->SetScrollRate(0, FromDIP(20)); m_scroll_area->SetSizer(s_sizer); m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour()); m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this); @@ -223,4 +223,4 @@ void CheckList::ShowMenu(wxMouseEvent &evt) wxPoint screen_pos = src->ClientToScreen(evt.GetPosition()); wxPoint local_pos = ScreenToClient(screen_pos); PopupMenu(&m, local_pos); -} \ No newline at end of file +} diff --git a/src/slic3r/GUI/Widgets/ProgressDialog.cpp b/src/slic3r/GUI/Widgets/ProgressDialog.cpp index c16f3bcf1f..6d42df1007 100644 --- a/src/slic3r/GUI/Widgets/ProgressDialog.cpp +++ b/src/slic3r/GUI/Widgets/ProgressDialog.cpp @@ -199,7 +199,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int m_sizer_main->Add(m_simplebook, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28)); } else { m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL ); - m_msg_scrolledWindow->SetScrollRate(0,5); + m_msg_scrolledWindow->SetScrollRate(0, FromDIP(20)); wxBoxSizer* m_msg_sizer= new wxBoxSizer(wxVERTICAL); m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0); diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 88e2df31c0..0857612ac3 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -1222,7 +1222,7 @@ ImageTransientPopup::ImageTransientPopup( wxWindow *parent, bool scrolled, wxBit m_panel->SetSize(300, 300); // And also actually enable them. - m_panel->SetScrollRate(10, 10); + m_panel->SetScrollRate(10, FromDIP(20)); } else { From ea471dc82de3ba540da1c16ad4cfddc8caf58f8c Mon Sep 17 00:00:00 2001 From: Chris Bennight Date: Sun, 27 Sep 2026 11:53:11 -0400 Subject: [PATCH 07/38] Fix model initialization without a wx application (#15866) Guard the smooth-normals preference read when CLI thumbnail generation initializes geometry without a wx application. Use the existing flat-normal path in that case and preserve GUI preferences. --- src/slic3r/GUI/GLModel.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 3dd15c3fdd..92296030e1 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -456,8 +456,9 @@ void GLModel::init_from(const indexed_triangle_set& its) data.reserve_vertices(3 * its.indices.size()); data.reserve_indices(3 * its.indices.size()); - // Read user preference: smooth normals enabled - const bool smooth_normals_enabled = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS); + // CLI thumbnail generation can initialize models without a wx application. + const bool smooth_normals_enabled = wxApp::GetInstance() != nullptr && wxGetApp().app_config != nullptr && + wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS); if (smooth_normals_enabled) { // Use per-corner smooth normals (via IGL) From 4964f49765f7963b9ee4b016fe5a3bc0581e1911 Mon Sep 17 00:00:00 2001 From: Kenneth Raplee <101818165+kenrap@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:21:03 +0000 Subject: [PATCH 08/38] Fix size_t/%d format mismatch in MsgDialog::add_button (#15852) Fix size_t/%d mismatch in MsgDialog button keys `m_buttons.size()` is a `size_t`, which does not match the `%d` conversion in printf-style variadics. Build the key with `std::to_string` instead; exact for any size_t, no behavior change for realistic counts. Co-authored-by: yw4z --- src/slic3r/GUI/MsgDialog.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 0f1d3f62d5..8c981a970f 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -194,9 +194,10 @@ Button* MsgDialog::add_button(wxWindowID btn_id, bool set_focus /*= false*/, con bd->button = btn; //bd->type = type; - mb->id = wxString::Format("%d", m_buttons.size()); + // m_buttons.size() is a size_t, which does not match the %d conversion + mb->id = wxString(std::to_string(m_buttons.size())); mb->buttondata = bd; - m_buttons[ wxString::Format("%d", m_buttons.size())] = mb; + m_buttons[ wxString(std::to_string(m_buttons.size()))] = mb; return btn; }; From 5298e49dd270e09077540000d779292b1e7d609f Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Sun, 27 Sep 2026 17:08:27 -0500 Subject: [PATCH 09/38] fix: Klipper/Moonraker upload errors show a raw Python traceback (#14841) --- src/slic3r/Utils/PrintHost.cpp | 60 +++++++- tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_printhost.cpp | 213 +++++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 tests/slic3rutils/test_printhost.cpp diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 04709fef4c..f3333c731d 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -115,10 +116,67 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config) return webui_url; } +namespace { + +// Moonraker (Klipper's API server) reports a raised exception as { "error": { "code", "message", "traceback" } } +// under every host type that connects to it, often with the cause only in the traceback. Returns the reason to show, +// or empty for any other body. +std::string moonraker_error_reason(const std::string &body) +{ + const auto root = nlohmann::json::parse(body, nullptr, false); + const auto err = root.find("error"); + if (err == root.end()) + return {}; + const auto message = err->find("message"); + const auto traceback = err->find("traceback"); + if (message == err->end() || traceback == err->end() || !message->is_string() || !traceback->is_string()) + return {}; + + const auto &msg = message->get_ref(); + const auto &tb = traceback->get_ref(); + if (msg.empty()) + return {}; + const auto end = tb.find_last_not_of(" \t\r\n"); + if (end == std::string::npos) + return msg; + + // Chained exceptions each start a new traceback; the one that failed the request is the last. + const auto header = tb.rfind("Traceback (most recent call last):", end); + + // Tornado renders a raised HTTPError as "HTTP : [ ()]", and the detail may span lines. + const auto code = err->find("code"); + if (code != err->end() && code->is_number_integer()) { + const std::string marker = "HTTP " + std::to_string(code->get()) + ": "; + const auto pos = tb.rfind(marker, end); + if (pos != std::string::npos && (header == std::string::npos || pos > header) && pos + marker.size() <= end) { + const std::string reason = tb.substr(pos + marker.size(), end + 1 - pos - marker.size()); + // An HTTPError whose detail equals its reason, like HTTPError(401, "Unauthorized"), renders the phrase twice. + return reason == msg + " (" + msg + ")" ? msg : reason; + } + } + + // Any other exception's type and message are everything from the first unindented line after its frames. + auto begin = (header == std::string::npos) ? std::string::npos : tb.find('\n', header); + while (begin != std::string::npos && begin < end) { + ++begin; + if (tb[begin] != ' ' && tb[begin] != '\r' && tb[begin] != '\n') + break; + begin = tb.find('\n', begin); + } + if (begin == std::string::npos || begin > end) { + const auto nl = tb.rfind('\n', end); + begin = (nl == std::string::npos) ? 0 : nl + 1; + } + return msg + " (" + tb.substr(begin, end + 1 - begin) + ")"; +} + +} // namespace + wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const { if (status != 0) { - auto wxbody = wxString::FromUTF8(body.data()); + const std::string reason = moonraker_error_reason(body); + auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason); return wxString::Format("HTTP %u: %s", status, wxbody); } else { if (error.find("curl:Timeout was reached") != std::string::npos) { diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 08f1a6dd8f..7f541f3701 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests test_plugin_install.cpp test_plugin_lifecycle.cpp test_plugin_printer_agent.cpp + test_printhost.cpp test_slicing_pipeline_bindings.cpp test_slicing_pipeline_config.cpp test_plugin_sort.cpp diff --git a/tests/slic3rutils/test_printhost.cpp b/tests/slic3rutils/test_printhost.cpp new file mode 100644 index 0000000000..557d4a5c89 --- /dev/null +++ b/tests/slic3rutils/test_printhost.cpp @@ -0,0 +1,213 @@ +#include + +#include + +#include "slic3r/Utils/PrintHost.hpp" + +using namespace Slic3r; + +namespace { + +class TestPrintHost : public PrintHost +{ +public: + using PrintHost::format_error; + + const char* get_name() const override { return "Test"; } + bool test(wxString&) const override { return true; } + wxString get_test_ok_msg() const override { return {}; } + wxString get_test_failed_msg(wxString&) const override { return {}; } + bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; } + bool has_auto_discovery() const override { return false; } + bool can_test() const override { return false; } + PrintHostPostUploadActions get_post_upload_actions() const override { return {}; } + std::string get_host() const override { return {}; } +}; + +std::string format_error(const std::string& body, const std::string& error, unsigned status) +{ + return TestPrintHost().format_error(body, error, status).ToStdString(); +} + +std::string envelope(int code, const std::string& message, const std::string& traceback) +{ + return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump(); +} + +std::string moonraker_error(int code, const std::string& message, const std::string& detail = {}) +{ + std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message; + if (!detail.empty()) + line += " (" + detail + ")"; + return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n"); +} + +// A real Moonraker body for uploading a file that is being printed. +constexpr const char* k_busy_file_403 = + R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON"; + +} // namespace + +TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]") +{ + const std::string msg = format_error(k_busy_file_403, "", 403); + INFO("actual: " << msg); + + CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)"); + CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback")); + CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py")); +} + +TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]") +{ + SECTION("a plain detail") + { + const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted"); + CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)"); + } + + SECTION("a detail whose own parentheses nest (a filename)") + { + const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)"; + const std::string body = moonraker_error(400, "Bad Request", detail); + CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")"); + } + + SECTION("a detail that contains the reason phrase") + { + const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied"); + CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)"); + } + + SECTION("a detail that spans lines") + { + const std::string detail = "Move out of range\nX=250.000 Y=10.000"; + const std::string body = moonraker_error(400, "Bad Request", detail); + CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")"); + } + + SECTION("a detail that only repeats the reason phrase is dropped") + { + const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized"); + CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized"); + } +} + +TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]") +{ + const std::string frame = "Traceback (most recent call last):\n" + " File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n" + " self._write(data)\n"; + + SECTION("a one-line message") + { + const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n"); + CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)"); + } + + SECTION("a message that spans lines") + { + const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n"); + CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)"); + } + + SECTION("raised while handling an HTTPError with the same code") + { + const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n" + "During handling of the above exception, another exception occurred:\n\n" + + frame + "OSError: [Errno 5] Input/output error\n"; + const std::string body = envelope(500, "Internal Server Error", traceback); + CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)"); + } + + SECTION("a traceback with no header") + { + const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error"); + CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)"); + } +} + +TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]") +{ + SECTION("message is the whole reason, no trailing detail") + { + const std::string body = moonraker_error(503, "Klippy is not ready"); + CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready"); + } + + SECTION("a message that itself contains parentheses is not duplicated") + { + const std::string reason = "Requested blocks (0-5) are unavailable"; + const std::string body = moonraker_error(400, reason); + CHECK(format_error(body, "", 400) == "HTTP 400: " + reason); + } +} + +TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]") +{ + struct Case + { + const char* name; + const char* body; + unsigned status; + const char* expected; + }; + + const auto c = GENERATE( + Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500, + "HTTP 500: Internal Server Error"}, + Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON", + 500, "HTTP 500: Internal Server Error"}); + + DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); } +} + +TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]") +{ + const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full"); + CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)"); +} + +TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]") +{ + SECTION("OctoPrint's string-valued error member") + { + const std::string body = R"JSON({"error": "File not found"})JSON"; + CHECK(format_error(body, "", 404) == "HTTP 404: " + body); + } + + SECTION("PrusaLink's top-level message, not under error") + { + const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON"; + CHECK(format_error(body, "", 409) == "HTTP 409: " + body); + } + + SECTION("a body that is not JSON") + { + const std::string html = "502 Bad Gateway"; + CHECK(format_error(html, "", 502) == "HTTP 502: " + html); + } + + SECTION("an error object with no traceback") + { + const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON"; + CHECK(format_error(body, "", 500) == "HTTP 500: " + body); + } + + SECTION("an error object whose traceback is null") + { + const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON"; + CHECK(format_error(body, "", 500) == "HTTP 500: " + body); + } + + SECTION("an envelope whose reason phrase is empty") + { + const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n"); + CHECK(format_error(body, "", 403) == "HTTP 403: " + body); + } + + SECTION("a transport error with no HTTP status") + { + CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect"); + } +} From cda158857870b79492abd176a3e48861cce97e05 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:36:08 +0800 Subject: [PATCH 10/38] Draw the Toolpaths Top-Down When the Camera Looks Down on the Print (#15883) The segments come in print order, bottom layer first, which seen from above is back to front: every hidden fragment is shaded before the one that covers it, and on an integrated GPU that overdraw is most of the frame. Drawing the instances last to first whenever the camera looks down lets the depth test reject the hidden fragments instead. Side views and views from below keep the print order, and the shadow-caster pass is unchanged. --- src/libvgcode/src/Shaders.hpp | 7 ++++++- src/libvgcode/src/ViewerImpl.cpp | 11 +++++++++++ src/libvgcode/src/ViewerImpl.hpp | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/libvgcode/src/Shaders.hpp b/src/libvgcode/src/Shaders.hpp index 596b42df5c..9f546d6dc8 100644 --- a/src/libvgcode/src/Shaders.hpp +++ b/src/libvgcode/src/Shaders.hpp @@ -34,6 +34,10 @@ static const char* Segments_Vertex_Shader = // ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not // world_position, so the caster would write a depth the receiver never looks up. "uniform float bias_scale;\n" +// draw the instances last to first, top layers before the ones they hide, so that early depth +// rejection discards most of the hidden fragments; set when the camera looks down on the print +"uniform int reverse_order;\n" +"uniform int instance_count;\n" "in int vertex_id;\n" "out vec3 color;\n" "// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n" @@ -59,7 +63,8 @@ static const char* Segments_Vertex_Shader = " return top_diffuse + front_diffuse + top_specular;\n" "}\n" "void main() {\n" -" int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n" +" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n" +" int id_a = int(texelFetch(segment_index_tex, instance).r);\n" " int id_b = id_a + 1;\n" " vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n" " vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n" diff --git a/src/libvgcode/src/ViewerImpl.cpp b/src/libvgcode/src/ViewerImpl.cpp index b1a7550b42..8212717931 100644 --- a/src/libvgcode/src/ViewerImpl.cpp +++ b/src/libvgcode/src/ViewerImpl.cpp @@ -763,6 +763,8 @@ void ViewerImpl::init(const std::string& opengl_context_version) m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex"); m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex"); m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex"); + m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order"); + m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count"); // ORCA: realistic view m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map"); m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp"); @@ -2090,6 +2092,15 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data())); glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data())); glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data())); + // The segments come in print order, bottom layer first. Seen from above, that is back to front, + // and every hidden fragment is shaded before the one that covers it. Drawing them last to first + // lets the depth test reject the hidden ones instead. The camera looks down when the world's + // up axis points towards it, which is the view matrix's (2, 2) entry being positive. + const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f; + glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0)); +#ifndef ENABLE_OPENGL_ES + glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast(m_enabled_segments_count))); +#endif // ENABLE_OPENGL_ES // ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades // with the lookup off. glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit)); diff --git a/src/libvgcode/src/ViewerImpl.hpp b/src/libvgcode/src/ViewerImpl.hpp index c9d658beb1..707f0f6bb7 100644 --- a/src/libvgcode/src/ViewerImpl.hpp +++ b/src/libvgcode/src/ViewerImpl.hpp @@ -362,6 +362,8 @@ private: int m_uni_segments_height_width_angle_tex_id{ -1 }; int m_uni_segments_colors_tex_id{ -1 }; int m_uni_segments_segment_index_tex_id{ -1 }; + int m_uni_segments_reverse_order_id{ -1 }; + int m_uni_segments_instance_count_id{ -1 }; int m_uni_segments_shadow_map_id{ -1 }; int m_uni_segments_shadow_light_vp_id{ -1 }; int m_uni_segments_shadow_intensity_id{ -1 }; From 3df9c215f8a19232388710bed2af501bc068822d Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Mon, 28 Sep 2026 10:16:55 +0100 Subject: [PATCH 11/38] Blocks: branded filaments, naming/loading fixes, RF50 bed type and other fixes. (#15804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR fixes some problems with the current profiles for Blocks printers - **Fix**: start gcode for the Blocks 0.6mm RF50 printer where the start gcode command had no newline separation on two commands so they were concatenated and would make printing fail. - **Added**: multi material plates selection for the RF50 printers, right now only textured PEI and smooth high temp plates are available, but in the future more to come. - **Naming consistancy** for Blocks Pro S100 processes and filaments, target labels used bare `@Blocks` while RD50 and RF50 used `@Blocks_RD50` and `@Blocks_RF50`. Renamed Pro S100 to `@Blocks_Pro_S100` to match the structure. - **Filament restructure**: replaced Blocks tuned-generic filaments (e.g. `Generic PLA @Blocks`) with real Blocks-branded filaments. One for each printer model. Every material keeps the same coverage we already had. - **Fix**: `sparse_infill_pattern` was misspelled `sparse_infill_patter` on 13 `@Blocks_RF50` process presets. Harmless due to inheritances but fixed non the less. - **Fix**: `Blocks RD50 V2 0.4 nozzle.json` was missing `printer_variant`. - **Normalize** all Blocks profiles to standard formatting (tab indentation, key order) and bumped `Blocks.json` version. - **RF50 retraction length** increased on all RF50 machines. ## Tests - [x] `orca_profile_tool.py check --vendor Blocks` - clean - [x] `orca_profile_tool.py check` - clean - [x] `normalize` / `generate-id` / `update-index --dry-run` — fully settled, no pending changes - [x] `check_profile.sh --vendor Blocks` → `profile_tool` check — passes - [ ] `validate_system` / `validate_slice` / `validate_filament_subtypes` / `validate_custom` — could not run locally (dev host's glibc 2.36 is older than the nightly validator binary's required 2.38); needs CI or a compatible host - [x] In-app confirmation that the Blocks bundle loads [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts) --- resources/profiles/Blocks.json | 210 +++++++++++++----- .../filament/Blocks ABS @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks ABS @Blocks_RF50.json | 13 ++ ...ABS @Blocks.json => Blocks ABS @base.json} | 19 +- .../filament/Blocks ASA @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks ASA @Blocks_RF50.json | 13 ++ ...ASA @Blocks.json => Blocks ASA @base.json} | 19 +- .../Blocks ASA-CF @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks ASA-CF @Blocks_RF50.json | 13 ++ ... @Blocks.json => Blocks ASA-CF @base.json} | 19 +- .../filament/Blocks PA @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks PA @Blocks_RF50.json | 13 ++ .../Blocks/filament/Blocks PA @base.json | 17 ++ .../Blocks PA-CF @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks PA-CF @Blocks_RF50.json | 13 ++ ...F @Blocks.json => Blocks PA-CF @base.json} | 19 +- .../filament/Blocks PC @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks PC @Blocks_RF50.json | 13 ++ .../Blocks/filament/Blocks PC @base.json | 17 ++ .../Blocks PETG @Blocks_Pro_S100.json | 16 ++ .../filament/Blocks PETG @Blocks_RD50_V2.json | 13 ++ .../filament/Blocks PETG @Blocks_RF50.json | 13 ++ ...TG @Blocks.json => Blocks PETG @base.json} | 24 +- .../filament/Blocks PLA @Blocks_Pro_S100.json | 16 ++ .../filament/Blocks PLA @Blocks_RD50_V2.json | 13 ++ .../filament/Blocks PLA @Blocks_RF50.json | 13 ++ ...PLA @Blocks.json => Blocks PLA @base.json} | 24 +- .../Blocks PLA-CF @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks PLA-CF @Blocks_RF50.json | 13 ++ .../Blocks/filament/Blocks PLA-CF @base.json | 23 ++ .../filament/Blocks PVA @Blocks_RD50_V2.json | 14 ++ .../filament/Blocks PVA @Blocks_RF50.json | 13 ++ .../Blocks/filament/Blocks PVA @base.json | 17 ++ .../filament/Blocks TPU @Blocks_Pro_S100.json | 16 ++ .../filament/Blocks TPU @Blocks_RD50_V2.json | 13 ++ .../filament/Blocks TPU @Blocks_RF50.json | 13 ++ ...TPU @Blocks.json => Blocks TPU @base.json} | 24 +- .../Blocks/filament/Generic PA @Blocks.json | 24 -- .../Blocks/filament/Generic PC @Blocks.json | 24 -- .../filament/Generic PLA-CF @Blocks.json | 30 --- .../Blocks/filament/Generic PVA @Blocks.json | 24 -- .../machine/BLOCKS Pro S100 0.4 nozzle.json | 5 +- .../machine/BLOCKS Pro S100 0.6 nozzle.json | 7 +- .../machine/BLOCKS Pro S100 0.8 nozzle.json | 7 +- .../machine/BLOCKS Pro S100 1.0 nozzle.json | 7 +- .../machine/BLOCKS Pro S100 1.2 nozzle.json | 7 +- .../Blocks/machine/BLOCKS Pro S100.json | 2 +- .../machine/BLOCKS RD50 V2 0.4 nozzle.json | 4 + .../machine/BLOCKS RD50 V2 0.6 nozzle.json | 5 +- .../machine/BLOCKS RD50 V2 0.8 nozzle.json | 5 +- .../Blocks/machine/BLOCKS RD50 V2.json | 2 +- .../machine/BLOCKS RF50 0.4 nozzle.json | 9 +- .../machine/BLOCKS RF50 0.6 nozzle.json | 13 +- .../machine/BLOCKS RF50 0.8 nozzle.json | 9 +- .../profiles/Blocks/machine/BLOCKS RF50.json | 3 +- .../Blocks/machine/fdm_klipper_common.json | 3 - .../0.12mm Fine 0.4 nozzle @Blocks_RF50.json | 6 +- ....16mm Optimal 0.4 nozzle @Blocks_RF50.json | 6 +- ....20mm Optimal 0.6 nozzle @Blocks_RF50.json | 6 +- ...Standard 0.4 nozzle @Blocks_Pro_S100.json} | 7 +- ...20mm Standard 0.4 nozzle @Blocks_RF50.json | 6 +- ...mm Draft 0.4 nozzle @Blocks_Pro_S100.json} | 7 +- .../0.24mm Draft 0.4 nozzle @Blocks_RF50.json | 6 +- ...26mm Standard 0.6 nozzle @Blocks_RF50.json | 6 +- ...m Extra Draft 0.4 nozzle @Blocks_RF50.json | 6 +- ...ra Draft 0.4 nozzle @Blocks_Pro_S100.json} | 7 +- ... Optimal 0.8 nozzle @Blocks_Pro_S100.json} | 7 +- ....30mm Optimal 0.8 nozzle @Blocks_RF50.json | 6 +- ... Optimal 1.0 nozzle @Blocks_Pro_S100.json} | 7 +- ...Standard 0.6 nozzle @Blocks_Pro_S100.json} | 7 +- .../0.32mm Draft 0.6 nozzle @Blocks_RF50.json | 6 +- ...m Extra Draft 0.6 nozzle @Blocks_RF50.json | 6 +- ...38mm Standard 0.8 nozzle @Blocks_RF50.json | 6 +- ...mm Draft 0.6 nozzle @Blocks_Pro_S100.json} | 7 +- ...Standard 0.8 nozzle @Blocks_Pro_S100.json} | 7 +- .../0.46mm Draft 0.8 nozzle @Blocks_RF50.json | 6 +- ...mm Draft 0.8 nozzle @Blocks_Pro_S100.json} | 7 +- ... Optimal 1.2 nozzle @Blocks_Pro_S100.json} | 7 +- ...Standard 1.0 nozzle @Blocks_Pro_S100.json} | 7 +- ...m Extra Draft 0.8 nozzle @Blocks_RF50.json | 6 +- ...mm Draft 1.0 nozzle @Blocks_Pro_S100.json} | 7 +- ...Standard 1.2 nozzle @Blocks_Pro_S100.json} | 7 +- ...mm Draft 1.2 nozzle @Blocks_Pro_S100.json} | 7 +- ...ra Draft 1.0 nozzle @Blocks_Pro_S100.json} | 7 +- ...ra Draft 1.2 nozzle @Blocks_Pro_S100.json} | 7 +- 85 files changed, 792 insertions(+), 362 deletions(-) create mode 100644 resources/profiles/Blocks/filament/Blocks ABS @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks ABS @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic ABS @Blocks.json => Blocks ABS @base.json} (75%) create mode 100644 resources/profiles/Blocks/filament/Blocks ASA @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks ASA @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic ASA @Blocks.json => Blocks ASA @base.json} (75%) create mode 100644 resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic ASA-CF @Blocks.json => Blocks ASA-CF @base.json} (75%) create mode 100644 resources/profiles/Blocks/filament/Blocks PA @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PA @Blocks_RF50.json create mode 100644 resources/profiles/Blocks/filament/Blocks PA @base.json create mode 100644 resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic PA-CF @Blocks.json => Blocks PA-CF @base.json} (76%) create mode 100644 resources/profiles/Blocks/filament/Blocks PC @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PC @Blocks_RF50.json create mode 100644 resources/profiles/Blocks/filament/Blocks PC @base.json create mode 100644 resources/profiles/Blocks/filament/Blocks PETG @Blocks_Pro_S100.json create mode 100644 resources/profiles/Blocks/filament/Blocks PETG @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PETG @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic PETG @Blocks.json => Blocks PETG @base.json} (68%) create mode 100644 resources/profiles/Blocks/filament/Blocks PLA @Blocks_Pro_S100.json create mode 100644 resources/profiles/Blocks/filament/Blocks PLA @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PLA @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic PLA @Blocks.json => Blocks PLA @base.json} (52%) create mode 100644 resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RF50.json create mode 100644 resources/profiles/Blocks/filament/Blocks PLA-CF @base.json create mode 100644 resources/profiles/Blocks/filament/Blocks PVA @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks PVA @Blocks_RF50.json create mode 100644 resources/profiles/Blocks/filament/Blocks PVA @base.json create mode 100644 resources/profiles/Blocks/filament/Blocks TPU @Blocks_Pro_S100.json create mode 100644 resources/profiles/Blocks/filament/Blocks TPU @Blocks_RD50_V2.json create mode 100644 resources/profiles/Blocks/filament/Blocks TPU @Blocks_RF50.json rename resources/profiles/Blocks/filament/{Generic TPU @Blocks.json => Blocks TPU @base.json} (58%) delete mode 100644 resources/profiles/Blocks/filament/Generic PA @Blocks.json delete mode 100644 resources/profiles/Blocks/filament/Generic PC @Blocks.json delete mode 100644 resources/profiles/Blocks/filament/Generic PLA-CF @Blocks.json delete mode 100644 resources/profiles/Blocks/filament/Generic PVA @Blocks.json rename resources/profiles/Blocks/process/{0.20mm Standard 0.4 nozzle @Blocks.json => 0.20mm Standard 0.4 nozzle @Blocks_Pro_S100.json} (62%) rename resources/profiles/Blocks/process/{0.24mm Draft 0.4 nozzle @Blocks.json => 0.24mm Draft 0.4 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.30mm Extra Draft 0.4 nozzle @Blocks.json => 0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100.json} (61%) rename resources/profiles/Blocks/process/{0.30mm Optimal 0.8 nozzle @Blocks.json => 0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.30mm Optimal 1.0 nozzle @Blocks.json => 0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.30mm Standard 0.6 nozzle @Blocks.json => 0.30mm Standard 0.6 nozzle @Blocks_Pro_S100.json} (62%) rename resources/profiles/Blocks/process/{0.40mm Draft 0.6 nozzle @Blocks.json => 0.40mm Draft 0.6 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.40mm Standard 0.8 nozzle @Blocks.json => 0.40mm Standard 0.8 nozzle @Blocks_Pro_S100.json} (62%) rename resources/profiles/Blocks/process/{0.50mm Draft 0.8 nozzle @Blocks.json => 0.50mm Draft 0.8 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.50mm Optimal 1.2 nozzle @Blocks.json => 0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.50mm Standard 1.0 nozzle @Blocks.json => 0.50mm Standard 1.0 nozzle @Blocks_Pro_S100.json} (62%) rename resources/profiles/Blocks/process/{0.60mm Draft 1.0 nozzle @Blocks.json => 0.60mm Draft 1.0 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.60mm Standard 1.2 nozzle @Blocks.json => 0.60mm Standard 1.2 nozzle @Blocks_Pro_S100.json} (62%) rename resources/profiles/Blocks/process/{0.70mm Draft 1.2 nozzle @Blocks.json => 0.70mm Draft 1.2 nozzle @Blocks_Pro_S100.json} (63%) rename resources/profiles/Blocks/process/{0.70mm Extra Draft 1.0 nozzle @Blocks.json => 0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100.json} (61%) rename resources/profiles/Blocks/process/{0.80mm Extra Draft 1.2 nozzle @Blocks.json => 0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100.json} (61%) diff --git a/resources/profiles/Blocks.json b/resources/profiles/Blocks.json index f24bcb81da..4c4936ab28 100644 --- a/resources/profiles/Blocks.json +++ b/resources/profiles/Blocks.json @@ -1,6 +1,6 @@ { "name": "Blocks", - "version": "02.04.00.04", + "version": "02.04.00.13", "force_update": "0", "description": "Blocks configurations", "machine_model_list": [ @@ -43,8 +43,8 @@ "sub_path": "process/0.16mm Optimal 0.4 nozzle @Blocks_RF50.json" }, { - "name": "0.20mm Standard 0.4 nozzle @Blocks", - "sub_path": "process/0.20mm Standard 0.4 nozzle @Blocks.json" + "name": "0.20mm Standard 0.4 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.20mm Standard 0.4 nozzle @Blocks_Pro_S100.json" }, { "name": "0.20mm Standard 0.4 nozzle @Blocks_RD50_V2", @@ -55,8 +55,8 @@ "sub_path": "process/0.20mm Standard 0.4 nozzle @Blocks_RF50.json" }, { - "name": "0.24mm Draft 0.4 nozzle @Blocks", - "sub_path": "process/0.24mm Draft 0.4 nozzle @Blocks.json" + "name": "0.24mm Draft 0.4 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.24mm Draft 0.4 nozzle @Blocks_Pro_S100.json" }, { "name": "0.24mm Draft 0.4 nozzle @Blocks_RD50_V2", @@ -75,8 +75,8 @@ "sub_path": "process/0.28mm Extra Draft 0.4 nozzle @Blocks_RF50.json" }, { - "name": "0.30mm Extra Draft 0.4 nozzle @Blocks", - "sub_path": "process/0.30mm Extra Draft 0.4 nozzle @Blocks.json" + "name": "0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100.json" }, { "name": "fdm_process_common 0.6 nozzle", @@ -111,8 +111,8 @@ "sub_path": "process/0.26mm Standard 0.6 nozzle @Blocks_RF50.json" }, { - "name": "0.30mm Standard 0.6 nozzle @Blocks", - "sub_path": "process/0.30mm Standard 0.6 nozzle @Blocks.json" + "name": "0.30mm Standard 0.6 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.30mm Standard 0.6 nozzle @Blocks_Pro_S100.json" }, { "name": "0.32mm Draft 0.6 nozzle @Blocks_RD50_V2", @@ -131,12 +131,12 @@ "sub_path": "process/0.38mm Extra Draft 0.6 nozzle @Blocks_RF50.json" }, { - "name": "0.40mm Draft 0.6 nozzle @Blocks", - "sub_path": "process/0.40mm Draft 0.6 nozzle @Blocks.json" + "name": "0.40mm Draft 0.6 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.40mm Draft 0.6 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.30mm Optimal 0.8 nozzle @Blocks", - "sub_path": "process/0.30mm Optimal 0.8 nozzle @Blocks.json" + "name": "0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100.json" }, { "name": "0.30mm Optimal 0.8 nozzle @Blocks_RD50_V2", @@ -155,8 +155,8 @@ "sub_path": "process/0.38mm Standard 0.8 nozzle @Blocks_RF50.json" }, { - "name": "0.40mm Standard 0.8 nozzle @Blocks", - "sub_path": "process/0.40mm Standard 0.8 nozzle @Blocks.json" + "name": "0.40mm Standard 0.8 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.40mm Standard 0.8 nozzle @Blocks_Pro_S100.json" }, { "name": "0.46mm Draft 0.8 nozzle @Blocks_RD50_V2", @@ -167,8 +167,8 @@ "sub_path": "process/0.46mm Draft 0.8 nozzle @Blocks_RF50.json" }, { - "name": "0.50mm Draft 0.8 nozzle @Blocks", - "sub_path": "process/0.50mm Draft 0.8 nozzle @Blocks.json" + "name": "0.50mm Draft 0.8 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.50mm Draft 0.8 nozzle @Blocks_Pro_S100.json" }, { "name": "0.54mm Extra Draft 0.8 nozzle @Blocks_RD50_V2", @@ -179,36 +179,36 @@ "sub_path": "process/0.54mm Extra Draft 0.8 nozzle @Blocks_RF50.json" }, { - "name": "0.30mm Optimal 1.0 nozzle @Blocks", - "sub_path": "process/0.30mm Optimal 1.0 nozzle @Blocks.json" + "name": "0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.50mm Standard 1.0 nozzle @Blocks", - "sub_path": "process/0.50mm Standard 1.0 nozzle @Blocks.json" + "name": "0.50mm Standard 1.0 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.50mm Standard 1.0 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.60mm Draft 1.0 nozzle @Blocks", - "sub_path": "process/0.60mm Draft 1.0 nozzle @Blocks.json" + "name": "0.60mm Draft 1.0 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.60mm Draft 1.0 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.70mm Extra Draft 1.0 nozzle @Blocks", - "sub_path": "process/0.70mm Extra Draft 1.0 nozzle @Blocks.json" + "name": "0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.50mm Optimal 1.2 nozzle @Blocks", - "sub_path": "process/0.50mm Optimal 1.2 nozzle @Blocks.json" + "name": "0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.60mm Standard 1.2 nozzle @Blocks", - "sub_path": "process/0.60mm Standard 1.2 nozzle @Blocks.json" + "name": "0.60mm Standard 1.2 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.60mm Standard 1.2 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.70mm Draft 1.2 nozzle @Blocks", - "sub_path": "process/0.70mm Draft 1.2 nozzle @Blocks.json" + "name": "0.70mm Draft 1.2 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.70mm Draft 1.2 nozzle @Blocks_Pro_S100.json" }, { - "name": "0.80mm Extra Draft 1.2 nozzle @Blocks", - "sub_path": "process/0.80mm Extra Draft 1.2 nozzle @Blocks.json" + "name": "0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100", + "sub_path": "process/0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100.json" } ], "filament_list": [ @@ -249,48 +249,148 @@ "sub_path": "filament/fdm_filament_tpu.json" }, { - "name": "Generic ABS @Blocks", - "sub_path": "filament/Generic ABS @Blocks.json" + "name": "Blocks ABS @base", + "sub_path": "filament/Blocks ABS @base.json" }, { - "name": "Generic ASA @Blocks", - "sub_path": "filament/Generic ASA @Blocks.json" + "name": "Blocks ASA @base", + "sub_path": "filament/Blocks ASA @base.json" }, { - "name": "Generic ASA-CF @Blocks", - "sub_path": "filament/Generic ASA-CF @Blocks.json" + "name": "Blocks ASA-CF @base", + "sub_path": "filament/Blocks ASA-CF @base.json" }, { - "name": "Generic PA @Blocks", - "sub_path": "filament/Generic PA @Blocks.json" + "name": "Blocks PA @base", + "sub_path": "filament/Blocks PA @base.json" }, { - "name": "Generic PA-CF @Blocks", - "sub_path": "filament/Generic PA-CF @Blocks.json" + "name": "Blocks PA-CF @base", + "sub_path": "filament/Blocks PA-CF @base.json" }, { - "name": "Generic PC @Blocks", - "sub_path": "filament/Generic PC @Blocks.json" + "name": "Blocks PC @base", + "sub_path": "filament/Blocks PC @base.json" }, { - "name": "Generic PETG @Blocks", - "sub_path": "filament/Generic PETG @Blocks.json" + "name": "Blocks PETG @base", + "sub_path": "filament/Blocks PETG @base.json" }, { - "name": "Generic PLA @Blocks", - "sub_path": "filament/Generic PLA @Blocks.json" + "name": "Blocks PLA @base", + "sub_path": "filament/Blocks PLA @base.json" }, { - "name": "Generic PLA-CF @Blocks", - "sub_path": "filament/Generic PLA-CF @Blocks.json" + "name": "Blocks PLA-CF @base", + "sub_path": "filament/Blocks PLA-CF @base.json" }, { - "name": "Generic PVA @Blocks", - "sub_path": "filament/Generic PVA @Blocks.json" + "name": "Blocks PVA @base", + "sub_path": "filament/Blocks PVA @base.json" }, { - "name": "Generic TPU @Blocks", - "sub_path": "filament/Generic TPU @Blocks.json" + "name": "Blocks TPU @base", + "sub_path": "filament/Blocks TPU @base.json" + }, + { + "name": "Blocks ABS @Blocks_RD50_V2", + "sub_path": "filament/Blocks ABS @Blocks_RD50_V2.json" + }, + { + "name": "Blocks ABS @Blocks_RF50", + "sub_path": "filament/Blocks ABS @Blocks_RF50.json" + }, + { + "name": "Blocks ASA @Blocks_RD50_V2", + "sub_path": "filament/Blocks ASA @Blocks_RD50_V2.json" + }, + { + "name": "Blocks ASA @Blocks_RF50", + "sub_path": "filament/Blocks ASA @Blocks_RF50.json" + }, + { + "name": "Blocks ASA-CF @Blocks_RD50_V2", + "sub_path": "filament/Blocks ASA-CF @Blocks_RD50_V2.json" + }, + { + "name": "Blocks ASA-CF @Blocks_RF50", + "sub_path": "filament/Blocks ASA-CF @Blocks_RF50.json" + }, + { + "name": "Blocks PA @Blocks_RD50_V2", + "sub_path": "filament/Blocks PA @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PA @Blocks_RF50", + "sub_path": "filament/Blocks PA @Blocks_RF50.json" + }, + { + "name": "Blocks PA-CF @Blocks_RD50_V2", + "sub_path": "filament/Blocks PA-CF @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PA-CF @Blocks_RF50", + "sub_path": "filament/Blocks PA-CF @Blocks_RF50.json" + }, + { + "name": "Blocks PC @Blocks_RD50_V2", + "sub_path": "filament/Blocks PC @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PC @Blocks_RF50", + "sub_path": "filament/Blocks PC @Blocks_RF50.json" + }, + { + "name": "Blocks PETG @Blocks_Pro_S100", + "sub_path": "filament/Blocks PETG @Blocks_Pro_S100.json" + }, + { + "name": "Blocks PETG @Blocks_RD50_V2", + "sub_path": "filament/Blocks PETG @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PETG @Blocks_RF50", + "sub_path": "filament/Blocks PETG @Blocks_RF50.json" + }, + { + "name": "Blocks PLA @Blocks_Pro_S100", + "sub_path": "filament/Blocks PLA @Blocks_Pro_S100.json" + }, + { + "name": "Blocks PLA @Blocks_RD50_V2", + "sub_path": "filament/Blocks PLA @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PLA @Blocks_RF50", + "sub_path": "filament/Blocks PLA @Blocks_RF50.json" + }, + { + "name": "Blocks PLA-CF @Blocks_RD50_V2", + "sub_path": "filament/Blocks PLA-CF @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PLA-CF @Blocks_RF50", + "sub_path": "filament/Blocks PLA-CF @Blocks_RF50.json" + }, + { + "name": "Blocks PVA @Blocks_RD50_V2", + "sub_path": "filament/Blocks PVA @Blocks_RD50_V2.json" + }, + { + "name": "Blocks PVA @Blocks_RF50", + "sub_path": "filament/Blocks PVA @Blocks_RF50.json" + }, + { + "name": "Blocks TPU @Blocks_Pro_S100", + "sub_path": "filament/Blocks TPU @Blocks_Pro_S100.json" + }, + { + "name": "Blocks TPU @Blocks_RD50_V2", + "sub_path": "filament/Blocks TPU @Blocks_RD50_V2.json" + }, + { + "name": "Blocks TPU @Blocks_RF50", + "sub_path": "filament/Blocks TPU @Blocks_RF50.json" } ], "machine_list": [ diff --git a/resources/profiles/Blocks/filament/Blocks ABS @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks ABS @Blocks_RD50_V2.json new file mode 100644 index 0000000000..7ce4729e96 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks ABS @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks ABS @Blocks_RD50_V2", + "inherits": "Blocks ABS @base", + "from": "system", + "setting_id": "qhQ0qQDGJgsueCAF", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic ABS;Generic ABS @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks ABS @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks ABS @Blocks_RF50.json new file mode 100644 index 0000000000..3f418a8d6b --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks ABS @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks ABS @Blocks_RF50", + "inherits": "Blocks ABS @base", + "from": "system", + "setting_id": "bGYMFFdKiROKuSiY", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic ABS @Blocks.json b/resources/profiles/Blocks/filament/Blocks ABS @base.json similarity index 75% rename from resources/profiles/Blocks/filament/Generic ABS @Blocks.json rename to resources/profiles/Blocks/filament/Blocks ABS @base.json index c3d09cfd18..e475770f64 100644 --- a/resources/profiles/Blocks/filament/Generic ABS @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks ABS @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic ABS @Blocks", + "name": "Blocks ABS @base", "inherits": "fdm_filament_abs", - "renamed_from": "Blocks Generic ABS", "from": "system", - "setting_id": "TL1Q1kA4t36GGObG", - "filament_id": "OFY9muEs", - "instantiation": "true", + "filament_id": "OFBBhiX8", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_flow_ratio": [ "0.95" ], @@ -81,13 +82,5 @@ ], "during_print_exhaust_fan_speed": [ "100" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Blocks ASA @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks ASA @Blocks_RD50_V2.json new file mode 100644 index 0000000000..bab020171f --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks ASA @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks ASA @Blocks_RD50_V2", + "inherits": "Blocks ASA @base", + "from": "system", + "setting_id": "HW7xDXThvpVFbMq5", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic ASA;Generic ASA @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks ASA @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks ASA @Blocks_RF50.json new file mode 100644 index 0000000000..603c2d7dfa --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks ASA @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks ASA @Blocks_RF50", + "inherits": "Blocks ASA @base", + "from": "system", + "setting_id": "lBW7SCsvawi4icNU", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic ASA @Blocks.json b/resources/profiles/Blocks/filament/Blocks ASA @base.json similarity index 75% rename from resources/profiles/Blocks/filament/Generic ASA @Blocks.json rename to resources/profiles/Blocks/filament/Blocks ASA @base.json index 0c1ee32085..8fed35b779 100644 --- a/resources/profiles/Blocks/filament/Generic ASA @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks ASA @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic ASA @Blocks", + "name": "Blocks ASA @base", "inherits": "fdm_filament_asa", - "renamed_from": "Blocks Generic ASA", "from": "system", - "setting_id": "xP7cB4RHgbm4cYDD", - "filament_id": "OFLPAxz3", - "instantiation": "true", + "filament_id": "OFTz2YXa", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_flow_ratio": [ "0.95" ], @@ -81,13 +82,5 @@ ], "during_print_exhaust_fan_speed": [ "100" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RD50_V2.json new file mode 100644 index 0000000000..bdfb288047 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks ASA-CF @Blocks_RD50_V2", + "inherits": "Blocks ASA-CF @base", + "from": "system", + "setting_id": "x5ksnGLGGy1PgDns", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic ASA-CF;Generic ASA-CF @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RF50.json new file mode 100644 index 0000000000..1304f52400 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks ASA-CF @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks ASA-CF @Blocks_RF50", + "inherits": "Blocks ASA-CF @base", + "from": "system", + "setting_id": "pcbd5qFiMZAvDPg1", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic ASA-CF @Blocks.json b/resources/profiles/Blocks/filament/Blocks ASA-CF @base.json similarity index 75% rename from resources/profiles/Blocks/filament/Generic ASA-CF @Blocks.json rename to resources/profiles/Blocks/filament/Blocks ASA-CF @base.json index 0d9f0aa862..fb1cdeb998 100644 --- a/resources/profiles/Blocks/filament/Generic ASA-CF @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks ASA-CF @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic ASA-CF @Blocks", + "name": "Blocks ASA-CF @base", "inherits": "fdm_filament_asa", - "renamed_from": "Blocks Generic ASA-CF", "from": "system", - "setting_id": "ju4ue7yJoz3ujny6", - "filament_id": "OF53eLVD", - "instantiation": "true", + "filament_id": "OF8b9I7o", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_type": [ "ASA-CF" ], @@ -84,13 +85,5 @@ ], "during_print_exhaust_fan_speed": [ "100" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Blocks PA @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PA @Blocks_RD50_V2.json new file mode 100644 index 0000000000..6486ec0ed8 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PA @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks PA @Blocks_RD50_V2", + "inherits": "Blocks PA @base", + "from": "system", + "setting_id": "gxD81n2jjrReLD29", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic PA;Generic PA @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PA @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PA @Blocks_RF50.json new file mode 100644 index 0000000000..6b11df1a22 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PA @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PA @Blocks_RF50", + "inherits": "Blocks PA @base", + "from": "system", + "setting_id": "PzKxRMDnQUzVWRer", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PA @base.json b/resources/profiles/Blocks/filament/Blocks PA @base.json new file mode 100644 index 0000000000..ff6ca4b599 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PA @base.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "name": "Blocks PA @base", + "inherits": "fdm_filament_pa", + "from": "system", + "filament_id": "OFidOgK4", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "10" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RD50_V2.json new file mode 100644 index 0000000000..1946ce16b6 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks PA-CF @Blocks_RD50_V2", + "inherits": "Blocks PA-CF @base", + "from": "system", + "setting_id": "EtbeKx3rbhUo1TtM", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic PA-CF;Generic PA-CF @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RF50.json new file mode 100644 index 0000000000..1b1e02f58e --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PA-CF @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PA-CF @Blocks_RF50", + "inherits": "Blocks PA-CF @base", + "from": "system", + "setting_id": "udI8Q4X7HaPIaPt4", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic PA-CF @Blocks.json b/resources/profiles/Blocks/filament/Blocks PA-CF @base.json similarity index 76% rename from resources/profiles/Blocks/filament/Generic PA-CF @Blocks.json rename to resources/profiles/Blocks/filament/Blocks PA-CF @base.json index 7ec957f6d1..360a090042 100644 --- a/resources/profiles/Blocks/filament/Generic PA-CF @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks PA-CF @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic PA-CF @Blocks", + "name": "Blocks PA-CF @base", "inherits": "fdm_filament_pa", - "renamed_from": "Blocks Generic PA-CF", "from": "system", - "setting_id": "T6SO0WRdnG6wcyG8", - "filament_id": "OFQLcbps", - "instantiation": "true", + "filament_id": "OFdWrXpL", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_type": [ "PA-CF" ], @@ -87,13 +88,5 @@ ], "during_print_exhaust_fan_speed": [ "80" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Blocks PC @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PC @Blocks_RD50_V2.json new file mode 100644 index 0000000000..cdeec3a9aa --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PC @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks PC @Blocks_RD50_V2", + "inherits": "Blocks PC @base", + "from": "system", + "setting_id": "HLc0shWnKwJzSFyW", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic PC;Generic PC @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PC @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PC @Blocks_RF50.json new file mode 100644 index 0000000000..ad23b4ba03 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PC @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PC @Blocks_RF50", + "inherits": "Blocks PC @base", + "from": "system", + "setting_id": "9a6bccZopMTeNHYu", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PC @base.json b/resources/profiles/Blocks/filament/Blocks PC @base.json new file mode 100644 index 0000000000..83b3d75300 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PC @base.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "name": "Blocks PC @base", + "inherits": "fdm_filament_pc", + "from": "system", + "filament_id": "OFlGY0MN", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], + "filament_flow_ratio": [ + "0.94" + ], + "filament_max_volumetric_speed": [ + "10" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PETG @Blocks_Pro_S100.json b/resources/profiles/Blocks/filament/Blocks PETG @Blocks_Pro_S100.json new file mode 100644 index 0000000000..3a6619c8ef --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PETG @Blocks_Pro_S100.json @@ -0,0 +1,16 @@ +{ + "type": "filament", + "name": "Blocks PETG @Blocks_Pro_S100", + "inherits": "Blocks PETG @base", + "from": "system", + "setting_id": "AhMR49fcFqtrQ6hI", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS Pro S100 0.4 nozzle", + "BLOCKS Pro S100 0.6 nozzle", + "BLOCKS Pro S100 0.8 nozzle", + "BLOCKS Pro S100 1.0 nozzle", + "BLOCKS Pro S100 1.2 nozzle" + ], + "renamed_from": "Blocks Generic PETG;Generic PETG @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PETG @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PETG @Blocks_RD50_V2.json new file mode 100644 index 0000000000..e48e2d8145 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PETG @Blocks_RD50_V2.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PETG @Blocks_RD50_V2", + "inherits": "Blocks PETG @base", + "from": "system", + "setting_id": "3v7ah1zVG8MERPSJ", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PETG @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PETG @Blocks_RF50.json new file mode 100644 index 0000000000..c810cf0e8f --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PETG @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PETG @Blocks_RF50", + "inherits": "Blocks PETG @base", + "from": "system", + "setting_id": "S7weR19wo9meJ6Yz", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic PETG @Blocks.json b/resources/profiles/Blocks/filament/Blocks PETG @base.json similarity index 68% rename from resources/profiles/Blocks/filament/Generic PETG @Blocks.json rename to resources/profiles/Blocks/filament/Blocks PETG @base.json index 45a2a13e9b..f272fa9049 100644 --- a/resources/profiles/Blocks/filament/Generic PETG @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks PETG @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic PETG @Blocks", + "name": "Blocks PETG @base", "inherits": "fdm_filament_petg", - "renamed_from": "Blocks Generic PETG", "from": "system", - "setting_id": "NgzPlYItKHbmGgv3", - "filament_id": "OFYPdQJh", - "instantiation": "true", + "filament_id": "OF9t1Ga0", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_flow_ratio": [ "0.94" ], @@ -81,18 +82,5 @@ ], "slow_down_layer_time": [ "5" - ], - "compatible_printers": [ - "BLOCKS Pro S100 0.4 nozzle", - "BLOCKS Pro S100 0.6 nozzle", - "BLOCKS Pro S100 0.8 nozzle", - "BLOCKS Pro S100 1.0 nozzle", - "BLOCKS Pro S100 1.2 nozzle", - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Blocks PLA @Blocks_Pro_S100.json b/resources/profiles/Blocks/filament/Blocks PLA @Blocks_Pro_S100.json new file mode 100644 index 0000000000..64f6bbefa0 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PLA @Blocks_Pro_S100.json @@ -0,0 +1,16 @@ +{ + "type": "filament", + "name": "Blocks PLA @Blocks_Pro_S100", + "inherits": "Blocks PLA @base", + "from": "system", + "setting_id": "vWojuLYIfduLGFLL", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS Pro S100 0.4 nozzle", + "BLOCKS Pro S100 0.6 nozzle", + "BLOCKS Pro S100 0.8 nozzle", + "BLOCKS Pro S100 1.0 nozzle", + "BLOCKS Pro S100 1.2 nozzle" + ], + "renamed_from": "Blocks Generic PLA;Generic PLA @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PLA @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PLA @Blocks_RD50_V2.json new file mode 100644 index 0000000000..ac283bbe06 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PLA @Blocks_RD50_V2.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PLA @Blocks_RD50_V2", + "inherits": "Blocks PLA @base", + "from": "system", + "setting_id": "y7y8NcXNdp17sJUs", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PLA @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PLA @Blocks_RF50.json new file mode 100644 index 0000000000..c59a35df74 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PLA @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PLA @Blocks_RF50", + "inherits": "Blocks PLA @base", + "from": "system", + "setting_id": "jt1MJF4eZzkkscEJ", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic PLA @Blocks.json b/resources/profiles/Blocks/filament/Blocks PLA @base.json similarity index 52% rename from resources/profiles/Blocks/filament/Generic PLA @Blocks.json rename to resources/profiles/Blocks/filament/Blocks PLA @base.json index 3a5c942899..c4051967af 100644 --- a/resources/profiles/Blocks/filament/Generic PLA @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks PLA @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic PLA @Blocks", + "name": "Blocks PLA @base", "inherits": "fdm_filament_pla", - "renamed_from": "Blocks Generic PLA", "from": "system", - "setting_id": "kjhuttx4T8OtTNmV", - "filament_id": "OFDSrzZ8", - "instantiation": "true", + "filament_id": "OFVGAC8g", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_flow_ratio": [ "0.95" ], @@ -39,18 +40,5 @@ ], "during_print_exhaust_fan_speed": [ "100" - ], - "compatible_printers": [ - "BLOCKS Pro S100 0.4 nozzle", - "BLOCKS Pro S100 0.6 nozzle", - "BLOCKS Pro S100 0.8 nozzle", - "BLOCKS Pro S100 1.0 nozzle", - "BLOCKS Pro S100 1.2 nozzle", - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RD50_V2.json new file mode 100644 index 0000000000..08050266b7 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks PLA-CF @Blocks_RD50_V2", + "inherits": "Blocks PLA-CF @base", + "from": "system", + "setting_id": "lhBtMKP1DmOB3HOI", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic PLA-CF;Generic PLA-CF @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RF50.json new file mode 100644 index 0000000000..1f27f49fdf --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PLA-CF @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PLA-CF @Blocks_RF50", + "inherits": "Blocks PLA-CF @base", + "from": "system", + "setting_id": "6CU83mAyhcZ4FCPI", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PLA-CF @base.json b/resources/profiles/Blocks/filament/Blocks PLA-CF @base.json new file mode 100644 index 0000000000..989a427df4 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PLA-CF @base.json @@ -0,0 +1,23 @@ +{ + "type": "filament", + "name": "Blocks PLA-CF @base", + "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "OF6FfwOP", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], + "filament_flow_ratio": [ + "0.95" + ], + "filament_max_volumetric_speed": [ + "20" + ], + "filament_type": [ + "PLA-CF" + ], + "slow_down_layer_time": [ + "7" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PVA @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks PVA @Blocks_RD50_V2.json new file mode 100644 index 0000000000..f1beff3b23 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PVA @Blocks_RD50_V2.json @@ -0,0 +1,14 @@ +{ + "type": "filament", + "name": "Blocks PVA @Blocks_RD50_V2", + "inherits": "Blocks PVA @base", + "from": "system", + "setting_id": "hREDqwKF6qyKkakp", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ], + "renamed_from": "Blocks Generic PVA;Generic PVA @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks PVA @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks PVA @Blocks_RF50.json new file mode 100644 index 0000000000..91a0a0e5e6 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PVA @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks PVA @Blocks_RF50", + "inherits": "Blocks PVA @base", + "from": "system", + "setting_id": "q6pKqHY7Iz4OZaAC", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks PVA @base.json b/resources/profiles/Blocks/filament/Blocks PVA @base.json new file mode 100644 index 0000000000..326e61b362 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks PVA @base.json @@ -0,0 +1,17 @@ +{ + "type": "filament", + "name": "Blocks PVA @base", + "inherits": "fdm_filament_pva", + "from": "system", + "filament_id": "OFqcNuE1", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "20" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks TPU @Blocks_Pro_S100.json b/resources/profiles/Blocks/filament/Blocks TPU @Blocks_Pro_S100.json new file mode 100644 index 0000000000..d186e824ef --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks TPU @Blocks_Pro_S100.json @@ -0,0 +1,16 @@ +{ + "type": "filament", + "name": "Blocks TPU @Blocks_Pro_S100", + "inherits": "Blocks TPU @base", + "from": "system", + "setting_id": "6Rkz2je91JRTRoTi", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS Pro S100 0.4 nozzle", + "BLOCKS Pro S100 0.6 nozzle", + "BLOCKS Pro S100 0.8 nozzle", + "BLOCKS Pro S100 1.0 nozzle", + "BLOCKS Pro S100 1.2 nozzle" + ], + "renamed_from": "Blocks Generic TPU;Generic TPU @Blocks" +} diff --git a/resources/profiles/Blocks/filament/Blocks TPU @Blocks_RD50_V2.json b/resources/profiles/Blocks/filament/Blocks TPU @Blocks_RD50_V2.json new file mode 100644 index 0000000000..088ad69bbb --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks TPU @Blocks_RD50_V2.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks TPU @Blocks_RD50_V2", + "inherits": "Blocks TPU @base", + "from": "system", + "setting_id": "mkCqRQTtXldzoEUg", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RD50 V2 0.4 nozzle", + "BLOCKS RD50 V2 0.6 nozzle", + "BLOCKS RD50 V2 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Blocks TPU @Blocks_RF50.json b/resources/profiles/Blocks/filament/Blocks TPU @Blocks_RF50.json new file mode 100644 index 0000000000..53773dd813 --- /dev/null +++ b/resources/profiles/Blocks/filament/Blocks TPU @Blocks_RF50.json @@ -0,0 +1,13 @@ +{ + "type": "filament", + "name": "Blocks TPU @Blocks_RF50", + "inherits": "Blocks TPU @base", + "from": "system", + "setting_id": "bCCvu5Ow1JkwIrRR", + "instantiation": "true", + "compatible_printers": [ + "BLOCKS RF50 0.4 nozzle", + "BLOCKS RF50 0.6 nozzle", + "BLOCKS RF50 0.8 nozzle" + ] +} diff --git a/resources/profiles/Blocks/filament/Generic TPU @Blocks.json b/resources/profiles/Blocks/filament/Blocks TPU @base.json similarity index 58% rename from resources/profiles/Blocks/filament/Generic TPU @Blocks.json rename to resources/profiles/Blocks/filament/Blocks TPU @base.json index 8b5873a745..9b26d57636 100644 --- a/resources/profiles/Blocks/filament/Generic TPU @Blocks.json +++ b/resources/profiles/Blocks/filament/Blocks TPU @base.json @@ -1,12 +1,13 @@ { "type": "filament", - "name": "Generic TPU @Blocks", + "name": "Blocks TPU @base", "inherits": "fdm_filament_tpu", - "renamed_from": "Blocks Generic TPU", "from": "system", - "setting_id": "6iVYOCZo8ugs4JA0", - "filament_id": "OFgbpcy9", - "instantiation": "true", + "filament_id": "OFYnedOZ", + "instantiation": "false", + "filament_vendor": [ + "Blocks" + ], "filament_max_volumetric_speed": [ "4" ], @@ -54,18 +55,5 @@ ], "during_print_exhaust_fan_speed": [ "80" - ], - "compatible_printers": [ - "BLOCKS Pro S100 0.4 nozzle", - "BLOCKS Pro S100 0.6 nozzle", - "BLOCKS Pro S100 0.8 nozzle", - "BLOCKS Pro S100 1.0 nozzle", - "BLOCKS Pro S100 1.2 nozzle", - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" ] } diff --git a/resources/profiles/Blocks/filament/Generic PA @Blocks.json b/resources/profiles/Blocks/filament/Generic PA @Blocks.json deleted file mode 100644 index e624226ded..0000000000 --- a/resources/profiles/Blocks/filament/Generic PA @Blocks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "filament", - "name": "Generic PA @Blocks", - "inherits": "fdm_filament_pa", - "renamed_from": "Blocks Generic PA", - "from": "system", - "setting_id": "hL3p8gAeciwDkjNK", - "filament_id": "OFg8ndtj", - "instantiation": "true", - "filament_flow_ratio": [ - "0.98" - ], - "filament_max_volumetric_speed": [ - "10" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" - ] -} diff --git a/resources/profiles/Blocks/filament/Generic PC @Blocks.json b/resources/profiles/Blocks/filament/Generic PC @Blocks.json deleted file mode 100644 index 7b5df77bc5..0000000000 --- a/resources/profiles/Blocks/filament/Generic PC @Blocks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "filament", - "name": "Generic PC @Blocks", - "inherits": "fdm_filament_pc", - "renamed_from": "Blocks Generic PC", - "from": "system", - "setting_id": "Xhs8PjtmwwVj0ksE", - "filament_id": "OFLakOUI", - "instantiation": "true", - "filament_flow_ratio": [ - "0.94" - ], - "filament_max_volumetric_speed": [ - "10" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" - ] -} diff --git a/resources/profiles/Blocks/filament/Generic PLA-CF @Blocks.json b/resources/profiles/Blocks/filament/Generic PLA-CF @Blocks.json deleted file mode 100644 index 3a3ac5daba..0000000000 --- a/resources/profiles/Blocks/filament/Generic PLA-CF @Blocks.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "type": "filament", - "name": "Generic PLA-CF @Blocks", - "inherits": "fdm_filament_pla", - "renamed_from": "Blocks Generic PLA-CF", - "from": "system", - "setting_id": "oKUqzwAWMrpiyGLf", - "filament_id": "OFWbdGsC", - "instantiation": "true", - "filament_flow_ratio": [ - "0.95" - ], - "filament_max_volumetric_speed": [ - "20" - ], - "filament_type": [ - "PLA-CF" - ], - "slow_down_layer_time": [ - "7" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" - ] -} diff --git a/resources/profiles/Blocks/filament/Generic PVA @Blocks.json b/resources/profiles/Blocks/filament/Generic PVA @Blocks.json deleted file mode 100644 index 5b6a3ad368..0000000000 --- a/resources/profiles/Blocks/filament/Generic PVA @Blocks.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "type": "filament", - "name": "Generic PVA @Blocks", - "inherits": "fdm_filament_pva", - "renamed_from": "Blocks Generic PVA", - "from": "system", - "setting_id": "tNw3oe7VovLkgUbB", - "filament_id": "OFDvXujf", - "instantiation": "true", - "filament_flow_ratio": [ - "0.98" - ], - "filament_max_volumetric_speed": [ - "20" - ], - "compatible_printers": [ - "BLOCKS RD50 V2 0.4 nozzle", - "BLOCKS RD50 V2 0.6 nozzle", - "BLOCKS RD50 V2 0.8 nozzle", - "BLOCKS RF50 0.4 nozzle", - "BLOCKS RF50 0.6 nozzle", - "BLOCKS RF50 0.8 nozzle" - ] -} diff --git a/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.4 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.4 nozzle.json index e8cc1893c7..43951cf9c5 100644 --- a/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.4 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.4 nozzle.json @@ -6,7 +6,7 @@ "setting_id": "G7HQgaspHXduDM8O", "instantiation": "true", "printer_model": "BLOCKS Pro S100", - "default_print_profile": "0.20mm Standard 0.4 nozzle @Blocks", + "default_print_profile": "0.20mm Standard 0.4 nozzle @Blocks_Pro_S100", "max_layer_height": [ "0.30" ], @@ -25,5 +25,8 @@ "printable_height": "900", "upward_compatible_machine": [ "BLOCKS Pro S100" + ], + "default_filament_profile": [ + "Blocks PLA @Blocks_Pro_S100" ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.6 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.6 nozzle.json index 49e519d2ec..a55153bab7 100644 --- a/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.6 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.6 nozzle.json @@ -6,7 +6,7 @@ "setting_id": "XkWdS210oUMQN17N", "instantiation": "true", "printer_model": "BLOCKS Pro S100", - "default_print_profile": "0.30mm Standard 0.6 nozzle @Blocks", + "default_print_profile": "0.30mm Standard 0.6 nozzle @Blocks_Pro_S100", "max_layer_height": [ "0.50" ], @@ -26,5 +26,8 @@ "upward_compatible_machine": [ "BLOCKS Pro S100" ], - "printer_variant": "0.6" + "printer_variant": "0.6", + "default_filament_profile": [ + "Blocks PLA @Blocks_Pro_S100" + ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.8 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.8 nozzle.json index 3c3a29f111..7e889bbf14 100644 --- a/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.8 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS Pro S100 0.8 nozzle.json @@ -6,7 +6,7 @@ "setting_id": "I6PdeCS7SbvIlkZf", "instantiation": "true", "printer_model": "BLOCKS Pro S100", - "default_print_profile": "0.40mm Standard 0.8 nozzle @Blocks", + "default_print_profile": "0.40mm Standard 0.8 nozzle @Blocks_Pro_S100", "max_layer_height": [ "0.56" ], @@ -26,5 +26,8 @@ "upward_compatible_machine": [ "BLOCKS Pro S100" ], - "printer_variant": "0.8" + "printer_variant": "0.8", + "default_filament_profile": [ + "Blocks PLA @Blocks_Pro_S100" + ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.0 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.0 nozzle.json index 1be375c34b..c84ad8c227 100644 --- a/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.0 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.0 nozzle.json @@ -6,7 +6,7 @@ "setting_id": "elLdUyy9RhUoa37o", "instantiation": "true", "printer_model": "BLOCKS Pro S100", - "default_print_profile": "0.50mm Standard 1.0 nozzle @Blocks", + "default_print_profile": "0.50mm Standard 1.0 nozzle @Blocks_Pro_S100", "max_layer_height": [ "0.7" ], @@ -26,5 +26,8 @@ "upward_compatible_machine": [ "BLOCKS Pro S100" ], - "printer_variant": "1.0" + "printer_variant": "1.0", + "default_filament_profile": [ + "Blocks PLA @Blocks_Pro_S100" + ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.2 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.2 nozzle.json index 426fdafd55..11aab094cf 100644 --- a/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.2 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS Pro S100 1.2 nozzle.json @@ -6,7 +6,7 @@ "setting_id": "xtsGSJ4JkbHTPi44", "instantiation": "true", "printer_model": "BLOCKS Pro S100", - "default_print_profile": "0.60mm Standard 1.2 nozzle @Blocks", + "default_print_profile": "0.60mm Standard 1.2 nozzle @Blocks_Pro_S100", "max_layer_height": [ "0.8" ], @@ -26,5 +26,8 @@ "upward_compatible_machine": [ "BLOCKS Pro S100" ], - "printer_variant": "1.2" + "printer_variant": "1.2", + "default_filament_profile": [ + "Blocks PLA @Blocks_Pro_S100" + ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS Pro S100.json b/resources/profiles/Blocks/machine/BLOCKS Pro S100.json index de9ea3756c..e034ffd9b1 100644 --- a/resources/profiles/Blocks/machine/BLOCKS Pro S100.json +++ b/resources/profiles/Blocks/machine/BLOCKS Pro S100.json @@ -8,5 +8,5 @@ "bed_model": "PRO S100 HotBed model.stl", "bed_texture": "PRO S100 HotBed texture.svg", "hotend_model": "BLOCKS PrintCore.stl", - "default_materials": "Generic PLA @Blocks;Generic PETG @Blocks;Generic TPU @Blocks" + "default_materials": "Blocks PLA @Blocks_Pro_S100;Blocks PETG @Blocks_Pro_S100;Blocks TPU @Blocks_Pro_S100" } diff --git a/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.4 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.4 nozzle.json index 3d2908a403..c5613b39ec 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.4 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.4 nozzle.json @@ -34,5 +34,9 @@ "printable_height": "500", "upward_compatible_machine": [ "BLOCKS RD50 V2" + ], + "printer_variant": "0.4", + "default_filament_profile": [ + "Blocks PLA @Blocks_RD50_V2" ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.6 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.6 nozzle.json index bf41f1070f..92cde6244c 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.6 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.6 nozzle.json @@ -35,5 +35,8 @@ "deretraction_speed": [ "80" ], - "printer_variant": "0.6" + "printer_variant": "0.6", + "default_filament_profile": [ + "Blocks PLA @Blocks_RD50_V2" + ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.8 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.8 nozzle.json index 23c0808be2..756e77efb7 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.8 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS RD50 V2 0.8 nozzle.json @@ -35,5 +35,8 @@ "deretraction_speed": [ "80" ], - "printer_variant": "0.8" + "printer_variant": "0.8", + "default_filament_profile": [ + "Blocks PLA @Blocks_RD50_V2" + ] } diff --git a/resources/profiles/Blocks/machine/BLOCKS RD50 V2.json b/resources/profiles/Blocks/machine/BLOCKS RD50 V2.json index 76dafd429e..1cc75448ac 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RD50 V2.json +++ b/resources/profiles/Blocks/machine/BLOCKS RD50 V2.json @@ -8,5 +8,5 @@ "bed_model": "RD50 V2 HotBed model.stl", "bed_texture": "RD50 V2 HotBed texture.svg", "hotend_model": "BLOCKS PrintCore.stl", - "default_materials": "Generic PLA @Blocks;Generic PETG @Blocks;Generic TPU @Blocks" + "default_materials": "Blocks PLA @Blocks_RD50_V2;Blocks PETG @Blocks_RD50_V2;Blocks TPU @Blocks_RD50_V2" } diff --git a/resources/profiles/Blocks/machine/BLOCKS RF50 0.4 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS RF50 0.4 nozzle.json index a589b6620b..9e523ef843 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RF50 0.4 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS RF50 0.4 nozzle.json @@ -51,7 +51,7 @@ "12" ], "retraction_length": [ - "1" + "1.2" ], "retraction_speed": [ "60" @@ -79,5 +79,10 @@ ], "retract_before_wipe": [ "0" - ] + ], + "default_filament_profile": [ + "Blocks PLA @Blocks_RF50" + ], + "support_multi_bed_types": "1", + "default_bed_type": "Textured PEI Plate" } diff --git a/resources/profiles/Blocks/machine/BLOCKS RF50 0.6 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS RF50 0.6 nozzle.json index fea3007abc..b0dbc7d2b9 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RF50 0.6 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS RF50 0.6 nozzle.json @@ -51,7 +51,7 @@ "12" ], "retraction_length": [ - "1" + "1.2" ], "retraction_speed": [ "60" @@ -75,8 +75,8 @@ "BLOCKS RF50" ], "machine_start_gcode": [ - "PRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer]", - "SET_PRESSURE_ADVANCE ADVANCE=0.046" + "PRINT_START EXTRUDER=[nozzle_temperature_initial_layer] BED=[bed_temperature_initial_layer]\n", + "SET_PRESSURE_ADVANCE ADVANCE=0.046\n" ], "printer_variant": "0.6", "wipe_distance": [ @@ -84,5 +84,10 @@ ], "retract_before_wipe": [ "0" - ] + ], + "default_filament_profile": [ + "Blocks PLA @Blocks_RF50" + ], + "support_multi_bed_types": "1", + "default_bed_type": "Textured PEI Plate" } diff --git a/resources/profiles/Blocks/machine/BLOCKS RF50 0.8 nozzle.json b/resources/profiles/Blocks/machine/BLOCKS RF50 0.8 nozzle.json index c8d6cec882..c361a8da16 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RF50 0.8 nozzle.json +++ b/resources/profiles/Blocks/machine/BLOCKS RF50 0.8 nozzle.json @@ -51,7 +51,7 @@ "12" ], "retraction_length": [ - "1" + "1.2" ], "retraction_speed": [ "60" @@ -80,5 +80,10 @@ ], "retract_before_wipe": [ "0" - ] + ], + "default_filament_profile": [ + "Blocks PLA @Blocks_RF50" + ], + "support_multi_bed_types": "1", + "default_bed_type": "Textured PEI Plate" } diff --git a/resources/profiles/Blocks/machine/BLOCKS RF50.json b/resources/profiles/Blocks/machine/BLOCKS RF50.json index 14ae8bc5f4..79aeec9ebc 100644 --- a/resources/profiles/Blocks/machine/BLOCKS RF50.json +++ b/resources/profiles/Blocks/machine/BLOCKS RF50.json @@ -8,5 +8,6 @@ "bed_model": "RF50 HotBed model.stl", "bed_texture": "RF50 HotBed texture.svg", "hotend_model": "BLOCKS PrintCore.stl", - "default_materials": "Generic PLA @Blocks;Generic PETG @Blocks;Generic TPU @Blocks" + "default_materials": "Blocks PLA @Blocks_RF50;Blocks PETG @Blocks_RF50;Blocks TPU @Blocks_RF50", + "not_support_bed_type": "Smooth Cool Plate;Engineering Plate;Textured Cool Plate;Cool Plate (SuperTack)" } diff --git a/resources/profiles/Blocks/machine/fdm_klipper_common.json b/resources/profiles/Blocks/machine/fdm_klipper_common.json index 5372b42547..eab501bf4b 100644 --- a/resources/profiles/Blocks/machine/fdm_klipper_common.json +++ b/resources/profiles/Blocks/machine/fdm_klipper_common.json @@ -123,9 +123,6 @@ "wipe": [ "1" ], - "default_filament_profile": [ - "Generic PLA @Blocks" - ], "bed_exclude_area": [ "0x0" ], diff --git a/resources/profiles/Blocks/process/0.12mm Fine 0.4 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.12mm Fine 0.4 nozzle @Blocks_RF50.json index 315256dcab..43474f76c1 100644 --- a/resources/profiles/Blocks/process/0.12mm Fine 0.4 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.12mm Fine 0.4 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.16mm Optimal 0.4 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.16mm Optimal 0.4 nozzle @Blocks_RF50.json index ca6facb720..b7351a4996 100644 --- a/resources/profiles/Blocks/process/0.16mm Optimal 0.4 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.16mm Optimal 0.4 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.20mm Optimal 0.6 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.20mm Optimal 0.6 nozzle @Blocks_RF50.json index b80b051a71..aa2b2c8e4a 100644 --- a/resources/profiles/Blocks/process/0.20mm Optimal 0.6 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.20mm Optimal 0.6 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_Pro_S100.json similarity index 62% rename from resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_Pro_S100.json index 9f66196279..d5a27fa23b 100644 --- a/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.20mm Standard 0.4 nozzle @Blocks", + "name": "0.20mm Standard 0.4 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_blocks_common", "from": "system", - "setting_id": "47aDtVL6E5DVr7Mt", + "setting_id": "QSEguMMChNFZsK5H", "instantiation": "true", "layer_height": "0.2", "bottom_shell_layers": "4", "top_shell_layers": "5", "compatible_printers": [ "BLOCKS Pro S100 0.4 nozzle" - ] + ], + "renamed_from": "0.20mm Standard 0.4 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_RF50.json index 5c88e00c2a..901f3f811d 100644 --- a/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.20mm Standard 0.4 nozzle @Blocks_RF50.json @@ -25,7 +25,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -61,5 +61,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_Pro_S100.json index 2bc124f2dc..2716ce0c66 100644 --- a/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.24mm Draft 0.4 nozzle @Blocks", + "name": "0.24mm Draft 0.4 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_blocks_common", "from": "system", - "setting_id": "ETADjxV6ViJQdnHA", + "setting_id": "uALfEFHGmup6vBYI", "instantiation": "true", "layer_height": "0.24", "bottom_shell_layers": "4", "top_shell_layers": "5", "compatible_printers": [ "BLOCKS Pro S100 0.4 nozzle" - ] + ], + "renamed_from": "0.24mm Draft 0.4 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_RF50.json index edb1bfcc2d..ab911910b9 100644 --- a/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.24mm Draft 0.4 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.26mm Standard 0.6 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.26mm Standard 0.6 nozzle @Blocks_RF50.json index d2f853b638..739b4f7168 100644 --- a/resources/profiles/Blocks/process/0.26mm Standard 0.6 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.26mm Standard 0.6 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.28mm Extra Draft 0.4 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.28mm Extra Draft 0.4 nozzle @Blocks_RF50.json index dd6b60d371..7c9a108287 100644 --- a/resources/profiles/Blocks/process/0.28mm Extra Draft 0.4 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.28mm Extra Draft 0.4 nozzle @Blocks_RF50.json @@ -22,7 +22,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.30mm Extra Draft 0.4 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100.json similarity index 61% rename from resources/profiles/Blocks/process/0.30mm Extra Draft 0.4 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100.json index 0cef66fb5a..c67ebf9341 100644 --- a/resources/profiles/Blocks/process/0.30mm Extra Draft 0.4 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.30mm Extra Draft 0.4 nozzle @Blocks", + "name": "0.30mm Extra Draft 0.4 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_blocks_common", "from": "system", - "setting_id": "UbuICsmppKDsaaZ4", + "setting_id": "HYKF5l2OGvpugSQy", "instantiation": "true", "bottom_shell_layers": "3", "top_shell_layers": "4", "layer_height": "0.30", "compatible_printers": [ "BLOCKS Pro S100 0.4 nozzle" - ] + ], + "renamed_from": "0.30mm Extra Draft 0.4 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100.json index bbb74c97a9..bbd486176f 100644 --- a/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.30mm Optimal 0.8 nozzle @Blocks", + "name": "0.30mm Optimal 0.8 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 0.8 nozzle", "from": "system", - "setting_id": "YqkYzp5zdH4R5upm", + "setting_id": "OUfZvizM5MGnvHfp", "instantiation": "true", "bottom_shell_layers": "3", "top_shell_layers": "3", "layer_height": "0.3", "compatible_printers": [ "BLOCKS Pro S100 0.8 nozzle" - ] + ], + "renamed_from": "0.30mm Optimal 0.8 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_RF50.json index 9d909c1dd4..f6763c9494 100644 --- a/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.30mm Optimal 0.8 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.30mm Optimal 1.0 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.30mm Optimal 1.0 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100.json index 8820018b9d..e7ce874a77 100644 --- a/resources/profiles/Blocks/process/0.30mm Optimal 1.0 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.30mm Optimal 1.0 nozzle @Blocks", + "name": "0.30mm Optimal 1.0 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.0 nozzle", "from": "system", - "setting_id": "8pBawPScHkyQu0SW", + "setting_id": "HGG9WxKVr89qNFDx", "instantiation": "true", "bottom_shell_layers": "4", "top_shell_layers": "4", "layer_height": "0.30", "compatible_printers": [ "BLOCKS Pro S100 1.0 nozzle" - ] + ], + "renamed_from": "0.30mm Optimal 1.0 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.30mm Standard 0.6 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.30mm Standard 0.6 nozzle @Blocks_Pro_S100.json similarity index 62% rename from resources/profiles/Blocks/process/0.30mm Standard 0.6 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.30mm Standard 0.6 nozzle @Blocks_Pro_S100.json index d2a815e849..00e37a7860 100644 --- a/resources/profiles/Blocks/process/0.30mm Standard 0.6 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.30mm Standard 0.6 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.30mm Standard 0.6 nozzle @Blocks", + "name": "0.30mm Standard 0.6 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 0.6 nozzle", "from": "system", - "setting_id": "EPI3PAVK9LpJOZWO", + "setting_id": "gO2I1YjBl4nf8yzr", "instantiation": "true", "bottom_shell_layers": "3", "top_shell_layers": "3", "layer_height": "0.30", "compatible_printers": [ "BLOCKS Pro S100 0.6 nozzle" - ] + ], + "renamed_from": "0.30mm Standard 0.6 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.32mm Draft 0.6 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.32mm Draft 0.6 nozzle @Blocks_RF50.json index 60c6eae19e..b19346e30a 100644 --- a/resources/profiles/Blocks/process/0.32mm Draft 0.6 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.32mm Draft 0.6 nozzle @Blocks_RF50.json @@ -22,7 +22,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.38mm Extra Draft 0.6 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.38mm Extra Draft 0.6 nozzle @Blocks_RF50.json index f28a975208..12922fa105 100644 --- a/resources/profiles/Blocks/process/0.38mm Extra Draft 0.6 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.38mm Extra Draft 0.6 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.38mm Standard 0.8 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.38mm Standard 0.8 nozzle @Blocks_RF50.json index 7b94febc1c..5c2245f114 100644 --- a/resources/profiles/Blocks/process/0.38mm Standard 0.8 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.38mm Standard 0.8 nozzle @Blocks_RF50.json @@ -22,7 +22,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.40mm Draft 0.6 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.40mm Draft 0.6 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.40mm Draft 0.6 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.40mm Draft 0.6 nozzle @Blocks_Pro_S100.json index 45fdd61179..915d160b6c 100644 --- a/resources/profiles/Blocks/process/0.40mm Draft 0.6 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.40mm Draft 0.6 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.40mm Draft 0.6 nozzle @Blocks", + "name": "0.40mm Draft 0.6 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 0.6 nozzle", "from": "system", - "setting_id": "CShVKoGspbMJNPw8", + "setting_id": "OjsLUl1heqv1cevs", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "2", "layer_height": "0.40", "compatible_printers": [ "BLOCKS Pro S100 0.6 nozzle" - ] + ], + "renamed_from": "0.40mm Draft 0.6 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.40mm Standard 0.8 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.40mm Standard 0.8 nozzle @Blocks_Pro_S100.json similarity index 62% rename from resources/profiles/Blocks/process/0.40mm Standard 0.8 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.40mm Standard 0.8 nozzle @Blocks_Pro_S100.json index 177b620e51..20f02ff7af 100644 --- a/resources/profiles/Blocks/process/0.40mm Standard 0.8 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.40mm Standard 0.8 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.40mm Standard 0.8 nozzle @Blocks", + "name": "0.40mm Standard 0.8 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 0.8 nozzle", "from": "system", - "setting_id": "TSRjmfWaatY7oLJB", + "setting_id": "laOTioBGySeudJpl", "instantiation": "true", "bottom_shell_layers": "3", "top_shell_layers": "4", "layer_height": "0.40", "compatible_printers": [ "BLOCKS Pro S100 0.8 nozzle" - ] + ], + "renamed_from": "0.40mm Standard 0.8 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.46mm Draft 0.8 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.46mm Draft 0.8 nozzle @Blocks_RF50.json index d49633e89d..be66641b28 100644 --- a/resources/profiles/Blocks/process/0.46mm Draft 0.8 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.46mm Draft 0.8 nozzle @Blocks_RF50.json @@ -22,7 +22,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.50mm Draft 0.8 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.50mm Draft 0.8 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.50mm Draft 0.8 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.50mm Draft 0.8 nozzle @Blocks_Pro_S100.json index fc1a875d67..8f358e60e2 100644 --- a/resources/profiles/Blocks/process/0.50mm Draft 0.8 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.50mm Draft 0.8 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.50mm Draft 0.8 nozzle @Blocks", + "name": "0.50mm Draft 0.8 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 0.8 nozzle", "from": "system", - "setting_id": "vOtZHrDS88Shmf2m", + "setting_id": "oAhgUZdADR7kP87e", "instantiation": "true", "bottom_shell_layers": "3", "top_shell_layers": "4", "layer_height": "0.50", "compatible_printers": [ "BLOCKS Pro S100 0.8 nozzle" - ] + ], + "renamed_from": "0.50mm Draft 0.8 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.50mm Optimal 1.2 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.50mm Optimal 1.2 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100.json index 6b59b1c101..11c79d1a65 100644 --- a/resources/profiles/Blocks/process/0.50mm Optimal 1.2 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.50mm Optimal 1.2 nozzle @Blocks", + "name": "0.50mm Optimal 1.2 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.2 nozzle", "from": "system", - "setting_id": "BKrEAKqDYzdH43wc", + "setting_id": "0eqNgX26jemsBfV5", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "3", "layer_height": "0.50", "compatible_printers": [ "BLOCKS Pro S100 1.2 nozzle" - ] + ], + "renamed_from": "0.50mm Optimal 1.2 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.50mm Standard 1.0 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.50mm Standard 1.0 nozzle @Blocks_Pro_S100.json similarity index 62% rename from resources/profiles/Blocks/process/0.50mm Standard 1.0 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.50mm Standard 1.0 nozzle @Blocks_Pro_S100.json index 80e3ba5649..0bd91d3405 100644 --- a/resources/profiles/Blocks/process/0.50mm Standard 1.0 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.50mm Standard 1.0 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.50mm Standard 1.0 nozzle @Blocks", + "name": "0.50mm Standard 1.0 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.0 nozzle", "from": "system", - "setting_id": "D40iksDm7mFjhSKn", + "setting_id": "z1qCW1cEMc4QvBbV", "instantiation": "true", "bottom_shell_layers": "3", "top_shell_layers": "3", "layer_height": "0.50", "compatible_printers": [ "BLOCKS Pro S100 1.0 nozzle" - ] + ], + "renamed_from": "0.50mm Standard 1.0 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.54mm Extra Draft 0.8 nozzle @Blocks_RF50.json b/resources/profiles/Blocks/process/0.54mm Extra Draft 0.8 nozzle @Blocks_RF50.json index a0e9121ceb..6700893916 100644 --- a/resources/profiles/Blocks/process/0.54mm Extra Draft 0.8 nozzle @Blocks_RF50.json +++ b/resources/profiles/Blocks/process/0.54mm Extra Draft 0.8 nozzle @Blocks_RF50.json @@ -21,7 +21,7 @@ "top_solid_infill_flow_ratio": "0.97", "bottom_solid_infill_flow_ratio": "1.08", "seam_position": "aligned", - "sparse_infill_patter": "grid", + "sparse_infill_pattern": "grid", "initial_layer_speed": "60", "initial_layer_infill_speed": "80", "initial_travel_speed": "100%", @@ -57,5 +57,7 @@ "accel_to_decel_enable": "1", "accel_to_decel_factor": "50%", "elefant_foot_compensation": "0.25", - "elefant_foot_compensation_layers": "1" + "elefant_foot_compensation_layers": "1", + "skirt_loops": "2", + "skirt_type": "perobject" } diff --git a/resources/profiles/Blocks/process/0.60mm Draft 1.0 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.60mm Draft 1.0 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.60mm Draft 1.0 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.60mm Draft 1.0 nozzle @Blocks_Pro_S100.json index e59174620f..f63ebde1bc 100644 --- a/resources/profiles/Blocks/process/0.60mm Draft 1.0 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.60mm Draft 1.0 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.60mm Draft 1.0 nozzle @Blocks", + "name": "0.60mm Draft 1.0 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.0 nozzle", "from": "system", - "setting_id": "UJUe8XCBzZf8NyCL", + "setting_id": "gTTgtfG8qb9My3hH", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "3", "layer_height": "0.60", "compatible_printers": [ "BLOCKS Pro S100 1.0 nozzle" - ] + ], + "renamed_from": "0.60mm Draft 1.0 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.60mm Standard 1.2 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.60mm Standard 1.2 nozzle @Blocks_Pro_S100.json similarity index 62% rename from resources/profiles/Blocks/process/0.60mm Standard 1.2 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.60mm Standard 1.2 nozzle @Blocks_Pro_S100.json index 598534c050..ba93c393a6 100644 --- a/resources/profiles/Blocks/process/0.60mm Standard 1.2 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.60mm Standard 1.2 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.60mm Standard 1.2 nozzle @Blocks", + "name": "0.60mm Standard 1.2 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.2 nozzle", "from": "system", - "setting_id": "PSBFrTykpkytPI2Q", + "setting_id": "ApcyToENCIANrI71", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "3", "layer_height": "0.60", "compatible_printers": [ "BLOCKS Pro S100 1.2 nozzle" - ] + ], + "renamed_from": "0.60mm Standard 1.2 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.70mm Draft 1.2 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.70mm Draft 1.2 nozzle @Blocks_Pro_S100.json similarity index 63% rename from resources/profiles/Blocks/process/0.70mm Draft 1.2 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.70mm Draft 1.2 nozzle @Blocks_Pro_S100.json index 905bb32a48..7950bce0ca 100644 --- a/resources/profiles/Blocks/process/0.70mm Draft 1.2 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.70mm Draft 1.2 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.70mm Draft 1.2 nozzle @Blocks", + "name": "0.70mm Draft 1.2 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.2 nozzle", "from": "system", - "setting_id": "0ZQ0USGX9SwAKr29", + "setting_id": "DEv7X7zaTfxRQmjo", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "3", "layer_height": "0.70", "compatible_printers": [ "BLOCKS Pro S100 1.2 nozzle" - ] + ], + "renamed_from": "0.70mm Draft 1.2 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.70mm Extra Draft 1.0 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100.json similarity index 61% rename from resources/profiles/Blocks/process/0.70mm Extra Draft 1.0 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100.json index 49eaf2c7b9..49d864ed8f 100644 --- a/resources/profiles/Blocks/process/0.70mm Extra Draft 1.0 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.70mm Extra Draft 1.0 nozzle @Blocks", + "name": "0.70mm Extra Draft 1.0 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.0 nozzle", "from": "system", - "setting_id": "69PFmjOVFBOIp3Qg", + "setting_id": "nUW7N9dJmTyQT4AG", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "3", "layer_height": "0.70", "compatible_printers": [ "BLOCKS Pro S100 1.0 nozzle" - ] + ], + "renamed_from": "0.70mm Extra Draft 1.0 nozzle @Blocks" } diff --git a/resources/profiles/Blocks/process/0.80mm Extra Draft 1.2 nozzle @Blocks.json b/resources/profiles/Blocks/process/0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100.json similarity index 61% rename from resources/profiles/Blocks/process/0.80mm Extra Draft 1.2 nozzle @Blocks.json rename to resources/profiles/Blocks/process/0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100.json index df6d97a9d7..22f7628081 100644 --- a/resources/profiles/Blocks/process/0.80mm Extra Draft 1.2 nozzle @Blocks.json +++ b/resources/profiles/Blocks/process/0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100.json @@ -1,14 +1,15 @@ { "type": "process", - "name": "0.80mm Extra Draft 1.2 nozzle @Blocks", + "name": "0.80mm Extra Draft 1.2 nozzle @Blocks_Pro_S100", "inherits": "fdm_process_common 1.2 nozzle", "from": "system", - "setting_id": "iBwRI5JJewW5HJSv", + "setting_id": "JMy62SonRfK0cAdL", "instantiation": "true", "bottom_shell_layers": "2", "top_shell_layers": "2", "layer_height": "0.80", "compatible_printers": [ "BLOCKS Pro S100 1.2 nozzle" - ] + ], + "renamed_from": "0.80mm Extra Draft 1.2 nozzle @Blocks" } From 770748725253bca43d95c40245db96e7b88eba28 Mon Sep 17 00:00:00 2001 From: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:59:39 +0100 Subject: [PATCH 12/38] Fix overhang slowdown and fan applied to whole walls ahead of an overhang (#15945) --- src/libslic3r/GCode.cpp | 51 ++--- src/libslic3r/GCode/ExtrusionProcessor.hpp | 184 +++++++++++-------- tests/fff_print/test_extrusion_processor.cpp | 93 ++++++++++ 3 files changed, 231 insertions(+), 97 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 77a05ed171..941818c78f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7969,6 +7969,20 @@ double GCode::calc_max_volumetric_speed(const double layer_height, const double return res; } +// ORCA: Overlap at or below which the overhang fan switches on; negative when it does not depend on overlap +// (Overhang_threshold_none cools every external perimeter). +static float overhang_fan_overlap_threshold(int overhang_fan_threshold) +{ + switch (overhang_fan_threshold) { + case (int) Overhang_threshold_1_4: return 0.9f; + case (int) Overhang_threshold_2_4: return 0.75f; + case (int) Overhang_threshold_3_4: return 0.5f; + case (int) Overhang_threshold_4_4: return 0.25f; + case (int) Overhang_threshold_bridge: return 0.05f; + default: return -1.f; + } +} + std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double speed) { std::string gcode; @@ -8311,6 +8325,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, ConfigOptionPercents overhang_overlap_levels({90, 75, 50, 25, 13, 0}); + // ORCA: Lets the path be split where the overhang fan switches, not only where the speed changes. + // Bridges and overhang perimeters are cooled regardless of overlap. + float fan_overlap_threshold = -1.f; + if (FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers && path.role() != erBridgeInfill && + path.role() != erOverhangPerimeter) + fan_overlap_threshold = overhang_fan_overlap_threshold(FILAMENT_CONFIG(overhang_fan_threshold)); + if (NOZZLE_CONFIG(slowdown_for_curled_perimeters)){ ConfigOptionFloatsOrPercents dynamic_overhang_speeds( {FloatOrPercent{100, true}, @@ -8331,7 +8352,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, FloatOrPercent{NOZZLE_CONFIG(overhang_4_4_speed).get_abs_value(ref_speed) * 100 / ref_speed, true}}); new_points = m_extrusion_quality_estimator.estimate_extrusion_quality(path, overhang_overlap_levels, dynamic_overhang_speeds, - ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters)); + ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters), + fan_overlap_threshold); }else{ ConfigOptionFloatsOrPercents dynamic_overhang_speeds( {FloatOrPercent{100, true}, @@ -8350,7 +8372,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, FloatOrPercent{NOZZLE_CONFIG(bridge_speed) * 100 / ref_speed, true}}); new_points = m_extrusion_quality_estimator.estimate_extrusion_quality(path, overhang_overlap_levels, dynamic_overhang_speeds, - ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters)); + ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters), + fan_overlap_threshold); } variable_speed = std::any_of(new_points.begin(), new_points.end(), [speed](const ProcessedPoint &p) { return fabs(double(p.speed) - speed) > 1; }); // Ignore small speed variations (under 1mm/sec) @@ -8502,28 +8525,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, if (role == erBridgeInfill || role == erOverhangPerimeter) { // ORCA: Split out bridge infill to internal and external to apply separate fan settings return true; } - switch (overhang_fan_threshold) { - case (int)Overhang_threshold_1_4: - return overlap <= 0.9f; - break; - case (int)Overhang_threshold_2_4: - return overlap <= 0.75f; - break; - case (int)Overhang_threshold_3_4: - return overlap <= 0.5f; - break; - case (int)Overhang_threshold_4_4: - return overlap <= 0.25f; - break; - case (int)Overhang_threshold_bridge: - return overlap <= 0.05f; - break; - case (int)Overhang_threshold_none: + if (overhang_fan_threshold == Overhang_threshold_none) return is_external_perimeter(role); - break; - default: - return false; - } + const float overlap_threshold = overhang_fan_overlap_threshold(overhang_fan_threshold); + return overlap_threshold >= 0.f && overlap <= overlap_threshold; }; std::string comment; diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 1d65e83f3e..8ed829aa6e 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -41,10 +41,12 @@ std::vector> estimate_points_properties(const POINTS& float flow_width, float max_line_length = -1.0f, float min_distance = -1.0f, - // Maps an overhang distance onto the speed it will be printed at. Interior sampling - // needs it to tell which of the points it could add would change the G-code, and is - // skipped without it. - const std::function& distance_to_speed = {}) + // Speed an overhang distance prints at. Without it, interior sampling + // is skipped and every line over 4mm is split. + const std::function& distance_to_speed = {}, + // Overlap (1 - distance / flow_width) at or below which the overhang + // fan switches on; negative when the fan does not depend on overlap. + float fan_overlap_threshold = -1.0f) { bool looped = input_points.front() == input_points.back(); std::function get_prev_index = [](size_t idx, size_t count) { @@ -125,29 +127,43 @@ std::vector> estimate_points_properties(const POINTS& points.push_back(next_point); } + // ORCA: How an overhang distance prints, which is what the passes below compare. A segment is printed at the lower + // of the speeds at its two ends, and with the overhang fan on if the overlap at either end turns it on. A point + // added to a path can therefore only change the G-code where it prints at a different speed or fan state from the + // points either side of it, and the passes below add points there and nowhere else. + const float width_inv = 1.f / flow_width; + // Whether an overhang distance turns the overhang fan on. The overlap test check_overhang_fan applies in GCode.cpp. + auto fan_on = [fan_overlap_threshold, width_inv](float distance) { + return fan_overlap_threshold >= 0.f && 1.f - distance * width_inv <= fan_overlap_threshold; + }; + // Whether two overhang distances are interchangeable ie have the same speed (beyond a 1mm/sec threshold that gcode.cpp filters out on) + // and the same fan state. + auto same_speed_and_fan = [&distance_to_speed, &fan_on](float a, float b) { + return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f && fan_on(a) == fan_on(b); + }; + // Whether the first overhang distance prints slower than the second, beyond the 1mm/sec gcode.cpp tolerance, or turns the overhang + // fan on where the second does not. Against a supported point (overhang distance 0) it tells whether an end is + // affected by the overhang. + auto slower_or_cooled = [&distance_to_speed, &fan_on](float a, float b) { + return distance_to_speed(a) < distance_to_speed(b) - 1.f || (fan_on(a) && !fan_on(b)); + }; + // ORCA: Interior sampling - // The passes below infer the support under a span from its endpoints alone, so an interior that is supported - // differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by - // full height walls reads as supported along its whole length. Probe the interior, keep the samples the - // endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly - // unsupported gets points where its support actually changes instead of one reading spread across all of it. - if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) { + // The passes below infer the support under a span from its endpoints alone, so a part that is supported + // differently from both ends is invisible to them. The outer perimeter of an overhang whose ends are supported by + // reads as supported along its whole length. Probe the interior, keep the samples the endpoint interpolation fails to predict, + // and bisect either side of each one, so a span that is only partly unsupported gets points where its support actually changes + // instead of one reading being spread across all of it. + // Skipped where there is nothing to find: min_distance <= 0 when no overhang can slow this path down, and + // fan_overlap_threshold < 0 when no overhang switches the fan on. It also needs distance_to_speed to tell which of + // the points it could add would change the G-code. + if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && distance_to_speed && (min_distance > 0 || fan_overlap_threshold >= 0.f)) { // Probe at least this densely before treating matching samples as evidence that a span is uniform. The // segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer - // together than min_spacing, so finer discovery would not produce a more precise speed transition. + // together than min_spacing, so finer discovery would not produce a more precise transition. const double max_probe_spacing = std::max(2., 4. * min_spacing); // A backstop for that length test, which on a non-finite length would never be met. constexpr int max_bisection_depth = 10; - // Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends - // read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever - // its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections - // interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart. - // The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all. - auto same_speed = [&distance_to_speed](float a, float b) { - return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f; - }; - // Whether the first reading is printed slower than the second, once they are known to differ. - auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); }; // Part of a segment still to bisect: its positions along the segment and bisections left. struct Subspan { double t0, t1; int depth; }; @@ -185,8 +201,8 @@ std::vector> estimate_points_properties(const POINTS& if (!interior.empty()) { std::sort(interior.begin(), interior.end(), [](const std::pair& l, const std::pair& r) { return l.first < r.first; }); - // Coarse probing keeps every sample it took until this pass can see which ones bracket a speed - // transition. Matching samples cannot be discarded during discovery: one may be the last + // Coarse probing keeps every sample it took until this pass can see which ones bracket a speed or + // fan transition. Matching samples cannot be discarded during discovery: one may be the last // supported point before a narrow unsupported pocket found by a later probe. size_t kept = 0; for (size_t i = 0; i < interior.size(); ++i) { @@ -195,15 +211,15 @@ std::vector> estimate_points_properties(const POINTS& const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end const float before = at_start ? curr.distance : interior[kept - 1].second; const float after = at_end ? next.distance : interior[i + 1].second; - // A sample is worth a point in the path only where it prints at a different speed from the - // readings either side of it. Differing from one of the segment's own ends is not enough on - // its own where the sample is the faster of the two: the segmentation pass below already - // ends the slowdown an end reads, at a distance taken from how far out that end is rather - // than from wherever bisection happened to stop, and a point here would leave the span - // beside the end too short for that pass to run at all. Support an end cannot account for, - // where the interior is the slower reading, is exactly what this pass is here to find. - const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before)); - const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after)); + // A sample is worth a point in the path only where it prints differently, in speed or fan state, + // from the points either side of it. Differing from one of the segment's own ends is not enough on + // its own where the sample is not the slower or cooled of the two: the segmentation pass below + // already confines the slowdown and cooling at an end, at a distance taken from how far out that + // end is rather than from wherever bisection happened to stop, and a point here would leave the + // span beside the end too short for that pass to run at all. Support an end cannot account for, + // where the interior is the slower or cooled of the two, is exactly what this pass is here to find. + const bool worth_before = !same_speed_and_fan(sample, before) && (!at_start || slower_or_cooled(sample, before)); + const bool worth_after = !same_speed_and_fan(sample, after) && (!at_end || slower_or_cooled(sample, after)); if (worth_before || worth_after) interior[kept++] = interior[i]; } @@ -238,52 +254,60 @@ std::vector> estimate_points_properties(const POINTS& if ((curr.distance > -boundary_offset && curr.distance < boundary_offset + 2.0f) || (next.distance > -boundary_offset && next.distance < boundary_offset + 2.0f)) { double line_len = (next.position - curr.position).norm(); - - // ORCA: Segment path to smaller lines by adding additional points only if the path has an overhang that - // will trigger a slowdown and the path is also reasonably large, i.e. 2mm in length or more - // If there is no overhang in the start/end point, dont segment it. - // Ignore this check if the control of segmentation for overhangs is disabled (min_distance=-1) - if ((min_distance > 0 && ((std::abs(curr.distance) > min_distance) || (std::abs(next.distance) > min_distance)) && line_len >= 2.f) || - (min_distance <= 0 && line_len > 4.0f)) { + + // ORCA: A line prints as slow as its slower end and is cooled if either end is, so an overhang at one + // end would otherwise slow down or cool the whole line. Split the line so that only the part beside + // that end prints that way, if the line is reasonably long (2mm or more) and at least one end prints + // slower or cooled compared with a supported point (overhang distance 0). Deciding on how the end + // prints, rather than on its overhang distance against min_distance, also catches an end whose overhang + // distance is exactly where the slowdown begins, such as an outline crossing at half a line width. + // Without distance_to_speed, split every line over 4mm. + const bool split_line = distance_to_speed ? + line_len >= 2.f && (slower_or_cooled(curr.distance, 0.f) || slower_or_cooled(next.distance, 0.f)) : + line_len > 4.0f; + if (split_line) { + // Each end's piece is that end's overhang distance plus 1.5 line widths (3 * boundary_offset) long: + // a0 ends the piece beside curr, a1 starts the piece beside next. double a0 = std::clamp((curr.distance + 3 * boundary_offset) / line_len, 0.0, 1.0); double a1 = std::clamp(1.0f - (next.distance + 3 * boundary_offset) / line_len, 0.0, 1.0); double t0 = std::min(a0, a1); double t1 = std::max(a0, a1); - if (t0 < 1.0) { - Vec p0 = curr.position + t0 * (next.position - curr.position); - auto [p0_dist, p0_near_l, p0_x] = unscaled_prev_layer.template distance_from_lines_extra( - p0.template cast()); - ExtendedPoint new_p{}; - new_p.position = p0; - new_p.distance = float(p0_dist + boundary_offset); - // ORCA: only create a new point in the path if the new point overhang distance will be used to generate a speed change - // or if this option is disabled (min_distance<=0) - if( (std::abs(p0_dist) > min_distance) || (min_distance<=0)){ - // ORCA: also filter out points that are introduced to the start of the path when their distance from the start point is - // not meaningful - if ((p0 - curr.position).norm() > min_spacing && (next.position - p0).norm() > min_spacing) { - new_points.push_back(new_p); - } - } + // Up to two cut points, in order along the line. Each takes its own overhang distance, so every + // piece prints by the overhang distances at its own two ends. t0 >= 1 or t1 <= 0 falls on the + // line's own end, so there is no cut. A cut closer than min_spacing to either end of the line is + // not meaningful and is filtered out (#6714). + ExtendedPoint cut[2]{}; + bool keep[2] = {false, false}; + for (int k = 0; k < 2; ++k) { + const double t = k == 0 ? t0 : t1; + if (k == 0 ? t >= 1.0 : t <= 0.0) + continue; + const Vec p = curr.position + t * (next.position - curr.position); + auto [p_dist, p_near_l, p_x] = unscaled_prev_layer.template distance_from_lines_extra( + p.template cast()); + cut[k].position = p; + cut[k].distance = float(p_dist + boundary_offset); + keep[k] = (p - curr.position).norm() > min_spacing && (next.position - p).norm() > min_spacing; } - if (t1 > 0.0) { - Vec p1 = curr.position + t1 * (next.position - curr.position); - auto [p1_dist, p1_near_l, p1_x] = unscaled_prev_layer.template distance_from_lines_extra( - p1.template cast()); - ExtendedPoint new_p{}; - new_p.position = p1; - new_p.distance = float(p1_dist + boundary_offset); - // ORCA: only create a new point in the path if the new point overhang distance will be used to generate a speed change - // or if this option is disabled (min_distance<=0) - if( (std::abs(p1_dist) > min_distance) || (min_distance<=0)){ - // ORCA: filter out points that are introduced to the end of the path when their distance from the end point is - // not meaningful - if ((p1 - curr.position).norm() > min_spacing && (next.position - p1).norm() > min_spacing) { - new_points.push_back(new_p); - } - } + if (distance_to_speed) { + // Only keep a cut that changes the G-code: one that prints differently from at least one of the + // points either side of it, which are the line's ends or the other cut where that is kept. A cut + // that prints like both would only split a move into two identical ones. + if (keep[0]) + keep[0] = !same_speed_and_fan(cut[0].distance, curr.distance) || + !same_speed_and_fan(cut[0].distance, keep[1] ? cut[1].distance : next.distance); + if (keep[1]) + keep[1] = !same_speed_and_fan(cut[1].distance, keep[0] ? cut[0].distance : curr.distance) || + !same_speed_and_fan(cut[1].distance, next.distance); + // Two cuts closer together than min_spacing would leave a micro segment between them, so only the + // first is kept. + if (keep[0] && keep[1] && (cut[1].position - cut[0].position).norm() <= min_spacing) + keep[1] = false; } + for (int k = 0; k < 2; ++k) + if (keep[k]) + new_points.push_back(cut[k]); } } new_points.push_back(next); @@ -423,7 +447,10 @@ public: const ConfigOptionFloatsOrPercents &speeds, float ext_perimeter_speed, float original_speed, - bool slowdown_for_curled_edges) + bool slowdown_for_curled_edges, + // Overlap at or below which the overhang fan switches on; negative when the fan + // does not depend on overlap. + float fan_overlap_threshold = -1.0f) { size_t speed_sections_count = std::min(overlaps.values.size(), speeds.values.size()); std::vector> speed_sections; @@ -463,7 +490,8 @@ public: } } - // If a meaningful (i.e. needing slowdown) overhang distance was not found, then we shouldn't split the lines + // If no overhang distance slows this path down, -1 turns interior sampling off unless the overhang fan can switch. + // Lines are only split where an end prints slower or cooled, so here only a fan switch splits them. if (!found) smallest_distance_with_lower_speed=-1.f; @@ -487,9 +515,17 @@ public: return round(final_speed); }; + // ORCA: The speed sections are built from ext_perimeter_speed, which can be above the speed this path prints at + // (original_speed, e.g. held down by resonance avoidance). Every segment is capped at original_speed below, so + // overhang distances whose speeds differ only above it print the same and must not count as a speed change when + // the path is split. + auto effective_speed = [&calculate_speed, original_speed](float distance) { + return std::min(calculate_speed(distance), original_speed); + }; + std::vector> extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1, - smallest_distance_with_lower_speed, calculate_speed); + smallest_distance_with_lower_speed, effective_speed, fan_overlap_threshold); const auto width_inv = 1.0f / path.width; std::vector processed_points; processed_points.reserve(extended_points.size()); diff --git a/tests/fff_print/test_extrusion_processor.cpp b/tests/fff_print/test_extrusion_processor.cpp index 76e331d66a..47f9a62ecf 100644 --- a/tests/fff_print/test_extrusion_processor.cpp +++ b/tests/fff_print/test_extrusion_processor.cpp @@ -244,6 +244,44 @@ float furthest_reading(const std::vector>& points) })->distance; } +// A wall along a supported edge of the previous layer, ending past or just short of the edge's end. Crossing the edge's +// end reads half a line width out. +constexpr double edge_run_length = 64.; // mm, wall start, measured from the end of the previous layer's edge +constexpr double edge_step = 0.384; // mm, how far this layer's contour extends past the previous layer's end +// The centreline is inset half a line width from the contour. +constexpr double edge_wall_end_past = edge_step - 0.5 * caged_wall_width; +constexpr double edge_wall_end_short = 0.05; // mm short of the edge, reading 0.21 - 0.05 = 0.16mm out +// Segmentation splits 1.5 line widths plus the end's reading from an end, so an end's slowdown and cooling stay within this. +constexpr double edge_affected_length = 3. * caged_wall_width; + +std::vector> sampled_wall_along_edge(double wall_end_x, + const std::function& distance_to_speed, + float min_distance, + float fan_overlap_threshold) +{ + const AABBTreeLines::LinesDistancer prev_layer(std::vector{ + {{0., 0.}, {edge_run_length + 10., 0.}}, + {{edge_run_length + 10., 0.}, {edge_run_length + 10., -10.}}, + {{edge_run_length + 10., -10.}, {0., -10.}}, + {{0., -10.}, {0., 0.}}, + }); + const double wall_y = -0.5 * caged_wall_width; + const Points wall{Point::new_scale(edge_run_length, wall_y), Point::new_scale(wall_end_x, wall_y)}; + + return estimate_points_properties(wall, prev_layer, caged_wall_width, -1.f, min_distance, + distance_to_speed, fan_overlap_threshold); +} + +// Length printed with the overhang fan on: segments with either end's overlap at or below the threshold. +double cooled_length(const std::vector>& points, float fan_overlap_threshold) +{ + double length = 0.; + for (size_t i = 0; i + 1 < points.size(); ++i) + if (1.f - std::max(points[i].distance, points[i + 1].distance) / float(caged_wall_width) <= fan_overlap_threshold) + length += (points[i + 1].position - points[i].position).norm(); + return length; +} + DynamicPrintConfig caged_overhang_config(const char* wall_generator){ DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ @@ -431,6 +469,61 @@ TEST_CASE("A supported wall between overhanging corners is slowed no further tha REQUIRE(sampled <= unsampled); } +// Regression: the line up to a step past the previous layer was not split, so the step's slowdown and cooling covered the +// whole wall. The split required an end reading beyond where the slowdown begins, and an edge crossing reads exactly +// there when the wall speed is held below the reference speed (e.g. resonance avoidance). +TEST_CASE("A wall stepping past the previous layer is slowed and cooled only beside the step", "[ExtrusionProcessor][Regression]") +{ + const float crossing_reading = 0.5f * float(caged_wall_width); + const std::function distance_to_speed = [crossing_reading](float distance) { + return distance < crossing_reading ? 70.f : 15.f; + }; + const float fan_overlap_threshold = 0.75f; // The fan switches on at a 25% overhang + + const std::vector> points = sampled_wall_along_edge(-edge_wall_end_past, distance_to_speed, crossing_reading, + fan_overlap_threshold); + const double slowed = slowed_length(points, distance_to_speed); + const double cooled = cooled_length(points, fan_overlap_threshold); + + REQUIRE(slowed > 0.); + REQUIRE(cooled > 0.); + REQUIRE(slowed < edge_affected_length); + REQUIRE(cooled < edge_affected_length); +} + +// Regression: the fan can switch on at a smaller overhang than the first slowdown. Splitting only on speed changes left +// the whole wall cooled when its end read between the two. +TEST_CASE("A wall is split where only the overhang fan changes", "[ExtrusionProcessor][Regression]") +{ + const float crossing_reading = 0.5f * float(caged_wall_width); + const std::function distance_to_speed = [crossing_reading](float distance) { + return distance < crossing_reading ? 70.f : 15.f; + }; + // The end reads 0.16mm out (overlap 0.62): cooled at a 25% threshold, but not slowed. + const float fan_overlap_threshold = 0.75f; + + const std::vector> points = sampled_wall_along_edge(edge_wall_end_short, distance_to_speed, crossing_reading, + fan_overlap_threshold); + const double cooled = cooled_length(points, fan_overlap_threshold); + + REQUIRE_THAT(slowed_length(points, distance_to_speed), Catch::Matchers::WithinAbs(0., 1e-9)); + REQUIRE(cooled > 0.); + REQUIRE(cooled < edge_affected_length); +} + +// With one speed and a fan threshold no reading reaches, only the wall's ends and the edge crossing remain. +TEST_CASE("A wall is left whole where neither its speed nor its cooling changes", "[ExtrusionProcessor]") +{ + const std::function distance_to_speed = [](float) { return 70.f; }; + // 95% overhang; the step reads 0.384mm out (overlap 0.09). + const float fan_overlap_threshold = 0.05f; + + const std::vector> points = sampled_wall_along_edge(-edge_wall_end_past, distance_to_speed, -1.f, + fan_overlap_threshold); + + REQUIRE(points.size() == 3); +} + TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){ const char* wall_generator = GENERATE("classic", "arachne"); From 00bde9265bb6602e17439d68e20c6f203d8d154d Mon Sep 17 00:00:00 2001 From: Kenneth Raplee <101818165+kenrap@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:42:25 +0000 Subject: [PATCH 13/38] Add missing OCCT (8.x) header includes (#15825) * Add missing TopTools includes to GeometryEngine.cpp * Add missing TDF_LabelSequence include in STEP.cpp --------- Co-authored-by: yw4z --- src/libslic3r/CAD/GeometryEngine.cpp | 2 ++ src/libslic3r/Format/STEP.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/src/libslic3r/CAD/GeometryEngine.cpp b/src/libslic3r/CAD/GeometryEngine.cpp index ede3924056..2b923938c2 100644 --- a/src/libslic3r/CAD/GeometryEngine.cpp +++ b/src/libslic3r/CAD/GeometryEngine.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/libslic3r/Format/STEP.cpp b/src/libslic3r/Format/STEP.cpp index 22e39b207f..8305174e73 100644 --- a/src/libslic3r/Format/STEP.cpp +++ b/src/libslic3r/Format/STEP.cpp @@ -25,6 +25,7 @@ #include "XCAFDoc_DocumentTool.hxx" #include "XCAFDoc_ShapeTool.hxx" #include "XCAFApp_Application.hxx" +#include "TDF_LabelSequence.hxx" #include "TopoDS_Solid.hxx" #include "TopoDS_Compound.hxx" #include "TopoDS_Builder.hxx" From f3a8f711fd4e57b685470b514428c3b6ddc0e754 Mon Sep 17 00:00:00 2001 From: Kenneth Raplee <101818165+kenrap@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:44:32 +0000 Subject: [PATCH 14/38] Catch Standard_Failure before std::exception (OCCT >= 8) (#15826) Co-authored-by: yw4z --- src/libslic3r/CAD/CadDocument.cpp | 4 ++-- src/slic3r/GUI/CAD/DesignPanel.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/CAD/CadDocument.cpp b/src/libslic3r/CAD/CadDocument.cpp index c20b636ae7..ea20ffd966 100644 --- a/src/libslic3r/CAD/CadDocument.cpp +++ b/src/libslic3r/CAD/CadDocument.cpp @@ -2673,10 +2673,10 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, ridge = pipe.Shape(); have_ridge = !ridge.IsNull(); } + } catch (const Standard_Failure&) { + have_ridge = false; // OCCT failure — on OCCT >= 8 Standard_Failure derives from std::exception, so this handler must come first } catch (const std::exception&) { have_ridge = false; // fall back to the bare cylinder/bore below - } catch (const Standard_Failure&) { - have_ridge = false; // OCCT failure (not a std::exception) — must be caught here too } if (f.thread_internal) { diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 0b4f86a618..d247ddfd26 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -4862,13 +4862,13 @@ void DesignPanel::on_import_mesh() try { shape = GeometryEngine::mesh_to_brep(mesh.its, MESH_IMPORT_TOLERANCE, MESH_IMPORT_MERGE_ANGLE_DEG, stats); - } catch (const std::exception& e) { - fail(_L("Mesh conversion failed: ") + wxString::FromUTF8(e.what())); - return; - } catch (const Standard_Failure& e) { // OCCT throws outside std::exception + } catch (const Standard_Failure& e) { // on OCCT >= 8 Standard_Failure derives from std::exception — must precede that handler fail(_L("Mesh conversion failed: ") + wxString::FromUTF8( e.GetMessageString() ? e.GetMessageString() : "OCCT error")); return; + } catch (const std::exception& e) { + fail(_L("Mesh conversion failed: ") + wxString::FromUTF8(e.what())); + return; } if (shape.IsNull()) { fail(_L("Mesh conversion produced no geometry")); return; } From 576cce2f72d2191cbbaa9ffe96ae0a4a2c588f2b Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 28 Sep 2026 18:47:41 +0800 Subject: [PATCH 15/38] test: bump elegoo profile to test OFL OTA E2E [TO BE REVERTED] (#15954) Merged by /bot merge on behalf of @peachismomo (id 52488812). Grants: resources/profiles/OrcaFilamentLibrary/filament/Elegoo, resources/profiles/OrcaFilamentLibrary.json, resources/profiles/Elegoo, resources/profiles/Elegoo.json Head: d4a18c633e31600b16b848d42799a93e96ddd67f --- resources/profiles/OrcaFilamentLibrary.json | 2 +- .../OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA @base.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/profiles/OrcaFilamentLibrary.json b/resources/profiles/OrcaFilamentLibrary.json index 3d8a191dc6..f54e45df5a 100644 --- a/resources/profiles/OrcaFilamentLibrary.json +++ b/resources/profiles/OrcaFilamentLibrary.json @@ -1,6 +1,6 @@ { "name": "OrcaFilamentLibrary", - "version": "02.04.00.12", + "version": "02.04.00.13", "force_update": "0", "description": "Orca Filament Library", "filament_list": [ diff --git a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA @base.json b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA @base.json index 3ba4c8c14f..624c4ed95b 100644 --- a/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA @base.json +++ b/resources/profiles/OrcaFilamentLibrary/filament/Elegoo/Elegoo PLA @base.json @@ -6,7 +6,7 @@ "filament_id": "OFvgE0Zh", "instantiation": "false", "fan_cooling_layer_time": [ - "80" + "81" ], "fan_min_speed": [ "50" From 2ccabb5695a8af6dec696785aad824c40ce61fd9 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 28 Sep 2026 13:08:23 +0800 Subject: [PATCH 16/38] add multi variant to orca-profiles skill add symbol link .agents --- .agents | 1 + .claude/skills/orca-profiles/SKILL.md | 230 +++++--- .../references/extruder-variants.md | 548 ++++++++++++++++++ .../references/filament-profiles.md | 333 ++++++----- .../skills/orca-profiles/references/ids.md | 145 ++--- .../references/machine-profiles.md | 222 ++++--- .../skills/orca-profiles/references/naming.md | 136 ++--- .../references/process-profiles.md | 158 ++--- .../references/review-checklist.md | 274 +++++---- .../orca-profiles/references/shared-bases.md | 254 ++++++++ .../orca-profiles/references/validation.md | 474 ++++++++++----- .../orca-profiles/references/vendor-bundle.md | 231 +++++--- .../orca-profiles/scripts/shared_settings.py | 241 ++++++++ 13 files changed, 2368 insertions(+), 879 deletions(-) create mode 120000 .agents create mode 100644 .claude/skills/orca-profiles/references/extruder-variants.md create mode 100644 .claude/skills/orca-profiles/references/shared-bases.md create mode 100644 .claude/skills/orca-profiles/scripts/shared_settings.py diff --git a/.agents b/.agents new file mode 120000 index 0000000000..c8161850a4 --- /dev/null +++ b/.agents @@ -0,0 +1 @@ +.claude \ No newline at end of file diff --git a/.claude/skills/orca-profiles/SKILL.md b/.claude/skills/orca-profiles/SKILL.md index 3b2130660f..40ab8c9927 100644 --- a/.claude/skills/orca-profiles/SKILL.md +++ b/.claude/skills/orca-profiles/SKILL.md @@ -1,78 +1,127 @@ --- name: orca-profiles -description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job. +description: Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id, and moving settings that sibling presets repeat onto shared bases after fix-variant or while drafting. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job. --- # OrcaSlicer system profiles -A bundle is `resources/profiles/.json` plus `/`. The vendor id is the -filename stem, not the index's display `name`. The index is the loader's only entry point: -unindexed presets never load. `OrcaFilamentLibrary` is the shared filament bundle; -`blacklist.json` is data, not a bundle. +This skill describes how OrcaSlicer system profiles are drafted and shaped: the rules, equations and +patterns a profile follows. Use it to draft new profiles, modify existing ones, fix profile issues and +review profile changes. -## Choose the reference for the task +A bundle is the index `resources/profiles/.json` plus the folder `/`. The vendor id is +the filename stem (`BBL`), not the index's display `name` (`Bambulab`). The index is the loader's only +entry point: an unindexed preset never loads. `OrcaFilamentLibrary` is the shared filament bundle, +loaded first; `blacklist.json` is data, not a bundle. -Read the relevant reference before editing; load others only when the task crosses those areas. -Paths below are relative to this skill. Commands run from the repository root. +## References + +Read the reference for the task before editing; load others only when the task crosses into them. +Paths below are relative to this skill. Commands run from the repository root; on Windows use `py -3` +for `python3`. | Task | Read | | --- | --- | -| Add or tune a filament, brand or material; fix compatibility / alias shadowing | [filament-profiles.md](references/filament-profiles.md) | -| Add a printer or nozzle; change models, variants, assets or extruder vectors | [machine-profiles.md](references/machine-profiles.md) | +| Add or tune a filament, brand or material; fix compatibility, alias shadowing or overlapping coverage | [filament-profiles.md](references/filament-profiles.md) | +| Add a printer or nozzle; change models, variants, assets or per-extruder vectors | [machine-profiles.md](references/machine-profiles.md) | +| Add or tune extruder variants (`extruder_type` Direct Drive / Bowden × nozzle volume type Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow variants) on a printer, process or filament | [extruder-variants.md](references/extruder-variants.md) | | Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) | -| Create a vendor bundle; diagnose loading or inheritance; migrate preset names | [vendor-bundle.md](references/vendor-bundle.md) | -| Name a preset; check what a name must equal | [naming.md](references/naming.md) | +| Draft several presets, or clean up after `fix-variant`: which base each shared value belongs on, when a new base pays off, proving nothing loads differently | [shared-bases.md](references/shared-bases.md) | +| Name a preset; check what a name must equal; base names, uniqueness, filenames | [naming.md](references/naming.md) | +| Create a vendor bundle; index, `version`, `inherits`, `include`; migrate preset names; diagnose why a bundle fails to load | [vendor-bundle.md](references/vendor-bundle.md) | | Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes | -| Review a profile diff | [review-checklist.md](references/review-checklist.md) | | Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) | +| Review a profile diff | [review-checklist.md](references/review-checklist.md) | -## Golden rules +## Rules -1. **Bump every changed bundle's `version`**, including `OrcaFilamentLibrary.json` when affected. - Increment the last component; carry `.99` into the third component (`02.04.00.99` → - `02.04.01.00`). The updater requires a strictly newer version. CI does not check this. -2. **Register every preset, bases included, parents before children.** `update-index` generates - the four `*_list` arrays; `check` requires its output. Index names must equal file `name` fields. -3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New - presets normally omit them until `generate-id`; bases must have no `setting_id`. - BBL's authoritative `setting_id` and a wrongly inherited `filament_id` need the explicit - handling in [ids.md](references/ids.md). -4. **Load failures can discard a whole vendor bundle.** Broken `inherits`, missing indexed files, - duplicate names, invalid model/variant references and unresolved filament ids affect more than - the edited preset. Inheritance stays within a bundle, except filaments may inherit the library. -5. **Preserve shipped selectable names.** Renaming, deleting or changing `instantiation` from - `"true"` to `"false"` needs `renamed_from` on a selectable successor. It is a `;`-separated string; - update in-tree references too. See [migration rules](references/vendor-bundle.md#renamed_from). -6. **Compatibility uses exact printer variant names.** Every instantiated non-library filament - needs a non-empty `compatible_printers` in its own file. Library fallbacks may omit it; - library printer-specific tunes use a non-empty list. One variant may be claimed by only one - profile per filament product (`filament_id`); an overlap is resolved by moving the variant to the - most specific preset, which is preferred over deleting a profile. See - [one variant, one profile](references/filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product). -7. **Preset values are strings or arrays of strings.** Use `"instantiation": "false"`, not `false`. - Model `nozzle_diameter` is a `;`-separated string; machine `nozzle_diameter` is an array. - Wrong types can abort loading; see [failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius). -8. **Verify setting keys against the code.** Unknown keys are silently discarded. Check - `PrintConfig.cpp` definitions and `PrintConfigDef::handle_legacy`; neighbours can contain dead - keys. `normalize` removes known obsolete keys, but does not detect arbitrary misspellings. -9. **Run the full profile checks before reporting completion.** A vendor-scoped pass is only a - development loop. Review also covers version bumps, assets, non-default processes and hardware - tuning that CI cannot establish. -10. **One all-printer preset per product; color is a runtime property, never a preset.** Never ship - presets that differ only by color — CI accepts them, so this is a review call. See - [color is a runtime property](references/filament-profiles.md#color-is-a-runtime-property). +1. **Bump the `version` of every bundle you change**, `OrcaFilamentLibrary.json` included when affected. + Increment the last component and carry `.99` into the third (`02.04.00.99` → `02.04.01.00`). The + updater installs only a strictly newer version, and CI does not check the bump. +2. **Register every preset, bases included, parents before children.** `update-index` writes the four + `*_list` arrays from the files on disk; `check` fails unless the index equals its output. Each index + entry's `name` must equal the file's `name`. +3. **Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New presets + normally omit them until `generate-id`; bases carry no `setting_id`. BBL's own `setting_id`s and a wrongly + inherited `filament_id` need the explicit handling in [ids.md](references/ids.md). +4. **A name is an identity; preserve shipped selectable names.** Every reference (`inherits`, + `compatible_printers`, `default_*`, `printer_model`) is the exact, case-sensitive `name`. Renaming or + deleting a shipped selectable preset, or flipping its `instantiation` from `"true"` to `"false"`, + needs `renamed_from` (a `;`-separated string) on a selectable successor + ([migration rules](references/vendor-bundle.md#renamed_from)); update in-tree references too. +5. **Values are strings or arrays of strings.** `"instantiation": "false"`, never `false`. A + `machine_model`'s `nozzle_diameter` is a `;`-separated string; a `machine`'s is an array. Custom + G-code is one string. Wrong types can abort loading of the bundle or of every vendor + ([failure scopes](references/vendor-bundle.md#failure-scopes)). +6. **Unknown keys are dropped silently.** Confirm every new key exists in + `src/libslic3r/PrintConfig.cpp`; a key a neighbouring file writes is no evidence it exists. `check` rejects, + and `normalize` removes, known obsolete keys, but neither detects an arbitrary misspelling. A key + missing from the definitions may be a legacy name the loader still renames + (`tool_change_gcode` → `change_filament_gcode`) or whose value it rewrites (`DirectDrive` → + `Direct Drive`); check `PrintConfigDef::handle_legacy` before removing one, and write the current name + in new edits. +7. **Write overrides only.** Inherit the bundle's bases and restate just what differs; follow the + bundle's existing layering and style, except that a new filament prefers the library's bases. A + value that every preset of a group shares goes on the group's base + ([shared bases](references/shared-bases.md)). +8. **One load error can discard a whole vendor bundle**: an unresolved `inherits`, a missing indexed + file, two selectable presets with one name, an unknown `printer_model` or `printer_variant`, a + filament with no resolvable `filament_id`, `nil` in a non-nullable key. `inherits` and `include` + resolve only inside the bundle, except that filaments may inherit from OrcaFilamentLibrary. +9. **Filament compatibility names exact printer variants.** Every instantiated filament outside the + library writes a non-empty `compatible_printers` in its own file. Library fallbacks may omit it; + library printer-specific tunes use a non-empty list. One variant may be claimed by only one preset + per filament product (`filament_id`); an overlap is resolved by moving the variant to the most + specific preset, which is preferred over deleting a preset + ([one variant, one profile](references/filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)). +10. **One all-printer preset per product; colour is a runtime property, never a preset.** Never ship + presets that differ only by colour. CI accepts them, so this is a review call + ([colour](references/filament-profiles.md#colour-is-a-runtime-property)). +11. **Write a variant key at full width or not at all.** A key in the four variant sets holds exactly + `N` values in the selectable preset that writes it, `N = S × k` in the + [sizing equation](references/extruder-variants.md#sizing-equation): one per variant, a (normal, + silent) pair per variant for the `machine_max_*` limits; one value is not "the same for every + variant". Profiles must be correct as written: `check` judges each selectable preset by the + equation, never by what the loader pads or cuts, and `check --strict` also holds what a preset + inherits to its own width, as BBL writes it. A base is never judged on its own: its array widths + count, under `--strict`, where they reach a preset, while the id and layout rules judge the + composed preset, inherited values included, without it. Declare the variant layout on a + multi-extruder printer whose extruders need different values + ([widths](references/extruder-variants.md#widths)). +12. **Run the full checks before reporting completion.** A `--vendor` run is only a development loop. + Review also covers version bumps, assets, non-default processes and hardware tuning, which CI cannot + establish. + +## Names + +| Type | Shape | What the loader uses | +| --- | --- | --- | +| `machine_model` | `` (`Bambu Lab X1 Carbon`) | the exact string, named by each variant's `printer_model`; also the `_cover.png` stem | +| `machine` | ` nozzle` | the exact string, named by `compatible_printers`; `printer_variant` holds the nozzle token (`0.4`, [rules](references/machine-profiles.md#printer_model-and-printer_variant)) | +| `process` | `mm @` | the exact string when referenced or selected; `@` is a label, compatibility comes from the preset's list or condition | +| `filament` | ` @` | text before the first `@` is the alias (shadowing, `filament_id`); the rest is a label, with reserved targets `@base` and `@System` | + +The shapes are convention; `check` enforces only uniqueness. Per-type conventions, base names and +filename rules are in [naming.md](references/naming.md). ## Creating or modifying a profile -1. **Inspect the diff and neighbouring presets.** Read their `name`, parent chain and children; - edits to a base or a leaf with descendants propagate. Match the bundle's structure and write +1. **Inspect the diff and the neighbouring presets.** Read their `name`, parent chain and children: + edits to a base, or to a leaf that others inherit, propagate. Match the bundle's structure and write only overrides. New files use tab indentation, LF and a trailing newline; preserve unrelated - formatting in existing files. Match filename case exactly and use cross-platform names. -2. **Author explicit metadata.** Set `type` yourself, especially for `machine` vs `machine_model`. - Use `"from": "system"` and string `instantiation` on config presets. Omit ids on new presets - unless [ids.md](references/ids.md) requires special handling; retain them on existing ones. - Complete compatibility, defaults, assets and any rename migration using the task reference. -3. **Bump the version**, then run the authoring commands in order for each affected bundle: + formatting in existing files. Match filename case exactly and use + [cross-platform names](references/naming.md#filenames-and-paths). +2. **Author explicit metadata.** Set `type` yourself (`machine` vs `machine_model` especially), and use + `"from": "system"` and a string `instantiation` on config presets. Omit ids on new presets unless + [ids.md](references/ids.md) requires special handling; retain them on existing ones. Complete + compatibility, defaults, assets and any rename migration using the task reference. +3. **Put shared values on shared bases** when drafting several presets, and after `fix-variant`, which + widens an array in every preset that writes it and moves nothing. Each value goes on the base of the + level that determines it, a new base only where it pays for itself, and a restructure must leave + every selectable preset loading what it loaded: `snapshot` before the edit, `compare` after it + ([shared-bases.md](references/shared-bases.md)). +4. **Bump the version**, then run the authoring commands in order for each affected bundle, reading + every diff and resolving every error before moving on: ```bash python3 scripts/orca_profile_tool.py normalize --vendor "" @@ -81,46 +130,59 @@ Paths below are relative to this skill. Commands run from the repository root. python3 scripts/orca_profile_tool.py check ``` - Writing commands support `--dry-run`. Inspect their diffs: `normalize` changes content and can - reformat entire files. Stop and resolve command errors before proceeding. - - **Do not use `trim` in this workflow:** it can delete newly authored, unindexed profiles. - Do not use `normalize --force` for routine edits. -4. **Validate:** + Writing commands accept `--dry-run`. `normalize` changes content and can reformat entire files; + rerun `update-index` after any change to `inherits` or `include`, since it orders by them. + **Do not use `trim` in this workflow:** it deletes unindexed files, including one you just added. Do + not use `normalize --force` for routine edits. An error in a bundle you did not touch predates your + change: confirm it on a clean checkout and report it rather than fixing it in the same change. +5. **Validate:** ```bash ./scripts/check_profile.sh --vendor "" # development loop ./scripts/check_profile.sh # full tree before the PR ``` - On Windows use `py -3` instead of `python3`, and `scripts\check_profile.bat -Vendor ""` - / `scripts\check_profile.bat`. Logs land in a per-user cache dir (see - [validation.md](references/validation.md)). - Id checks remain tree-wide under `--vendor`; filament-only bundles skip the default slice check. - See [validation.md](references/validation.md) for flags, coverage and error remedies. -5. **Verify the changed behavior.** Slice newly added non-default processes explicitly, and - [test in the app](references/validation.md#testing-in-the-app) for selection or UI behavior. - Report checks actually run, failures/skips and any hardware tuning still unverified. + On Windows use `scripts\check_profile.bat -Vendor ""` / `scripts\check_profile.bat`. Logs + land in a per-user cache dir ([validation.md](references/validation.md)). Id checks stay tree-wide + under `--vendor`, and filament-only bundles skip the default slice check. Under `--vendor` read only + `profile_tool` and `validate_slice`: the other three checks fail on library presets that name other + vendors' printers ([why](references/validation.md#the-five-checks)). +6. **Verify the changed behaviour.** Slice newly added non-default processes and filaments + [explicitly](references/validation.md#checking-a-copy-of-the-tree), and + [test in the app](references/validation.md#testing-in-the-app) for selection or UI behaviour. Report + the checks actually run, their failures and skips, and any hardware tuning still unverified. -## Symptom → first reference +## Symptom → first look | Symptom | Start here | | --- | --- | -| A vendor disappears | Loader log / `validate_system`; [bundle failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius) | -| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` | -| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility | -| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) | -| Presets differ only by color, or an all-printer library preset lacks `@System` | [Color is a runtime property](references/filament-profiles.md#color-is-a-runtime-property) | -| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) | -| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) | -| A check fails | [Error → remedy](references/validation.md#error--remedy) | +| A vendor disappears | the app's log or the `validate_system` log; [failure scopes](references/vendor-bundle.md#failure-scopes) | +| A setting has no effect | key spelling or a legacy name (`PrintConfigDef::handle_legacy`), value type, or a config key placed on a `machine_model` | +| A preset exists but is not selectable | index registration, `instantiation`, whether it is installed (chosen in the setup wizard, or listed in the model's `default_materials`), compatibility | +| A filament is missing, duplicated, or matches the wrong spool | [compatibility and alias shadowing](references/filament-profiles.md#compatible_printers), [ids](references/ids.md) | +| Presets differ only by colour, or an all-printer library preset lacks `@System` | [colour is a runtime property](references/filament-profiles.md#colour-is-a-runtime-property) | +| High Flow (or a second extruder) slices with Standard (or extruder 1) values; a variant switch is missing; a variant's tuned values never arrive | [extruder variants](references/extruder-variants.md#variant-strings), [widths](references/extruder-variants.md#widths), [variant names](references/validation.md#variant-names) | +| Values land on the wrong extruder or mode after a variant was added | [inserting a variant](references/extruder-variants.md#adding-a-variant-inserts-its-values-at-its-variant-index), [padding and composition](references/extruder-variants.md#padding-truncation-and-composition) | +| A resolved value matches neither the file nor its `inherits` parent; an array is not the width the file wrote, or a child got only a base's first value | [composition order and `include`](references/vendor-bundle.md#inherits-and-include) | +| A bed temperature is ignored | [the twelve plate keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) | +| A change is absent from the running app | version bump and [installed profile location](references/validation.md#testing-in-the-app) | +| A check fails | [error → remedy](references/validation.md#error--remedy) | ## Source of truth -When guidance and behavior disagree, inspect the current checkout: -`scripts/orca_profile_tool.py` for tooling and flags; `src/libslic3r/Preset*.cpp` for loading and -compatibility; `src/libslic3r/PrintConfig.cpp` for setting types and legacy handling; -`src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` for -validation coverage. `docs/HLSD/filament_id.md` defines filament identity. The -[profile development guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md) +When this skill and the checkout disagree, the checkout wins: `scripts/orca_profile_tool.py` for the +tool and its flags; `src/libslic3r/Preset*.cpp` for loading and compatibility; +`src/libslic3r/PrintConfig.cpp` for keys, types, nullable options, the variant key sets and legacy +handling; `src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` +for validation coverage; `docs/HLSD/filament_id.md` for filament identity. The wiki's +[profile guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md) is a tutorial; confirm loader and CLI details against these sources. + +## Editing this skill + +The skill describes what a profile must be, not what the shipped tree currently is. State rules, +equations, patterns and profile shapes; never inventories of shipped defects, counts, dated +measurements or lists of which vendors do what. Test each sentence: if editing the profiles alone, +with the engine and tool unchanged, could make it false, state the rule behind it or give a generic +example instead. Example files named as models to copy, and commit hashes cited as the reason for a +rule, are fine. diff --git a/.claude/skills/orca-profiles/references/extruder-variants.md b/.claude/skills/orca-profiles/references/extruder-variants.md new file mode 100644 index 0000000000..322d79d56f --- /dev/null +++ b/.claude/skills/orca-profiles/references/extruder-variants.md @@ -0,0 +1,548 @@ +# Extruder variants + +Use this when a printer's hotend or extruder can be in more than one hardware configuration that +needs different settings: a Standard and a High Flow nozzle, a TPU High Flow, E3D High Flow or Extra High Flow nozzle, or +a Direct Drive and a Bowden extruder on one machine. Variants let one printer, process and filament preset carry a +separate value per configuration; the user picks the configuration in the sidebar, and slicing uses +the matching values. They are not for nozzle **diameter**: that stays one `machine` preset per +`printer_variant`. + +## Variant strings + +- A **variant string** is `" "`: the extruder's `extruder_type` + value, a space, and its nozzle volume type (the project's `nozzle_volume_type`, seeded by the + printer preset's `default_nozzle_volume_type`), e.g. `"Direct Drive High Flow"`. +- A **variant** is one entry of a preset's variant list, named by its variant string (plus an extruder + id for printer and process lists). A variant-aware key holds **one value per variant** (a + (normal, silent) pair for the `machine_max_*` limits), and each preset declares its variants in + that list. Slicing picks, for each extruder, the variant whose + variant string equals the current `extruder_type` + nozzle volume type. +- Matching is an **exact string compare** of the whole string (plus the extruder id for printer and + process lists) against the preset's own resolved list, in any order. With no match the **first + variant** is used, silently: variant index 0 for printer and process keys (extruder 1's first + variant, whichever extruder asks), the filament's own first variant for filament keys. Printer and + process keys are matched only on a printer with several extruders or whose + `extruder_variant_list` offers several variant strings (`support_different_extruders`); on any + other printer their variant index 0 is read whatever it names. Nothing rejects a profile for a + variant mismatch; mistakes surface only as wrong values in the G-code. +- Variant index and array length follow [Widths](#widths). + +The complete enum is `s_keys_map_ExtruderType` and `s_keys_map_NozzleVolumeType` in +`src/libslic3r/PrintConfig.cpp` (`grep -A5 s_keys_map_NozzleVolumeType` there to confirm): + +| Part | Values | +| --- | --- | +| Extruder type | `Direct Drive`, `Bowden` | +| Nozzle volume type | `Standard`, `High Flow`, `TPU High Flow`, `E3D High Flow`, `Extra High Flow` (`Hybrid` exists but is runtime-only) | + +So the ten legal variant strings are the two extruder types × the five writable nozzle volume types, +and this table is the whole test of legality: a string's presence in a shipped profile is no evidence +for it. `Hybrid` (an extruder with several sub-nozzles) is never a variant: a filament on it reads the +variant of its own nozzle volume type from the project's `filament_volume_map`, printer and process +keys get one variant per nozzle volume type the extruder holds (`extruder_nozzle_stats`), and any +other lookup reads `Standard`. Never write it in a variant string. The same per-filament and +per-type reading applies on any extruder once `extruder_nozzle_stats` lists more nozzle volume +types than there are extruders. + +Every other string (a nozzle volume type name from another slicer, a typo, a variant copied from a +shipped profile) is a **dead variant**: nothing selects it, and since lookup is by string it does not +shift the variants beside it; it still counts toward the variant length when arrays are sized. +`orca_profile_tool.py check` reports it as an error in every bundle, BBL included +([variant names](validation.md#variant-names)). Legacy names are errors too: in the four variant +lists, `default_nozzle_volume_type` and `nozzle_volume_type`, the loader still rewrites `Normal` → +`Standard` and `Big Traffic` → `High Flow`, so a ported `Direct Drive Normal` variant would load, but +`check` rejects the spelling and names the enum name to write. `DirectDrive` is rewritten only in +`extruder_type`, so `DirectDrive Standard` in a variant list is dead. Write the enum names; +`normalize` does not convert them. + +### A nozzle the enum does not name + +The nozzle volume types are fixed by the engine, and a profile cannot add one. A nozzle the table does +not name needs the type added in code first, which is outside profile work; until then any string +for it is a dead variant. Once the engine has the type, its variant string is the enum name after +the extruder type, e.g. `"Direct Drive "`, and `check` accepts it with no tool change, since it +reads the enum from `PrintConfig.cpp`. Existing arrays are unaffected +([slice time](#slice-time-and-existing-users)). + +## When to use variants + +| Hardware | Do | +| --- | --- | +| One extruder, one nozzle type | Nothing. Without a variant list the printer has one variant ([variant length](#widths)) and no printer-key lookup takes place; a filament with several variants still gets the one for the printer's variant string. | +| Bowden-only printer | Nothing either. A list-less printer matches no string, so every printer-key lookup reads variant index 0 whether that variant is called `"Bowden Standard"` or `"Direct Drive Standard"`; naming it is cosmetic while it is the printer's only variant. A multi-extruder Bowden printer that declares the layout writes `"Bowden Standard"` variants: `"Direct Drive Standard"` entries match none of its extruders, so every extruder reads variant index 0 (and `check` reports them under [Printer rule 3](#printer-machine)). A filament with a `"Bowden Standard"` variant does get that variant there. | +| Nozzle types the user swaps (Standard / High Flow / TPU High Flow / E3D High Flow / Extra High Flow) | The printer lists them as variants; tune the keys that really differ per nozzle volume type. | +| Extruders of different types on one machine | One `extruder_type` per extruder, each extruder listing its own variants. | +| Several extruders that need different values in a variant key (retraction, z-hop, `nozzle_volume`, the `machine_max_*` limits) | Declare the layout even with a single nozzle volume type: `extruder_variant_list` with one `" Standard"` per extruder, and the flattened pair. Without it the arrays still hold one value per extruder ([variant length](#widths)), but the loader keeps only extruder 1's value of a list-less printer's arrays, so every extruder prints with it. | + +A preset without variant keys keeps working on a variant printer: its single variant is applied to +every extruder. So adding variants to a printer does not break existing processes or library +filaments; it only makes per-variant tuning possible. + +## Printer (`machine`) + +```json +"extruder_type": ["Direct Drive", "Direct Drive"], +"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow,Direct Drive TPU High Flow"], +"printer_extruder_id": ["1", "1", "2", "2", "2"], +"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow", "Direct Drive TPU High Flow"], +"default_nozzle_volume_type": ["Standard", "Standard"] +``` + +Rules: + +1. `extruder_variant_list` has **one entry per extruder**; each entry is the `,`-joined variants that + extruder supports. It is the per-extruder menu the sidebar offers. It is in no + [variant set](#the-four-key-sets), so the variant-length resize leaves it alone. Extruder 1's + first variant, variant index 0 of the flattened pair, is the fallback of every extruder whose + variant is missing when the arrays are collapsed for slicing + ([slice time](#slice-time-and-existing-users)); it is not necessarily the configuration the user + sees, which is `default_nozzle_volume_type` (rule 4). +2. `printer_extruder_variant` is that list **flattened** extruder-major, one entry per variant, and + `printer_extruder_id` gives each entry its 1-based extruder. These two size and address every + variant. Write all three keys and keep them in agreement. At load with + `single_extruder_multi_material` off, and in the app when the printer tab loads a printer with a + different number of extruders, the pair is rebuilt from `extruder_variant_list` (one + `Direct Drive Standard` per extruder when the list is absent) and the variant arrays are resized to + the rebuilt pair, padded with their first value or cut. The resize skips the `machine_max_*` limits + and `hotend_heating_rate` / `hotend_cooling_rate`: they keep their width, and an extruder beyond it + reads their first value, so extruder 2 and up of a list-less printer take extruder 1's normal limit + as their silent one too. With the three in agreement that changes nothing; a pair written without + the list is replaced. A listed variant the pair lacks is a menu choice that reads variant index 0. + - The pair without `extruder_variant_list` slices, but the sidebar offers no variant switch and + the app cannot add variants to a list-less process: nothing is lost while every extruder + has exactly one variant, and every further variant is unreachable. + - A missing or one-value `printer_extruder_id` is extruder 1 at every index + ([the id trap](#padding-truncation-and-composition)) unless the load-time rebuild above + replaces the pair (`single_extruder_multi_material` off). + - `check` reports against this rule ([variant arrays](validation.md#variant-arrays)). Errors: a + pair that is not the flattening, an id array that does not give each entry its extruder, a list + of more than one variant without the pair, and a pair without the list that puts several variants + on one extruder. A pair without the list and one variant per extruder is a warning where the + load-time rebuild would replace it (`single_extruder_multi_material` off, and a pair other than + one `Direct Drive Standard` per extruder), and passes otherwise. +3. Every variant string in an entry must start with that extruder's `extruder_type`. +4. `default_nozzle_volume_type` has one value per extruder and must name a nozzle volume type that + extruder's variants list; it seeds the sidebar. The live choice is `nozzle_volume_type` in the + project config, never a preset key. +5. Size every array by the set its key belongs to (the three sets are listed in full in + [The four key sets](#the-four-key-sets); the lengths are worked through in the + [sizing equation](#sizing-equation)): exactly the length below in each selectable preset that + writes it, or leave the key out and the preset takes what reaches it, the default or its base's + array. One value is no + shorthand for "the same for every variant"; write the value for every variant: + +| Set | Length | Keys | +| --- | --- | --- | +| `printer_extruder_options` | extruders (`E`) | the 8 per-extruder keys outside the variant scheme (`extruder_type`, `nozzle_diameter`, `default_nozzle_volume_type`, …) | +| `printer_options_with_variant_1` | variant length (`S`) | the full list below; not guessable from names | +| `printer_options_with_variant_2` | 2 × variant length | the 16 `machine_max_*` limits, at [stride 2](#widths): a (normal, silent) pair per variant | + +Put the variant layout on the shared base of all printers that share the hardware, and let the +nozzle-diameter siblings inherit it, restating only the variant arrays whose values change. `check` +does not judge a base on its own; its arrays count where they reach a selectable preset, under +`check --strict`. The loader stores a base at its **own** `printer_extruder_variant`, one variant +when it writes none whatever its extruder count, and cuts a wider array to its first values before +any child inherits it; so a multi-extruder base whose extruders need different values declares the +layout itself ([composition](#padding-truncation-and-composition)). When a variant array can move from +the presets to a base is in [shared-bases.md](shared-bases.md#variant-arrays-on-a-base). + +## Process + +1. `print_extruder_variant` + `print_extruder_id` list the (extruder id, variant string) pairs of the + process's variants. A variant is found by that pair, never by position: what is **required** is + that every pair a compatible printer (by list or condition) can select is present, in any order. A + missing pair reads the process's variant index 0 for that extruder, an extra pair is a variant + nothing selects, and neither is reported. The exception is a single-extruder printer whose + `extruder_variant_list` offers one variant string: no pair is matched there and variant index 0 + is read, so a process shared with such a printer lists that printer's pair first. **Mirroring** the printer's `printer_extruder_variant` + + `printer_extruder_id` entry for entry is the convention; follow it, so the arrays compare by eye, + but a different order with every pair present is a nit, not a defect. A process shared by printers + whose pairs differ falls back to variant index 0 on the pairs it lacks; give each layout its own base. +2. Every key in `print_options_with_variant` ([the full list](#the-four-key-sets), which is not + "every speed") has exactly one value per variant, or is left out. `print_extruder_id` needs its + value per variant too: one value pads to extruder 1 everywhere. `check` holds a + `print_extruder_id` that reaches the preset, written or inherited, to one entry per variant on any + printer, without `--strict`, and warns when it is absent and a variant repeats. +3. Only add process variants if speeds or accelerations really differ per nozzle volume type or + extruder. Otherwise omit the variant keys: a one-variant process needs no list, because match and + no-match both read variant index 0, and that variant is copied to every extruder at slice time. +4. Put the lists on the process base for that printer layout, so leaves stay small. + +## Filament + +```json +"filament_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"], +"filament_max_volumetric_speed": ["21", "29"], +"filament_flow_ratio": ["0.98", "0.98"], +"filament_retraction_length": ["nil", "0.4"] +``` + +1. `filament_extruder_variant` lists variants **without extruder ids**: a filament's High Flow variant + is used on whichever extruder is in High Flow. Its entries must be distinct, since the lookup + returns the first equal string and a repeated entry is a variant nothing selects. There is no + filament id key: `filament_extruder_id` exists only as a G-code placeholder, not as a filament + option (its option and its set entry are commented out in `PrintConfig.cpp`), so a filament file that writes it loses the key as unknown. + `filament_extruder_compatibility` is unrelated to variants (it says which extruders the filament + may be loaded into). +2. Every key in `filament_options_with_variant` ([the full list](#the-four-key-sets)) that reaches the + preset, whether written, included or inherited, is resized to its variant count: write it at exactly + that width, or leave it out. Keys outside the set + (`filament_type`, plate temperatures, `fan_max_speed`, `slow_down_min_speed`, `pressure_advance`, + …) are never addressed by variant index; the preset contributes their first value however wide a + file writes them. +3. Cover every variant the material is meant to print on across its `compatible_printers`. Leave out + a variant deliberately when the material should not be tuned for it (e.g. a TPU High Flow variant + only on TPU filaments); an extruder reporting that variant string then reads the first variant. +4. Order the variants like the printer's, Standard first, so the first-variant fallback is the + conservative one. +5. Tune what really differs: `filament_max_volumetric_speed` is the usual difference between nozzle + volume types, then flow ratio, temperature and retraction. Use measured values; never copy the + Standard value into the High Flow variant and call it tuned. +6. `nil` is legal per variant in the nullable override keys, occupies one entry like any value, and + keeps the printer's value for that variant only. +7. Declare a multi-variant list with the arrays it sizes: on the filament, in its own file or in a + template it pulls in with [`include`](vendor-bundle.md#inherits-and-include). The filament may be + compatible with printers that list fewer variants, or none: each printer takes the filament's variant + for its own variant string, else the filament's first variant. A one-variant list on a shared base + is harmless, because one variant is the width a list-less preset has anyway; and a list-less base is + stored at that width, so a wider array on it reaches its children as its first value only. + A key such a base writes reaches a multi-variant child as one value, which the loader spreads over + the child's variants; `check --strict` reports it at the child, which restates it at its own width. + +## Widths + +Every variant key is a flat array addressed by **variant index**: index `n` belongs to entry `n` of +the preset's own variant list (`printer_extruder_variant`, `print_extruder_variant` or +`filament_extruder_variant`). The index is found by exact compare of the string +`" "` (plus the 1-based extruder id for printer and process lists), +and the value is read at `index × stride`: + +| Stride | Keys | Layout | +| --- | --- | --- | +| 1 | `printer_options_with_variant_1`, `print_options_with_variant`, `filament_options_with_variant` | `[variant 0, variant 1, …]` | +| 2 | `printer_options_with_variant_2` (the `machine_max_*` limits) | `[variant 0 normal, variant 0 silent, variant 1 normal, variant 1 silent, …]` | + +So a variant array is `variant length × stride` long. The **variant length** is the number of +entries in the preset's variant list, counted on the preset's config **after** `include` and +`inherits` are applied, dead variants included. An inherited array arrives already resized to the +base's own variant length, an included one at the width its file wrote +([composition](#padding-truncation-and-composition)): + +| Preset | Variant length | +| --- | --- | +| `machine` | `len(printer_extruder_variant)`; without it, the variants `extruder_variant_list` offers; without both, **one per extruder** (`len(nozzle_diameter)`), since the list's default is one `Direct Drive Standard` per extruder. `extruders_count` is a printer-tab field, not a preset key. The loader honours that default only halfway for a system preset: it sizes the composed preset by the one-entry default `printer_extruder_variant`, cutting every variant array to its first value, and only then (with `single_extruder_multi_material` off) rebuilds the pair to one variant per extruder ([Printer rule 2](#printer-machine)) and pads the arrays with that value. The rule's width is still one per extruder; to give extruders different values, declare the layout. | +| `process` | `len(print_extruder_variant)`; without one, 1 | +| `filament` | `len(filament_extruder_variant)`; without one, 1 | + +The per-extruder keys in `printer_extruder_options` ([listed with the sets](#the-four-key-sets)) are +outside this scheme and stay one value per extruder. Keys outside [the four sets](#the-four-key-sets) +are never variant-resized or addressed by variant index, however wide a shipped file writes them; a +per-extruder vector holds `len(nozzle_diameter)` values, and a shorter one acts as padded with its +first value ([machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers)). + +### Sizing equation + +For a key in one of the four variant sets: + +``` +E = extruders = len(nozzle_diameter) = len(extruder_type) + = len(default_nozzle_volume_type) = len(extruder_variant_list) +V_i = variants listed for extruder i = ","-separated entries of extruder_variant_list[i], + each " " +S = variant length = V_1 + V_2 + … + V_E + = len(printer_extruder_variant) = len(printer_extruder_id) +k = stride = 2 for printer_options_with_variant_2, else 1 + +N = values the key holds, one per variant: + machine (printer_options_with_variant_1, _2) = S × k + process (print_options_with_variant) = len(print_extruder_variant) = len(print_extruder_id) + filament (filament_options_with_variant) = len(filament_extruder_variant) +values[s × k + m] = variant index s, mode m (m = 0 normal, m = 1 silent; only m = 0 at stride 1) +variant index s = (printer_extruder_id[s], printer_extruder_variant[s]) +``` + +`printer_extruder_variant` is not sized by the equation; it defines `S`: it is `extruder_variant_list` +flattened extruder by extruder, and `printer_extruder_id[s]` is the extruder that index `s` came from. +A process that mirrors the printer's pairs, as [Process rule 1](#process) asks, has `N = S`; a +filament lists each variant string it is tuned for once, with no extruder id, so its `N` is its own +and independent of any one printer: it serves every printer in its `compatible_printers`, and a +variant string no extruder of a printer reports is simply never read there (a two-extruder printer +with `S = 7` serves a filament whose `N` is 3, and the filament keeps its 3 on a printer that offers +two variant strings). The +pair is what the loader reads, so the equality with the sum holds when the three keys agree, as +[Printer rule 2](#printer-machine) requires. + +A selectable preset that writes a variant key writes it at its own `N`; any other width, one value +included, is an error. A preset that leaves a key out takes what reaches it, the default or an array +it inherits or includes, which the loader resizes to the preset's `N`; `check --strict` holds that +array to the preset's `N` too. A base is not judged on its own: its arrays count only where they reach +a preset that does not override them. BBL is the model for strict: every printer-specific machine, +process and filament declares its layout and restates every variant key at its own `N`, even where all +the values are the same, keys Orca added to the sets included. +Without the lists, `extruder_variant_list` defaults to one +`Direct Drive Standard` per extruder, so a machine has `V_i = 1` and `S = E`, and a process or filament +has `N = 1`; the loader cuts a list-less machine to its first variant and, with +`single_extruder_multi_material` off, widens it again with that value ([variant length](#widths)). The per-extruder keys outside the sets +(`printer_extruder_options` plus `extruder_offset` and `extruder_colour`) and `extruder_variant_list` +itself hold `E` values. + +### Sizing examples + +**One extruder, two nozzle volume types**: `E = 1`, `V_1 = 2`, so `S = 2`; stride-1 keys hold 2 +values, `machine_max_*` hold 4: + +```json +"nozzle_diameter": ["0.4"], +"extruder_type": ["Direct Drive"], +"extruder_variant_list": ["Direct Drive Standard,Direct Drive High Flow"], +"printer_extruder_id": ["1", "1"], +"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive High Flow"], +"default_nozzle_volume_type": ["Standard"], +"retraction_length": ["0.8", "1.0"], +"machine_max_speed_x": ["500", "200", "600", "250"] +``` + +The matching process lists the same two pairs (`print_extruder_id` `["1", "1"]`, +`print_extruder_variant` as above) and holds 2 values per key, e.g. `outer_wall_speed` +`["200", "260"]`; a filament for it lists `["Direct Drive Standard", "Direct Drive High Flow"]` and +holds 2 values per key, e.g. `filament_max_volumetric_speed` `["16", "24"]`. + +**Four extruders, one nozzle volume type each** (a tool changer): `E = 4`, every `V_i = 1`, so +`S = 4`; stride-1 keys hold 4 values, `machine_max_*` hold 8: + +```json +"nozzle_diameter": ["0.4", "0.4", "0.6", "0.4"], +"extruder_type": ["Direct Drive", "Direct Drive", "Direct Drive", "Direct Drive"], +"extruder_variant_list": ["Direct Drive Standard", "Direct Drive Standard", + "Direct Drive Standard", "Direct Drive Standard"], +"printer_extruder_id": ["1", "2", "3", "4"], +"printer_extruder_variant": ["Direct Drive Standard", "Direct Drive Standard", + "Direct Drive Standard", "Direct Drive Standard"], +"default_nozzle_volume_type": ["Standard", "Standard", "Standard", "Standard"], +"retraction_length": ["0.8", "0.8", "1.2", "0.8"], +"machine_max_speed_x": ["500", "200", "500", "200", "500", "200", "500", "200"] +``` + +The (normal, silent) pair is repeated per extruder: `["500", "200"]` would be width 2 against +`S × k = 8` (the loader pads it to `500, 200, 500, 500, …`) and `["500"]` width 1; both are errors. The +process mirrors the four pairs (`print_extruder_id` `["1", "2", "3", "4"]`) with 4 values per key, or +omits the variant keys altogether when nothing differs per extruder (then one value per key). A +filament for it lists only `["Direct Drive Standard"]`: one entry, so one value per key. Drop the +layout from this printer and the widths stay the same, since the default list gives `S = E = 4`; but +the loader then keeps only the first variant ([variant length](#widths)): `retraction_length` +becomes `0.8` on every extruder and extruder 3 loses its `1.2`. + +### Adding a variant inserts its values at its variant index + +Indexes run extruder-major: extruder 1's variants in `extruder_variant_list` order, then extruder +2's. A new variant's values go in at its index, not at the end. Giving extruder 1 of a two-extruder +printer a High Flow option, when only extruder 2 had one: + +| Key | Before | After | +| --- | --- | --- | +| `extruder_variant_list` | `["Direct Drive Standard", "Direct Drive Standard,Direct Drive High Flow"]` | `["Direct Drive Standard,Direct Drive High Flow", "Direct Drive Standard,Direct Drive High Flow"]` | +| `printer_extruder_id` | `["1", "2", "2"]` | `["1", "1", "2", "2"]` | +| `printer_extruder_variant` | `[Standard, Standard, High Flow]` | `[Standard, High Flow, Standard, High Flow]` (full strings in the file) | +| `retraction_length` (stride 1) | `["0.8", "1.0", "1.2"]` | `["0.8", "?", "1.0", "1.2"]`: one value at index 1 | +| `machine_max_speed_x` (stride 2) | `["500", "200", "600", "250", "700", "300"]` | `["500", "200", "?", "?", "600", "250", "700", "300"]`: a (normal, silent) pair at position 2 | +| `print_extruder_id` / `print_extruder_variant` / `outer_wall_speed` | mirror the printer | the same insertion at index 1 | +| `filament_extruder_variant` `[Standard, High Flow]` | — | unchanged: filament variants carry no extruder id, so the existing High Flow variant now serves both extruders | + +Every `?` is a measured value for that nozzle, on the machine limits as much as on retraction. +Removing or renaming a variant shifts the later values the same way in reverse; a variant string that +no longer matches the enum is simply a variant nothing selects. + +### Padding, truncation and composition + +**Every key in the set widens, whether or not a file restates it.** At load each variant key of the +composed config is resized to the length of the preset's own `*_extruder_variant` (the one-entry +default when it writes none) × stride: a short array is **padded by repeating its first value**, a +long one is truncated to its first values, without a word from the loader or the validator. The rule +does not follow from this padding: a file writes a variant key at **exactly `variant length × stride`** +or not at all. Any other length, one value included (which the loader spreads over every variant, at +stride 2 over normal *and* silent alike), is a mistake the loader hides and +`orca_profile_tool.py check` reports as an error in the selectable preset that writes it, even when +every value is the same; `check --strict` also reports an array that reaches a selectable preset at +another width +([variant arrays](validation.md#variant-arrays)). + +The loader sizes a list-less preset of any type to one variant at this step, a list-less machine +included: a two-extruder machine without a layout that writes `retraction_length` `["0.8", "0.9"]`, the +width the equation asks for, stores `["0.8"]`, which the pair rebuild and the slice-time collapse hand +to both extruders. A 3-value array on a preset of variant length 2 keeps its first two; at length 4 it +becomes `[a, b, c, a]`. + +Composition hands down widths in two ways: + +- **`inherits` hands down the parent's resized arrays.** A base is stored after its own resize, at + the length of its own `*_extruder_variant` (one variant for a base of any type that writes none, + whatever its extruder count), so an array wider than the base's list is cut to its first values + before any child sees it, and a child that adds variants gets those first values padded. Widen an + array only on a preset whose own resolved list already has the entries. `check` judges selectable + presets only, at the width each file wrote: an array a base's own resize cuts is not seen (a review + item), and an inherited array of another width than a child's list is left to the loader's resize + unless `check --strict`, which asks the child to restate it at its own width. +- **`include` hands down the template's diff at its pre-resize width.** The template contributes every + key where its composed config differs from the built-in defaults, taken before its own resize, so the + arrays it writes arrive at the width its file wrote, and the includer's own list sizes them. A key the + template sets to the built-in default is not passed on + ([`include`](vendor-bundle.md#inherits-and-include)). + +The id keys are the trap in this padding: `printer_extruder_id` and `print_extruder_id` are members +with default `[1]`, so a missing or one-value id array beside a longer variant list is padded to +extruder 1 at every index. That is right on a single-extruder printer and wrong on a multi-extruder +one, where every variant is then addressed as extruder 1's. A machine escapes it only where the +load-time pair rebuild of [Printer rule 2](#printer-machine) runs (`single_extruder_multi_material` +off); a process's `print_extruder_id` is never rebuilt at load. `check` +holds an id array that reaches a selectable preset, written or inherited, to one entry per variant on +any printer; `fix-variant` never pads an id array or a variant list, since those address the +variants rather than fill them. + +Consequences: + +- A base that gains a variant silently pads every descendant that restates a variant array at the old + width, and a short `machine_max_*` array copies variant 0's *normal* limit into the silent entries + too; a descendant that restates nothing inherits the widened array and needs no edit. Extend, in one + change: the printer base and each nozzle-diameter sibling that restates a variant array, the process + bases that mirror the printer's variants, and the filaments that should cover the variant. +- A one-value override is reported, and the loader spreads it over **all** variants, overwriting the + ones that should differ. +- A process or filament that omits the new variant is not padded; the variant resolves to its index 0. + +### Slice time and existing users + +**Slice time collapses variants to extruders.** When printer, process and filaments are combined on a +printer with several extruders or several variant strings, each printer and process variant key is +re-gathered to one entry per extruder (× stride) in extruder order, using each extruder's live nozzle +volume type, or to one entry per nozzle volume type for an extruder that holds several (Hybrid, or +`extruder_nozzle_stats` listing more types than there are extruders); on any other printer they are +not re-gathered and variant index 0 is read. Filament keys are re-gathered to one entry per filament, +on a printer with a single variant too once a filament has several variants, and under a dynamic +nozzle map to one entry per variant each filament prints through. Custom G-code and the +`machine_max_*` limits therefore index by extruder or filament, never by variant index; variant order +matters only inside the preset. An extruder with no matching variant reads variant index 0, extruder +1's first variant, whichever extruder it is; under layered nozzle grouping, `get_config_index_base` +reads the collapsed arrays and falls back to the same extruder's first entry instead. + +**Existing user presets and projects follow the variant string, not the position.** A user preset +stores every variant array it changed (nullable keys as per-variant diffs, `nil` where equal to the +parent). On load each parent variant takes the child's value for the variant with the same extruder id +and variant string; variants the parent gained keep the parent's value, and a child whose arrays the +parent cannot map keeps its own. Per-object process overrides are remapped when a printer change +alters the extruder count or the length of `printer_extruder_variant`, by variant string alone (no +extruder id; of several matching values the smallest wins), and a single-value override applies to +every variant. Adding or reordering variants in a shipped preset is therefore safe for existing +users; renaming a variant, or moving it to another extruder id, loses their values for it. + +**A new nozzle volume type changes no array.** Adding one to the code widens nothing until a profile +lists the new variant string; until then every existing array keeps its length and meaning. + +## The four key sets + +Membership is literal (four `std::set` initializers in `src/libslic3r/PrintConfig.cpp`) +and **not guessable from names**: `ironing_speed`, `skirt_speed`, `wipe_speed`, `scarf_joint_speed`, +`small_support_perimeter_speed` and `wipe_tower_max_purge_speed` are process speeds outside the set, +while every process `*_acceleration` and `*_jerk` key is inside, and so are +`small_perimeter_threshold`, `top_solid_infill_flow_ratio` and `slowdown_for_curled_perimeters`; +`filament_flush_temp` is in and `filament_flush_temp_fast` out; `use_firmware_retraction` is out and +`travel_slope` and `retract_lift_enforce` in. The three variant-list keys and the two id +keys are members of their own set (the list sizes itself, a no-op; the id keys are padded like any +other member, [the id trap](#padding-truncation-and-composition)), while `extruder_variant_list` is in +no set: the variant-length resize leaves it alone, and only the pair rebuild of +[Printer rule 2](#printer-machine) pads it. The per-extruder `printer_extruder_options` is listed +last for contrast; it is not a variant set. + +`check` and `fix-variant` read the four sets from `PrintConfig.cpp` on every run, so they follow the +engine. The lists below are from the 2026-09-25 checkout; regenerate them from the repository root +before relying on them (the recipe strips comments, since an initializer can carry a commented-out entry): + +```bash +python3 - <<'EOF' +import re +src = open('src/libslic3r/PrintConfig.cpp', encoding='utf-8', errors='replace').read() +for name in ['printer_options_with_variant_1', 'printer_options_with_variant_2', + 'print_options_with_variant', 'filament_options_with_variant', + 'printer_extruder_options']: + body = re.search(r'std::set\s+' + name + r'\s*=\s*\{(.*?)\};', src, re.S).group(1) + body = re.sub(r'/\*.*?\*/', '', body, flags=re.S) + body = re.sub(r'//[^\n]*', '', body) + print(name, sorted(set(re.findall(r'"([^"]+)"', body)))) +EOF +``` + +**`printer_options_with_variant_1`**, machine, stride 1 (27): `deretraction_speed`, `hotend_cooling_rate`, `hotend_heating_rate`, `long_retractions_when_cut`, `nozzle_flush_dataset`, `nozzle_type`, `nozzle_volume`, `printer_extruder_id`, `printer_extruder_variant`, `retract_after_wipe`, `retract_before_wipe`, `retract_length_toolchange`, `retract_lift_above`, `retract_lift_below`, `retract_lift_enforce`, `retract_restart_extra`, `retract_restart_extra_toolchange`, `retract_when_changing_layer`, `retraction_distances_when_cut`, `retraction_length`, `retraction_minimum_travel`, `retraction_speed`, `travel_slope`, `wipe`, `wipe_distance`, `z_hop`, `z_hop_types` + +**`printer_options_with_variant_2`**, machine, stride 2 (16): `machine_max_acceleration_e`, `machine_max_acceleration_extruding`, `machine_max_acceleration_retracting`, `machine_max_acceleration_travel`, `machine_max_acceleration_x`, `machine_max_acceleration_y`, `machine_max_acceleration_z`, `machine_max_jerk_e`, `machine_max_jerk_x`, `machine_max_jerk_y`, `machine_max_jerk_z`, `machine_max_junction_deviation`, `machine_max_speed_e`, `machine_max_speed_x`, `machine_max_speed_y`, `machine_max_speed_z` + +**`print_options_with_variant`**, process, stride 1 (45): `bridge_acceleration`, `bridge_speed`, `default_acceleration`, `default_jerk`, `default_junction_deviation`, `enable_overhang_speed`, `gap_infill_speed`, `infill_jerk`, `initial_layer_acceleration`, `initial_layer_infill_speed`, `initial_layer_jerk`, `initial_layer_speed`, `initial_layer_travel_acceleration`, `initial_layer_travel_jerk`, `initial_layer_travel_speed`, `inner_wall_acceleration`, `inner_wall_jerk`, `inner_wall_speed`, `internal_bridge_speed`, `internal_solid_infill_acceleration`, `internal_solid_infill_speed`, `outer_wall_acceleration`, `outer_wall_jerk`, `outer_wall_speed`, `overhang_1_4_speed`, `overhang_2_4_speed`, `overhang_3_4_speed`, `overhang_4_4_speed`, `print_extruder_id`, `print_extruder_variant`, `slowdown_for_curled_perimeters`, `small_perimeter_speed`, `small_perimeter_threshold`, `sparse_infill_acceleration`, `sparse_infill_speed`, `support_interface_speed`, `support_speed`, `top_solid_infill_flow_ratio`, `top_surface_acceleration`, `top_surface_jerk`, `top_surface_speed`, `travel_acceleration`, `travel_jerk`, `travel_speed`, `travel_speed_z` + +**`filament_options_with_variant`**, filament, stride 1 (48): `activate_air_filtration`, `activate_air_filtration_during_print`, `activate_air_filtration_on_completion`, `complete_print_exhaust_fan_speed`, `during_print_exhaust_fan_speed`, `filament_adaptive_volumetric_speed`, `filament_cooling_before_tower`, `filament_deretraction_speed`, `filament_extruder_variant`, `filament_flow_ratio`, `filament_flush_temp`, `filament_flush_volumetric_speed`, `filament_ironing_flow`, `filament_ironing_inset`, `filament_ironing_spacing`, `filament_ironing_speed`, `filament_long_retractions_when_cut`, `filament_max_volumetric_speed`, `filament_pre_cooling_temperature`, `filament_pre_cooling_temperature_nc`, `filament_preheat_temperature_delta`, `filament_ramming_travel_time`, `filament_ramming_travel_time_nc`, `filament_ramming_volumetric_speed`, `filament_ramming_volumetric_speed_nc`, `filament_retract_after_wipe`, `filament_retract_before_wipe`, `filament_retract_length_nc`, `filament_retract_length_toolchange`, `filament_retract_lift_above`, `filament_retract_lift_below`, `filament_retract_lift_enforce`, `filament_retract_restart_extra`, `filament_retract_restart_extra_toolchange`, `filament_retract_when_changing_layer`, `filament_retraction_distances_when_cut`, `filament_retraction_length`, `filament_retraction_minimum_travel`, `filament_retraction_speed`, `filament_wipe`, `filament_wipe_distance`, `filament_z_hop`, `filament_z_hop_types`, `long_retractions_when_ec`, `nozzle_temperature`, `nozzle_temperature_initial_layer`, `retraction_distances_when_ec`, `volumetric_speed_coefficients` + +**`printer_extruder_options`**, machine, one value per extruder, not a variant set (8): +`default_nozzle_volume_type`, `extruder_max_nozzle_count`, `extruder_printable_area`, +`extruder_printable_height`, `extruder_type`, `max_layer_height`, `min_layer_height`, +`nozzle_diameter`. These are never addressed by variant index; `extruder_offset`, `extruder_colour` and +`extruder_variant_list` are per extruder too. A shorter array acts as padded with its first value, and +entries beyond the extruder count are never read +([per-extruder vectors](machine-profiles.md#multi-extruder-idex-and-tool-changers)). + +## Checking and testing + +Checklist for a new variant profile set: + +1. Decide the variants per extruder from the real hardware; pick legal strings only. +2. Printer base: `extruder_type`, `extruder_variant_list`, flattened `printer_extruder_variant` + + `printer_extruder_id`, `default_nozzle_volume_type`; every variant array at variant length, every + `machine_max_*` at 2 × variant length as (normal, silent) pairs, even where the values are the + same, values in variant order. +3. A process base per variant layout, or no variant keys at all. +4. Filaments for the printer: a variant list covering the intended variants, every variant key at that + width, measured values per nozzle volume type. +5. Run the usual authoring commands and full checks. `check` holds each array of steps 2–4 to the + width of the selectable preset that writes it, `check --strict` also to every selectable preset it + reaches, and + the printer's layout keys through every selectable preset + ([variant arrays](validation.md#variant-arrays)); `check_variant_names` holds every variant string, + `extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the enums + ([variant names](validation.md#variant-names)). The choice of variants, the variant order and + the measured values are not checked. +6. In the app, for each nozzle volume type in the sidebar combo: slice and confirm the G-code uses that + variant's values (e.g. volumetric speed limit, retraction). `validate_slice` only slices the default + nozzle volume type. + +To review rather than author, run the same list against the diff. `check` catches a wrong array length, +a layout key out of step, and a variant string the enums cannot build. The failures no check catches: a +new variant appended instead of inserted at its index, an array widened on a base whose list is +shorter (cut before any child inherits it), a process lacking a pair its printer can select, and a High +Flow variant copied from Standard. + +### UI facts to design around + +- The single-extruder sidebar shows a **nozzle volume type combo** (tooltip `Flow`, in place of the + nozzle-diameter selector) only when `extruder_variant_list` offers more than one distinct variant + string (`support_different_extruders`); four extruders listing `Direct Drive Standard` each show + none. +- The two-extruder sidebar's per-extruder nozzle volume type combos are shown for BBL printers only. + Another vendor's multi-extruder printer falls back to the single-extruder layout: one combo (for the + first extruder) when the variants differ, otherwise the nozzle-diameter selector, so the other + extruders' nozzle volume type stays at `default_nozzle_volume_type`. +- High Flow is hidden from the combo when `printer_variant` is `0.2` or the printer model is + `Bambu Lab X1E`, and E3D High Flow unless `printer_variant` is `0.4` or `0.6`. A variant listed on + another nozzle diameter is never selectable there. +- The filament tab shows a variant switch built from the filament's own `filament_extruder_variant`; + a multi-variant filament whose tab shows none did not resolve its variant list through `include` or + `inherits`. The printer and process tabs show one entry per extruder instead, labelled with that + extruder's live nozzle volume type (two for Hybrid), and only on two-extruder printers whose + `extruder_variant_list` offers more than one variant string; elsewhere they edit the variant of the + nozzle volume type selected in the sidebar (an X1C shows no switch). + +### Worked examples in the tree + +`BBL/machine/fdm_bbl_3dp_001_common.json` (one extruder), `fdm_bbl_3dp_002_common.json` (two +extruders), `Bambu Lab X1 Carbon 0.4 nozzle.json` (Standard + High Flow), `Bambu Lab H2D 0.4 +nozzle.json` (extruders with different variant sets), `Bambu Lab X2D 0.4 nozzle.json` (Direct Drive + +Bowden), with their `@BBL` processes and filaments. The H2D and X2D examples also carry +`E3D High Flow` variants. BBL's multi-variant filament lists come from `fdm_filament_template_*` +presets that the printer-specific filaments pull in with `include`, not from their `inherits` chain. diff --git a/.claude/skills/orca-profiles/references/filament-profiles.md b/.claude/skills/orca-profiles/references/filament-profiles.md index 873e2a6510..bc80176eaa 100644 --- a/.claude/skills/orca-profiles/references/filament-profiles.md +++ b/.claude/skills/orca-profiles/references/filament-profiles.md @@ -1,8 +1,8 @@ # Filament profiles and OrcaFilamentLibrary -`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**; its config map -becomes the base bundle, so any vendor may inherit a library preset by name. It is the only cross-bundle -parent — vendor-to-vendor inheritance always fails. +`OrcaFilamentLibrary` is the filament-only bundle the loader reads **first**, so any vendor's filament may +inherit a library preset by name. It is the only cross-bundle parent: vendor-to-vendor inheritance +always fails. ## Where a filament goes @@ -10,63 +10,65 @@ parent — vendor-to-vendor inheritance always fails. | --- | --- | | Generic material for all printers | `OrcaFilamentLibrary/filament/Generic @System.json` | | A brand's product, all printers | `OrcaFilamentLibrary/filament//` | -| A brand's tune for one printer | `OrcaFilamentLibrary/filament///` — recommended; `/filament//` also works | -| A printer vendor's tune of a generic or its own product | `/filament/` | +| A brand's tune for one printer vendor | `OrcaFilamentLibrary/filament///` (recommended); `/filament//` also works | +| A printer vendor's tune of a generic, or its own product | `/filament/` | -Both locations for the last-but-one row are supported: `OrcaFilamentLibrary/filament///.json` +Both locations in the third row are supported: `OrcaFilamentLibrary/filament///.json` (the shape the wiki shows) and `/filament//`. The library path is the one a -filament vendor should contribute to — `OrcaFilamentLibrary/filament//` is the brand's own -folder, while a printer vendor's folder belongs to that printer vendor. Brand tunes do ship under -printer vendors' folders today (Polymaker and SUNLU among others). +filament brand should contribute to: `OrcaFilamentLibrary/filament//` is the brand's own folder, +while a printer vendor's folder belongs to that printer vendor. -Library layout: `filament/base/fdm_filament_*.json` type roots, root-level `Generic @System.json` -generics, and one subfolder per brand, which may nest printer-specific tunes one level deeper. Adding -a brand means adding a folder here; the folder name is a directory label only — `filament_vendor` inside the JSON is the real vendor string. +Library layout: `filament/base/fdm_filament_*.json` material roots, root-level +`Generic @System.json` generics, and one subfolder per brand, which may nest printer-specific +tunes one level deeper. Adding a brand means adding a folder here; the folder name is a directory label +only, and `filament_vendor` inside the JSON is the real vendor string. ## The three-part shape ```jsonc -// Fiberon PA6-CF @base.json — the product root, holds identity + material values +// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @base.json — the product root: identity + material values { "type": "filament", "name": "Fiberon PA6-CF @base", "from": "system", "instantiation": "false", "inherits": "fdm_filament_pa", - "filament_id": "OFkOviHk", // generated here; variants inherit it + "filament_id": "OFkOviHk", // minted here by generate-id; every child inherits it "filament_vendor": ["Polymaker"], "filament_type": ["PA6-CF"], /* … */ } -// Fiberon PA6-CF @System.json — the selectable shim, 7 keys +// OrcaFilamentLibrary/filament/Polymaker/Fiberon PA6-CF @System.json — the selectable all-printer shim, 7 keys { "type": "filament", "name": "Fiberon PA6-CF @System", "from": "system", "instantiation": "true", "inherits": "Fiberon PA6-CF @base", "setting_id": "…", "compatible_printers": [] } -// /filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune -{ … "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"], +// BBL/filament/Polymaker/Fiberon PA6-CF @BBL X1C.json — a printer tune (BBL keeps its own copy of the @base) +{ …, "inherits": "Fiberon PA6-CF @base", "filament_max_volumetric_speed": ["14"], "compatible_printers": ["Bambu Lab X1 Carbon 0.4 nozzle", …] } ``` -- `@base` is the convention for a root. A base carries **no** `setting_id`, no `compatible_printers`, no - `filament_settings_id`. Only the `setting_id` half is enforced, and nothing violates it; the other two - are unchecked and plenty of bases still carry them. Do not copy that from a neighbouring file. -- Every `@System` must be `"instantiation": "true"`. DREMC ships `@System` presets set to `"false"`, - which therefore ship but can never be selected; no check catches it. -- A duplicated brand `@base` across bundles is normal and intentional (`Fiberon PA6-CF @base` exists in - both the library and BBL with the same id, differing only in MVS) — bases never enter the preset - collection, so there is no duplicate-name error. -- You may inherit from an instantiated preset as well as from a base; it is common. +- `@base` is the convention for a root; a root is really `instantiation: "false"`. A base carries + **no** `setting_id`, no `compatible_printers` and no `filament_settings_id`. Only the `setting_id` + half is enforced; the other two are unchecked, so a neighbouring base that carries them is no model. +- Every `@System` shim must be `"instantiation": "true"`; one set to `"false"` would ship but could + never be selected, and no check catches it. The shim exists only for products in the library. +- `filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the root and + should not appear in a printer tune. +- A brand `@base` duplicated across bundles is legal (a vendor bundle may keep its own copy of a + library product root, with the same id): bases never become selectable presets and the + duplicate-name error covers only those, so there is none. +- You may inherit from an instantiated preset as well as from a base. -## Color is a runtime property +## Colour is a runtime property -`filament_id` identifies a product, not a color; filament sync/AMS reads the color from the spool at -runtime. A product ships one all-printer preset and the color is chosen at runtime — never a sibling -preset that differs only by color. A material family (PLA vs PLA Matte vs PLA Silk) is a new product; a -color is not. A printer tune keeps the product alias and does not multiply per color either. +`filament_id` identifies a product, not a colour; filament sync and AMS read the colour from the spool +at runtime. A product ships one all-printer preset and the colour is chosen at runtime, never a sibling +preset that differs only by colour. A material family (PLA vs PLA Matte vs PLA Silk) is a new product; +a colour is not. A printer tune keeps the product alias and does not multiply per colour either. -CI does not catch this — per-color presets pass `check` — so it is a review call. +CI does not catch this (per-colour presets pass `check`), so it is a review call. ## The two most common contributions -**A printer vendor tuning a generic.** Keep the `Generic X` base name so the alias shadows the library -preset on your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the -library's is correct — the product really is the library's generic), and give it a non-empty -`compatible_printers` in its own body: +**A printer vendor tuning a generic.** Keep the `Generic X` alias so it shadows the library preset on +your printers, inherit `Generic X @System`, declare **no** `filament_id` (inheriting the library's is +correct: the product really is the library's generic), and give it a non-empty `compatible_printers` in +its own file: ```jsonc // /filament/Generic PETG @Acme One 0.4 nozzle.json @@ -76,13 +78,14 @@ library's is correct — the product really is the library's generic), and give "compatible_printers": ["Acme One 0.4 nozzle"] } ``` -**A printer vendor's own branded product.** Give it a `@base` root so `generate-id` can mint the id (see -[ids.md](ids.md) — inheriting `Generic X @System` directly makes the id unfixable by the tool), then one -instantiated leaf per printer in the same bundle. No `@System` shim: that is only for a product entering -OrcaFilamentLibrary. +**A printer vendor's own branded product.** Give it a `@base` root on a material base so `generate-id` +can mint the id, then one instantiated leaf per printer in the same bundle. Inheriting +`Generic X @System` directly gives the product the generic's id, which the tool cannot fix +([ids.md](ids.md#what-generate-id-does-and-does-not-fix)). No `@System` shim: that is only for a product +entering OrcaFilamentLibrary. ```jsonc -// /filament/Acme Aura PETG @base.json — instantiation false, no setting_id +// /filament/Acme Aura PETG @base.json — instantiation false, no setting_id { "type": "filament", "name": "Acme Aura PETG @base", "from": "system", "instantiation": "false", "inherits": "fdm_filament_pet", "filament_vendor": ["Acme"], "filament_type": ["PETG"] } // filament_id minted here @@ -94,174 +97,190 @@ OrcaFilamentLibrary. "compatible_printers": ["Acme One 0.4 nozzle"] } ``` -Omit `filament_settings_id` from new presets — it is runtime bookkeeping the app rewrites to the preset +Omit `filament_settings_id` from new presets: it is runtime bookkeeping the app rewrites to the preset name. +To offer either kind by default, add its name to each model's `default_materials`; put it first in the +machine's `default_filament_profile` only if it should be the preselected filament +([machine keys](machine-profiles.md#other-keys)). + ## `compatible_printers` -- **Library fallbacks:** empty `[]` or absent, so they are offered on all printers except where - [alias shadowing](#alias-shadowing) supplies a printer-specific tune. -- **Library printer-specific tunes:** non-empty, listing exact printer **variant** names. These can +- **Library fallbacks** (`@System`): empty `[]` or absent, so they are offered on all printers except + where [alias shadowing](#alias-shadowing) supplies a printer-specific tune. +- **Library printer-specific tunes**: non-empty, listing exact printer **variant** names. These supersede a same-alias fallback just like a tune in a printer vendor's bundle. -- **Instantiated filaments in every other vendor:** non-empty, listing exact printer **variant** names. - Enforced twice but not identically: the C++ `has_errors` reads the *flattened* config, so an inherited list satisfies it, - while the Python check reads the file's **own** key. Write the list in the file itself. This is the - most common filament CI failure. -- Emptying it to "make it apply everywhere" fails that check *and* creates a duplicate-`filament_id` - collision against the library generic on every printer. -- Copying a base's full printer list onto a nozzle-specific variant produces duplicate combobox entries — - a real shipped bug twice over. +- **Instantiated filaments in every other vendor**: non-empty, listing exact printer **variant** names. + Enforced twice, but not identically: `validate_system` reads the resolved config, so an inherited list + satisfies it, while `check` reads the file's **own** key. Write the list in the file itself. This is + the most common filament CI failure. +- Emptying it to "make it apply everywhere" fails that check *and* collides with the library generic's + `filament_id` on every printer. +- Copying a base's full printer list onto a nozzle-specific tune produces duplicate combobox entries: a + real shipped bug twice over. ## Overlapping coverage: one variant, one profile per product -`filament_id` is the **product** key, not the preset key — every variant of one product shares it -(`//`). So if one printer variant appears in the -`compatible_printers` of two presets of that product, the slicer cannot tell them apart at AMS match time. -The C++ validator reports `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"`. -`orca_profile_tool.py check` does **not** see it and passes. Resolve the overlap by **specificity**: keep -the variant on the most specific profile and remove it from every more general one. Deleting a profile is -the least preferred fix — moving coverage keeps the tune that users rely on. +`filament_id` is the **product** key, not the preset key: every preset of one product shares it +(`//`). So if one printer variant appears in the +`compatible_printers` of two presets of that product, the slicer cannot tell them apart at AMS match +time. `validate_system` reports `Ambiguous AMS filament match: N filament presets share filament_id "X" +and are all compatible with printer "Y"`; `orca_profile_tool.py check` does **not** see it and passes. +Resolve the overlap by **specificity**: keep the variant on the most specific profile and remove it from +every more general one. Deleting a profile is the least preferred fix: moving coverage keeps the tune +that users rely on. -Judge specificity from the profile's `compatible_printers` — how many variants it actually covers — and -use the name only as a secondary, easily-vague hint; decide by the lists, with a best judgement call on -the name. Naming conventions differ by vendor: BBL's is the reference (`@ ` for a whole +Judge specificity from the profile's `compatible_printers` (how many variants it actually covers) and +use the name only as a secondary, often vague hint; decide by the lists, with a judgement call on the +name. Naming conventions differ by vendor: BBL's is the reference (`@ ` for a whole model, `@ nozzle` for one variant, `@` for a vendor-wide generic), but others vary (`@`, a printer serial, or Creality's `@-all`). A name never overrides -the list — see [preset naming](naming.md) for the shapes. +the list; see [preset naming](naming.md#filament) for the shapes. Specificity, most to least: -1. **Variant-specialized** — lists a single printer variant (BBL-style - `... @ nozzle`). -2. **Model-specialized** — lists the variants of one printer model (BBL-style `... @ `). - It should cover every variant of its model, not only the nozzle it was authored for. -3. **Family / series** — lists variants spanning a printer family or series. -4. **Generic / catch-all** — vendor-wide, covering many unrelated models (often the bare +1. **Variant-specialized**: lists a single printer variant (BBL-style + `… @ nozzle`). +2. **Model-specialized**: lists the variants of one printer model (BBL-style `… @ `). It + should cover every variant of its model, not only the nozzle it was authored for. +3. **Family / series**: lists variants spanning a printer family or series. +4. **Generic / catch-all**: vendor-wide, covering many unrelated models (often the bare `Generic @`). Rules: - A model-specialized profile is extended to **all** variants of its model, and each variant it thereby - starts covering is removed from the family and generic profiles that also listed it — including variants - that had no overlap before. Apply it per nozzle, not just 0.4. + starts covering is removed from the family and generic profiles that also listed it, including + variants that had no overlap before. Apply it per nozzle, not just 0.4. - Apply it **per product**: trim only the material that has a specialized profile from the generic; a material whose product has no specialized profile keeps the variant in the generic. - Never strip coverage a variant has nowhere else to get. If a variant has no variant-level specialized - profile, the next level down keeps it; when the model has specialized profiles, the model-level one wins - over the family/generic. -- Moving coverage is preferred over deleting. If a profile must be deleted, remove the more general - one, not the specialized profile that carries the tune. -- After moving coverage, repoint the affected `default_filament_profile` (machine) and clean the model's - `default_materials`: they should name the most specific profile that covers the variant, and should not - keep generic entries that no longer cover the model. This rule applies equally when adding or fixing - defaults. + profile, the next level down keeps it; when the model has specialized profiles, the model-level one + wins over the family and generic ones. +- Moving coverage is preferred over deleting. If a profile must be deleted, remove the more general one, + not the specialized profile that carries the tune. +- After moving coverage, repoint the affected machine's `default_filament_profile` and clean the model's + `default_materials`: they should name the most specific profile that covers the variant, and should + not keep generic entries that no longer cover the model. This rule applies equally when adding or + fixing defaults. -Multiple profiles of one product with **disjoint** `compatible_printers` is the intended end state. +Several profiles of one product with **disjoint** `compatible_printers` is the intended end state. Adding coverage to the specialized profile and removing it from the generic is the preferred direction. -**Detection caveat:** `orca_profile_tool.py check` is blind to this; only the C++ validator behind the full -`./scripts/check_profile.sh` reports it (`validate_system`). Always confirm with that, not the vendor-scoped -loop. +**Detection caveat:** `orca_profile_tool.py check` is blind to this; only the validator behind the full +`./scripts/check_profile.sh` reports it (`validate_system`). Always confirm with that, not the +vendor-scoped loop. ## Alias shadowing -A printer-specific filament in either the library or a vendor bundle supersedes the library fallback -on the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`, -right-trimmed (no `@` → the whole name). So -`QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`. +A printer-specific filament in either the library or a vendor bundle supersedes the library fallback on +the printers it lists. The matching key is the **alias**: the preset name up to the **first** `@`, +right-trimmed (no `@` → the whole name). So `QIDI ABS-GF@Q2-Series` aliases to `QIDI ABS-GF`. -A library preset with an empty `compatible_printers` collects, into `m_excluded_from`, every printer named -by any same-alias preset that *has* a non-empty list, and is then hidden on those printers. +A library preset with an empty `compatible_printers` is hidden on every printer that a same-alias preset +lists in a non-empty `compatible_printers`, whether that preset is in the library or in a vendor bundle +(a printer matches by its own name or its parent's). Two consequences: -- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing - an alias do not exclude each other — overlapping lists for the same product trip the - duplicate-`filament_id` check instead. -- This is why adding `Generic PLA @` to a vendor silently removes the library `Generic PLA` - from that printer. Intended — and the reason a vendor tuning a generic must **keep the `Generic X` - base name**. +- **Only an unrestricted library fallback can be shadowed.** Two printer-specific presets sharing an + alias do not hide each other; overlapping lists for the same product trip the ambiguous-match error + above instead. +- This is why adding `Generic PLA @` to a vendor silently removes the library + `Generic PLA @System` from that printer. That is intended, and the reason a vendor tuning a generic + must **keep the `Generic X` alias**. -The literal spelling `Generic @System` is load-bearing beyond shadowing: `find_preset2` rewrites an -unresolved name containing "Generic" into that form and retries against the library, which is how 3MF -and project recovery works. +The literal spelling `Generic @System` is load-bearing beyond shadowing: when a user preset, an +imported preset or a 3MF project names a parent that no longer resolves and contains `Generic`, the +loader rewrites the name into `Generic @System` and retries. Only the library ships those names, +so keep them. ## `filament_id`, `filament_vendor`, `filament_type` -`filament_id` is minted from the triple `(filament_vendor, filament_type, name-before-first-@)`. -`filament_vendor` and `filament_type` are therefore **identity, not decoration** — editing either -re-mints the id. Read `docs/HLSD/filament_id.md` before changing any of them, and see -[ids.md](ids.md) for the tooling. +`filament_id` is minted from the triple `(filament_vendor, filament_type, alias)`. `filament_vendor` and +`filament_type` are therefore **identity, not decoration**: editing either, or the alias, re-mints the +id. Read `docs/HLSD/filament_id.md` before changing any of them, and see [ids.md](ids.md) for the +tooling. -A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error** that -discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X` inheriting -`Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its own. +A filament with no resolvable `filament_id` anywhere in its `inherits` chain is a **hard load error** +that discards the vendor bundle. The id inherits across bundles, so a vendor's `Generic ABS @X` +inheriting `Generic ABS @System` gets the library's id for free; a vendor's own product must resolve its +own. -- `filament_type` **must be a JSON array** — the one vector key the Python check enforces. A scalar - `"PP"` once hung the filament/printer selection UI. +- `filament_type` **must be a JSON array**: the one vector key `check` rejects as a scalar outright. A + scalar `"PP"` once hung the filament and printer selection UI. - It is an **open** enum: an unlisted value is accepted silently and falls back to 190–300 °C defaults - and adhesion 1.0. Off-list values do ship. Prefer a value from `MaterialType::all()` in + and adhesion 1.0. Prefer a value from `MaterialType::all()` in `src/libslic3r/MaterialType.cpp`, or add a row there. - Generics use `filament_vendor: ["Generic"]`, which `fdm_filament_common` already defaults to. ## `"nil"` -Legal in any key whose `ConfigOptionDef` is `nullable`. In a filament preset that is most of the -`filament_*` family, plus `long_retractions_when_ec` and `retraction_distances_when_ec`. About half are -the extruder overrides (`filament_retraction_length`, `filament_z_hop`, `filament_wipe`, -`filament_retract_*`, `filament_retraction_speed`, `filament_deretraction_speed`, -`filament_retraction_minimum_travel`, `filament_wipe_distance`, `filament_long_retractions_when_cut`, -`filament_retraction_distances_when_cut`, …), where `nil` means *keep the printer/extruder's own value*. -The rest are ordinary nullable options (`filament_flow_ratio`, `filament_flush_temp`, -`filament_adaptive_volumetric_speed`, …) where it means *unset*. +`"nil"` is legal only in an option defined as nullable (`add_nullable`, or `nullable = true`, in +`src/libslic3r/PrintConfig.cpp`). Anywhere else it fails the file, and with it the **whole bundle** +(`Failed loading configuration file`, after `Deserializing nil into a non-nullable object` or +`Invalid value provided for parameter : nil`). To leave a non-nullable key unset, omit it; do not +write `nil`. -Anywhere else it throws `Deserializing nil into a non-nullable object`. To not set a non-nullable key, -omit it — do not write `nil`. +In filament presets a minority of the `filament_*` keys are nullable, plus `long_retractions_when_ec` +and `retraction_distances_when_ec`. About half of them are the extruder overrides (`filament_retraction_length`, +`filament_z_hop`, `filament_wipe`, `filament_retract_*`, `filament_retraction_speed`, +`filament_deretraction_speed`, `filament_retraction_minimum_travel`, `filament_wipe_distance`, +`filament_long_retractions_when_cut`, `filament_retraction_distances_when_cut`, …), where `nil` means +*keep the printer's or extruder's own value*. The rest are ordinary nullable options +(`filament_flow_ratio`, `filament_flush_temp`, `filament_adaptive_volumetric_speed`, …), where it means +*unset*. Check the option's definition before writing `nil` anywhere else. -## What to review per nozzle +## Tuning per nozzle and per variant -Across `@X` / `@X 0.N nozzle` sibling pairs the keys that differ, most often first, are +Between a product's `@X` and `@X 0.N nozzle` tunes the keys that usually differ, most often first, are `filament_max_volumetric_speed`, `filament_retraction_length`, `slow_down_min_speed`, -`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance`. -`filament_cost`, `filament_density`, `filament_type` and `filament_vendor` belong on the `@base` and -should not appear in a printer tune. +`filament_flow_ratio`, `slow_down_layer_time`, `nozzle_temperature` and `pressure_advance` (switched on +by `enable_pressure_advance`). Use measured values for the material, hotend, extruder and nozzle combination. Neither maximum -volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a -0.4 preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value -or a required direction of change from diameter alone. +volumetric speed nor pressure advance has a universal nozzle-only lookup table. When cloning a 0.4 +preset for a 0.2 nozzle, explicitly revisit flow limits; do not infer a pressure-advance value, or a +required direction of change, from diameter alone. -## Style - -Overrides, not full copies: a typical instantiated filament preset carries around a dozen non-meta keys, -and a library leaf two or three. Presets that restate fifty-plus keys from their parent do still ship — -Phrozen's single filament preset is that style — but they are the pattern to move away from, not to -copy. Commit `6943b6ddc3` is the stated model (flip true bases to `instantiation: "false"`, strip -`compatible_printers`/`setting_id`/`filament_settings_id`, add `renamed_from` on the survivor). - -Prefer the library's `fdm_filament_*` bases over a vendor-local copy. Phrozen's local -`fdm_filament_common` has drifted from the library's. - -Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`, `name`, -`renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then everything else in -the order you wrote it. Not enforced — a file that leads with `compatible_printers` passes `check`. - -**Every vector-typed (`co*s`) key must be a JSON array.** Only `filament_type` is an outright error, but -`normalize` silently arrayifies five more (`filament_cost`, `filament_density`, -`temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`) and `check` fails when -it would. Every other vector key is on you — including `filament_start_gcode`, `filament_end_gcode`, -`filament_extruder_variant`, `compatible_printers` and the plate temperatures. +On a printer with extruder variants, a filament tunes these per variant too: +`filament_max_volumetric_speed`, `filament_flow_ratio`, `nozzle_temperature` and the retraction +overrides carry one value per variant of `filament_extruder_variant` (Standard, High Flow, …). The +exact key set is [`filament_options_with_variant`](extruder-variants.md#the-four-key-sets); +`slow_down_min_speed` and `pressure_advance` are not in it. Keep every such array at exactly that width, even where the +setting does not differ per variant, and measure the High Flow variant rather than copying Standard +([extruder-variants.md](extruder-variants.md#filament)). ## Bed temperature is twelve keys, not one -There is no single "bed temperature". Which plate key applies depends on `curr_bed_type`, whose six -selectable values (`btPC`, `btEP`, `btPEI`, `btPTE`, `btPCT`, `btSuperTack`; `btDefault` maps to no key) -`get_bed_temp_key()` turns into `cool_plate_temp`, `eng_plate_temp`, `hot_plate_temp`, -`textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp` — each with an -`*_initial_layer` twin. +There is no single "bed temperature". The plate type selected for the printer (`Cool Plate`, +`Engineering Plate`, `High Temp Plate`, `Textured PEI Plate`, `Textured Cool Plate`, `Supertack Plate`) +picks one of six keys, each with an `_initial_layer` twin: `cool_plate_temp`, `eng_plate_temp`, +`hot_plate_temp`, `textured_plate_temp`, `textured_cool_plate_temp` and `supertack_plate_temp`. -`textured_cool_plate_temp` is the one most often forgotten. A printer with `support_multi_bed_types` off -hides the selector, and the printer preset's -`default_bed_type` decides which plate is selected for it, but `curr_bed_type` can still hold a stale -value carried over from another printer — so set every plate the printer plausibly has, as the sibling -presets in the bundle do. +`textured_cool_plate_temp` is the one most often forgotten. A non-BBL printer with +`support_multi_bed_types` off hides the plate selector and uses the printer preset's `default_bed_type` +(High Temp Plate, `hot_plate_temp`, when unset or invalid), but a loaded project or a CLI config can +still carry another plate. So set every plate the printer plausibly has, as the sibling presets in the +bundle do. + +## Style + +- Overrides, not full copies: an instantiated filament preset carries around a dozen non-meta keys, + and a library `@System` shim two or three. A preset that restates fifty-plus keys from its parent is + the pattern to move away from, not to copy. Commit `6943b6ddc3` is the stated model for converting such presets: flip true + bases to `instantiation: "false"`, strip their `compatible_printers`, `setting_id` and + `filament_settings_id`, and add `renamed_from` on the surviving selectable preset. A value every + printer tune of a product shares goes on the product's `@base` + ([shared bases](shared-bases.md#levels)). +- Prefer the library's `fdm_filament_*` bases over a vendor-local copy; for a new preset, even in a + bundle whose older presets use one: a local copy drifts from the library's. +- Canonical key order, written by `orca_profile_tool.py normalize` when it rewrites a file: `type`, + `name`, `renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, `instantiation`, then + everything else in the order you wrote it. Not enforced on its own: a file that leads with + `compatible_printers` passes `check`. +- **Every vector-typed (`co…s`) key must be a JSON array.** Only a scalar `filament_type` is an outright + error; `normalize` silently arrayifies five more (`filament_cost`, `filament_density`, + `temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`), and `check` fails + when it would. Every other vector key is on you, including `filament_start_gcode`, + `filament_end_gcode`, `filament_extruder_variant`, `compatible_printers` and the plate temperatures. diff --git a/.claude/skills/orca-profiles/references/ids.md b/.claude/skills/orca-profiles/references/ids.md index b6a09e9a2f..214a3d719c 100644 --- a/.claude/skills/orca-profiles/references/ids.md +++ b/.claude/skills/orca-profiles/references/ids.md @@ -1,76 +1,78 @@ # `setting_id` and `filament_id` Orca-generated ids are deterministic hashes of identity. **Never invent an id or copy a sibling's -`setting_id`.** Use `scripts/orca_profile_tool.py`; the two special cases are +`setting_id`.** +Use `scripts/orca_profile_tool.py`; the two special cases are [a wrongly inherited filament id](#what-generate-id-does-and-does-not-fix) and [BBL's authoritative setting ids](#bbls-exception-precisely). -`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id` — the id landscape, the -checks CI runs, and the Bambu catalog map. This page is the tooling half. +`docs/HLSD/filament_id.md` is the authoritative design document for `filament_id`: the id landscape, +the checks CI runs, and the Bambu catalog map. This page is the tooling half. | | `setting_id` | `filament_id` | | --- | --- | --- | | Identifies | one selectable preset | one filament **product** | -| Key hashed | `//` | `filament_product///` | -| Shape | 16 base62 chars | `OF` + 6 base62 chars | +| Key hashed | `//` | `filament_product///`, using the resolved (inherited) first values and the name up to the first `@`, right-trimmed | +| Shape | 16 base62 characters | `OF` + 6 base62 characters | | Required on | every `instantiation: "true"` preset | every **instantiated** filament, own or inherited | -| Forbidden on | bases (`instantiation != "true"`) | — (a base is exactly where it belongs) | -| Scope | globally unique across the tree | shared by every variant of the product, in every bundle | +| Forbidden on | bases (`instantiation` not `"true"`) | — (a product's root base is exactly where it belongs) | +| Scope | unique across the whole tree | shared by every preset of the product, in every bundle | -`` is `machine` / `process` / `filament` — the vendor is the **folder** name (`BBL`), not the -display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament, or editing -its `filament_vendor` or `filament_type`, also changes its `filament_id`. +`` is `machine`, `process` or `filament`, and the vendor is the **folder** name (`BBL`), not the +display name (`Bambulab`). Renaming a preset changes its `setting_id`; renaming a filament's alias, or +editing its `filament_vendor` or `filament_type`, also changes its `filament_id`, and the old id is not +forwarded. ## The tool -Use `scripts/orca_profile_tool.py` with a subcommand: +`scripts/orca_profile_tool.py` takes a subcommand: | Command | Does | | --- | --- | -| `check` | everything CI's `profile_tool` step runs — see [validation.md](validation.md) | +| `check` | everything CI's `profile_tool` step runs; see [validation.md](validation.md#orca_profile_toolpy-check) | | `generate-id` | writes `setting_id` and `filament_id` | | `normalize` | rewrites profile files into their canonical shape | -| `trim` | deletes profile files no `.json` list references | +| `trim` | deletes profile files no `.json` list references | | `update-index` | rebuilds the `*_list` sections from the files on disk | -The order after adding, renaming or deleting files — each step feeds the next, so it is not -interchangeable — is `normalize` → `update-index` → `generate-id` → `check`. -The [authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands. +The order after adding, renaming or deleting files is `normalize` → `update-index` → `generate-id` → +`check`. Each step feeds the next, so it is not interchangeable. The +[authoring workflow](../SKILL.md#creating-or-modifying-a-profile) has the commands. -> **`trim` deletes.** It removes every profile file the index does not list — including the one you just -> added and have not registered yet. Register first, or skip `trim` entirely; it is a cleanup sweep, not +> **`trim` deletes.** It removes every profile file the index does not list, including the one you just +> added and have not registered yet. Register first, or skip `trim` entirely: it is a cleanup sweep, not > part of landing a profile. Preview with `--dry-run`. **Register, then mint.** The `filament_id` pass reads `.json`'s `filament_list`, not the filesystem (the `setting_id` pass walks the filesystem, so a bundle whose index has not landed yet is -still assignable). A new filament file is therefore invisible to `generate-id`'s filament_id pass until -it is registered — its `setting_id` is written regardless. +still assignable). A new filament file is therefore invisible to `generate-id`'s `filament_id` pass +until it is registered; its `setting_id` is written regardless. -- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`) - and writes nothing. -- `--filament-id` / `--setting-id` narrow `generate-id`; they exclude each other, and passing neither - writes both. -- `--vendor` is repeatable and narrows **only what is written** — the id is a function of the triple +- `--dry-run` works on every writing command (`generate-id`, `normalize`, `trim`, `update-index`) and + writes nothing. +- `--filament-id` / `--setting-id` narrow `generate-id` to one pass; they exclude each other, and + passing neither writes both. +- `--vendor` is repeatable and narrows **only what is written**: an id is a function of its own key alone, so a narrowed run writes exactly what a full run would. An unknown vendor exits 1 before any write. `--vendor` on `check` narrows the per-vendor checks only; the `setting_id` and `filament_id` passes stay tree-wide. -- `--profiles DIR` points any command at another tree — see +- `--profiles DIR` points any command at another tree; see [Checking a copy of the tree](validation.md#checking-a-copy-of-the-tree). - `--profile-type` narrows `normalize`, `trim` and `update-index` to `machine_model`, `process`, `filament` or `machine`. - Exit codes: 0 clean, 1 errors found (`generate-id` still writes what it could), 2 argparse misuse. - Output is ANSI-coloured; searching for the literal `[ERROR]` still works. -`generate-id` is **idempotent and byte-preserving** — BOM and CRLF kept, one key line touched per pass. -A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated filament gets -both a `filament_id` and a `setting_id`, and a BBL file with a misspelled `settings_id` has that line -dropped and its value restored under the right key. `normalize` is the opposite by design — it rewrites -whole files into canonical shape — which is why `check` demands it already be a no-op. Some bundles have -CRLF committed (OrcaFilamentLibrary, Anycubic and RH3D among them), so a `normalize` pass there rewrites -every line — read the diff before committing it. +`generate-id` is **idempotent and byte-preserving**: BOM and CRLF are kept, and each pass touches only +its one key line. A legitimate `generate-id` diff is one or two changed lines per file: a new instantiated +filament gets both a `filament_id` and a `setting_id`. `normalize` is the opposite by design (it +rewrites whole files into canonical shape), which is why `check` demands it already be a no-op. A file +committed with CRLF line endings changes on every line under `normalize`; read the diff before +committing it. -On a clean tree `check` and `generate-id --dry-run` both exit 0 with zero findings. That is the -baseline to restore before opening a PR. +Exit 1 from `generate-id` does not mean nothing was written: it writes every id it can and reports the +rest, so read the diff before rerunning. On a clean tree `check` and `generate-id --dry-run` both exit 0 +with zero findings; that is the baseline to restore before opening a PR. ## What `generate-id` does and does not fix @@ -78,35 +80,37 @@ Writes: - a `setting_id` into any instantiated preset that lacks one, or whose value does not match the formula; - strips a `setting_id` from a base; -- deletes the misspelled `settings_id` key; +- deletes the misspelled key `settings_id`, moving its value to `setting_id` only on an instantiated BBL + preset that lacks one (everywhere else the old value is discarded and a fresh id minted); - a `filament_id` into the id-less **root(s)** of an instantiated filament that resolves none; - rewrites a **declared** `filament_id` that is not the mint of its own triple. Refuses to write (reports only): a base62 collision between two products, an empty `filament_vendor` or -`filament_type`, a broken `inherits` chain, roots of one filament resolving divergent `(vendor, type)` -pairs. +`filament_type`, a broken `inherits` chain, and roots of one filament resolving different +`(filament_vendor, filament_type)` pairs. -**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b, and it is the trap -most likely to bite. It happens when a branded filament inherits a generic for its settings: +**Does not fix: a preset that *inherits* a wrong `filament_id`.** This is check 2b (the label `docs/HLSD/filament_id.md` and the tool use), and it is the trap most likely to bite. +It happens when a branded filament inherits a generic for its settings: ```jsonc { "name": "Phrozen Aura PETG @Phrozen Arco 0.4 nozzle", - "inherits": "Generic PETG @System" } // resolves the OFL generic's id — wrong product + "inherits": "Generic PETG @System" } // resolves the library generic's id: wrong product ``` -The preset resolves *an* id, so `generate-id` neither inserts nor rewrites, and `check` fails with +The preset resolves *an* id, so `generate-id` neither inserts nor rewrites one, and `check` fails with `inherits filament_id "X" but its own triple "V/T/N" mints "Y"`. Two fixes, in order of preference: 1. **Give the product a `@base` root** inheriting a material base (`fdm_filament_pet`, - `fdm_filament_pla`, …). No `fdm_filament_*` base carries a `filament_id`, so the filament now resolves - none and `generate-id` mints it for you. This is also the shape the rest of the tree uses. -2. **Declare the tool-computed key on the preset itself.** Use the expected value reported by `check` - or compute it with the function below; this is not a manually chosen id. Make sure the preset - resolves the right `filament_vendor` and `filament_type` first — with - neither set, the triple resolves through the generic parent and the branded product is minted - under vendor `Generic`. If you need the id before the file exists: + `fdm_filament_pla`, …) with its own `filament_vendor` and `filament_type`. No `fdm_filament_*` base + carries a `filament_id`, so the filament now resolves none and `generate-id` mints it on the root. + This is the product-root shape ([the three-part shape](filament-profiles.md#the-three-part-shape)). +2. **Declare the tool-computed id on the preset itself.** Use the expected value `check` reports, or + compute it with the function below; this is not a manually chosen id. First make sure the preset + resolves the right `filament_vendor` and `filament_type`: with neither set, the triple resolves + through the generic parent and the branded product is minted under vendor `Generic`. If you need the + id before the file exists: ```bash python3 -c "import sys; sys.path.insert(0,'scripts'); from orca_profile_tool import generate_filament_id as g; print(g('Polymaker','PLA','PolyLite PLA'))" @@ -114,36 +118,39 @@ Two fixes, in order of preference: ``` The quoting works unchanged in cmd and PowerShell; only swap `python3` for `py -3`. + `generate_preset_setting_id('', '', '')` is the `setting_id` equivalent. - The `setting_id` equivalent is `generate_preset_setting_id('', '', '')`. +A vendor's tune of a generic that keeps the `Generic X` alias is not this case: inheriting the +generic's id is correct there, because the product really is the library generic +([filament-profiles.md](filament-profiles.md#the-two-most-common-contributions)). ## BBL's exception, precisely -`RESERVED_VENDORS = {"BBL"}` covers **`setting_id` assignment only**, keyed on the *folder* name: +The exception covers **`setting_id` assignment only**, keyed on the *folder* name `BBL`: -- The tool never mints or replaces a BBL `setting_id`. A new instantiated BBL preset with no - `setting_id` therefore **cannot be fixed by the tool**, yet the presence rule still applies to it — - carry over Bambu's authoritative id by hand. +- The tool never mints or replaces a `setting_id` in `BBL/`: those are Bambu's own ids. A new + instantiated BBL preset with no `setting_id` therefore **cannot be fixed by the tool**, yet the + presence rule still applies to it: carry over Bambu's authoritative id by hand. - BBL is not exempt from anything else: bases still get their `setting_id` stripped, ids must still be - globally unique, and BBL `filament_id`s are minted like everyone else's — every one of them is an - `OF*`. + unique across the tree, and BBL `filament_id`s are minted like everyone else's, as `OF…` ids. ## Ids other systems compose -No id from another system is the mint of a triple, so `check` rejects it like any other bad id — same -error, same remedy, whoever wrote it. Three such spaces exist near the tree; recognise them so you do -not copy one into a profile: +No id from another system is the mint of a triple, so `check` rejects one used as a `filament_id` like +any other bad id: same error, same remedy, whoever wrote it. Three such spaces exist near the tree; +recognise them so you do not copy one into a profile: -- **Bambu's `GF*` catalog** — external and opaque, correlated to Orca's ids by the generated - `resources/printers/bambu_filament_ids.json`. `GF` is a *prefix*, not a spelling the tree avoids: most - BBL `setting_id`s start with `G`, and `blacklist.json` and - `BBL/filament/filaments_color_codes.json` both reference Bambu catalog ids by design. The rule is - about `filament_id` and nothing else. -- **Qidi's `QD_*`** — composed at runtime by the box (`QD___`), not a preset id. -- **`P` + 7 hex, and `"null"`** — what `CreatePresetsDialog.cpp` gives a *user*-created filament. +- **Bambu's `GF…` catalog**: external and opaque, correlated to Orca's ids by the generated + `resources/printers/bambu_filament_ids.json`. `blacklist.json` and + `BBL/filament/filaments_color_codes.json` reference Bambu catalog ids by design. The rule is about + `filament_id` and nothing else: every BBL `setting_id` starts with `G`, and that is Bambu's own + preset id, not a leaked catalog id. +- **Qidi's `QD_…`**: composed at runtime by the printer's filament box + (`QD___`), not a preset id. +- **`P` + 7 hex digits, and `"null"`**: what the app gives a *user*-created filament. ## Tests -`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows). Note the -`-t scripts` argument; without it the imports fail. CI runs them as the first, non-`continue-on-error` -step of the profile job — see [validation.md](validation.md#ci). +`python3 -m unittest discover -s scripts/tests -t scripts` (`py -3 -m …` on Windows) runs the tool's +unit tests. Note the `-t scripts` argument; without it the imports fail. CI runs them as the first, +non-`continue-on-error` step of the profile job; see [validation.md](validation.md#ci). diff --git a/.claude/skills/orca-profiles/references/machine-profiles.md b/.claude/skills/orca-profiles/references/machine-profiles.md index 01071af6b1..de338f14ee 100644 --- a/.claude/skills/orca-profiles/references/machine-profiles.md +++ b/.claude/skills/orca-profiles/references/machine-profiles.md @@ -1,23 +1,23 @@ # Printer models and variants -Both live in `resources/profiles//machine/*.json`; models go in `machine_model_list`, variants -and shared bases in `machine_list`. Every one of them is registered. Some vendors (Elegoo, Eryone, -InfiMech, FlyingBear) nest a further subfolder under `machine/`, so recurse rather than globbing -`machine/*.json`. +Both live in `resources/profiles//machine/`; models go in `machine_model_list`, variants and +shared bases in `machine_list`. Every one of them is registered. A bundle may nest further subfolders +under `machine/`, so recurse rather than globbing `machine/*.json`. -## A `machine_model` is not a config preset +## `machine_model`: a record, not a config preset -It is parsed by a hand-written key switch, and only these keys are stored (`version` and `url` are -matched and discarded): +The loader reads a fixed set of keys from a `machine_model` and stores only these (`version` and `url` +are recognised and discarded): `name`, `model_id`, `nozzle_diameter`, `machine_tech`, `family`, `bed_model`, `bed_texture`, `hotend_model`, `default_materials`, `not_support_bed_type`, `image_bed_type`, `bottom_texture_end_name`, `bottom_texture_rect`, `bottom_texture_rect_longer`, `middle_texture_rect`, `use_double_extruder_default_texture`. -**Everything else is silently dropped.** Only `name` and `nozzle_diameter` are required. Dead keys ship -on real models today — `url`, `default_bed_type`, even a `desciption` typo — so a neighbour carrying a -key is no evidence it does anything. Printer config options belong on the `machine` preset, never here. +**Everything else is silently dropped**, a printer config key such as `default_bed_type` or a +misspelling included, so a neighbour carrying a key is no evidence it does anything. Printer config options belong on the `machine` preset, never here. The loader drops a model +silently if its index entry has no name or its `nozzle_diameter` yields no sizes; `check` also requires +the file's own `name`. ```json { @@ -36,28 +36,28 @@ key is no evidence it does anything. Printer config options belong on the `machi | Field | Notes | | --- | --- | -| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check_name_consistency` forces it to equal the file's `name`, so they coincide. | -| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity. Changing it changes device matching. | -| `machine_tech` | only `starts_with("SL")` means SLA; everything else is FFF. Write `FFF`; a few models write `FGF`, which is a label with no effect. | -| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (Qidi writes `0.4;0.2;0.6;0.8` to put the default first). This list is the authoritative set of legal `printer_variant` values. | -| `default_materials` | `;`-separated filament **preset names**. Used to preselect in the wizard *and* by `PresetBundle::load_installed_filaments` to auto-install a printer's filaments on first run, so a dangling entry costs a real user a filament. Not `,`; case-sensitive (`@System`). `check` fails on a dangling name here or in `default_filament_profile`. | +| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check` forces it to equal the file's `name`, so they coincide. | +| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity; changing it changes device matching. | +| `machine_tech` | only a value starting with `SL` means SLA; everything else is FFF. Write `FFF`; `FGF` behaves as FFF. | +| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (`0.4;0.2;0.6;0.8` puts the default first). This list is the authoritative set of legal `printer_variant` values. | +| `default_materials` | `;`-separated filament **preset names**, not `,`; case-sensitive (`@System`); order is ignored. Used to preselect filaments in the setup wizard *and* to install a printer's filaments on first run, so a dangling entry costs a real user a filament. Every name must exist (`check` fails on a name matching no filament file, here or in `default_filament_profile`), and every variant of the model needs at least one entry compatible with it (`validate_system`). | | `family` | a wizard grouping label only; give every model one. | ### Assets -`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (by id). -Majority convention: `_buildplate_model.stl` and `_buildplate_texture.svg`. An empty string -is the legal "none", and is the norm for `hotend_model`. +`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (named by the +vendor id). Convention: `_buildplate_model.stl` and `_buildplate_texture.svg`. An +empty string is the legal "none", and is the norm for `hotend_model`. -**Nothing checks that the file exists.** A missing `hotend_model` falls back to -`resources/profiles/hotend.stl`; a missing `bed_model`/`bed_texture` just renders nothing. Broken -references already ship. Verify by hand. +**Nothing checks that the files exist.** A missing `hotend_model` falls back to +`resources/profiles/hotend.stl`; a missing `bed_model` makes the bed render as a generic custom bed, and a missing +`bed_texture` renders no texture. +Verify by hand, in exact case. -Every model also has a `_cover.png` in the vendor folder — treat it as required, not optional. -240×240 is the cap `scripts/optimize_cover_images.py` enforces and the size most covers already use. -A missing cover degrades to a placeholder in both the wizard and the sidebar. +Every model also has a `_cover.png` in the vendor folder; treat it as required, not optional. +240×240 is the cap `scripts/optimize_cover_images.py` enforces. A missing cover degrades to a placeholder in both the wizard and the sidebar. -## The `machine` variant +## `machine`: the variant ```json { @@ -79,116 +79,154 @@ A missing cover degrades to a placeholder in both the wizard and the sidebar. Minimum viable key set: `type`, `name`, `from`, `instantiation`, `setting_id`, `inherits`, `printer_model`, `printer_variant`, `nozzle_diameter`, `printable_area`, `printable_height`, -`default_print_profile`. The four keys without which the preset will not load at all are `name`, -`instantiation`, `printer_model` and `printer_variant`; `default_filament_profile` is an array -(`["Generic PLA @System"]`) and the model's `default_materials` a `;`-separated string. Unlike a -`machine_model`, a `machine` **is** config-loaded, so a key belonging to another preset type is a -reported error (a misspelled key is still silent). +`default_print_profile`. `default_filament_profile` is optional; when written it +is an array (`["Generic PLA @System"]`), while the model's `default_materials` is a `;`-separated +string. Unlike a `machine_model`, a `machine` **is** a config preset, so a key belonging to another +preset type is a reported error and is removed; a misspelled key is still dropped silently. -### `printer_variant` — three hard rules +### `printer_model` and `printer_variant` -1. Non-empty, and an exact member of the model's `;`-separated `nozzle_diameter` list. -2. `printer_model` non-empty and naming a model of this vendor. -3. In validation mode, for instantiated presets only: split `printer_variant` on `+`, each token must - start with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set** - must equal `set(nozzle_diameter)`. +1. `printer_model` is non-empty and names a model of this bundle exactly. +2. `printer_variant` is non-empty and an exact token of that model's `;`-separated `nozzle_diameter` + list. +3. For instantiated presets, when validating: split `printer_variant` on `+`; each token must start + with a number (a trailing non-numeric suffix such as `HF` is ignored), and the resulting **set** must + equal the set of `nozzle_diameter` values. -Rules 1 and 2 are loader-enforced — failing either drops the preset *and* the whole bundle. Rule 3 only -raises a validation error: the preset still loads, but the validator exits non-zero. +Rules 1 and 2 are loader-enforced: failing either discards the whole bundle. Rule 3 only raises a +validation error: the preset still loads, but the validator exits non-zero. -`nozzle_diameter` lists one entry **per physical nozzle**; `printer_variant` lists the **distinct** -diameters joined with `+`. Snapmaker U1 is the worked case: `["0.4","0.4","0.6","0.6"]` against -`"0.4+0.6"` — it passes because the comparison is on sets. +`nozzle_diameter` lists one entry **per extruder**; `printer_variant` lists the **distinct** diameters +joined with `+`: `["0.4","0.4","0.6","0.6"]` against `"0.4+0.6"` passes because the comparison is on +sets. -The conventional values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`. Suffixed forms -(`0.4HF`, `0.6HF`, `0.8HF`, `0.4HS`) are Flashforge-only and the `+` form is rare. A variant is **not** -required to be unique within a model — Volumic ships `EXO42 IDRE`, `… COPY MODE` and `… MIRROR MODE` all -at `0.4` under the one model `EXO42 IDRE`. +Write `printer_variant` as a bare diameter matching the model's list, with no unit. The conventional +values are `0.2`, `0.25`, `0.4`, `0.5`, `0.6`, `0.8` and `1.0`; a suffixed form (`0.4HF`, `0.4HS`) or +the `+` form is legal under rule 3. A `printer_variant` is **not** required to be unique within a +model: an IDEX model's normal, `COPY MODE` and `MIRROR MODE` presets can all be `0.4`. The converse is **unchecked**: a nozzle size in the model's list with no matching variant is offered in -the wizard and resolves to nothing. `Wanhao France`'s `D12 500 PRO M2 DIRECT` ships that bug today. +the wizard and resolves to nothing. A variant whose `printer_model` names a sibling model by mistake +leaves its own model's size in exactly that state. -### Other fields worth knowing +### Other keys -- `default_print_profile` is a **scalar**, matched by exact preset name. Not a `;` list. The named +- `default_print_profile` is a **scalar**, matched by exact preset name; not a `;` list. The named process must be compatible with this printer through its resolved list or condition. `validate_slice` attempts to select it and rejects generic Default fallbacks, but compatibility - updates can choose another compatible preset. Check the exact default reference yourself. -- `default_filament_profile` is an **array**, one name per element. -- `printable_area` is an array of `"XxY"` strings — four points for a rectangle, one per segment for a - delta or circular bed. -- `gcode_flavor` is usually set once in the base; `klipper`, `marlin`, `marlin2` and `reprapfirmware` - cover nearly every shipped printer. -- `printer_settings_id` is junk — most files carrying it disagree with their own name. Do not copy it - when cloning a bundle. -- `min_layer_height` / `max_layer_height` are **machine** keys (per extruder), never process keys. + updates can choose another compatible preset, and `check` does not resolve the name. Check the exact + default reference yourself. +- `default_filament_profile` is an **array**, one name per element. Entry 0 is the filament preselected + when the printer is chosen; entry *i* is the preferred replacement when filament *i* is incompatible, + and any listed name outranks an unlisted one. The validator checks every entry. The list of a + printer's filaments is the model's `default_materials`: a new filament goes into `default_materials`; + put it first in `default_filament_profile` only if it should become the preselected one. +- `printable_area` is an array of `"XxY"` strings: four points for a rectangle; a delta or other circular bed + is a polygon with one point per segment. +- A non-BBL printer shows the plate selector only with `support_multi_bed_types` `"1"`; otherwise it + uses its `default_bed_type` (a plate name such as `"Textured PEI Plate"`; High Temp Plate when unset). + Filaments still set every plate + ([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)). +- `gcode_flavor` is usually set once in the base; the common values are `klipper`, `marlin`, `marlin2` + and `reprapfirmware`. +- `printer_settings_id` does nothing in a preset file: the app replaces it with the selected preset's + name before slicing. Omit it, and do not copy it when cloning a bundle. +- `min_layer_height` / `max_layer_height` are **machine** keys (one per extruder), never process keys. ## Bases -Nearly every machine-bearing vendor registers a base literally named `fdm_machine_common`, and Klipper -vendors add `fdm_klipper_common` on top of it. Two levels is the usual depth. +The conventional machine root is a base named `fdm_machine_common`, with `fdm_klipper_common` on top +of it for Klipper printers. Which values a hardware +family or model base holds, and when adding one pays off, is in [shared-bases.md](shared-bases.md). **There is no leading-underscore convention for bases.** ## Adding a printer to an existing bundle -1. Choose the names first — model, variant(s), process(es); everything else references them. +1. Choose the names first: model, variant(s), process(es); everything else references them + ([naming.md](naming.md)). 2. Add the model (`machine_model_list`) and one `machine` variant per nozzle; the minimum key sets are above. Bed assets and `_cover.png` go directly in `/`. 3. Add at least one process per variant naming it in `compatible_printers` ([process-profiles.md](process-profiles.md#adding-a-quality-tier-or-a-nozzles-processes)). -4. Register everything (or run `update-index`), bump the version, run the id tool, validate. +4. Add the variants to the filaments they should offer, and set the model's `default_materials` so every + variant has a compatible entry. +5. Register everything (`update-index`), bump the version, run the id tool, validate: the + [authoring workflow](../SKILL.md#creating-or-modifying-a-profile). ## Adding a nozzle variant 1. Extend the model's `nozzle_diameter` (`"0.4"` → `"0.4;0.6"`). -2. Add the variant preset. Either inherit the shared base (the usual choice) or the 0.4 sibling (Elegoo, - BBL, Prusa and Qidi do this — smaller diff, but the sibling's edits now reach this file too). -3. Override what actually changes with nozzle: `nozzle_diameter`, `printer_variant`, - `default_print_profile`, `default_filament_profile`, `min_layer_height`/`max_layer_height`, and +2. Add the variant preset. Either inherit the shared base (the usual choice), or the 0.4 sibling + (a smaller diff, but the sibling's edits now reach this file too). Follow the bundle. +3. Override what actually changes with the nozzle: `nozzle_diameter`, `printer_variant`, + `default_print_profile`, `default_filament_profile`, `min_layer_height` / `max_layer_height`, and retraction if the vendor tunes it. -4. Add at least one process for the new nozzle — see [process-profiles.md](process-profiles.md). +4. Add at least one process for the new nozzle (see [process-profiles.md](process-profiles.md)), and + extend the filaments' `compatible_printers` so at least one `default_materials` entry covers the new + variant. 5. Register both, bump the version, run the id tool, validate. -## Multi-extruder, IDEX and tool-changers +## Multi-extruder, IDEX and tool changers -Per-extruder vectors are **silently resized** to the nozzle count, with no error. Padding repeats the -**first** value, not the last — `["0.4","0.6"]` on a 4-nozzle machine becomes `0.4, 0.6, 0.4, 0.4`. -Longer vectors are truncated. +Per-extruder vectors hold one value per extruder (`len(nozzle_diameter)`), and a wrong length raises no +error: a short vector acts as padded with its **first** value, not the last (`extruder_offset` +`["0x0","50x0"]` on a 4-extruder machine reads as `0x0, 50x0, 0x0, 0x0`), and entries beyond the +extruder count are never read. -Note the two sizing families: the plain per-extruder keys (`extruder_offset`, `extruder_colour`, -`extruder_printable_height`, `min_layer_height`, `max_layer_height`, `nozzle_diameter`) are sized to the -extruder count, while `printer_options_with_variant_1` (`retraction_length`, `z_hop`, `wipe`, -`nozzle_type`, the rest of the retraction family) is sized to `printer_extruder_variant` instead. +Note the two sizing families. The plain per-extruder keys (`printer_extruder_options`: `extruder_type`, +`nozzle_diameter`, `default_nozzle_volume_type`, `extruder_printable_height`, `min_layer_height`, +`max_layer_height`, …, given in full with the [key sets](extruder-variants.md#the-four-key-sets), plus +`extruder_offset` and `extruder_colour`) hold one value per extruder. The variant sets +(`retraction_length`, `z_hop`, `wipe`, `nozzle_type`, the `machine_max_*` limits at stride 2; +[the full lists](extruder-variants.md#the-four-key-sets)) are sized to the variant length: +`len(printer_extruder_variant)`, or one variant per extruder when the resolved preset writes no layout, +since `extruder_variant_list` defaults to one `Direct Drive Standard` per extruder +([widths](extruder-variants.md#widths)). `extruders_count` is a printer-tab field, not a preset key; the +loader drops it. - Give **one entry per extruder** for ordinary per-extruder vectors such as `extruder_offset`, - `extruder_colour`, `min_layer_height` and `max_layer_height`; size the variant-dependent family - to `printer_extruder_variant` instead. - A single `["0x0"]` `extruder_offset` on a dual or multi-tool machine — which already ships — pads every - toolhead to the same offset, so the offset never applies. -- Overriding `nozzle_diameter` to a different count without re-stating every per-extruder vector is the - other half of the trap — `Snapmaker U1 (0.4+0.6 nozzle)` inherits 5-entry vectors against 4 nozzles. + `extruder_colour`, `min_layer_height` and `max_layer_height`. Size the variant sets to the variant + length × stride, one value per variant (per extruder when there is no layout; a (normal, silent) + pair for the `machine_max_*` limits), even where the values are the same; the loader keeps only the first value of a list-less printer's variant arrays, so declare + the layout when the extruders differ. A single `["0x0"]` `extruder_offset` on a dual or + multi-extruder machine pads every extruder to the same offset, so the offset never applies. +- Overriding `nozzle_diameter` to a different count without restating every per-extruder vector is the + other half of the trap: a 4-extruder preset on a 5-extruder base inherits 5-entry vectors against 4 + extruders. + The reverse is silent too: a base is stored resized to its own `printer_extruder_variant` (one variant + when it writes none, whatever its extruder count), so a wider variant array on it reaches the children + as its first value padded ([composition](extruder-variants.md#padding-truncation-and-composition)). + `check` does not judge the base on its own; where its extruders need different values, declare the + layout on the base. -Copy targets: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4 -nozzle.json` (a clean minimal variant on a base that gives every vector five entries), and -`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. The BBL extruder-variant machinery -(`extruder_variant_list`, `printer_extruder_id`, `default_nozzle_volume_type`) is used by a handful of -vendors — do not copy it into a new bundle (`nozzle_volume_type` itself is not a machine-preset key). +Structure to copy: `Custom/machine/fdm_toolchanger_common.json` + `Custom/machine/MyToolChanger 0.4 +nozzle.json` (a minimal variant on a base that gives the per-extruder vectors five entries), and +`Ratrig/machine/RatRig V-Core 4 IDEX 300 0.4 nozzle.json` for IDEX. Take the structure from them and +the widths from the [sizing equation](extruder-variants.md#sizing-equation). Both are list-less, so +every variant array holds one value per extruder and the loader keeps only the first: fine while every +extruder shares the same retraction and limits. Add the extruder-variant layout +(`extruder_variant_list`, `printer_extruder_variant` / `printer_extruder_id`, +`default_nozzle_volume_type`) when the hardware has swappable nozzle volume types or mixed extruder +types, or as soon as one extruder needs its own value in a variant key; how to author it, and the +matching process and filament variants, is in [extruder-variants.md](extruder-variants.md). +(`nozzle_volume_type` itself is not a machine-preset key.) ## Custom G-code The keys are `machine_start_gcode`, `machine_end_gcode`, `change_filament_gcode`, -`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Both a single string with -embedded `\n` and a JSON array of lines are legal and both are in use — do not convert one into the -other. Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`; `{elsif}` is rare but real (Qidi's -`layer_change_gcode` uses it). +`machine_pause_gcode`, `before_layer_change_gcode` and `layer_change_gcode`. Each is one string with +embedded `\n`. Never split G-code into a JSON array of lines: the loader joins array elements with `,` +into a single line (a one-element array is equivalent to the string): a two-element +`machine_start_gcode` becomes one line, `PRINT_START …,SET_PRESSURE_ADVANCE ADVANCE=0.046`. +Conditionals are `{if …}` / `{elsif …}` / `{else}` / `{endif}`. -Placeholder errors only surface when the config is actually expanded, which means `validate_slice`: +Placeholder errors only surface when the G-code is actually expanded, which means `validate_slice`: ```bash ./scripts/check_profile.sh --vendor "" validate_slice # Windows: scripts\check_profile.bat -Vendor "" validate_slice ``` -What the sweep covers is in [validation.md](validation.md#validate_slice); no `CP TOOLCHANGE START` in -the output means `change_filament_gcode` never expanded. +What the sweep covers is in [validation.md](validation.md#validate_slice); a printer whose output has no +`CP TOOLCHANGE START` fails it, because its `change_filament_gcode` never expanded. diff --git a/.claude/skills/orca-profiles/references/naming.md b/.claude/skills/orca-profiles/references/naming.md index cad82ebb6b..e2fd5a4d88 100644 --- a/.claude/skills/orca-profiles/references/naming.md +++ b/.claude/skills/orca-profiles/references/naming.md @@ -1,90 +1,94 @@ # Preset naming -A preset's `name` is the loader's key, not decoration. The index registers it; `inherits`, -`compatible_printers` and the `default_*` keys reference it by the exact string; `renamed_from` depends -on it; and `setting_id` / `filament_id` hash it (see [ids.md](ids.md)). Two presets of one type in a -bundle may not share a name (`check_preset_name_uniqueness`). Treat a name change as an identity change, -not a relabel. - -Naming is convention only where the loader does not parse it. What the loader actually acts on: - -| Type | Shape | Acted on | -| --- | --- | --- | -| `machine_model` | `` | the exact string, named by a variant's `printer_model` | -| `machine` | ` nozzle` | the exact string, named by `compatible_printers`; `printer_variant` must equal a nozzle diameter | -| `process` | `mm @` | the exact string when referenced or selected; the `@` half is a label | -| `filament` | ` @` | text before the first `@` is the **alias**, used for shadowing; the rest is a label | +A preset's `name` is its identity, not decoration. The index registers it; `inherits`, +`compatible_printers`, `printer_model` and the `default_*` keys reference it by the exact, +case-sensitive string; `renamed_from` migrates it; `setting_id` and `filament_id` hash it +([ids.md](ids.md)). Treat a name change as an identity change that needs +[migration](vendor-bundle.md#renamed_from), not a relabel. Which part of each name the loader acts on +is the table in [SKILL.md](../SKILL.md#names); this page holds the conventions. ## `machine_model` -`` — the vendor-prefixed model name (`Bambu Lab X1 Carbon`, `Creality K1`, `Prusa CORE One`). A -variant names it verbatim in `printer_model`; a mismatch makes the variant invalid. `check_name_consistency` -forces the index entry to equal the file's `name`, and the variant's `printer_model` targets this string -([A `machine_model` is not a config preset](machine-profiles.md#a-machine_model-is-not-a-config-preset)). -It is also the `_cover.png` and bed-asset stem. +``, vendor-prefixed: `Bambu Lab X1 Carbon`, `Creality K1`, `Prusa CORE One`. Each variant's +`printer_model` names it verbatim (a mismatch discards the bundle), and its index entry equals the +file's `name`. It is also the stem of `_cover.png` and, by convention, of the bed assets +(`_buildplate_model.stl`). ## `machine` (variant) -` nozzle` is near-universal (`Bambu Lab X1 Carbon 0.4 nozzle`). `printer_variant` holds -the bare nozzle (`0.4`) and must be an exact member of the model's `nozzle_diameter` list — the hard -rules are in [The `machine` variant](machine-profiles.md#the-machine-variant). Casing varies -(`nozzle` / `Nozzle`): match the bundle, not this page. A variant that is not nozzle-specific (a special -toolhead, a multi-material build) may drop the suffix — still an exact reference. Bases are named -`fdm_machine_common` / `fdm__common`. +` nozzle` is near-universal (`Bambu Lab X1 Carbon 0.4 nozzle`). Casing varies +(`nozzle` / `Nozzle`): match the bundle, not this page. A variant that is not nozzle-specific (a +special toolhead, a multi-material build, IDEX copy and mirror modes such as +` COPY MODE (0.4 nozzle)`) may drop or reshape the suffix; it is still an exact reference. `printer_variant` +holds the nozzle token: `0.4`, a suffixed `0.4HF`, or `0.4+0.6` for mixed nozzles +([rules](machine-profiles.md#printer_model-and-printer_variant)). ## `process` -`mm @` — [process-profiles.md](process-profiles.md#naming) has the -quality ladder and the `fdm_process_*` base names. The quality label stays before `@` and the printer -target after it: a printer model in the quality slot leaves the tier undescribed. The `@` is a -human label, not a reference: it usually does not equal a real variant, and compatibility comes from the -resolved `compatible_printers` list or condition. +`mm @`. The quality word stays before `@` and the printer target after +it: a printer model in the quality position leaves the tier undescribed. The `@` is a label, +and need not equal any variant name; compatibility comes from `compatible_printers` or the +condition. The quality ladder and per-nozzle labels are in +[process-profiles.md](process-profiles.md#naming). ## `filament` -` @`. The product half is what `filament_id` hashes and what survives as the **alias** up -to the first `@`; the target half is a label except for reserved forms: +` @`. The product half, up to the first `@` and right-trimmed, is the **alias**: +shadowing matches on it and `filament_id` hashes it. The target half is a label, except for the +reserved forms: -- `@base` — a non-instantiated product root. `@base` is convention; a base is really identified by - `instantiation: "false"` and no `setting_id` ([the three-part shape](filament-profiles.md#the-three-part-shape)). -- `@System` — the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product - (` @System`, empty `compatible_printers`); not enforced, so a deviation is worth a review - comment. The literal `Generic @System` is load-bearing for 3MF/project recovery, beyond the - alias rule ([alias shadowing](filament-profiles.md#alias-shadowing)). -- `@`, `@ `, `@ nozzle` — printer tunes, BBL's shape. - Other vendors differ (a bare model, a printer serial, Creality's `@-all`). Specificity is judged - from `compatible_printers`, not the name +- `@base`: a non-instantiated product root. Convention only; a base is really + `instantiation: "false"` without `setting_id` + ([the three-part shape](filament-profiles.md#the-three-part-shape)). +- `@System`: the OrcaFilamentLibrary selectable shim, and the convention for an all-printer product + (` @System`, empty `compatible_printers`). Not enforced, so a deviation is worth a review + comment. The literal `Generic @System` is also load-bearing for project recovery + ([alias shadowing](filament-profiles.md#alias-shadowing)). +- Printer tunes. BBL's shape is the reference: `@` (vendor-wide), `@ ` (one + model), `@ nozzle` (one variant). Other vendors differ: a bare model + (`QIDI ABS-GF@Q2-Series`), a printer serial, Creality's `@-all`. Judge specificity from + `compatible_printers`, never from the name ([one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product)). -- Color is not part of the product name: ` ` presets are not authored; the color is - chosen at runtime - ([color is a runtime property](filament-profiles.md#color-is-a-runtime-property)). +- No colour in the product name: ` ` presets are not authored; colour is chosen at + runtime ([colour](filament-profiles.md#colour-is-a-runtime-property)). -## Checking names - -Check every newly added profile's `name` against its type and role: model, selectable preset or base. -Apply the same checks to an intentional name change. The human-readable naming shapes are not enforced -by `check`: inspect the added or renamed profiles in the diff, using neighbouring names as context and -following the bundle's established style where the type-specific conventions allow variation. - -For bases (`instantiation: "false"`), use the type-specific conventions: +## Bases | Type | Base names | | --- | --- | -| `machine` | `fdm_machine_common`, `fdm__common`, or an established machine-family base name | -| `process` | `fdm_process_*`, including shared roots and per-layer-height / per-nozzle bases such as `fdm_process_single_0.20` | -| `filament` | `fdm_filament_*` material roots or ` @base` product roots | +| `machine` | `fdm_machine_common`, `fdm__common`, `fdm_klipper_common`, or an established machine-family base | +| `process` | `fdm_process_*`: shared roots and per-layer-height or per-nozzle bases such as `fdm_process_single_0.20` or `fdm_process___nozzle_` | +| `filament` | `fdm_filament_*` material roots, ` @base` product roots | -Shared base names across bundles are intentional, including product roots such as `Fiberon PA6-CF @base`. -Investigate a newly authored base that retains an unrelated selectable preset's name from a copy. +There is no leading-underscore convention. Base names repeat across bundles by design: +every bundle may have its own `fdm_process_common`, and a product root such as `Fiberon PA6-CF @base` +can exist in both the library and a vendor. Investigate a newly authored base that kept an unrelated +selectable preset's name from a copy. -**Name uniqueness is checked by CI.** `check_preset_name_uniqueness` checks type + name within each -bundle. `check_machine_model_name_uniqueness` checks model names across the entire tree, even with -`--vendor`: `Preset::get_printer_type` matches `printer_model` against all vendors' models and returns -the first match, so a duplicate makes lookup depend on vendor order. Both run as part of -`python3 scripts/orca_profile_tool.py check`. +## Uniqueness -## Not the same as the filename +- Type + name is unique within a bundle, indexed or not; `check` enforces it. +- `machine_model` names are unique across the whole tree; `check` enforces it even with `--vendor`. + Printer-type lookup matches `printer_model` against every vendor's models and takes the first, so a + duplicate makes it depend on vendor order. +- At load, two selectable presets with one name discard the bundle, and a duplicate across vendors + is a validator error. Two bases with one name, or a base and a selectable preset, load silently and + the first in the index wins; an unindexed twin is therefore one `sub_path` edit away from becoming + the parent every child resolves to. -The loader keys off `name`, and a filename that disagrees usually still loads. Index `name` must equal the -file's `name`, and the filename should match `sub_path`; a mismatch that differs only in case breaks -another platform ([cross-platform paths](validation.md#cross-platform-paths)). +## Filenames and paths + +The loader keys off `name`, and a filename that disagrees usually still loads, but keep the filename +equal to the `name` and to the index `sub_path`. Match the exact case of every `sub_path` and asset +filename: Linux filesystems distinguish case even when a macOS or Windows checkout does not, and +preset-name references are case-sensitive on every platform. Avoid Windows-invalid characters +(`< > : " | ? *`), reserved device names such as `CON` and `NUL` (with any extension), and trailing +spaces or dots in a path component; a space right before `.json` is not a trailing space. + +## Checking names + +Check the `name` of every newly added profile, and every intentional rename, against its type and role +(model, selectable preset or base). `check` does not enforce the shapes on this page: inspect the +added or renamed presets in the diff, use neighbouring names as context, and follow the bundle's +established style where the conventions allow variation. Preserve shipped names during ordinary +tuning; renaming a shipped selectable preset needs `renamed_from`. diff --git a/.claude/skills/orca-profiles/references/process-profiles.md b/.claude/skills/orca-profiles/references/process-profiles.md index 98f31bac1a..785c9ee358 100644 --- a/.claude/skills/orca-profiles/references/process-profiles.md +++ b/.claude/skills/orca-profiles/references/process-profiles.md @@ -1,14 +1,15 @@ # Process profiles -Processes live in `resources/profiles//process/` — selectable leaves and shared bases alike, and +Processes live in `resources/profiles//process/`, selectable leaves and shared bases alike, and every one of them is registered in `process_list`. There are no global processes shared across vendors. ## Naming -`"mm @"` — near-universal, so match it. +`"mm @"` is near-universal, so match it: the quality word before `@`, +the printer label after it ([naming.md](naming.md#process)). -Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to -the layer-height / nozzle ratio; it is a naming convention, not a loader constraint: +Follow the bundle's existing quality vocabulary. BBL's common ladder relates the quality word to the +layer height / nozzle ratio; it is a naming convention, not a loader constraint: | Quality | Ratio | 0.2 nozzle | 0.4 | 0.6 | 0.8 | | --- | --- | --- | --- | --- | --- | @@ -20,49 +21,50 @@ the layer-height / nozzle ratio; it is a naming convention, not a loader constra | Extra Draft | 0.7× | 0.14 | 0.28 | 0.42 | 0.56 | This is the `fdm_process_single__nozzle_` ladder; 0.4 is commonly the unsuffixed nozzle default. -Match neighbouring names rather than renaming shipped tiers to fit the table. +Newer BBL printers add High Quality, Balanced Quality and Strength tiers. Match neighbouring names rather +than renaming shipped tiers to fit the table. On a model with several nozzles, processes for the other +nozzles usually carry the nozzle in the label (`0.30mm Standard @BBL X1C 0.6 nozzle`); follow the bundle. -The `@target` is a human label, not a reference: most do not equal any real printer variant name. +The `@target` is a human label, not a reference: it need not equal any printer variant name. Compatibility comes from the resolved list or condition, not this label. ## Shape -A selectable leaf's only truly universal keys are `type`, `setting_id`, `name` and `instantiation`; -`inherits` and `from` are near-universal — plus compatibility. No slicing key is universal; even -`layer_height` is more often inherited than restated. A base has `type`, `name`, `instantiation`, almost -always `from`, and **no** `setting_id`. +A selectable leaf has `type`, `setting_id`, `name` and `instantiation`, normally `inherits` and +`from`, plus compatibility; its slicing keys, `layer_height` included, normally come from its bases. A +base has `type`, `name`, `instantiation`, `from`, and **no** `setting_id`. -**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model — +**Target shape: a 7-key leaf.** `OrcaArena` is the cleanest model: `fdm_process_common` → `fdm_process_arena_common` → `fdm_process_arena__nozzle_` → leaf, where the -leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation`, +leaf carries only `type`, `name`, `inherits`, `from`, `setting_id`, `instantiation` and `compatible_printers`, and the per-nozzle base holds the layer height and all eight line widths. +[shared-bases.md](shared-bases.md#levels) says which level each process setting belongs to. -BBL, WonderMaker and Z-Bolt are uniform in *layering* — every leaf inherits a base, names its printers -directly and holds no layer height of its own — but not in key count. Imitate BBL's layering, not its -content: its leaves carry doubled `print_extruder_variant` arrays that no single-variant vendor needs. +BBL's *layering* is a model too (every leaf inherits a base, names its printers directly and holds no +layer height of its own), but not its content: its leaves carry multi-variant `print_extruder_variant` +arrays that no single-variant vendor needs ([extruder-variants.md](extruder-variants.md#process)). -Nearly every vendor ships its own `fdm_process_common` as the inherits-less root. Those files are not -identical; copying another vendor's version into a new bundle is normal. +A bundle has its own `fdm_process_common` as the inherits-less root, since a process inherits only +inside its bundle; starting a new bundle's from another vendor's copy is fine. -Beware leaf-inherits-leaf: Prusa chains several levels deep through sibling leaves, and Elegoo and -Flashforge do it too, so editing one selectable process silently changes others. Check a leaf's children -before editing it. +Beware leaf-inherits-leaf: a bundle may chain selectable processes several levels deep, so editing one +silently changes others. Check a leaf's children before editing it. ## Compatibility -Most leaves set `compatible_printers` directly; some inherit it from a base, and Prusa's fall through to +A leaf sets `compatible_printers` directly, inherits it from a base, or falls through to `compatible_printers_condition`. After resolving `inherits`, **every selectable process has one or the -other** — that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is +other**: that is the invariant to review against. Unlike filaments, inheriting `compatible_printers` is legitimate for a process, and no check enforces its presence. -- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead code**. Use one or - the other. -- A condition that fails to parse means *compatible with everything* — a warning, not an error. A typo +- A non-empty `compatible_printers` makes `compatible_printers_condition` **dead**. Use one or the + other. +- A condition that fails to parse means *compatible with everything*: a warning, not an error. A typo widens compatibility instead of narrowing it. -- Matching is `boost::regex` **`regex_match`** — a full-string match, which is why every shipped - condition wraps its keyword in `.*`. Because it is boost rather than `std`, `.` also spans the newlines - inside `printer_notes`. -- A `printer_notes` keyword that prefixes another model's keyword matches both. Prusa guards it: +- A regex in a condition must match the **whole** string, so wrap the keyword in `.*`; `.` also spans + the newlines inside `printer_notes`. +- A `printer_notes` keyword that prefixes another model's keyword matches both. Guard it with a + character class after the keyword, and combine terms with `and`: ``` printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/ @@ -70,76 +72,84 @@ legitimate for a process, and no check enforces its presence. The `[^_a-zA-Z0-9]` exists because `PRINTER_MODEL_COREONE_L` also contains `PRINTER_MODEL_COREONE`. -`compatible_printers` is almost always one element. A leaf listing a whole model family is where a newly -added printer is usually forgotten. +A leaf listing a whole model family is where a newly added printer is usually forgotten. -## What to review per nozzle +## Values to review per nozzle | Key group | Review | | --- | --- | | `line_width` and per-region widths | resolved widths suit the nozzle and layer height | -| `layer_height`, `initial_layer_print_height` | within the printer's limits | +| `layer_height`, `initial_layer_print_height` | within the printer's `min_layer_height` / `max_layer_height` | | print speeds | consistent with flow limits and hardware tuning | -| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behavior | +| shell layers, wall loops, accelerations, support Z distances | preserve the intended thickness, motion and support behaviour | -**A common starting pattern is nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In that pattern, at 0.4, -`inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and +**A common starting pattern is line width = nozzle + 0.02 mm**: 0.22 / 0.42 / 0.62 / 0.82 / 1.02. In +that pattern, at 0.4, `inner_wall_line_width`, `sparse_infill_line_width`, `skin_infill_line_width` and `skeleton_infill_line_width` widen to 0.45 and `initial_layer_line_width` to 0.5; at 0.2, `initial_layer_line_width` widens to 0.25. Also derived, and easily missed: -`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41). -These are examples, not required values; preserve intentional vendor tuning and percentage/automatic -widths, and validate their resolved values. +`ironing_inset = line_width / 2` (0.11 / 0.21 / 0.31 / 0.41). These are examples, not required values; +preserve intentional vendor tuning and percentage or automatic widths, and validate their resolved +values. -`min_layer_height` and `max_layer_height` are machine keys — no process file sets them. +`min_layer_height` and `max_layer_height` are machine keys; no process file sets them. -## Slice-time content checks +### Slicing limits -`Print::validate()` enforces four rules at slice time: +Slicing rejects a process that breaks one of these (the message in italics): -1. `initial_layer_print_height` ≤ min `nozzle_diameter` -2. `layer_height` ≤ min `nozzle_diameter` — *"Layer height cannot exceed nozzle diameter."* -3. `line_width` and the seven per-region widths (inner/outer wall, sparse infill, internal solid infill, - top surface, skin, skeleton) > `layer_height` — *"Line width too small"*. `support_line_width` only - when the object has support or a raft; `initial_layer_line_width` is never checked. -4. every width ≤ 5 × max `nozzle_diameter` — *"Line width too large"* +1. `initial_layer_print_height` ≤ the smallest `nozzle_diameter` (with a raft, the nozzle of the raft's + first-layer extruder). +2. `layer_height` ≤ the smallest `nozzle_diameter`: *"Layer height cannot exceed nozzle diameter."* +3. `line_width` and the seven per-region widths (inner and outer wall, sparse infill, internal solid + infill, top surface, skin, skeleton) > `layer_height`: *"Line width too small"*. + `support_line_width` is checked only when the object has support or a raft; + `initial_layer_line_width` is never checked. A width that resolves to 0 (automatic) is skipped. +4. Every width ≤ 5 × the largest `nozzle_diameter`: *"Line width too large"*. -Two further rules cover `bridge_line_width` (≤ nozzle diameter; > `layer_height` unless `thick_bridges` -and `thick_internal_bridges` are both on). The sweep starts from printer defaults rather than -enumerating every process. **A new non-default process gets no dedicated slice coverage in CI.** +Two further rules cover `bridge_line_width`: it must not exceed the nozzle diameter, and must exceed +`layer_height` unless `thick_bridges` and `thick_internal_bridges` are both on. The slice sweep starts +from printer defaults rather than enumerating every process: **a new non-default process gets no +dedicated slice coverage in CI.** ## What CI checks on a process Structure, not content: `process_list` name consistency **and** index coverage the other way, two files claiming one process name, the `extruder_clearance_radius` / `extruder_clearance_max_radius` conflict -pair, duplicate JSON keys, a file `normalize` would rewrite, and the five `setting_id` rules (the fifth -rejects the misspelled key `settings_id`). `compatible_printers` presence is checked for **filaments -only**. +pair, duplicate JSON keys, a file `normalize` would rewrite, the five `setting_id` rules (present on +selectable presets, absent from bases, equal to the formula outside `BBL/`, unique across the tree, and +no misspelled key `settings_id`), and the variant arrays: every array of `print_options_with_variant` +exactly `variant length × stride` wide in each selectable process that writes it, and a written +`print_extruder_id` one entry per variant +([variant arrays](validation.md#variant-arrays)). `compatible_printers` presence is checked for +**filaments only**. -Note the C++ loader derives a missing `setting_id` on the fly, so the validator will not fail a process -without one — only `orca_profile_tool.py check` catches it. Running the validator alone gives a false -all-clear. +The loader derives a missing `setting_id` on the fly, so the validator accepts a process without one; +only `orca_profile_tool.py check` catches it. +Running the validator alone gives a false all-clear. ## Silent failures specific to processes -- **Unknown or misspelled keys are discarded with no error and no warning.** They ship all over the - process tree, both plain typos (`inital_layer_height`, `tree_support_bramch_diameter_angle`, - `sparse_infill_patter`) and keys copied from other slicers that Orca never defined. -- Keys on the tool's `OBSOLETE_KEYS` list (`adaptive_layer_height`, `overhang_totally_speed`, …) are - rejected by `check`'s normalization pass across preset types; `normalize` removes them. - The additional per-key obsolete warnings read `filament/` only. -- A dangling `compatible_printers` inside an `instantiation: "false"` base is invisible to - `check_preset_references`: a base never becomes a `Preset` at all (its config goes into `config_maps` - and the loader returns early), so it is in no collection for the check to walk. -- Orphan bases that nothing inherits are scattered through the tree — usually the leftover of a - half-finished nozzle addition. +- **Unknown or misspelled keys are discarded with no error and no warning**, both plain typos + (`inital_layer_height`, `tree_support_bramch_diameter_angle`, `sparse_infill_patter`) and keys + copied from other slicers that Orca never defined. +- Keys on the tool's obsolete list (`adaptive_layer_height`, `overhang_totally_speed`, …) are rejected + by `check`'s normalization pass across preset types; `normalize` removes them. The additional per-key + obsolete warnings read `filament/` only. +- A dangling `compatible_printers` inside an `instantiation: "false"` base is reported only through a + selectable child that inherits it unchanged; it goes unreported when every child overrides the list, + or when the base has no instantiated children. +- Nothing flags an orphan base that nothing inherits, usually the leftover of a half-finished nozzle + addition. ## Adding a quality tier or a nozzle's processes -1. Choose the layer height and quality label using the vendor's existing ladder. -2. If the vendor has per-nozzle bases, add one (`fdm_process___nozzle_`) with the layer +1. Choose the layer height and quality label using the bundle's existing ladder. +2. If the bundle has per-nozzle bases, add one (`fdm_process___nozzle_`) with the layer height, nozzle-appropriate line widths, `initial_layer_print_height` and `ironing_inset`. 3. Add the leaf: 7 keys, `compatible_printers` naming the exact printer variant(s). -4. Register both in `process_list`, parent first. Bump the version, run the id tool, validate. -5. Slice this process explicitly with its intended printer; the sweep gives non-default tiers no - dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and - resolved compatibility too — the sweep may fall back or select another compatible process. +4. Register both in `process_list`, parent first, bump the version, run the id tool and validate: the + [authoring workflow](../SKILL.md#creating-or-modifying-a-profile). +5. Slice this process explicitly with its intended printer + ([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)); the sweep gives non-default + tiers no dedicated coverage. If it is a printer's `default_print_profile`, verify the exact name and + resolved compatibility too: the sweep may fall back or select another compatible process. diff --git a/.claude/skills/orca-profiles/references/review-checklist.md b/.claude/skills/orca-profiles/references/review-checklist.md index 1d49cf2524..9037e364b4 100644 --- a/.claude/skills/orca-profiles/references/review-checklist.md +++ b/.claude/skills/orca-profiles/references/review-checklist.md @@ -1,37 +1,43 @@ # Reviewing a profile change -Start with delivery, identity and backward compatibility, then check the affected preset types. -The table highlights gaps that need human review. What CI *does* run: -[validation.md](validation.md). +Run `./scripts/check_profile.sh` on the applied diff first ([validation.md](validation.md) says what CI +runs), then work through the items below: delivery, identity and backward compatibility first, then the +affected preset types. The table lists the gaps CI cannot see, so only a reviewer catches them. | Not checked by CI | Consequence | | --- | --- | -| The `version` bump | The change never reaches an upgrading user | -| A misspelled setting key | Setting silently has no effect | +| The `version` bump | The change never reaches an upgrading user; an absent `version` hides the vendor from the setup wizard | +| A misspelled setting key | The setting silently has no effect | | A filename Windows cannot check out, or one that differs from its `sub_path` only in case | Works on the author's machine, breaks the bundle on another platform | -| `bed_model` / `bed_texture` / `hotend_model` pointing at a missing asset | Bed renders as Custom, hotend falls back to the generic model | +| `bed_model` / `bed_texture` / `hotend_model` / cover pointing at a missing asset | A missing bed model renders a generic custom bed and a missing texture renders none, the hotend falls back to the generic model, the cover shows a placeholder | | A nozzle size in a model's list with no matching variant | The size is offered and resolves to nothing | | A non-default process | `validate_slice` gives non-default quality tiers no dedicated coverage | | Whether the intended default survived compatibility selection | The sweep can select a different compatible preset | -| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) | +| A dangling `compatible_printers` inside a base whose children all override it (or that has no instantiated children) | The reference check walks resolved selectable presets, so it reports a base's list only through a child that inherits it unchanged (a bad `inherits` in a base *is* caught) | +| A base nothing inherits | Dead weight, usually the leftover of an unfinished nozzle addition | | A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name | -| A preset differentiated only by color, or an all-printer library preset without `@System` | Per-color presets split one product across ids and the selector fills with near-duplicates; CI stays green | -| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated | -| A name that ignores its type's convention — a model in a `process` quality slot, or an unrelated target label left in a copied preset | The selector misrepresents the preset's quality or intended printer | +| A preset differentiated only by colour, or an all-printer library preset without `@System` | Per-colour presets split one product across several ids and the selector fills with near-duplicates; CI stays green | +| Plate temperatures for plates the printer has | The user's plate reads an unset or inherited temperature | +| Per-extruder vector length on a multi-extruder printer | Silently padded (with the **first** value) or truncated | +| A new variant appended instead of inserted at its variant index, a variant array widened on a base with a shorter list, or a variant a filament/process lacks | Values shift onto the wrong extruder, are cut before any child inherits them, or resolve to the first variant: High Flow silently gets Standard values | +| A name that ignores its type's convention: a printer model in a `process` quality position, or an unrelated target label or base name left in a copied preset | The selector misrepresents the preset's quality or intended printer | +| Values: temperatures, speeds, widths, pressure advance | A wrong value prints wrong while CI stays green | ## 1. Was the vendor `version` bumped? For **every** bundle whose folder the diff touches, `resources/profiles/.json` must have its -`version` incremented — last component, carrying `.99` into the third component. A library change -means bumping `OrcaFilamentLibrary.json`. +`version` incremented: last component, carrying `.99` into the third component. A library change means +bumping `OrcaFilamentLibrary.json`. -*Why:* nothing in CI checks it, and `PresetUpdater` reinstalls only when `vendor_ver < resource_ver` — -without a bump the change reaches neither an upgrading user nor the author's own running app. +*Why:* nothing in CI checks it, and the app reinstalls a bundled profile set only when its version is +newer than the installed one: without a bump the change reaches neither an upgrading user nor the +author's own running app. Without any `version` the vendor vanishes from the setup wizard, and neither +`check` nor the validator reports it. ## 2. Was the index rebuilt, and does the diff contain only this change? -`check` now fails on an unregistered file, on an index `update-index` would reorder, and on a file -`normalize` would rewrite — so a PR that skipped them arrives red, and you do not have to spot the +`check` fails on an unregistered file, on an index `update-index` would reorder, and on a file +`normalize` would rewrite, so a PR that skipped them arrives red and you do not have to spot the omission yourself. Three things are still yours: - **The index diff belongs to this change.** `update-index` rewrites whole `*_list` sections. If the @@ -41,11 +47,15 @@ omission yourself. Three things are still yours: registration; `validate_custom` detects the break only for names covered by released fixtures. - **`normalize` edits content, not just layout.** It drops `version` and `is_custom_defined` from preset files, removes obsolete keys, deletes six print-speed keys from filament profiles, and resolves - `extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger. - Check that the keys it removed were meant to go. + `extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger + ([what normalize changes](validation.md#normalize-and-update-index-are-part-of-the-check)). Check that + the keys it removed were meant to go. -Obsolete keys fail `check`'s normalization pass and should be removed with `normalize`. -`check` also reports per-key obsolete warnings for filament profiles in the selected vendors. +Index order is dependency order, not alphabetical: parents and include templates before the presets +that use them, then, in name order, the entries that neither depend on nor are depended on by another +entry of their own list (such as a leaf filament whose only parent is in the library), and any entry on +a dependency cycle. Judge a hand-placed entry only by +`update-index --dry-run`: if it reports nothing to rebuild, the position is not a finding. *Why:* the index is the loader's only entry point. Out-of-order entries fail with `can not find inherits` and take the whole vendor bundle down; an unindexed file gets reviewed, merged and never loads. @@ -55,8 +65,8 @@ and take the whole vendor bundle down; an unindexed file gets reviewed, merged a No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved. -A rewritten or removed `filament_id` means a product's identity moved — a rename, or an edited -`filament_vendor` / `filament_type` — and the old id is not forwarded anywhere. Confirm that was +A rewritten or removed `filament_id` means a product's identity moved (a renamed alias, or an edited +`filament_vendor` / `filament_type`), and the old id is not forwarded anywhere. Confirm that was intended, and that a new id is not a rename in disguise. *Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied @@ -64,136 +74,204 @@ intended, and that a new id is not a rename in disguise. ## 4. Does anything disappear for existing users? -A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped preset removes the -name from the preset collection. It needs `renamed_from` on a successor — and only one preset may claim a -given old name. The claimed old name must **not** still be a live preset; the redirect is inert if it is. +A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped selectable preset +removes the name from the preset collection. It needs `renamed_from` on a selectable successor +([renamed_from](vendor-bundle.md#renamed_from)), and only one preset may claim a given old name. The +claimed old name must **not** still be a live preset; the redirect is inert if it is. *Why:* user presets inheriting it die with `can not find parent for config !`; 3MF-embedded presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon; -`6943b6ddc3` redid it correctly. CI's `validate_custom` catches the shipped-name case — but not an inert -`renamed_from`. +`6943b6ddc3` redid it correctly with `renamed_from`. CI's `validate_custom` catches the shipped-name +case, but not an inert `renamed_from`. ## 5. Is `compatible_printers` right? -Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary -and written in the preset's own file — golden rule 6, with the flattened-vs-own-key trap in -[filament-profiles.md](filament-profiles.md#compatible_printers). Watch for a nozzle-specific variant that -inherited or copied the base's full printer list, and for two presets of one product with overlapping -lists — duplicate combobox entries and an ambiguous AMS match. +Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary, +and written in the preset's own file ([SKILL.md rule 9](../SKILL.md#rules); the resolved-vs-own-key trap +is in [filament-profiles.md](filament-profiles.md#compatible_printers)). A `machine_model` name instead +of a variant name is the usual mistake: `check` passes it, the validator reports +`references unknown compatible_printers`. Watch for a nozzle-specific tune that inherited or copied the +base's full printer list, and for two presets of one product with overlapping lists: duplicate combobox +entries and an ambiguous AMS match. Two presets of one product (`filament_id`) must not share a variant. Resolve it by specificity: move the -variant to the most specific preset and remove it from the more general ones — preferred over deleting a -profile. Then repoint the machine's `default_filament_profile` and the model's `default_materials` at the -profile that now covers it. See +variant to the most specific preset and remove it from the more general ones, which is preferred over +deleting a profile. Then repoint the machine's `default_filament_profile` and the model's +`default_materials` at the profile that now covers it. See [one variant, one profile](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product). -*Why:* real shipped bugs twice (`b7b3418baf` "showing up everywhere", `ff83aa41ef` duplicate Flashforge -entries). The Python `check` passes on an overlap; only the full `check_profile.sh` (`validate_system`) -reports `Ambiguous AMS filament match`. +*Why:* real shipped bugs twice (`b7b3418baf` filaments "showing up everywhere", `ff83aa41ef` duplicate +Flashforge entries). The Python `check` passes on an overlap; only the full `check_profile.sh` +(`validate_system`) reports `Ambiguous AMS filament match`. -## 6. Model ↔ variant ↔ process consistency +## 6. One product, one all-printer preset; colour is not a preset -- New nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching - `printer_variant`, and at least one process listing that variant. -- `default_print_profile` is one exact name (not a `;` list), and that process's resolved - compatibility list or condition includes this printer. +No presets that differ only by colour: `filament_id` identifies a product, and the colour comes from the +spool at runtime. An all-printer library product is a ` @System` shim with an empty +`compatible_printers` ([colour](filament-profiles.md#colour-is-a-runtime-property)). + +*Why:* per-colour presets pass every check, so this is a review call. + +## 7. Model ↔ variant ↔ process consistency + +- A new nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching + `printer_variant`, and at least one process listing that variant. Every size in the model's list has a + variant (unchecked). +- `default_print_profile` is one exact name (not a `;` list), and that process's resolved compatibility + list or condition includes this printer. - `default_filament_profile` is an array of names that exist. +- Each variant of the model has at least one compatible entry in the model's `default_materials`. -*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is -weaker: the sweep attempts the named default, then updates compatibility and rejects generic Default -fallbacks. Another compatible process can conceal a bad reference, so inspect it even after a pass. +*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is weaker: +the sweep attempts the named default, then updates compatibility and rejects generic Default fallbacks, +so another compatible process can conceal a bad reference. Inspect it even after a pass. -## 7. Types and spellings +## 8. Types and spellings Every value a string or an array of strings; `filament_type` an array; `instantiation` the string -`"true"`/`"false"` — golden rule 7. Check index metadata and model `nozzle_diameter` especially; -wrong types there can abort loading for **every** vendor. +`"true"` / `"false"`; custom G-code one string, never an array of lines ([SKILL.md rule 5](../SKILL.md#rules)). +Check index metadata and model `nozzle_diameter` especially: wrong types there can abort loading for +**every** vendor. The part only a reviewer can do: check new setting keys against `src/libslic3r/PrintConfig.cpp`. A -misspelled key is silently discarded (rule 8), the single most common way a profile edit does nothing -while CI stays green. +misspelled key is silently discarded ([rule 6](../SKILL.md#rules)), the single most common way a profile +edit does nothing while CI stays green. -## 8. Blast radius of a base edit +## 9. Blast radius of a base edit -A change to `fdm_*_common.json` reaches every child at once. Ask which presets it touches — several -reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether the edited leaf has -children of its own: Prusa, Flashforge and Elegoo all chain leaf-inherits-leaf several levels deep. +A change to `fdm_*_common.json` or any other base reaches every child at once. Ask which presets it +touches: several reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether +the edited leaf has children of its own: a bundle may chain leaf-inherits-leaf several levels deep. A +newly added base that nothing inherits is dead weight, and a dangling +`compatible_printers` inside a base is reported only through a child that inherits it unchanged. A diff +that only moves values between presets and bases must leave every selectable preset loading what it +loaded: ask for the `compare` result ([shared bases](shared-bases.md#nothing-loads-differently)), and +check each new base against the [balance rules](shared-bases.md#balance). -## 9. Do the numbers make sense for the nozzle? +## 10. Do the numbers make sense for the nozzle and material? -Check resolved widths and layer heights against the nozzle, and flow limits / pressure advance -against the actual hardware and material. The patterns in [process-profiles.md](process-profiles.md) -are examples, not mandatory values; [filament-profiles.md](filament-profiles.md) explains what to -revisit for a nozzle change. A cloned preset's unchanged MVS needs particular scrutiny. +Check resolved widths and layer heights against the nozzle, temperatures against the material (PLA +values under an ASA name print wrong), and flow limits / pressure advance against the actual hardware +and material. The patterns in [process-profiles.md](process-profiles.md#values-to-review-per-nozzle) +are examples, not mandatory values; [filament-profiles.md](filament-profiles.md#tuning-per-nozzle-and-per-variant) +explains what to revisit for a nozzle change. A cloned preset's unchanged volumetric speed needs +particular scrutiny. Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving numbers nobody measured. -## 10. Asset references (not checked anywhere) +## 11. Plate temperatures + +A filament sets the plate temperature for every plate the printer plausibly has, as its siblings do; +`textured_cool_plate_temp` is the one most often forgotten +([twelve keys](filament-profiles.md#bed-temperature-is-twelve-keys-not-one)). + +## 12. Asset references (not checked anywhere) `bed_model`, `bed_texture`, `hotend_model` and `_cover.png` exist under -`resources/profiles//`. Broken references already ship; nothing checks them. +`resources/profiles//`, in exact case. Nothing checks them. -## 11. `default_materials` (checked by CI) +## 13. `default_materials` and `default_filament_profile` (checked by CI) -`check` fails on a `default_materials` / `default_filament_profile` name that resolves to no system -filament, so a dangling entry no longer reaches review. When compatibility moves between profiles of a -product, the machine's `default_filament_profile` and the model's `default_materials` must be repointed at -the most specific profile that still covers the variant, dropping generic entries that no longer apply — -the same [specificity rule](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) -applies when adding or fixing defaults. Scope the run while working on one vendor: +`check` fails on a `default_materials` / `default_filament_profile` name that matches no filament file, +and `validate_system` on a variant with no compatible system filament in `default_materials`, so a +dangling entry no longer reaches review. When compatibility moves between profiles of a product, the +machine's `default_filament_profile` and the model's `default_materials` must be repointed at the most +specific profile that still covers the variant, dropping generic entries that no longer apply; the same +[specificity rule](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) applies +when adding or fixing defaults. Scope the run while working on one vendor: ```bash python3 scripts/orca_profile_tool.py check --vendor "" # py -3 on Windows ``` -## 12. Per-extruder vector lengths (not checked) +## 14. Per-extruder vectors (not checked) and variant arrays (widths and layout checked) -One entry per extruder for the plain per-extruder vectors; the `printer_options_with_variant_1` keys are -sized to `printer_extruder_variant` instead. A wrong length is silently padded — repeating the **first** -value, not the last — or truncated. The two sizing families and the worked cases are in +One entry per extruder for the plain per-extruder vectors; the variant sets are sized to the variant +length, `len(printer_extruder_variant)` or one per extruder when the resolved preset has no layout (a +per-extruder difference in those keys still needs the layout, since the loader keeps only the +first value of a list-less printer's arrays). A wrong length is silently padded, repeating the **first** +value, not the last, or truncated. The two sizing families and the worked cases are in [machine-profiles.md](machine-profiles.md#multi-extruder-idex-and-tool-changers). -## 13. Non-default processes get no slice coverage +`check` reports a variant array of any type that is not exactly the `variant length × stride` width +of the selectable preset that writes it, one value included, as an error whatever the values; +`check --strict` also reports one that reaches a selectable preset at another width. A base is not +judged on its own. On a machine it +also reports layout keys that disagree: `printer_extruder_variant` / `printer_extruder_id` not the +flattening of `extruder_variant_list`, a variant without its extruder's `extruder_type` prefix, a +`default_nozzle_volume_type` the extruder does not list, a list of several variants without the pair, a +pair without the list that puts several variants on one extruder. On a machine or a process, an id +array, written or inherited, whose length differs from its variant list is an error; the id and +layout rules judge the composed preset without `--strict`. Two layout findings are warnings: a +pair without the list that `single_extruder_multi_material` off would replace at load, and a process +without `print_extruder_id` whose variants repeat. The full rules are in +[variant arrays](validation.md#variant-arrays). `check_variant_names` separately holds every variant +string, `extruder_type`, `nozzle_volume_type` and `default_nozzle_volume_type` to the engine's enums +in every bundle, BBL included, failing on a dead variant, a legacy spelling and a variant list that +names one variant twice ([variant names](validation.md#variant-names)). + +On a multi-variant printer, still check by hand: + +- that a new variant's values sit at its variant index (machine limits as a (normal, silent) pair at + `2 × index`) in every file of the chain that restates the key, `include` templates included; +- that each process lists every (extruder id, variant) pair its printers can select; +- that no array was widened on a base with a shorter variant list: the loader cuts it before the + children inherit it ([composition](extruder-variants.md#padding-truncation-and-composition)), and + `check`, even `--strict`, composes without that cut, so it passes; +- that only keys in [the four sets](extruder-variants.md#the-four-key-sets) carry per-variant values; +- that the High Flow variants carry measured values rather than copies + ([checking and testing](extruder-variants.md#checking-and-testing)). + +## 15. Non-default processes get no slice coverage `validate_slice` starts from printer defaults; it does not enumerate every process. Slice a new or -changed non-default tier explicitly with its intended printer. - -## 14. Housekeeping worth a nit, not a block - -`"from"` other than `"system"` (the preset-bundle loader ignores it, though the CLI's config-file loader -rejects anything but `system`/`user`/`User`), `printer_settings_id` copied from another -vendor, and a filename that disagrees with the preset's `name` (common; the loader keys off `name`). - -## 15. Cross-platform filenames and paths (not checked) - -Check for Windows-invalid characters, reserved device names, trailing path-component spaces/dots, -and case mismatches in `sub_path` or asset paths. See [cross-platform paths](validation.md#cross-platform-paths). +changed non-default tier explicitly with its intended printer +([on a copy of the tree](validation.md#checking-a-copy-of-the-tree)). ## 16. Do the preset names follow the conventions? -Check **every newly added profile and intentional name change**, including models and bases, -against [the naming conventions](naming.md#checking-names). Preserve shipped names during -ordinary tuning; renaming a shipped selectable preset requires the migration in item 4. +Check **every newly added profile and intentional name change**, including models and bases, against +[the naming conventions](naming.md#checking-names): no printer model in a process quality position, no +target label or base name left over from a copied preset, the bundle's established style. Preserve +shipped names during ordinary tuning; renaming a shipped selectable preset requires the migration in +item 4. -*Why:* CI checks name uniqueness, but does not enforce the naming conventions. Catch naming -mistakes before the names ship and existing projects depend on them. +*Why:* CI checks name uniqueness, but does not enforce the naming conventions. Catch naming mistakes +before the names ship and existing projects depend on them. + +## 17. Cross-platform filenames and paths (not checked) + +Check for Windows-invalid characters, reserved device names, trailing path-component spaces or dots, +and case mismatches between `sub_path` or asset references and the files on disk. See +[filenames and paths](naming.md#filenames-and-paths). + +## 18. Housekeeping worth a nit, not a block + +`"from"` other than `"system"` (the bundle loader ignores it, though loading the file as a CLI config +rejects anything but `system` / `user` / `User`), `printer_settings_id` copied from another vendor, +redundant overrides that restate the parent's value, siblings that each repeat a value their base could +hold ([shared bases](shared-bases.md)), and a filename that disagrees with the preset's +`name` (the loader keys off `name`). --- ## Reporting the review -A finding is: **one defect**, its file, what breaks at runtime or in CI, and the fix. Split independent -defects into separate findings even when they live in one file — five id problems in one bullet get one -fix and four survivors. +A finding is **one defect**: its file (or quoted lines), what breaks at runtime or in CI, and the fix. +Split independent defects into separate findings even when they live in one file: five id problems in +one bullet get one fix and four survivors. Say which findings `check` or the validator reports and which +only a reader catches: a missing version bump, a misspelled key and wrong temperatures pass CI, so a +contributor who only reruns the tools fixes what CI flags and resubmits the rest. Severity discriminates only if it is earned: | Severity | Means | | --- | --- | | blocker | the bundle fails to load, or a preset is unreachable at runtime | -| major | CI fails, or existing users lose a preset | -| minor | wrong-but-working: dead keys, `from`, naming, redundant overrides | +| major | CI fails, existing users lose a preset, or a value prints wrong while CI stays green (PLA temperatures under an ASA name; a misspelled key whose intended value differs from the inherited one) | +| minor | wrong but working: redundant or dead keys that change nothing, `from`, naming | -Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it — one invented count -makes a reader stop trusting the right ones. Report a command's result only if you ran it. +Compute every number and id (`orca_profile_tool.py`, a scripted count) or omit it: one invented count +makes a reader stop trusting the right ones. Report a command's result only if you ran it. End with a +verdict: can it merge as it stands? diff --git a/.claude/skills/orca-profiles/references/shared-bases.md b/.claude/skills/orca-profiles/references/shared-bases.md new file mode 100644 index 0000000000..500cd34006 --- /dev/null +++ b/.claude/skills/orca-profiles/references/shared-bases.md @@ -0,0 +1,254 @@ +# Shared bases + +Use this when drafting several presets at once, after `fix-variant`, or when sibling presets repeat the +same values. Each value is written once, on the base of the group it is true for, and a selectable +preset holds its identity and what makes it different. The work has two halves: choosing the groups, +which is judgment, and moving the values, which must leave every preset loading exactly what it loaded +before. + +## When + +- **Drafting** a printer family, a quality ladder or a product line: place each setting at its + [level](#levels) before writing any preset, then write the presets as overrides + ([Rule 7](../SKILL.md#rules)). +- **After `fix-variant`.** It resizes each array in the selectable preset that writes it and never + moves or deletes a value ([variant arrays](validation.md#variant-arrays)). A family whose presets all + wrote one value now repeats the widened array in every preset, and a preset that restated what it + inherits now restates it wider. +- **Converting full copies**: a machine that inherits nothing, or a filament that restates fifty-plus + keys, is the style to move away from ([filament style](filament-profiles.md#style)). + +Out of scope: + +- **BBL.** Its profiles are synced from BambuStudio: a restructure is overwritten by the next sync and + makes every later sync diff unreadable. Fix BBL values in the file that holds them. +- **OrcaFilamentLibrary bases for one vendor's values.** A library base reaches every bundle's + filaments. Put a vendor's shared values on a base in its own bundle (a product `@base`, or a vendor + base that inherits the library's), and change a library base only for a value true of every filament + below it in every bundle. +- **Families the change does not touch.** Restructure the presets you are already changing; a + bundle-wide pass is a change of its own. Commit a restructure without any value change, so `compare` + alone verifies it. + +## Nothing loads differently + +A restructure changes where values are written, never what a selectable preset loads. Presets keep +their `name` and `setting_id`, and user presets and projects refer to a system preset by name and store +their own changes against what it loads, so an unchanged load changes nothing for users. Prove it with +the bundled helper, from the repository root (every subcommand takes `--profiles ` to work on a +copy of the tree): + +```bash +python3 .claude/skills/orca-profiles/scripts/shared_settings.py snapshot # before any edit +python3 .claude/skills/orca-profiles/scripts/shared_settings.py compare # after: "0 difference(s)", exit 0 +``` + +`snapshot` records every selectable preset of every bundle as the loader stores it: the parent's stored +config, each `include` at the width its file wrote, the preset's own keys, then every variant array +resized to the preset's own variant list ([composition](vendor-bundle.md#inherits-and-include)). It +models the step `check` does not see: a base is stored after its own resize, so an array wider than a +base's list reaches its children cut. It also leaves out what the loader reads from each file and never +hands down (`name`, `type`, `from`, `instantiation`, `inherits`, `include`, `setting_id`, +`renamed_from`, `description`, `version`, `url`, `is_custom_defined`), so those keys never move to a +base. Nor does it record `print_settings_id`, `printer_settings_id` or `filament_settings_id`: the app +replaces them with the selected presets' names before slicing, so a file's value never counts. Delete +them from the presets you restructure rather than moving them to a base. + +It does not know the built-in defaults, so `compare` lists a key written on one side only (no file of +the preset's chain writes it on the other) apart from the differences, and exits 1 for either. A +difference is a value the edit changed: undo it, or make it a separate, deliberate change. A one-sided +key is no change only when its written value is the option's default in `PrintConfig.cpp`: confirm +each, as when a preset that loaded the default by leaving a key out must now write it +([Balance 5](#balance)). A value equal to the default needs writing nowhere when no base above writes +another: delete it from the presets rather than moving it, and confirm the one-sided keys. + +## Levels + +A base stands for a level of the vendor's catalogue, and a setting lives at the level that determines +it. The test for a shared value: if it had to change for one preset of the group, should it change for +all of them? If yes, it is the group's and goes on the group's base. A value that is only equal today +(two unrelated printers with the same acceleration) stays in each preset: a base built on coincidence +is later edited for one preset and silently changes the others. For a default with exceptions +([Balance 5](#balance)), ask the question of the presets that inherit the default. + +The tables give each setting's usual level; the test decides for a given bundle: where each toolhead +(Bowden or Direct Drive) has its own default filament, `default_filament_profile` follows the +toolhead, not the nozzle. Levels run +coarse to fine, and a chain need not visit every level: each preset or base inherits the next coarser +level that has a base. + +| Machine level | Base | Settings it determines | +| --- | --- | --- | +| Vendor | `fdm_machine_common`, `fdm__common` | the vendor's defaults for every printer | +| Firmware | `fdm_klipper_common`, `fdm_marlin_common` | `gcode_flavor`, G-code that calls the firmware's macros (layer change, pause, filament change), `host_type`, `print_host`, the thumbnail format | +| Hardware family: models that share a frame, motion system, toolhead or extruder layout | `fdm___common` (`fdm_qidi_x3_common`, `fdm_machine_eryone_ER20_common`) | the `machine_max_*` limits, `extruder_clearance_*`, the toolhead's retraction where every nozzle shares it, `z_hop` and wipe, fitted hardware such as `auxiliary_fan`, the extruder count and per-extruder vectors, the variant layout ([Printer rule 2](extruder-variants.md#printer-machine)) | +| Model: one `machine_model`, which for an IDEX printer includes its mode | the default-nozzle preset where the bundle hangs its other nozzle presets off it; otherwise `fdm___common` | `printable_area`, `printable_height`, `bed_exclude_area`, the model's start G-code, a COPY or MIRROR mode's settings | +| Nozzle: the selectable preset | none | `printer_model` and `printer_variant`, which name the preset's model and nozzle and stay in every preset as the tree writes them; `nozzle_diameter`, `min_layer_height`, `max_layer_height`, `default_print_profile`, `default_filament_profile`, retraction the vendor tunes per nozzle | + +A family may split once more, into toolhead or revision groups that exist only within it: +`fdm___common` → `fdm___mk1_common`. A split that crosses another axis is not a +level: when every controller comes with every toolhead, a toolhead base under each controller base +repeats the same values in each ([Balance 6](#balance)). + +| Process level | Base | Settings it determines | +| --- | --- | --- | +| Vendor | `fdm_process_common`, `fdm_process__common` | strategy: seam, wall order, infill and support patterns | +| Printer family or variant layout | `fdm_process___common` (`fdm_process_arena_common`, BBL's `fdm_process_dual_common`) | speeds, accelerations and jerk of that motion system; the variant layout ([Process rule 4](extruder-variants.md#process)) | +| Layer height × nozzle | `fdm_process___nozzle_` (BBL's `fdm_process_single_0.20`) | `layer_height`, line widths, shell layers, speeds scaled to the layer | +| Quality × printer: the selectable preset | none | `compatible_printers`, and what is unique to that combination | + +| Filament level | Base | Settings it determines | +| --- | --- | --- | +| Material | `fdm_filament_` in OrcaFilamentLibrary, shared by every bundle | material defaults | +| Product | ` @base` | `filament_id` (minted here, [ids](ids.md)), `filament_vendor`, `filament_type`, density, cost, the product's temperatures and cooling | +| Printer or nozzle tune: the selectable preset | none | `compatible_printers` (always in its own file, [Rule 9](../SKILL.md#rules)), volumetric speed, flow ratio, pressure advance and retraction measured on that printer | + +**Equal where they must differ is a copy.** A key that follows a finer level, such as the layer-height +limits and line widths that follow the nozzle diameter or `printable_area` that follows the bed, never +moves above that level. When presets that differ in it carry the same value, the value was copied: leave +it in the presets and report it (the [worked example](#worked-example-an-idex-family) has one). + +## Balance + +1. **One group, one base; use the existing one first.** A base is the home of the presets below it, + whatever its name. In a bundle whose presets are all one family, the vendor base is the family base, + so the family's values go there. In a bundle that hangs the other nozzles off the default-nozzle preset, that preset is the model's home. + Never create a base whose presets are exactly its parent's. Where two existing bases already serve + the same presets (a copied `fdm_machine_common` above the vendor's own base), the finer one is the + home, and merging the pair is a change of its own. Moving a key into an existing home adds no file. + A base value that no preset below it loads is dead: replace it with the group's value when there is + one; otherwise leave it and report it, since a future preset would inherit it. +2. **A new base must stand for a level and pay for itself.** Its file costs five metadata keys and an + index entry, so create it only when it takes `k` keys off `n` selectable presets with + `(n − 1) × k > 6`, where a custom G-code value counts as one key per G-code line: what it removes + must outnumber what it adds. A model with two nozzles that share three short keys keeps them in both. +3. **No ad-hoc bases:** never a base for presets that merely agree (the ones with 0.8 mm retraction), + and never a base with one preset below it. +4. **Keep every selectable preset within four ancestors**, selectable parents included. When a level would push a preset past that, fold it into the level above or leave + its keys in the presets. +5. **A default with exceptions.** A value that only some presets below a shared base load moves to that + base, whichever axis it follows, when two conditions hold. More presets load it than any other value + (on a tie the key stays in the presets), and `w − a > 1`, where `w` presets drop their copy and `a` + presets that take the key from the base and load another value, the built-in default included, must + now write theirs. Presets that write another value keep it and are unaffected. The base then holds + the group's default, and the exceptions stay visible in their own files. +6. **One chain; the other axes stay in the presets.** `inherits` follows one axis. When presets vary + along several (bed size × controller × toolhead), first fill the existing homes by + rules 1 and 5. Then give new bases to the axis whose bases pay most: sum rule 2's count over its + bases, less the keys a re-parented preset must now write because it no longer inherits them from its + old parent; on a tie, follow the layering the bundle already has. Leave the other axes' keys in the + presets, and never repeat one axis's bases under each group of another. Outside BBL, whose synced + presets need theirs, add no `include` template for a second axis: the loader reads `include` only + since #15869 (2026-09-25), and an app that predates it ignores the key, so the template's settings + never reach the preset. + +Name a new base after its level ([base names](naming.md#bases)). The name must be unique in its bundle +and must not equal a selectable preset's: two such presets load silently and the first in the index wins +([uniqueness](naming.md#uniqueness)). + +## Restated values + +`candidates` lists every key a file writes that it would load unchanged without writing it. Delete it +when the value is what the presets below the base that supplies it share: more of them load it, +written or inherited, than any other value. A family that restates machine limits, clearances and +G-code every other printer of the bundle loads from `fdm_klipper_common` drops its copies. When most +presets below that base load another value, the match is a coincidence: keep the key, and move it to +the level of the presets that share it. Keys of the nozzle level stay in the preset in +either case. + +After a restructure the report still lists keys that are right where they are: the nozzle-level keys +each preset keeps, and arrays a list-less multi-extruder base writes at its presets' width for +`check --strict`. + +## Variant arrays on a base + +- The loader stores a base with its variant arrays resized to the base's own variant list, one variant + when it writes none, so an array on a narrower base reaches its presets as its first value, padded. + Move a variant array to a base only when the base declares the presets' variant list (and, for a + machine or process, their ids), moving the layout keys with it as + [Printer rule 2](extruder-variants.md#printer-machine) and [Process rule 4](extruder-variants.md#process) + ask; or when the presets load its first value anyway: every value is equal, or the presets are + list-less machines, which the loader cuts to one variant. `compare` catches a cut. +- Write the array on the base at the width of the presets it serves, their `N` + ([sizing equation](extruder-variants.md#sizing-equation)), so `check --strict` judges the right width + where it reaches them. When the presets below a base need different widths (single- and + dual-extruder models on one base), leave the array in the presets, or on bases that each serve one + width. +- On a list-less multi-extruder family base, declare the extruder count: `nozzle_diameter` and the other + per-extruder vectors at one entry per extruder. The base then has its presets' width, and + `fix-variant --strict` writes an array that reaches the presets at another width into the base once, + instead of into every preset. +- Deleting a restated variant array leaves the preset on the inherited array. Plain `check` accepts + that; `check --strict` reports it when the inherited width differs, which is why a bundle held to + `--strict` restates the array at each preset's width. + +## Procedure + +1. **Snapshot** the tree before any edit, `fix-variant` included. After `fix-variant`, run `compare`: a + difference is a value its padding changed (it repeats the last value, the loader the first). Set that + value deliberately, then snapshot again as the baseline for the restructure. +2. **List the candidates:** + + ```bash + python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "" --type machine + python3 .claude/skills/orca-profiles/scripts/shared_settings.py candidates --vendor "" --type machine --group-by printer_model + ``` + + It prints the [restated values](#restated-values), then, per base, the keys every selectable preset + below it loads with one value and how many of those presets write it themselves, and under + `default with exceptions` the values that pass [Balance 5](#balance), with `w` and `a`. With + `--group-by ` it groups the selectable presets by that key's value instead and names each + group's nearest common base, where a new base would go: `printer_model` for models, `gcode_flavor` + for firmware, `extruder_type` or `default_filament_profile` for toolheads, `filament_id` for + filament products, `layer_height` for processes. The report is evidence, not a plan: it cannot tell + a shared value from a coincidence or a copy. +3. **Decide each key** by [Levels](#levels), [Balance](#balance) and + [Restated values](#restated-values): delete the restatements of shared values, move group values up + to the group's home, and create only the bases that pay. +4. **Edit.** Each new base gets `"type"`, `"name"`, `"from": "system"`, `"instantiation": "false"`, no + `setting_id`, and `inherits` set to the presets' old parent. Point the presets' `inherits` at it and + delete the moved keys from them. Bump the version, then run `normalize`, `update-index` (it orders + parents first) and `generate-id --dry-run`, which must write nothing: bases take no id and presets + keep theirs. +5. **In a bundle held to `--strict`, run `fix-variant --strict` now**, so it writes into the new bases. +6. **Verify.** `compare` prints `0 difference(s)`, and every one-sided key it lists is a default. + `check` reports no error it did not report before, and neither does `check --strict` where the bundle + passes it. Then run the [authoring checks](../SKILL.md#creating-or-modifying-a-profile). +7. **Report** each base added (name, level, presets below it, keys it holds), the keys left in presets + and why, the copies found, and the `compare` result. + +## Worked example: an IDEX family + +A Klipper bundle's IDEX family has 36 selectable machines (bed size 300, 400 or 500 × normal, COPY or +MIRROR mode × 0.4, 0.5, 0.6 or 0.8 nozzle) that all inherit `fdm_klipper_common` directly and write 49 +keys each. `fix-variant` has widened their retraction arrays to two values and their machine limits +to four. + +- **Restated.** All 36 restate 10 values that every other printer of the bundle loads from + `fdm_klipper_common`: three machine limits, the three clearances, wipe, `retract_before_wipe`, and the + layer-change and pause G-code. Delete them. +- **Family.** All 36 load one value for 24 more keys: the other machine limits, `extruder_offset`, + retraction, `z_hop`, `single_extruder_multi_material`, `manual_filament_change`, the remaining G-code + except the start G-code, and thumbnails. A new family base, `fdm___idex_common`, + holds them, plus `nozzle_diameter` `["0.4", "0.4"]` for the extruder count: at least + `(36 − 1) × 24 = 840`. +- **Model.** Each `printer_model` (a bed size in one mode) has four nozzle presets that share + `printable_area`, `printable_height` and a three-line `machine_start_gcode`: `(4 − 1) × 5 = 15`, so + one base per model, nine in all. The family does not hang its other nozzles off a default-nozzle + preset, so the model level here is a base. +- **A coincidence.** The twelve 500 presets' `printable_height` 500 equals `fdm_klipper_common`'s, but + every preset of the bundle writes its own height and 300 is the most common. The 500 is the base's + leftover, not a shared value, so it stays on the model bases. +- **A copy.** In COPY and MIRROR mode, every nozzle of a model carries the 0.4 nozzle's + `min_layer_height`, `max_layer_height` and `retract_lift_below`: 0.06, 0.3 and 0.2 on the 0.8 nozzle, + where normal mode has 0.12, 0.5 and 0.3. `candidates --group-by printer_model` lists the first and + last as shared by the model, and `max_layer_height` as restated from `fdm_klipper_common`, whose value + is also 0.3. They follow the nozzle, so they stay in the presets and are reported for tuning. +- **Nozzle.** Each preset keeps `printer_model`, `printer_variant`, `nozzle_diameter`, + `min_layer_height`, `max_layer_height` and `retract_lift_below` beside its metadata. +- **Result.** 36 full presets become 36 short ones on 10 new bases. `compare` reports 0 differences, + `check --vendor ""` passes as before, and `generate-id --dry-run` writes nothing. + `check --strict` reports more errors than before, because the deleted variant arrays now reach the + presets at `fdm_klipper_common`'s one value. `fix-variant --strict` writes those arrays into the + family base alone, after which `check --strict` passes for the family and `compare` still reports 0 + differences. diff --git a/.claude/skills/orca-profiles/references/validation.md b/.claude/skills/orca-profiles/references/validation.md index c23b555f49..b5c647f839 100644 --- a/.claude/skills/orca-profiles/references/validation.md +++ b/.claude/skills/orca-profiles/references/validation.md @@ -1,8 +1,8 @@ # Validating profiles ```bash -./scripts/check_profile.sh # everything CI runs -./scripts/check_profile.sh --vendor "" # fast loop +./scripts/check_profile.sh # everything CI runs +./scripts/check_profile.sh --vendor "" # development loop ./scripts/check_profile.sh profile_tool validate_slice # named checks only ``` @@ -12,153 +12,316 @@ scripts\check_profile.bat -Vendor "" scripts\check_profile.bat profile_tool validate_slice ``` -`check_profile.bat` is a shim around `check_profile.ps1` — same checks, same order, same logs; -the flags take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`, -`-WorkDir`, `-LogLevel`) and positional check names are unchanged. `-p`, `-v` and `-l` are aliases, so -`-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a +`check_profile.bat` is a shim around `check_profile.ps1`: same checks, same order, same logs. Its flags +take PowerShell spellings (`-Vendor`, `-ProfilesDir`, `-Validator`, `-Download`, `-Refresh`, +`-WorkDir`, `-LogLevel`), and positional check names are unchanged. `-p`, `-v` and `-l` are aliases, +so `-v Elegoo -l 2` reads the same on both platforms. It passes `-ExecutionPolicy Bypass` because a default Windows client refuses to run a checked-out `.ps1` at all. The `.ps1` finds Python itself, probing `py -3`, then `python`, then `python3`; run the tool by hand with `py -3` for the same reason. -Every check in the run happens even after an earlier one fails; the script exits non-zero if any did, and writes -`logs/.log` plus, on failure, `pr_comment.md` under a per-user cache dir — the same report CI posts on the PR. -That dir is `~/Library/Caches/orca-profile-check` on macOS, `${XDG_CACHE_HOME:-~/.cache}/orca-profile-check` on Linux -and `%LOCALAPPDATA%\orca-profile-check` on Windows; it is named apart from OrcaSlicer's own per-user dirs and sits -outside the checkout, so every worktree shares one copy. `--work-dir` / `-WorkDir` overrides it. A stale `.lock` -there after a crash must be removed by hand. +Every check runs even after an earlier one fails; the script exits non-zero if any failed, and writes +`logs/.log` plus, on failure, `pr_comment.md` (the same report CI posts on the PR) under a +per-user cache dir: + +| Platform | Cache dir | +| --- | --- | +| macOS | `~/Library/Caches/orca-profile-check` | +| Linux | `${XDG_CACHE_HOME:-~/.cache}/orca-profile-check` | +| Windows | `%LOCALAPPDATA%\orca-profile-check` | + +It is named apart from OrcaSlicer's own per-user dirs and sits outside the checkout, so every worktree +shares one copy and each run overwrites its `logs/`. `--work-dir` / `-WorkDir` overrides it. + +When other worktrees or agents may run checks too, pass `--work-dir ` from the start +and capture the console output yourself: the shared `logs/` can belong to another run by the time you +read them. With `--work-dir`, also pass `--validator` pointing at the cached nightly, so the new dir +does not download it again: + +| Platform | Cached validator | +| --- | --- | +| Linux | `/validator/OrcaSlicer_profile_validator` | +| macOS | `/validator/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator` | +| Windows | `\validator\OrcaSlicer_profile_validator.exe` | + +Copying the cached `profile-fixtures/` into the new dir reuses the fixture archives; the fixture +`manifest.json` is still downloaded on every run, so `validate_custom` needs the network either way. +`another run is using ` means a live run holds `/.lock`: leave it and use your own +`--work-dir`. Only a `.lock` with no `check_profile` process alive is a crash leftover; delete it by +hand. ## The five checks | Check | Command it runs | Catches | | --- | --- | --- | -| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize`/`update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, id length, **all `setting_id` and `filament_id` rules** | -| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits`/`compatible_*`, duplicate `filament_id` per printer | -| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion, unresolvable printer defaults | -| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra — see below | -| `validate_custom` | `validator -p -l 2` | a shipped preset name that a past release offered no longer resolving | +| `profile_tool` | `python3 scripts/orca_profile_tool.py check` | index coverage **both ways**, preset-name collisions, files `normalize` / `update-index` would still rewrite, duplicate JSON keys, filament `compatible_printers`, `filament_type` array, conflict keys, variant strings and array widths and layout keys, dangling `default_materials`, id length, **all `setting_id` and `filament_id` rules** ([below](#orca_profile_toolpy-check)) | +| `validate_system` | `validator -p resources/profiles -l 2` | load errors, missing filament `compatible_printers`, dangling `inherits` / `compatible_*`, duplicate `filament_id` per printer (`Ambiguous AMS filament match`), printer defaults that name no compatible system filament | +| `validate_slice` | `validator -p … -s -l 2` | custom G-code expansion and unresolvable printer defaults, by slicing | +| `validate_filament_subtypes` | `validator -p … -l 2 -f` | nothing extra; see below | +| `validate_custom` | `validator -p -l 2` | a shipped preset name that a past release offered no longer resolving | -**`-f` is a no-op.** It is declared `po::bool_switch()->default_value(true)`, so the duplicate-`filament_id` -check runs whether or not you pass it — `validate_system` already fails on duplicates. The binary's own -`--help` ("Off unless this flag is present") does not reflect that default. +**`-f` is a no-op.** It defaults to on, so the duplicate-`filament_id` check runs whether or not you +pass it, and `validate_system` already fails on duplicates. The binary's own `--help` ("Off unless this +flag is present") does not reflect that default. -### `validate_custom` — the backward-compatibility gate +**A `--vendor` run reads differently from CI.** The validator's `-v` loads that vendor plus +OrcaFilamentLibrary and nothing else, so every library tune whose `compatible_printers` names another +vendor's printers fails `validate_system`, `validate_filament_subtypes` and each `validate_custom` +fixture with thousands of `references unknown compatible_printers "Bambu Lab …"` lines. Under +`--vendor`, `profile_tool` and `validate_slice` are the meaningful results; for the other three, filter +the log for your vendor's files and treat only those lines as findings. The unscoped run is the CI +result; run it before the PR. -Downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets — -a `__orca_test` copy of every system preset that -release shipped, cut with the validator's own `-g 1` mode — unpacks each over a copy of the current tree -and loads it. Each entry holds only `inherits` plus a canned diff, so the one failure it adds over -`validate_system` is a shipped preset name disappearing. (The whole current tree sits under each fixture, -so every `validate_system` error fails it too.) This is what makes a rename or an -`instantiation` flip a CI failure rather than just a user complaint, and the reason `renamed_from` is -mandatory. +### `validate_custom`: the backward-compatibility gate + +It downloads one fixture archive per past release (v1.9.0 onwards) of *generated mock* user presets: a +`__orca_test` copy of every system preset that release shipped, cut with the +validator's own `-g 1` mode. It unpacks each over a copy of the current tree and loads it. Each entry +holds only `inherits` plus a canned diff, so the one failure it adds over `validate_system` is a shipped +preset name disappearing. This is what makes a rename, a deletion or an `instantiation` flip a CI +failure rather than just a user complaint, and the reason `renamed_from` is mandatory. + +The whole current tree sits under each fixture, so every `validate_system` error fails +`validate_custom` too: fix `validate_system` first. Under `--vendor` it copies only the top-level index +files, `/` and `OrcaFilamentLibrary/`, and picks fixtures by the index's display `name` +(`Bambulab` for `BBL`), not the file stem; fixture presets without that prefix are covered only by an +unscoped run, and it warns `validate_custom checked nothing` when none match. ### `validate_slice` -Slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime tower. -It selects `default_print_profile` and the first `default_filament_profile`, then updates compatibility; -that update can select a different compatible preset. Confirm the intended defaults yourself rather -than treating a passing sweep as proof that those exact presets were sliced. -A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no g-code, or -emits no `CP TOOLCHANGE START`. It cannot be scoped to a filament-only vendor -(`No instantiable printer presets found for vendor OrcaFilamentLibrary`); `check_profile.sh` records it -as SKIP for a vendor with no `machine/` folder. +It slices a two-colour cube on every instantiable printer in the tree, sequentially, forcing the prime +tower. It selects `default_print_profile` and the first `default_filament_profile`, then updates +compatibility; that update can select a different compatible preset. Confirm the intended defaults +yourself rather than treating a passing sweep as proof that those exact presets were sliced. + +A printer fails if it cannot be selected, falls back to a Default preset, throws, produces no G-code, +or emits no `CP TOOLCHANGE START` (`change_filament_gcode` never expanded). Non-default processes and +filaments get no dedicated coverage; [slice them on a copy](#checking-a-copy-of-the-tree). In the +default set, a bundle without a `machine/` folder is recorded as SKIP; naming `validate_slice` +explicitly for it fails (`No instantiable printer presets found for vendor OrcaFilamentLibrary`). The +validator logs `[error]` lines that do not fail a check (such as `could not found extruder_type`); only +each check's PASS or FAIL counts. ## `orca_profile_tool.py check` -`check` is one subcommand of the tool that also owns -`generate-id`, `normalize`, `trim` and `update-index`; see [ids.md](ids.md) for the writing half. +`check` is one subcommand of the tool that also owns `fix-variant`, `generate-id`, `normalize`, +`trim` and `update-index`; [ids.md](ids.md#the-tool) has the writing half. -| Per vendor | Catches | -| --- | --- | -| `check_preset_name_uniqueness` | two files in one bundle claiming one type + name — indexed or not | -| `check_index_coverage` | a file on disk that no `*_list` references (**an error, not a warning**) | -| `check_name_consistency` | an index entry whose `name` disagrees with the file, or whose `sub_path` is missing | -| `check_normalized` | a file `normalize` would rewrite, and an index `update-index` would rebuild | -| `check_filament_compatible_printers` | an instantiated non-library filament with no `compatible_printers` of its own | -| `check_conflict_keys` | `extruder_clearance_radius` alongside `extruder_clearance_max_radius` | -| `check_vector_type_keys` | a vector option written as a scalar (`"filament_type": "PLA"`) | -| `check_filament_id_length` | a declared `filament_id` longer than 8 characters | -| `check_machine_default_materials` | every `default_materials` / `default_filament_profile` name resolves | -| `check_obsolete_keys` | per-key warnings for ignored options; **filament files only** | +| Catches | Scope | Function in the tool | +| --- | --- | --- | +| two files in one bundle claiming one type + name, indexed or not | per vendor | `check_preset_name_uniqueness` | +| a file on disk that no `*_list` references (**an error, not a warning**) | per vendor | `check_index_coverage` | +| an index entry whose `name` disagrees with the file, or whose `sub_path` is missing | per vendor | `check_name_consistency` | +| a file `normalize` would rewrite, an index `update-index` would rebuild | per vendor | `check_normalized` | +| duplicate JSON keys in a file | every file read | the JSON loader | +| an instantiated non-library filament with no non-empty `compatible_printers` of its own | per vendor | `check_filament_compatible_printers` | +| `extruder_clearance_radius` alongside `extruder_clearance_max_radius` | per vendor | `check_conflict_keys` | +| a scalar `filament_type` (`"filament_type": "PLA"`); the five other filament vectors `normalize` arrayifies surface as `normalize would convert to an array` | per vendor | `check_vector_type_keys`, `check_normalized` | +| a variant string the two enums cannot build (a dead variant, a legacy spelling included), a variant list naming one variant twice, an `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` that is not an enum name ([variant names](#variant-names)) | per vendor | `check_variant_names` | +| a variant array not exactly `variant length × stride` wide in a selectable preset that writes it (with `--strict`, also in one it reaches); machine variant layout keys that disagree; a process id array that does not pair each variant ([variant arrays](#variant-arrays)) | per vendor | `check_variant_arrays` | +| a declared `filament_id` longer than 8 characters | per vendor | `check_filament_id_length` | +| a `default_materials` name, or a `default_filament_profile` name, matching no filament file ([below](#default-material-references)) | per vendor | `check_machine_default_materials` | +| per-key warnings for ignored options, **filament files only** | per vendor | `check_obsolete_keys` | +| `setting_id` uniqueness, every `filament_id` rule, and `machine_model` names duplicated across bundles | **tree-wide, ignoring `--vendor` entirely** | `check_setting_id_uniqueness`, `check_filament_ids`, `check_machine_model_name_uniqueness` | -Tree-wide, **ignoring `--vendor` entirely**: `check_setting_id_uniqueness` and `check_filament_ids`. So a -vendor-scoped run can and does fail on another vendor's files — and it saves seconds, not minutes. +Because the id and model-name checks stay tree-wide, a vendor-scoped run can and does fail on another +vendor's files, and it saves seconds, not minutes. Unscoped, the per-vendor pass covers every bundle. The only exclusion is the stray `user/` directory -(see below); `OrcaFilamentLibrary` is held to the same rules as any vendor, its sole exemption being -that a library filament may leave `compatible_printers` empty — exactly what -`check_filament_compatible_printers` allows. `check_normalized` covers every bundle with an index. +(below); OrcaFilamentLibrary is held to the same rules as any vendor, its sole exemption being that a +library filament may leave `compatible_printers` empty. `check_normalized` covers every bundle with an +index. Notes that matter: -- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit code. -- A nonexistent `--vendor` is a hard error — `[ERROR] unknown vendor "" in `, exit 1. -- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable. -- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor pass - and warned about (`No profiles found for vendor: at …/.json`, and the "Checked vendors" count - goes up by one). The one exception is `user/`, the validator's data dir, which an unscoped `check` - skips by name; `--vendor user` still checks and warns about it. Warnings never change the exit code. - `normalize`, `trim` and `update-index` ignore strays too — they define a bundle as *a directory with a +- Exit codes: **0** clean, **1** errors found, **2** argparse misuse. Warnings never change the exit + code. +- A nonexistent `--vendor` is a hard error: `[ERROR] unknown vendor "" in `, exit 1. +- `--vendor ""` means all vendors; `check_profile.sh` relies on that. `--vendor` is repeatable + (`check --vendor A --vendor OrcaFilamentLibrary`); `check_profile.sh` takes one. +- A **stray directory** under `resources/profiles/` still gets counted as a vendor by the per-vendor + pass and warned about (`No profiles found for vendor: at …/.json`, and the "Checked + vendors" count goes up by one): usually an emptied folder, or a `user/` left by a direct validator + run. An unscoped `check` skips `user/` by name; `--vendor user` still checks and warns about it. + `normalize`, `trim` and `update-index` ignore strays too: they define a bundle as *a directory with a matching index file*. - Each remedy is printed once for the whole run, not once per file, as a `[WARNING]` under the errors - ("2 unreferenced file(s) above: delete them, or run … update-index"). Read those lines: they name the + (`2 unreferenced file(s) above: delete them, or run … update-index`). Read those lines: they name the command that fixes the batch. -- The trailing summary always suggests `normalize`. That is right for the shape errors and misleading for - everything else — an id error needs `generate-id`, a dangling `default_materials` needs a human. +- When there are errors or warnings, the trailing summary suggests `normalize`. That is right for the + shape errors and misleading for everything else: an id error needs `generate-id`, a dangling + `default_materials` needs a human. +- Other options: `--dry-run` on every writing command, `--profiles DIR` to point any command at another + tree, `--profile-type` to narrow `normalize`, `trim` and `update-index` to one type + ([ids.md](ids.md#the-tool)). - `resources/profiles/check_unused_setting_id.py` is a legacy BBL-only diagnostic, not part of profile CI. Use `orca_profile_tool.py check` for current id validation. -### Obsolete-key diagnostics +### Obsolete keys -`check` always reports per-key warnings for obsolete options in filament profiles. -The normalization check also rejects obsolete keys across preset types; `normalize` removes them. +`check` always reports per-key warnings for obsolete options in filament profiles. Its normalization +check also rejects obsolete keys across all preset types (`normalize would remove `); `normalize` +removes them. ### Default-material references -The materials check finds `default_materials` / `default_filament_profile` entries naming a preset -that does not exist. The three authoring errors it surfaces are `,` instead of `;`, wrong case -(`@system`), and a whole `;`-joined string stuffed into one array element. +The materials check finds `default_materials` / `default_filament_profile` entries naming a preset that +does not exist. It reads each `machine/` file's own key (a model's `default_materials`, a variant's +`default_filament_profile`; a file that writes both is checked on `default_materials` only) and accepts +any `name` found in the vendor's or OrcaFilamentLibrary's `filament/` files, bases and unindexed files +included. The three authoring errors it surfaces are `,` instead of `;`, wrong case (`@system`), and a +whole `;`-joined string stuffed into one array element. Only the validator (`validate_system`) requires +an instantiated system filament that is compatible with each variant. + +### Variant arrays + +`check_variant_arrays` composes every selectable preset the loader's way (the parent, then each +`include` in order, then the file's own keys; a filament's `inherits` may fall through to +OrcaFilamentLibrary) and holds each key of [the four variant sets](extruder-variants.md#the-four-key-sets) +that the preset writes itself to exactly its `variant length × stride` +([widths](extruder-variants.md#widths)). The variant length is the length of the composed preset's +own `*_extruder_variant` list; without one, a machine's is the number of variants its +`extruder_variant_list` offers, else its extruder count (the list's default is one +`Direct Drive Standard` per extruder), and a process's or filament's is 1. Any other width is an +error, one value included and even when every value is the same. A key the preset does not write +takes what reaches it, the default or an array it inherits or includes, which the loader resizes; it +is not checked. A base is not judged on its own: what it writes counts only where it reaches a preset +that does not override it. + +`check --strict` also holds every selectable preset to its own width for each key that reaches it, +so a preset whose variants differ from those of the file its array comes from restates the array +(the error names that file). That is BBL's practice and the target for new printer-specific presets; +CI runs `check` without `--strict`. + +An id array that reaches a selectable preset, `printer_extruder_id` or `print_extruder_id`, written or +inherited, must have one entry per entry of its variant list on any printer. This rule and the +machine layout rules below judge the composed preset without `--strict`, whichever file writes the +keys. Beside a written variant list this rule reports it instead of the width +rule, so it is reported once. A process that +lists variants without `print_extruder_id` gets a **warning** when a variant repeats (every entry then +reads as extruder 1, so the repeated variant is unreachable), nothing otherwise. + +On a machine the layout keys are held to [Printer rules 1–4](extruder-variants.md#printer-machine), +every failure an error unless marked: + +- With `extruder_variant_list` written: the list has one entry per extruder, as many as + `nozzle_diameter`; every variant starts with its extruder's `extruder_type`; + `default_nozzle_volume_type` names a nozzle volume type that extruder lists; + `printer_extruder_variant` is the list flattened extruder-major and `printer_extruder_id` gives each + entry its 1-based extruder (an id array left out reads as extruder 1 everywhere, which passes when + those are the flattening's ids). Without the pair, the list may offer one variant in total. +- With the pair written and no `extruder_variant_list`: one variant per extruder at most. A pair that + `single_extruder_multi_material` off would replace with the default at load is a **warning**; a pair + the rebuild would leave as it is passes. + +It does **not** see a base's own resize: it composes at the width each file wrote, so an array wider +than a base's list, which the loader cuts before any child inherits it, passes even with `--strict` +([composition](extruder-variants.md#padding-truncation-and-composition)). Nor does it see per-extruder +vectors outside the sets (`extruder_offset`, `printer_extruder_options`, …), which no variant list +sizes. What a variant holds (a High Flow variant copied from Standard, a variant inserted at the wrong +index) is review work +([item 14](review-checklist.md#14-per-extruder-vectors-not-checked-and-variant-arrays-widths-and-layout-checked)); +whether it is a name the engine can select at all is `check_variant_names`'. + +`python3 scripts/orca_profile_tool.py fix-variant` resizes every array the width rule rejects in the +selectable preset that writes it, leaves bases alone and adds no key: extra values are dropped, missing ones +repeat the last value (the last normal/silent pair at stride 2; a lone value fills normal and silent +alike). It leaves the variant lists and id arrays to you, since they address the variants rather than +fill them. `fix-variant --strict` then also writes each key that reaches a selectable preset at +another width: into the most general file on the way down to the preset whose own width is the +preset's and whose selectable presets taking it all need that width, else into the preset itself. +Presets of every bundle count towards that agreement; `--vendor` limits the files written and +`--dry-run` previews. The loader pads with the first value where `fix-variant` repeats the last, so a +padded array need not load as before, and +trimming deletes values: when the extra values were meant as per-extruder or per-variant values, +declare the variant layout instead ([Printer rule 2](extruder-variants.md#printer-machine)) and keep +them. `fix-variant` moves no value, so a family whose presets all wrote one value repeats the widened +array in each; put it on the family's base afterwards ([shared bases](shared-bases.md)). + +### Variant names + +`check_variant_names` reads the four list keys plus `extruder_type`, `nozzle_volume_type` and +`default_nozzle_volume_type` of every preset the bundle's index references, bases included, and holds +each entry to the names the engine's two enum maps define (`s_keys_map_ExtruderType`, +`s_keys_map_NozzleVolumeType`, read from `PrintConfig.cpp` on every run). The bundle's own files are +judged, not the composed config: a bad string is the writing file's error, once. Every finding is an +error, and no bundle is exempt: BBL, whose bundle is imported from BambuStudio, is held to OrcaSlicer's +enums like any other. + +- A variant string outside ` ` is a **dead variant**: it still + counts toward the variant length the arrays are sized by, so the values written for it silently never + reach the G-code. `Hybrid` too, which is runtime-only, and an empty entry. A name BambuStudio's enum + has and OrcaSlicer's lacks is dead here as well, and passes with no tool change once the engine gains + that nozzle volume type. +- A legacy name the loader still rewrites in these keys (`Normal` → `Standard`, `Big Traffic` → + `High Flow`) is an error that names the enum name to write; the profile has to spell the enum + name. `DirectDrive` is only rewritten in `extruder_type`, so a variant string carrying it is dead. +- A variant list naming one variant twice is an error: the lookup returns the first equal string, so + the repeat is unreachable and its value sits at an index no extruder reads. A filament list takes + strings, `extruder_variant_list` takes them per extruder, and a process takes `(extruder id, variant)` + pairs — one string on two extruders is two pairs, not a repeat. +- An `extruder_type`, `nozzle_volume_type` or `default_nozzle_volume_type` value that is not an enum + name is an error: they are enum options, so an unknown value fails the validator's load of the + whole bundle, while the app silently loads the option's default instead. A legacy spelling + (`DirectDrive`, `Normal`, `Big Traffic`) and `Hybrid`, an enum value no profile writes, are errors + too. + +The variant *order*, the choice of variants, and the values themselves are not checked. ### `normalize` and `update-index` are part of the check -`check` fails when either command would still change something, so they are not optional polish — the +`check` fails when either command would still change something, so they are not optional polish: the file that gets reviewed has to be the file that ships. What `normalize` changes is narrow and fixed: -adds a missing `type`, deletes a `version` or `is_custom_defined` key from a *preset* file, deletes six -print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`, -`inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`), deletes the -obsolete keys in `PrintConfigDef::handle_legacy`'s `ignore` set across preset types, resolves the -`extruder_clearance_*` conflict pair by keeping the larger, arrayifies five filament options besides -`filament_type`, and hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`, -`filament_id`, `instantiation` to the front. A file it changes is then rewritten whole — tab-indented, -LF, one trailing newline, keys reordered. + +- adds a missing `type`; +- deletes a `version` or `is_custom_defined` key from a *preset* file; +- deletes six print-speed keys from filament profiles (`initial_layer_print_speed`, `outer_wall_speed`, + `inner_wall_speed`, `infill_speed`, `top_surface_speed`, `travel_speed`); +- deletes the obsolete keys the loader ignores (the `ignore` set in `PrintConfigDef::handle_legacy`), + across preset types; +- resolves the `extruder_clearance_*` conflict pair by keeping the larger; +- arrayifies six filament options (`filament_type`, `filament_cost`, `filament_density`, + `temperature_vitrification`, `filament_max_volumetric_speed`, `filament_vendor`); +- hoists `type`, `name`, `renamed_from`, `inherits`, `from`, `setting_id`, `filament_id`, + `instantiation` to the front. + +A file it changes is then rewritten whole: tab-indented, LF, one trailing newline, keys reordered. A +file committed with CRLF line endings therefore changes on every line; read the diff before committing +it. **Set `type` explicitly when authoring.** For a file in `machine/` without it, normalization guesses -`machine` only if its name contains `nozzle`, otherwise `machine_model`. That heuristic cannot -reliably classify shared machine bases or unusually named variants. +`machine` only if its name contains `nozzle` (case-insensitive), otherwise `machine_model`. That +heuristic cannot reliably classify shared machine bases or unusually named variants. -The Python obsolete-key set is checked against the C++ source by a unit test. Active options -and legacy aliases that the loader migrates (such as `extruder_type` and -`extruder_clearance_max_radius`) are preserved. +The tool's obsolete-key set is checked against the loader's ignore list by a unit test. Active options +are preserved, including live keys whose *values* the loader rewrites (`extruder_type`: `DirectDrive` → +`Direct Drive`; the variant-string keys: `Normal` / `Big Traffic` → `Standard` / `High Flow`), and so are +legacy key names the loader migrates (such as `extruder_clearance_max_radius`). Two things it therefore does **not** enforce: - **Formatting and key order on their own.** A file with none of those problems is skipped entirely, so 4-space indent, a missing trailing newline, and a file that leads with `compatible_printers` all pass - `check`. They stay latent until something else trips `normalize` and the whole file reformats inside an - unrelated diff. (`normalize --force` rewrites every file, which is not something to run on a shipped - bundle.) + `check`. They stay latent until something else trips `normalize` and the whole file reformats inside + an unrelated diff. (`normalize --force` rewrites every file; do not run it on a shipped bundle.) - **A misspelled setting key.** `inital_layer_height` and `sparse_infill_densiti` pass `check` cleanly. - Verify new keys against `PrintConfig.cpp` and `PrintConfigDef::handle_legacy`. + Verify new keys against `src/libslic3r/PrintConfig.cpp` and the loader's legacy handling + (`PrintConfigDef::handle_legacy`: renamed keys, rewritten values and ignored keys). + +`check` does not catch a dangling `default_print_profile` either; check that name by hand. ## The validator binary -Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` (`-DORCA_TOOLS=ON`). -Both scripts find a local build under `build*/` — `check_profile.sh` tries Release, RelWithDebInfo, then -Debug, and `check_profile.ps1` adds MinSizeRel — else they download the nightly into the -`validator` subdirectory of the per-user cache dir (see above). Pass `--download` / `-Download` to -match CI exactly, since a stale local build is used silently. Windows looks for +Built from `src/dev-utils/OrcaSlicer_profile_validator.cpp` with `-DORCA_TOOLS=ON`. Both scripts use a +local build under `build*/` when one exists, else they download the nightly into the `validator` +subdirectory of the cache dir. `check_profile.sh` searches Release, then RelWithDebInfo, then Debug +(each under `build*/src/` and `build*/*/src/`), then single-config `build*/src`, and +prefers a host-architecture build tree; `check_profile.ps1` tries Release, RelWithDebInfo, MinSizeRel, +then Debug. A stale local build is used silently; `--download` / `-Download` skips local builds and uses the +nightly. The download and the fixtures stay cached until `--refresh` / `-Refresh`, so +`--download --refresh` (`-Download -Refresh`) matches CI exactly. Windows looks for `OrcaSlicer_profile_validator.exe`. -If your build lives somewhere else entirely, point at it with `--validator` / `-Validator`, or set +If your build lives somewhere else, point at it with `--validator` / `-Validator`, or set `ORCA_PROFILE_VALIDATOR` (`$env:ORCA_PROFILE_VALIDATOR` in PowerShell). | Flag | Meaning | @@ -167,11 +330,12 @@ If your build lives somewhere else entirely, point at it with `--validator` / `- | `-l ` | log level; CI uses 2 | | `-v ` | load only that vendor **plus** OrcaFilamentLibrary | | `-s` | slice sweep | +| `-o ` | with `-s`, save each printer's G-code there | | `-f` | no-op (see above) | | `-g 1` | regenerate user-preset fixtures; takes a value, and wipes the user preset dir first | -On ARM64 Linux the nightly is x86-64 only — the script warns and downloads anyway, producing a binary -that will not run. Build it locally instead. +On ARM64 Linux the nightly is x86-64 only: the script warns and downloads anyway, producing a binary +that will not run. Build it locally with `-DORCA_TOOLS=ON` and pass `--validator`. Running the validator directly uses the profile tree as its data directory and can create `user/` there. Prefer the wrappers, which stash existing user presets and restore them afterward. After a @@ -185,16 +349,27 @@ Use `--profiles DIR` on the Python tool and `-p DIR` on the validator. The wrapp ```bash ./scripts/check_profile.sh --profiles "" +# Windows: scripts\check_profile.bat -ProfilesDir "" ``` -On Windows use `scripts\check_profile.bat -ProfilesDir ""`. +To slice a process or filament that is not a printer's default, or to read the G-code, work on a copy: +copy `resources/profiles` and `resources/info` into one scratch dir (the validator reads `info/` next to +the tree), point the printer's `default_print_profile` or first `default_filament_profile` at the preset +in the copy, and run the validator with `-o`: + +```bash + -p /profiles -v "" -s -o +``` + +Each printer's G-code is saved as `__.gcode`; its embedded config +(`; print_settings_id = …`, `; filament_settings_id = …`) shows what was actually sliced. ## Testing in the app -Editing this checkout's `resources/profiles` does not update a separately installed application. -Test with a build using the edited resources and a bumped bundle version; the updater installs newer -bundles under `/system/`, and the preset cache also depends on the bundle version. -Use Help ▸ Show Configuration Folder to locate the active data directory: +Editing this checkout's `resources/profiles` does not update a separately installed application. Test +with a build using the edited resources and a bumped bundle version: the updater installs newer bundles +under `/system/`, and the preset cache also depends on the bundle version. Use Help ▸ Show +Configuration Folder to locate the active data directory: | Platform | Default data directory | | --- | --- | @@ -202,58 +377,71 @@ Use Help ▸ Show Configuration Folder to locate the active data directory: | Linux | `$XDG_CONFIG_HOME/OrcaSlicer`, or `~/.config/OrcaSlicer` when unset | | Windows | `%APPDATA%\OrcaSlicer` | -A portable `data_dir` next to the executable takes precedence. Use a separate test configuration -for a clean-install check; preserve the normal configuration and user presets. - -## Cross-platform paths - -Match the exact case of each `sub_path` and asset filename; Linux filesystems commonly distinguish -case even when a macOS or Windows checkout does not. Preset-name references are case-sensitive -on every platform. Avoid Windows-invalid characters (`< > : " | ? *`), reserved device names -such as `CON` / `NUL` (including with extensions), and trailing spaces or dots in path components. -Keep stems tidy too, but a space immediately before `.json` is not a trailing path-component space. +A portable `data_dir` next to the executable takes precedence. Use a separate test configuration for a +clean-install check; preserve the normal configuration and user presets. ## Error → remedy | Message | Fix | | --- | --- | -| `can not find inherits for ` | parent missing, unregistered, or listed **after** the child | -| `can not find filament_id for ` | nothing in the chain declares one — run `generate-id` | -| `can not find parent for config !` | a shipped name disappeared — add `renamed_from` | -| `Missing instantiation attribute for ` | key absent **or** not the string `"true"`/`"false"` | +| `can not find inherits for ` | parent missing, unregistered, misspelled, or listed **after** the child | +| `can not find include` | the template is misspelled, registered after the includer, or selectable | +| `can not find filament_id for ` | nothing in the chain declares one: run `generate-id` | +| `can not find parent for config !` | a shipped name disappeared: add `renamed_from` | +| `Failed loading configuration file ` | that file could not be loaded and the whole bundle was discarded: a JSON error, or a value its option cannot take, such as `nil` in a non-nullable key (`Invalid value provided for parameter : nil`, `Deserializing nil into a non-nullable object`); the lines above it name the cause | +| `Missing instantiation attribute for ` | key absent **or** not the string `"true"` / `"false"` | | `contains incorrect keys: , which were removed` | a key valid for a different preset type | -| `defines invalid printer variant ""` | not in the model's `nozzle_diameter` list | -| `has printer_variant "" that does not match its nozzle_diameter` | the set comparison in [machine-profiles.md](machine-profiles.md) | -| `references unknown compatible_printers "

"` | the printer was renamed or deleted; fix the reference | +| `defines invalid printer variant ""` | not a token of the model's `nozzle_diameter` list | +| `has printer_variant "" that does not match its nozzle_diameter` | [the set comparison](machine-profiles.md#printer_model-and-printer_variant) | +| `references unknown compatible_printers "

"` | the printer was renamed or deleted, or a `machine_model` name was used instead of a variant name: fix the reference. Under `--vendor`, usually another vendor's printer ([why](#the-five-checks)) | | `references renamed compatible_printers "" (now "")` | in-tree references must name the current preset; `renamed_from` does not excuse them | -| `Filament preset "" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file — the flattened-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) | -| `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"` | make the lists disjoint by [specificity](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product) — the specialized profile keeps the variant, the general ones drop it; prefer this over deleting a profile — or fix an `inherits` pointing at another material's `@base`. `orca_profile_tool.py check` does not catch this; only `validate_system` here does | -| `Layer height cannot exceed nozzle diameter.` / `Line width too small` | `Print::validate()` flow rules | +| `Filament preset "" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file; the resolved-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) | +| `Ambiguous AMS filament match: N filament presets share filament_id "X" and are all compatible with printer "Y"` | make the lists disjoint by [specificity](filament-profiles.md#overlapping-coverage-one-variant-one-profile-per-product): the specialized profile keeps the variant, the general ones drop it; prefer this over deleting a profile. Or fix an `inherits` pointing at another product's `@base`. `orca_profile_tool.py check` does not catch this; only `validate_system` does | +| `Layer height cannot exceed nozzle diameter.` / `Line width too small` / `Line width too large` | the [slicing limits](process-profiles.md#values-to-review-per-nozzle) | | `[ERROR] … no .json list references it, so it never loads` | `update-index`, or delete the file | -| `[ERROR] … references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` | +| `[ERROR] … no .json list references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` | | `[ERROR] … normalize would ` / `.json: update-index would rebuild ` | run that command and commit the result | | `[ERROR] has N profiles named ""` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup | -| `[ERROR] … must not have a setting_id` / `is missing a setting_id` | `generate-id --setting-id` | -| `inherits filament_id "X" but its own triple … mints "Y"` | `generate-id` will **not** fix this — see [ids.md](ids.md) | -| `vendor 's config version: invalid` | the `version` string is not Semver-parseable | -| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or model; see [failure scopes](vendor-bundle.md#failure-modes-ranked-by-blast-radius) | -| `Printer "

" fell back to a default preset` | final process or filament selection is a generic Default preset; check named defaults, visibility and available compatible presets. An incompatible default may instead be replaced without this error | -| `Printer "

" sliced but the filament change never fired` | `change_filament_gcode` never expanded | +| `Duplicate key error in : Duplicate key detected: ` | a key written twice in one file; keep the intended one | +| `… must not have a setting_id` / `… is missing a setting_id` / `setting_id "X" in does not match the expected "Y" …` | `generate-id --setting-id` (by hand in `BBL/`, see [ids.md](ids.md#bbls-exception-precisely)) | +| `filament_id "X" declared by … does not match the mint of its triple …` | `generate-id --filament-id`; if the id should not be declared here at all, remove it so the preset inherits its root's | +| `inherits filament_id "X" but its own triple "V/T/N" mints "Y"` | `generate-id` will **not** fix this; see [ids.md](ids.md#what-generate-id-does-and-does-not-fix) | +| `"" has N values for variant length S at stride k, which takes M` (with ` (no , so …)` after `S`, naming where `S` came from, when the preset writes no list, and ` (it comes from )` at the end under `--strict` for an array the preset does not write) | exactly `S × k` values in variant order, or leave the key out ([widths](extruder-variants.md#widths)); on a list-less multi-extruder printer whose extruders differ, declare the layout; `fix-variant` cuts or pads to that width ([variant arrays](#variant-arrays)); an `S` you did not expect means the variant list did not resolve through `include` or `inherits` | +| `printer_extruder_variant […] is not extruder_variant_list flattened extruder-major […]` / `printer_extruder_id […] does not give each entry of printer_extruder_variant its 1-based extruder […]` / `printer_extruder_id has N entries for the M entries of printer_extruder_variant` | write the pair as the flattening of the list, ids in step ([Printer rule 2](extruder-variants.md#printer-machine)) | +| `extruder_variant_list has N entries for M extruder(s)` / `extruder_variant_list entry i "…" holds a variant that does not start with extruder i's extruder_type` / `default_nozzle_volume_type "…" is not a nozzle volume type extruder i lists` | [Printer rules 1, 3 and 4](extruder-variants.md#printer-machine) | +| `extruder_variant_list offers N variants but the preset writes no printer_extruder_variant/printer_extruder_id` / `printer_extruder_variant lists several variants for extruder N but the preset writes no extruder_variant_list` / `[WARNING] … with single_extruder_multi_material off the loader replaces printer_extruder_variant …` | write all three layout keys ([Printer rule 2](extruder-variants.md#printer-machine)) | +| `print_extruder_id has N entries for the M entries of print_extruder_variant` / `[WARNING] … print_extruder_variant repeats a variant but print_extruder_id is absent` | one id per variant entry, mirroring the printer's pairs ([Process rule 1](extruder-variants.md#process)) | +| ` holds "…", which no extruder can select: "…" is not a nozzle volume type the enum has (…)` / `… it does not start with an extruder type the enum has (…)` / `… is empty` / `… Hybrid names the sub-nozzles of one hybrid extruder at runtime` | write a legal variant string, ` ` from the two enums ([variant names](#variant-names), [variant strings](extruder-variants.md#variant-strings)); in every bundle, BBL included | +| `… holds the legacy variant "…"` / `extruder_type spells the legacy name "…"` / `nozzle_volume_type spells the legacy name "…"` / `default_nozzle_volume_type spells the legacy name "…"` | write the enum name the message gives: the loader still rewrites the legacy one, but `check` rejects it | +| ` lists "…" N times` / `print_extruder_variant lists the pair (extruder N, "…") N times` | drop the repeat and its value from every variant array: the lookup returns the first equal string | +| `extruder_type "…" is not one of (…)` / `nozzle_volume_type "…" is not one of (…)` / `default_nozzle_volume_type "…" is not one of (…)` / `… names "Hybrid", which the engine computes for a hybrid extruder at runtime` | they are enum options, so an unknown value fails the validator's load of the whole bundle and silently becomes the default in the app; use `s_keys_map_ExtruderType` / `s_keys_map_NozzleVolumeType`, `Hybrid` excepted ([variant strings](extruder-variants.md#variant-strings)) | +| `[WARNING] No profiles found for vendor:

` | a directory with no matching index (an emptied folder, or a `user/` left by a direct validator run); remove it | +| `… has no compatible system filament in its model's "default_materials"` | add a system filament preset compatible with that variant to the model's `default_materials` | +| `… names the unknown system filament "" in its "default_materials"` / `… "default_filament_profile"` | name an existing system (not user, not base) filament exactly; `;` separators, exact case | +| `Missing filament profile: '' referenced in ` | the same, caught by `check`; usual causes are `,` instead of `;`, wrong case (`@system`), or a `;`-joined list packed into one array element | +| `machine_model name "" is declared by N bundles` | model names are unique across the tree; rename the new model | +| `vendor 's config version: invalid` | the `version` string does not parse; write `MM.mm.pp.bb` | +| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or a model; see [failure scopes](vendor-bundle.md#failure-scopes) | +| `Printer "

" fell back to a default preset` | the final process or filament selection is a generic Default preset: check the named defaults, their visibility and that compatible presets exist. An incompatible default may instead be replaced without this error | +| `Printer "

" sliced but the filament change never fired (no CP TOOLCHANGE START)` | `change_filament_gcode` never expanded | ## CI -`.github/workflows/check_profiles.yml`, job **"Check profiles"**, on `pull_request` into `main` or -`release/*`, paths `resources/profiles/**`, `resources/printers/**`, `scripts/**` and the workflow itself. -There is no push trigger — a direct push to main runs no profile validation. +`.github/workflows/check_profiles.yml`, job **"Check profiles"**, runs on `pull_request` into `main` or +`release/*` touching `resources/profiles/**`, `resources/printers/**`, `scripts/**`, +`src/libslic3r/PrintConfig.cpp` (where the tool reads the variant key sets) or the workflow itself. +There is no push trigger: a direct push to main runs no profile validation. -The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit -tests. That step is deliberately **not** `continue-on-error`: a broken tool makes everything it then says -about the profiles worthless. Every check after it is `continue-on-error` with a final gate, so one run -reports all five results. On failure a second workflow posts or replaces a single PR comment marked -``, with each failing log truncated to 30 KB; it deletes the comment -once the run is green. +The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit tests +(run them locally after changing `scripts/`, with `py -3` on Windows, and keep `-t scripts` or the +imports fail). That step is deliberately **not** `continue-on-error`: a broken tool makes everything it +then says about the profiles worthless. Every check after it is `continue-on-error` with a final gate, +so one run reports all five results. On failure a second workflow posts or replaces a single PR comment +marked ``, holding the start of each failing log (30 KB per check, +12 KB per `validate_custom` fixture; reproduce locally for the full list); it deletes the comment once +the run is green. The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer -self-merge a `resources/profiles//` PR with no human review — so whatever CI does not check -is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that touches the -tooling always needs a maintainer. +self-merge a PR limited to their own `resources/profiles//` folder with no human review, so +whatever CI does not check is what ships unreviewed. Its denied patterns refuse `^scripts/` and any +`.py`, so a PR that touches the tooling always needs a maintainer. diff --git a/.claude/skills/orca-profiles/references/vendor-bundle.md b/.claude/skills/orca-profiles/references/vendor-bundle.md index 29aac13331..d6c7710469 100644 --- a/.claude/skills/orca-profiles/references/vendor-bundle.md +++ b/.claude/skills/orca-profiles/references/vendor-bundle.md @@ -1,7 +1,7 @@ -# The vendor bundle and the loader +# Vendor bundles -A bundle is `resources/profiles/.json` (the index) plus `resources/profiles//`. -The **vendor id is the filename stem**, not the `name` inside — several differ (`BBL.json` is named +A bundle is `resources/profiles/.json` (the index) plus `resources/profiles//`. The +**vendor id is the filename stem**, not the `name` inside; the two may differ (`BBL.json` is named "Bambulab"). Asset paths and the `setting_id` formula use the id; the `validate_custom` fixture prefix uses the `name`. @@ -13,16 +13,16 @@ uses the `name`. "version": "02.04.00.03", "force_update": "0", "description": "Phrozen configurations", - "machine_model_list": [ { "name": "...", "sub_path": "machine/....json" } ], - "machine_list": [ ... ], - "process_list": [ ... ], - "filament_list": [ ... ] + "machine_model_list": [ { "name": "Phrozen Arco", "sub_path": "machine/Phrozen Arco.json" } ], + "machine_list": [ … ], + "process_list": [ … ], + "filament_list": [ … ] } ``` -The loader reads `name`, `version`, `url` and the four `*_list` arrays. -`description` is only logged. `force_update` is read by `PresetUpdater`, never by the loader. -`sub_path` is relative to the **vendor folder**. +The loader reads `name`, `version`, `url` and the four `*_list` arrays. `description` is only logged; +`force_update` is read by the profile updater, never by the loader. `sub_path` is relative to the +**vendor folder**. | List | Holds | | --- | --- | @@ -35,141 +35,180 @@ The loader reads `name`, `version`, `url` and the four `*_list` arrays. 1. **Everything is registered, bases included.** Every preset file on disk has exactly one entry in the matching list, and no unindexed preset file is left in the tree. -2. **Parents before children.** `inherits` resolves against a per-kind map filled as the list is walked - (`configs.clear()` then process, filaments, printers). A parent listed after its child produces - `can not find inherits for ` and the bundle is discarded. -3. **The index entry's `name` must equal the `name` inside the sub_path file.** `check_name_consistency` - walks the index looking for the files; `check_index_coverage` walks the files looking for them in the - index. The `renamed_from` escape hatch `check_name_consistency`'s docstring promises is commented out. +2. **Parents before children, includes before includers.** The lists load processes first, then + filaments, then printers, each in index order, and `inherits` and `include` resolve only against + presets of that type already loaded from it. A parent + listed after its child produces `can not find inherits for ` and the bundle is + discarded; an include listed after its includer is `can not find include`, a counted error that + leaves the includer without those keys. +3. **The index entry's `name` equals the `name` inside the `sub_path` file.** `renamed_from` does not + excuse a mismatch. -All three are `check` errors now, and `update-index` writes an index that satisfies all three from the -files on disk — including the parents-first ordering, by topological sort. Hand-editing the index is +All three are `check` errors, and `update-index` writes an index that satisfies all three from the +files on disk, including the dependency ordering (parents and templates before the presets that use +them, then, in name order, entries that neither depend on nor are depended on by another entry of their own +list, and any entry on a dependency cycle). Hand-editing the index is fine for a one-line addition, but the committed result must equal what `update-index` writes, because `check` compares them. -The loader itself reports none of this: an unregistered file, or an entry with a typo'd key -(`"subpath"`), is silently dropped. (A typo'd `sub_path` is a `check` error naming the entry.) +The loader itself reports none of this: an unregistered file, or an entry with a misspelled key +(`"subpath"`), is silently dropped. (A misspelled `sub_path` value is a `check` error naming the entry.) -`BBL/cli_config.json` and `BBL/filament/filaments_color_codes.json` are auxiliary data loaded by path, -not presets. The tool's `NON_PROFILE_FILES` excludes these basenames from preset maintenance. +`BBL/cli_config.json` and, in `BBL/filament/`, `filaments_color_codes.json`, `filament_id_map.json`, +`filament_name_map.json` and `support_recommended_params.json` are auxiliary data files read by path, +not presets. The last three carry a `type` key and look like presets; the tool excludes all five from +preset maintenance. ## `version` -Parsed by a four-component Semver where the 4th is folded in as `patch = patch*100 + value`. Write it -zero-padded, `MM.mm.pp.bb`; a couple of bundles drop a component or the padding, but do not imitate them. +Four components, `MM.mm.pp.bb`, compared as a version number in which the fourth is folded into the +third (`patch × 100 + build`). Write all four components, zero-padded. -- **Bump the version for every bundle the PR touches.** `PresetUpdater` installs bundled resources - only when their version is newer than the installed version; the `.opc` preset cache is also - versioned. Nothing in profile CI checks the bump. -- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both parse to `2.4.100`. A bundle - that reaches `.99` carries into the third component (`02.03.02.99` → `02.03.03.00`). -- An **absent** version is worse than a stale one: the validator still passes, but `Semver::valid()` - excludes `0.0.0`, so the vendor is dropped from the configuration wizard entirely and the preset cache - is disabled for it. An *unparseable* version is not silent — it throws and discards the whole bundle - (see the failure table below). +- **Bump the version for every bundle the change touches.** The app installs bundled profiles only + when their version is newer than the installed one, and the `.opc` preset cache is also keyed on + the version. Nothing in profile CI checks the bump. +- **Keep the last component ≤ 99.** `02.04.00.100` and `02.04.01.00` both read as + `2.4.100`. A bundle that + reaches `.99` carries into the third component (`02.03.02.99` → `02.03.03.00`). +- An **absent** version is worse than a stale one: it reads as `0.0.0`, which is not a valid version. + `check` and the validator still pass, but the vendor is dropped from the setup wizard entirely and gets no preset cache. Confirm the key exists. An + *unparseable* version is not silent: it discards the whole bundle (`vendor 's config version: + invalid`). ## Common preset keys | Key | Value | | --- | --- | -| `type` | `machine_model` / `machine` / `process` / `filament` | -| `name` | the preset name; the filename is *not* authoritative | -| `inherits` | the parent's exact `name` — no path, no `.json` | +| `type` | `machine_model`, `machine`, `process` or `filament` | +| `name` | the preset name, the identity every reference uses; the filename is *not* authoritative | +| `inherits` | the parent's exact `name`: no path, no `.json` | +| `include` | a template's exact `name`, or an array of them, layered under this preset's own keys ([below](#inherits-and-include)) | | `instantiation` | the **string** `"true"` (selectable) or `"false"` (base) | -| `from` | `"system"` by convention; the vendor loader never reads it | -| `setting_id` | required on instantiated presets, forbidden on bases — generated | -| `renamed_from` | `;`-separated list of old names this preset supersedes | +| `from` | `"system"` for shipped presets | +| `setting_id` | generated; required on instantiated presets, forbidden on bases | +| `renamed_from` | `;`-separated old names this preset supersedes ([below](#renamed_from)) | These are config-preset keys; `machine_model` records have their own -[schema](machine-profiles.md#a-machine_model-is-not-a-config-preset). Keep `from` as `"system"` -for shipped presets. The vendor loader ignores it, but the CLI config-file loader accepts only -`system`, `user` or `User` and handles their inheritance differently. +[key set](machine-profiles.md#machine_model-a-record-not-a-config-preset). Keep `from` as `"system"`: +the bundle loader ignores it, but loading the file as a CLI config accepts only `system`, `user` or +`User` and handles their inheritance differently. -`instantiation` is the one metadata key that is hard-gated: a missing key or any value other than the -strings `"true"`/`"false"` is an error (`Missing instantiation attribute for `). A JSON boolean -`true` fails harder — it throws inside `load_from_json` and takes the **whole vendor bundle** down. +`instantiation` is the one metadata key the validator gates: a missing key or any value other than the +strings `"true"` / `"false"` is a counted error (`Missing instantiation attribute for `) that fails +the validator, though the preset still loads and is treated as selectable. A file with no +`instantiation` whose name contains `gcode`, or that has no `name`, silently becomes an include-only +template. A JSON boolean `true` fails harder: it takes the **whole vendor bundle** down. -### `inherits` +## `inherits` and `include` -Resolution is an exact-name lookup **within the same bundle**, plus one exception: filaments may inherit -from `OrcaFilamentLibrary`, which is loaded first and becomes the base bundle. Vendor-to-vendor -inheritance always fails. You can inherit from an instantiated preset as well as from a base; it is -common. +`inherits` resolves by exact name **within the same bundle**, plus one exception: filaments may inherit +from OrcaFilamentLibrary, which is loaded first. Vendor-to-vendor inheritance always fails, and an +unresolved `inherits` discards the bundle. You can inherit from an instantiated preset as well as from a +base. -### `renamed_from` +`"include": ["", …]` (or one bare name) pulls in `instantiation: "false"` presets of the same type +from the same bundle (never the library), registered before the includer. It shares a block of keys +between presets that do not share a parent: a variant layout, a G-code template. Only `"false"` presets +can be included, so a name that resolves to nothing (misspelled, registered after the includer, or a +selectable preset) is a counted error (`can not find include`) and the preset loads without it. + +**How a preset's config is composed:** start from the parent's stored config (a root starts from the +built-in defaults), apply each preset named in `include` in the order listed, then the preset's own +keys. Later layers win, so precedence is own keys > later includes > earlier includes > the `inherits` +chain. Only then is every variant key of the composed config resized to its variant length +([widths](extruder-variants.md#widths)), and keys of another preset type removed. The two routes hand +down different widths: + +- **`inherits` hands down the resized config.** A base is stored *after* its own resize, at the length + of its own `*_extruder_variant` (one variant for a base of any type that writes none, whatever its + extruder count). A child therefore inherits the base's arrays at the base's width: an array wider than + that is cut to its first values before any child sees it, and a child that adds variants gets those + first values padded. So widen an array only on a preset whose own variant list already has the + entries; a wide array on a narrow base is silently lost at load, and `check`, which composes at the + width each file wrote and judges selectable presets only, misses that cut. +- **`include` hands down the template's diff, at its pre-resize width.** An included preset contributes + every key where its own composed config (its parent, its own includes and its own keys) differs from + the built-in defaults, taken *before* its resize. So keys the template inherits are passed on too, + arrays it writes arrive at the width its file wrote, and a key it sets to the built-in default value + is not passed on at all, so it cannot override what the includer inherited. Resizing happens on the + includer, not on the template. + +## `renamed_from` One JSON string, `;`-separated for several old names. -- Write `"A;B"`, never `"A ; B"` — an unquoted item keeps its trailing space and can never match. +- Write `"A;B"`, never `"A ; B"`: a space after a `;` is skipped, but a space before it stays part of + the name (`"A "`), which can never match. - When `renamed_from` is **absent** and the name contains `@`, the loader auto-adds the `@`-removed form (`X @Y` → `X Y`) as a rename alias. Declaring an explicit `renamed_from` **suppresses** that, so a - preset that needs both the `@`-removed form and a real old name must list both. No shipped profile - currently does, which means any preset that gained a `renamed_from` quietly lost its `X Y` alias. + preset that needs both the `@`-removed form and a real old name must list both; a preset that gains + a `renamed_from` without it quietly loses its `X Y` alias. - It rescues names stored **outside** the tree: user presets and 3MF projects. It does **not** rescue - in-tree `inherits` (exact lookup), it does **not** satisfy `check_name_consistency`, the validator - reports an in-tree reference that only resolves through it (`references renamed compatible_printers - "OLD" (now "NEW")`), and `machine_model` records never read it at all. -- Only one preset may claim a given old name — two that do is a counted error + in-tree `inherits` (exact lookup), it does **not** satisfy the index-name rule, the validator reports + an in-tree reference that only resolves through it (`references renamed compatible_printers "OLD" + (now "NEW")`), and `machine_model` records never read it at all. +- Only one preset may claim a given old name; two that do is a counted error (`… was marked as renamed from "Y" … as well`). But the redirect is **inert while a live preset still - carries that name**, and nothing checks *that*; Z-Bolt ships a folder of such dead entries. + carries that name**, and nothing checks *that*, so a neighbour's `renamed_from` is no model. -## Failure modes, ranked by blast radius +## Failure scopes | Scope | Cause | | --- | --- | -| **All vendors, zero system profiles** | a non-string `version`, `name` or `url` at the top level of a vendor index (`"version": 2`), or non-string `nozzle_diameter` on a model — `nlohmann::type_error` escapes the per-vendor `std::runtime_error` catch | -| **The whole vendor bundle** | unparseable `version`; index JSON parse error; a `sub_path` file missing or unparseable; unresolvable `inherits`; duplicate preset name within the vendor; empty/unknown `printer_model` or `printer_variant`; a filament resolving no `filament_id` | -| **One preset** | `instantiation` missing or a wrong string; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for `) | -| **Logged, not counted** | a raw JSON number in a preset — `invalid json type for `, the value is dropped and the exit code stays 0 | -| **Nothing reported by the loader** | unregistered file; misspelled setting key; missing bed/hotend asset. Only the first of those is a `check` error; the other two reach users | +| **Every vendor except OrcaFilamentLibrary, and all user presets** | a non-string where the index or a `machine_model` expects a string (`"version": 2` at the top level of an index, a numeric `name` or `url`, a non-string `nozzle_diameter` or other model key): `[json.exception.type_error.302] type must be string`, and the validator reports `Validation failed` | +| **The whole vendor bundle** | index JSON parse error; unparseable `version`; a listed file missing or unparseable; a value its option cannot take, such as `nil` in a non-nullable key (`Failed loading configuration file`); unresolved `inherits`; two selectable presets with one name; empty or unknown `printer_model` / `printer_variant`; a filament resolving no `filament_id`; a JSON boolean `instantiation` | +| **A counted error; the preset still loads** | `instantiation` missing or not `"true"` / `"false"`; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for `); an `include` naming nothing usable (`can not find include`, loads without it) | +| **The rest of the file, logged only** | an array with a non-string element (`[0.4]`, `invalid json array`): that key and every key after it in the file are dropped, and no error is counted | +| **One value, logged only** | a raw JSON number in a preset (`invalid json type for `): the value is dropped and the exit code stays 0 | +| **Nothing reported by the loader** | unregistered file; two bases with one name, or a base and a selectable preset with one name (the first in the index wins); misspelled setting key; missing bed, hotend or cover asset. `check` catches the first two; the others reach users | -Deleting a file the index still lists surfaces as a *parse error* on line 1, not "file not found" — the -loader `ifstream`s the missing path and nlohmann reports `unexpected end of input`. +Deleting a file the index still lists surfaces as a *parse error* on line 1 (`unexpected end of input`), not "file +not found". -Preset names are a **single global namespace across every vendor**: a duplicate within one vendor is a -hard bundle failure, a duplicate across vendors is reported as `Found duplicated preset: in -vendor: ` and still counts as an error. `check_preset_name_uniqueness` catches the within-bundle -case earlier and more precisely — including an *unindexed* twin, which is one `sub_path` edit away from -silently becoming the parent every child resolves to (`std::map::emplace` keeps the first insertion, so -index order decides). Base names, by contrast, repeat across bundles by design: `fdm_process_common` -exists in nearly all of them. +Selectable preset names are a **single namespace across every vendor**: a duplicate within one vendor +is a hard bundle failure, and a duplicate across vendors is reported as `Found duplicated preset: +in vendor: ` and still counts as an error. `check` catches the within-bundle case earlier and +more precisely, bases included, and including an *unindexed* twin, which is one `sub_path` edit away +from silently becoming the parent every child resolves to (the first registered preset of a name wins, +so index order decides). Base names, by contrast, repeat across bundles by design: +every bundle may have its own `fdm_process_common` ([uniqueness](naming.md#uniqueness)). ## Starting a whole new vendor bundle -Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab` or `M3D`** are -the minimal shape — a shared machine base, the model, one variant, a shared process base, two -processes, and an empty `filament_list` that takes the library generics. Do *not* start from `Phrozen`: -it carries local `fdm_filament_*` copies that have drifted from the library, and a filament preset that -restates most of its parent — the style this skill advises against. +Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab`** is the +minimal shape: a shared machine base, the model, one variant, a shared process base, two processes, and +an empty `filament_list`, so the printer takes the library generics. Do *not* start from a bundle that +carries local `fdm_filament_*` copies, which drift from the library, or filament presets that restate +most of their parent, the style this skill advises against. Write the machine files **last**, so you only visit them once: -1. **Choose the names first** — model, variant(s), process(es). Everything else references them. +1. **Choose the names first**: model, variant(s), process(es). Everything else references them + ([naming.md](naming.md)). 2. `resources/profiles/.json`: `name`, `version` (`01.00.00.00`), `force_update: "0"`, - `description`, and all four `*_list` arrays (an empty `filament_list` is fine). -3. The shared bases — `/machine/fdm_machine_common.json` and - `/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`. - For a Klipper printer add your own `/machine/fdm_klipper_common.json` inheriting the machine + `description`, and all four `*_list` arrays (empty is fine: `update-index` fills them once the files + exist, so this step only needs the bundle metadata to be right). +3. The shared bases: `/machine/fdm_machine_common.json` and + `/process/fdm_process_common.json`, both `"instantiation": "false"` with no `setting_id`. For + a Klipper printer add your own `/machine/fdm_klipper_common.json` inheriting the machine base; there is no shared one, because a `machine` preset can only inherit inside its own bundle. 4. One selectable process per variant, each naming its variant in `compatible_printers`. 5. Bed assets and `_cover.png`, all directly in `/`. None of them is needed for the - bundle to load, and nothing in CI checks them — but the bed files are inert unless the `machine_model` - names them in `bed_model` / `bed_texture`, and the cover is found by convention as + bundle to load, and nothing in CI checks them; but the bed files are inert unless the + `machine_model` names them in `bed_model` / `bed_texture`, and the cover is found by convention as `_cover.png`. -6. The `machine_model` record and the `machine` variants, now that every value they reference exists — +6. The `machine_model` record and the `machine` variants, now that every value they reference exists; the minimum key sets and the `default_*` shapes are in - [machine-profiles.md](machine-profiles.md#the-machine-variant). -7. Run the tool and validate — follow + [machine-profiles.md](machine-profiles.md#machine-the-variant). +7. Run the tool and validate: follow [Creating or modifying a profile](../SKILL.md#creating-or-modifying-a-profile). `generate-id` is not optional for a new bundle: the validator loads presets that have no `setting_id`, but `check` fails - every one of them. `update-index` will fill the four `*_list` arrays for you once the files exist, so - step 2 only needs the bundle metadata to be right. + every one of them. ## `resources/profiles_template/` A separate tree (`Template.json` + `Template/`) holding filament and process templates. It is **not** a -scaffold for shipped profiles — `CreatePresetsDialog.cpp` reads it for the in-app "create a custom -printer/filament" wizard, so editing it changes what users get when they create a custom preset. -`check_profile.sh`'s validator checks default to `resources/profiles` (redirectable with `-p`), and so -does `orca_profile_tool.py` (redirectable with `--profiles`); -neither covers this tree. +scaffold for shipped profiles: the app's "create a custom printer / filament" dialog reads it, so +editing it changes what users get when they create a custom preset. `check_profile.sh`'s validator +checks default to `resources/profiles` (redirectable with `-p`), and so does `orca_profile_tool.py` +(redirectable with `--profiles`); neither covers this tree. diff --git a/.claude/skills/orca-profiles/scripts/shared_settings.py b/.claude/skills/orca-profiles/scripts/shared_settings.py new file mode 100644 index 0000000000..261fe19da4 --- /dev/null +++ b/.claude/skills/orca-profiles/scripts/shared_settings.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Find settings to move onto shared bases, and prove a move changed nothing. + + snapshot OUT.json write every selectable preset's config as the loader stores it + compare BEFORE.json report every value that differs from the snapshot; exit 1 if any + candidates --vendor V restated values; per base, the settings its presets all share and the + defaults with exceptions that would pay + +Every subcommand takes --profiles DIR (default resources/profiles). + +Configs are composed the loader's way: the parent's stored config, then each include at +the width its file wrote, then the preset's own keys, and every variant key resized to the +preset's own variant list (one variant without one), padded with its first value or cut. +A base is stored after that resize, so a variant array wider than a base's list reaches its +children cut. Not modelled: the built-in defaults. A key no file in a preset's chain writes +loads its default, so compare reports a key written on one side only separately: it is no +change when the written value is the option's default in PrintConfig.cpp. Nor is it modelled +that an include template does not pass on a key equal to the default. Reads +scripts/orca_profile_tool.py. +""" +import argparse +import json +import os +import sys +from collections import Counter, defaultdict + +# The profile tool lives in /scripts; this file in /.claude/skills/orca-profiles/scripts. +sys.path[:0] = [os.path.join(os.getcwd(), "scripts"), + os.path.join(os.path.dirname(os.path.abspath(__file__)), *[os.pardir] * 4, "scripts")] +import orca_profile_tool as tool # noqa: E402 + +TYPES = ("machine", "process", "filament") +# Keys the loader reads from each file as metadata; neither inherits nor include passes them on. +PER_FILE = {"type", "name", "from", "instantiation", "setting_id", "renamed_from", "description", + "inherits", "include", "version", "url", "is_custom_defined"} +# Keys the app replaces with the selected presets' names before slicing: a file's value never counts. +REPLACED = {"print_settings_id", "printer_settings_id", "filament_settings_id"} +# What a restructure changes by design, or what never reaches a slice. +MOVED_BY_DESIGN = {"inherits", "include"} | REPLACED +# Keys that stay in their own file: metadata, identity, and each preset's compatibility. +NEVER_SHARED = PER_FILE | REPLACED | {"filament_id", "compatible_printers", "compatible_prints", + "printer_variant", "printer_model"} + + +class Tree: + def __init__(self, profiles_dir): + self.dir = profiles_dir + self.scheme = tool._variant_scheme() + self.vendors = tool.list_vendor_names(profiles_dir) + self.bundles = {v: tool.load_vendor_configs(profiles_dir, v) for v in self.vendors} + self.cache = {} + + def lookup(self, vendor, ptype, name, in_ofl=False): + """(vendor the name resolves in, (rel, data)); filaments fall back to the library.""" + if not in_ofl and name in self.bundles[vendor][ptype]: + return vendor, self.bundles[vendor][ptype][name] + if ptype == "filament" and tool.OFL in self.bundles and name in self.bundles[tool.OFL][ptype]: + return tool.OFL, self.bundles[tool.OFL][ptype][name] + return None, None + + def composed(self, vendor, ptype, name, drop=None, seen=frozenset()): + """Config before the preset's own resize: parent stored, includes, own keys.""" + owner, found = self.lookup(vendor, ptype, name, vendor == tool.OFL) + if found is None or (owner, name) in seen: + return {} + seen = seen | {(owner, name)} + data = found[1] + config = {} + if data.get("inherits"): + config.update(self.stored(owner, ptype, data["inherits"], seen)) + include = data.get("include") or [] + for included in [include] if isinstance(include, str) else include: + if included in self.bundles[owner][ptype]: + config.update(self.composed(owner, ptype, included, seen=seen)) + config = {k: v for k, v in config.items() if k not in PER_FILE} + config.update((k, v) for k, v in data.items() if k != drop) + return config + + def stored(self, vendor, ptype, name, seen=frozenset()): + key = (vendor, ptype, name) + if key not in self.cache: + self.cache[key] = self.resize(ptype, self.composed(vendor, ptype, name, seen=seen)) + return self.cache[key] + + def resize(self, ptype, config): + list_key, strides = self.scheme[ptype] + length = len(tool._as_list(config[list_key])) if list_key in config else 1 + out = dict(config) + for key, stride in strides.items(): + if key in out: + values = tool._as_list(out[key]) + need = length * stride + out[key] = values[:need] + values[:1] * (need - len(values)) + return out + + def presets(self, vendors=None, ptypes=TYPES): + for vendor in vendors or self.vendors: + for ptype in ptypes: + for name, (rel, data) in sorted(self.bundles[vendor][ptype].items()): + yield vendor, ptype, name, rel, data + + +def snapshot(tree): + return {f"{vendor}/{ptype}/{name}": {k: v for k, v in tree.stored(vendor, ptype, name).items() + if k not in MOVED_BY_DESIGN} + for vendor, ptype, name, _rel, data in tree.presets() + if data.get("instantiation") == "true"} + + +def compare(before, after): + changed, one_sided = 0, [] + for preset in sorted(before.keys() | after.keys()): + old, new = before.get(preset), after.get(preset) + if old is None or new is None: + print(f"{preset}: {'added' if old is None else 'removed'}") + changed += 1 + continue + for key in sorted(old.keys() | new.keys()): + if key not in old or key not in new: + one_sided.append(f"{preset}: {key} " + f"{json.dumps(old[key]) if key in old else '(built-in default)'} -> " + f"{json.dumps(new[key]) if key in new else '(built-in default)'}") + elif old[key] != new[key]: + print(f"{preset}: {key} {json.dumps(old[key])} -> {json.dumps(new[key])}") + changed += 1 + for line in one_sided: + print(line) + print(f"{changed} difference(s)") + if one_sided: + print(f"{len(one_sided)} key(s) written on one side only: each is a difference unless the " + f"written value is the option's default in src/libslic3r/PrintConfig.cpp") + return changed + len(one_sided) + + +def candidates(tree, vendor, ptypes, group_by): + for ptype in ptypes: + entries = {name: data for _v, _t, name, _rel, data in tree.presets([vendor], (ptype,))} + selectable = [n for n, d in entries.items() if d.get("instantiation") == "true"] + stored = {n: tree.stored(vendor, ptype, n) for n in entries} + list_key = tree.scheme[ptype][0] + + # Restated: a key a file writes that it would inherit unchanged without writing it. + restated = {} + for name, data in entries.items(): + restated[name] = sorted( + k for k in data if k not in NEVER_SHARED and k != list_key + and (data.get("inherits") or data.get("include")) + and tree.resize(ptype, tree.composed(vendor, ptype, name, drop=k)).get(k) == stored[name][k]) + if restated[name]: + print(f"{vendor}/{ptype} {name}: restates what it inherits: {', '.join(restated[name])}") + + # Groups: every preset with selectable presets below it, or the --group-by values. + chain = {n: [] for n in selectable} + for name in selectable: + node = entries[name].get("inherits") + while node in entries and node not in chain[name]: + chain[name].append(node) + node = entries[node].get("inherits") + groups = defaultdict(list) + for name in selectable: + if group_by: + groups[json.dumps(stored[name].get(group_by))].append(name) + else: + for base in chain[name]: + groups[base].append(name) + printed = {} + for label, members in sorted(groups.items(), key=lambda g: -len(g[1])): + if len(members) < 2 or label == "null": + continue + if frozenset(members) in printed: + print(f"\n{vendor}/{ptype} {label}: the same presets as {printed[frozenset(members)]}") + continue + common = [b for b in chain[members[0]] if all(b in chain[m] for m in members[1:])] + home = common[0] if group_by and common else None if group_by else label + + def below(m): + """m and the files between it and the group's home.""" + return [m] + chain[m][:chain[m].index(home)] if home in chain[m] else [m] + shared, defaults = [], [] + for key in sorted(set().union(*(entries[m].keys() for m in members)) - NEVER_SHARED): + loaded = [json.dumps(stored[m].get(key)) for m in members] + counts = Counter(loaded).most_common(2) + if len(counts) == 1: + writers = sum(key in entries[m] and key not in restated[m] for m in members) + if writers >= 2: + shared.append(f"{key} ({writers} write it)") + continue + (top, held), (_, runner_up) = counts + if held == runner_up or top == "null" or home is None: + continue + # Balance 5: w presets drop their copy; a presets that take the key from the home + # (no file on their way to it writes it) and load another value must write theirs. + w = sum(key in entries[m] and v == top for m, v in zip(members, loaded)) + a = sum(v != top and not any(key in entries[f] for f in below(m)) + for m, v in zip(members, loaded)) + if w - a > 1: + shown = top if len(top) <= 40 else top[:37] + "..." + defaults.append(f"{key} = {shown}: {held} load it, {w} write it, " + f"{a} would have to write their own") + if shared or defaults: + where = f"; nearest common base {common[0]}" if group_by and common else "" + title = f"{group_by} = {label}" if group_by else label + print(f"\n{vendor}/{ptype} {title}: {len(members)} presets{where}") + printed[frozenset(members)] = title + for line in shared: + print(f" {line}") + if defaults: + print(" default with exceptions:") + for line in defaults: + print(f" {line}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + profiles = argparse.ArgumentParser(add_help=False) + profiles.add_argument("--profiles", default=os.path.join("resources", "profiles"), + help="profiles directory (default: resources/profiles)") + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("snapshot", parents=[profiles]).add_argument("out") + sub.add_parser("compare", parents=[profiles]).add_argument("before") + cand = sub.add_parser("candidates", parents=[profiles]) + cand.add_argument("--vendor", required=True) + cand.add_argument("--type", choices=TYPES, action="append") + cand.add_argument("--group-by", help="group selectable presets by this key's value instead of by " + "base: printer_model, gcode_flavor, extruder_type, filament_id, layer_height, ...") + args = parser.parse_args() + tree = Tree(args.profiles) + if args.command == "snapshot": + presets = snapshot(tree) + with open(args.out, "w", encoding="utf-8") as f: + json.dump(presets, f, sort_keys=True) + print(f"{len(presets)} selectable presets written to {args.out}") + elif args.command == "compare": + with open(args.before, encoding="utf-8") as f: + sys.exit(1 if compare(json.load(f), snapshot(tree)) else 0) + else: + candidates(tree, args.vendor, args.type or TYPES, args.group_by) + + +if __name__ == "__main__": + main() From 71bb400e46fb329b0597612bdb541f469002f977 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 28 Sep 2026 17:04:42 +0800 Subject: [PATCH 17/38] Add multi-variant support to the app --- src/libslic3r/PresetBundle.cpp | 12 +- src/libslic3r/Print.cpp | 7 +- src/libslic3r/PrintApply.cpp | 4 +- src/libslic3r/PrintConfig.cpp | 57 ++- src/libslic3r/PrintConfig.hpp | 20 +- src/slic3r/GUI/CaliHistoryDialog.cpp | 16 +- .../GUI/CalibrationWizardPresetPage.cpp | 65 ++-- src/slic3r/GUI/CalibrationWizardSavePage.cpp | 1 + src/slic3r/GUI/DeviceCore/DevCalib.cpp | 3 +- src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp | 8 +- src/slic3r/GUI/DeviceCore/DevNozzleSystem.h | 11 +- src/slic3r/GUI/DeviceManager.cpp | 6 + src/slic3r/GUI/DragDropPanel.cpp | 3 +- src/slic3r/GUI/FilamentMapPanel.cpp | 8 +- src/slic3r/GUI/Plater.cpp | 328 +++++++++++++++--- src/slic3r/GUI/PrintOptionsDialog.cpp | 1 + src/slic3r/GUI/SelectMachine.cpp | 3 + src/slic3r/GUI/SyncAmsInfoDialog.cpp | 4 + src/slic3r/GUI/Tab.cpp | 53 ++- src/slic3r/GUI/Widgets/MultiNozzleSync.cpp | 94 ++--- src/slic3r/GUI/Widgets/MultiNozzleSync.hpp | 6 +- src/slic3r/GUI/Widgets/SwitchButton.cpp | 121 ++++++- src/slic3r/GUI/Widgets/SwitchButton.hpp | 20 +- src/slic3r/Utils/CalibUtils.cpp | 2 + tests/fff_print/test_multifilament.cpp | 53 +++ .../test_config_variant_expansion.cpp | 87 ++++- tests/libslic3r/test_nozzle_volume_type.cpp | 48 +++ 27 files changed, 846 insertions(+), 195 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index c8fe00b17c..b2cc67f970 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -199,7 +199,8 @@ DynamicPrintConfig PresetBundle::construct_full_config( if (num_filaments <= 1) { // BBS: update filament config related with variants DynamicPrintConfig filament_config = in_filament_presets[0].config; - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_config.has_multi_variant_filament())) filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition()); @@ -223,7 +224,8 @@ DynamicPrintConfig PresetBundle::construct_full_config( filament_temp_configs.resize(num_filaments); for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_temp_configs[i].has_multi_variant_filament())) filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); } @@ -4664,7 +4666,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio if (num_filaments <= 1) { //BBS: update filament config related with variants DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config; - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_config.has_multi_variant_filament())) filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition()); @@ -4758,7 +4761,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio filament_temp_configs.resize(num_filaments); for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_temp_configs[i].has_multi_variant_filament())) filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ed3ac6de88..90a37bb8c6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -3938,6 +3938,9 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< { int extruder_count = 1, extruder_volume_type_count = 1; bool support_multi = m_ori_full_print_config.support_different_extruders(extruder_count); + // Orca: resolve the filament variants wherever Print::apply does, a multi-variant filament + // on a single-variant printer included. + const bool expand_filaments = (extruder_count > 1) || support_multi || m_ori_full_print_config.has_multi_variant_filament(); std::vector> nozzle_volume_types; extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); @@ -3978,7 +3981,7 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< m_full_print_config = m_ori_full_print_config; std::set filament_keys = filament_options_with_variant; filament_keys.insert("filament_self_index"); - if ((extruder_count > 1) || support_multi) + if (expand_filaments) m_full_print_config.update_values_to_printer_extruders_for_multiple_filaments(m_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant"); const std::vector &extruder_retract_keys = print_config_def.extruder_retract_keys(); @@ -3995,7 +3998,7 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, m_config.filament_map_2.values); } - if ((extruder_count > 1) || support_multi) { + if (expand_filaments) { t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end()); keys.push_back("filament_self_index"); m_config.apply_only(m_full_print_config, keys, true); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 90d60b5917..373288dc2c 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1296,7 +1296,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant", &dynamic_slot_indices); - else if ((extruder_count > 1) || different_extruder) + // Orca: also on a printer with a single extruder variant once a filament defines several + // (e.g. Standard and High Flow), so each filament takes its variant for that extruder. + else if ((extruder_count > 1) || different_extruder || new_full_config.has_multi_variant_filament()) new_full_config.update_values_to_printer_extruders_for_multiple_filaments(m_ori_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant"); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 1d994d7874..7d916a6e05 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -626,7 +626,8 @@ static const t_config_enum_values s_keys_map_NozzleVolumeType = { { "Standard", nvtStandard }, { "High Flow", nvtHighFlow }, { "TPU High Flow", nvtTPUHighFlow }, - { "Hybrid", nvtHybrid } + { "Hybrid", nvtHybrid }, + { "E3D High Flow", nvtE3DHighFlow } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType) @@ -676,9 +677,34 @@ int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_ty for (int index = 0; index < int(variant_list.size()); ++index) { if (extruder_variant == variant_list[index] && variant_ids_1based[index] == variant_id_1based) { return index; } } + // Without this variant, use the id's own first variant (usually Standard), not variant index 0, + // which belongs to the first filament or extruder. + for (int index = 0; index < int(variant_list.size()); ++index) { + if (variant_ids_1based[index] == variant_id_1based) { return index; } + } return 0; } +std::set get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id) +{ + std::set supported_types; + + auto *variant_list = printer_config.option("extruder_variant_list"); + auto *extruder_types = printer_config.option("extruder_type"); + if (!variant_list || !extruder_types || extruder_id < 0 || + extruder_id >= (int) variant_list->values.size() || extruder_id >= (int) extruder_types->values.size()) + return supported_types; + + const ExtruderType extruder_type = ExtruderType(extruder_types->values[extruder_id]); + for (NozzleVolumeType volume_type : get_valid_nozzle_volume_type()) { + // An unsupported extruder type yields an empty name, which would match any list. + const std::string variant = get_extruder_variant_string(extruder_type, volume_type); + if (!variant.empty() && variant_list->values[extruder_id].find(variant) != std::string::npos) + supported_types.insert(volume_type); + } + return supported_types; +} + std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type) { if (nozzle_volume_type > nvtMaxNozzleVolumeType) { @@ -5923,15 +5949,18 @@ void PrintConfigDef::init_fff_params() def->label = "Nozzle Volume Type"; def->tooltip = "Nozzle volume type for extruders."; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - // Order must match the NozzleVolumeType enum values (Standard=0, High Flow=1, Hybrid=2, TPU High Flow=3). + // Listed in display order. A position is not the enum value (E3D High Flow is 5, after the reserved 4), + // so map a position to its NozzleVolumeType through enum_keys_map. def->enum_values.push_back(L("Standard")); def->enum_values.push_back(L("High Flow")); def->enum_values.push_back(L("Hybrid")); def->enum_values.push_back(L("TPU High Flow")); + def->enum_values.push_back(L("E3D High Flow")); def->enum_labels.push_back(L("Standard")); def->enum_labels.push_back(L("High Flow")); def->enum_labels.push_back(L("Hybrid")); def->enum_labels.push_back(L("TPU High Flow")); + def->enum_labels.push_back(L("E3D High Flow")); def->mode = comSimple; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -5944,10 +5973,12 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back(L("High Flow")); def->enum_values.push_back(L("Hybrid")); def->enum_values.push_back(L("TPU High Flow")); + def->enum_values.push_back(L("E3D High Flow")); def->enum_labels.push_back(L("Standard")); def->enum_labels.push_back(L("High Flow")); def->enum_labels.push_back(L("Hybrid")); def->enum_labels.push_back(L("TPU High Flow")); + def->enum_labels.push_back(L("E3D High Flow")); def->mode = comDevelop; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -5986,7 +6017,7 @@ void PrintConfigDef::init_fff_params() // Per-nozzle volume type. Forward-compat-only registration with no slicing consumer — nothing in // src/ reads it; the engine resolves per-nozzle volume types from `extruder_nozzle_stats` tokens // instead. Kept registered so a project/config carrying it loads without an unknown-option - // substitution warning. Registers Standard/High Flow/TPU High Flow only (no Hybrid). + // substitution warning. Registers the physical types only (no Hybrid). // Internal use only, no translation. def = this->add("extruder_nozzle_volume_type", coEnums); def->label = "Extruder nozzle volume type"; @@ -5995,9 +6026,11 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("Standard"); def->enum_values.push_back("High Flow"); def->enum_values.push_back("TPU High Flow"); + def->enum_values.push_back("E3D High Flow"); def->enum_labels.push_back("Standard"); def->enum_labels.push_back("High Flow"); def->enum_labels.push_back("TPU High Flow"); + def->enum_labels.push_back("E3D High Flow"); def->mode = comDevelop; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -10070,6 +10103,13 @@ bool DynamicPrintConfig::is_using_different_extruders() return ret; } +bool DynamicPrintConfig::has_multi_variant_filament() const +{ + auto variants = dynamic_cast(this->option("filament_extruder_variant")); + auto diameters = dynamic_cast(this->option("filament_diameter")); + return variants && diameters && variants->size() > diameters->size(); +} + bool DynamicPrintConfig::support_different_extruders(int& extruder_count) const { std::set variant_set; @@ -11070,6 +11110,11 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen return; } std::vector filament_maps = opt_filament_map->values; + auto opt_ids = id_name.empty()? nullptr: dynamic_cast(this->option(id_name)); + // Orca: a map shorter than the filament count must not drop the filaments past its end; + // they take the first extruder. + if (opt_ids && !opt_ids->values.empty()) + filament_maps.resize(std::max(filament_maps.size(), *std::max_element(opt_ids->values.begin(), opt_ids->values.end())), 1); size_t filament_count = filament_maps.size(); //apply process settings auto opt_extruder_type = dynamic_cast(printer_config.option("extruder_type")); @@ -11088,7 +11133,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen // indexed out of bounds. if (opt_filament_volume_maps && opt_filament_volume_maps->values.size() == filament_count) filament_volume_maps = opt_filament_volume_maps->values; - auto opt_ids = id_name.empty()? nullptr: dynamic_cast(this->option(id_name)); std::vector variant_index; variant_index.resize(filament_count, -1); @@ -11105,9 +11149,10 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen //variant index variant_index[f_index] = get_index_for_extruder(f_index+1, id_name, extruder_type, nozzle_volume_type, variant_name); if (variant_index[f_index] < 0) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%") + // Orca: a filament need not define every extruder variant (a Direct Drive filament on a + // Bowden printer), so this is not an invalid state: the filament's first variant is used. + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%") %__LINE__ %s_keys_names_ExtruderType[extruder_type] % s_keys_names_NozzleVolumeType[nozzle_volume_type] % (f_index+1) %filament_maps[f_index]; - assert(false); //for some updates happens in a invalid state(caused by popup window) //we need to avoid crash variant_index[f_index] = 0; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 679811d699..4c5e2e8f5f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -30,6 +30,8 @@ namespace Slic3r { +class DynamicPrintConfig; + enum GCodeFlavor : unsigned char { gcfMarlinLegacy, gcfKlipper, @@ -519,8 +521,10 @@ enum NozzleVolumeType { // with more than one sub-nozzle (extruder_max_nozzle_count > 1); matched as Standard for // preset lookup and never emitted in profile variant strings nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only + // 4 is reserved: E3D High Flow is 5 in BambuStudio's slice_info and device numbering. + nvtE3DHighFlow = 5, // physical variant, E3D high-flow hotend on 0.4/0.6 nozzles // Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable. - nvtMaxNozzleVolumeType = nvtTPUHighFlow + nvtMaxNozzleVolumeType = nvtE3DHighFlow }; enum FilamentMapMode { @@ -558,11 +562,19 @@ static std::set get_valid_nozzle_volume_type() { // Hybrid is not a physical nozzle variant: presets never define it, so it must not // produce a variant string. if (t == nvtHybrid) continue; + // Skip the reserved gap between nvtTPUHighFlow (3) and nvtE3DHighFlow (5). + if (i > nvtTPUHighFlow && i < nvtE3DHighFlow) continue; type.insert(t); } return type; } +// The nozzle volume types the given extruder physically provides, as declared by the printer +// profile's extruder_variant_list. An empty set means the profile could not be read and must be +// treated as "unknown", not as "none". nvtHybrid is never reported: it describes an extruder +// holding a mix of nozzles, not a nozzle the profile can offer. +extern std::set get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id); + std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type); static std::string bed_type_to_gcode_string(const BedType type) @@ -699,8 +711,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS -class DynamicPrintConfig; - // Defines each and every configuration option of Slic3r, including the properties of the GUI dialogs. // Does not store the actual values, but defines default values. class PrintConfigDef : public ConfigDef @@ -821,6 +831,10 @@ public: //BBS bool is_using_different_extruders(); bool support_different_extruders(int& extruder_count) const; + // Whether any filament defines more than one variant (filament_extruder_variant longer than + // filament_diameter). Its variants then have to be resolved even on a printer with a single + // extruder variant, which picks the filament's variant of the same variant string. + bool has_multi_variant_filament() const; // Counts the config slots of a printer: one per (extruder x nozzle volume type) as described by // extruder_nozzle_stats, or simply one per extruder when the stats are absent/mismatched. // Fills nozzle_volume_types with each extruder's volume types in ascending enum order. diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 7055f441ac..9b85e80c10 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -818,12 +818,14 @@ NewCalibrationHistoryDialog::NewCalibrationHistoryDialog(wxWindow *parent, const if (support_nozzle_volume(curr_obj)) { Label *nozzle_name_title = new Label(top_panel, _L("Nozzle")); m_comboBox_nozzle_type = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY); - wxArrayString nozzle_items; + // The labels are in display order, not enum order (E3D High Flow is 5 but the fifth label), so each + // item carries its NozzleVolumeType as client data. const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { nozzle_items.push_back(_L(item)); } + for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i) + m_comboBox_nozzle_type->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap, + (void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i])); } - m_comboBox_nozzle_type->Set(nozzle_items); m_comboBox_nozzle_type->SetSelection(-1); flex_sizer->Add(nozzle_name_title); flex_sizer->Add(m_comboBox_nozzle_type); @@ -891,7 +893,7 @@ int NewCalibrationHistoryDialog::get_nozzle_combo_id_code() const void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event) { - // Mirror the picked hotend's flow onto the (Orca index-based) nozzle-type combo. + // Mirror the picked hotend's flow onto the nozzle-type combo. if (!curr_obj || !m_comboBox_nozzle_id || !m_comboBox_nozzle_type || !curr_obj->GetNozzleSystem()) return; @@ -902,7 +904,9 @@ void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event) DevNozzle nozzle = curr_obj->GetNozzleSystem()->GetNozzleByPosId(pos); if (nozzle.IsNormal()) { NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.GetNozzleFlowType()); - m_comboBox_nozzle_type->SetSelection(static_cast(volume_type)); + for (unsigned int i = 0; i < m_comboBox_nozzle_type->GetCount(); ++i) + if (NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(i))) == volume_type) + m_comboBox_nozzle_type->SetSelection(i); } } @@ -949,7 +953,7 @@ void NewCalibrationHistoryDialog::on_ok(wxCommandEvent &event) msg_dlg.ShowModal(); return; } - m_new_result.nozzle_volume_type = NozzleVolumeType(m_comboBox_nozzle_type->GetSelection()); + m_new_result.nozzle_volume_type = NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(m_comboBox_nozzle_type->GetSelection()))); } auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()]; diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index 7a83d39dd3..f4160885cb 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -803,6 +803,29 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent) #define NOZZLE_LIST_DEFAULT 1 float nozzle_diameter_list[NOZZLE_LIST_COUNT] = {0.2, 0.4, 0.6, 0.8 }; +// The nozzle_volume_type labels are in display order, not enum order (E3D High Flow is 5 but the fifth +// label), so each item carries its NozzleVolumeType as client data and is selected by that value. +static void select_nozzle_volume(ComboBox *combo, NozzleVolumeType volume_type) +{ + for (unsigned int i = 0; i < combo->GetCount(); ++i) + if (NozzleVolumeType(intptr_t(combo->GetClientData(i))) == volume_type) { + combo->SetSelection(i); + return; + } +} + +static void fill_nozzle_volume_combo(ComboBox *combo) +{ + combo->Clear(); + const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); + if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { + for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i) + combo->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap, + (void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i])); + } + select_nozzle_volume(combo, NozzleVolumeType::nvtStandard); +} + void CalibrationPresetPage::init_selection_values() { // init nozzle diameter and nozzle volume @@ -813,15 +836,7 @@ void CalibrationPresetPage::init_selection_values() } m_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT); - m_comboBox_nozzle_volume->Clear(); - const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); - if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { - m_comboBox_nozzle_volume->AppendString(_L(item)); - } - } - - m_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard)); + fill_nozzle_volume_combo(m_comboBox_nozzle_volume); } Preset* cur_printer_preset = get_printer_preset(curr_obj, 0.4); @@ -866,15 +881,7 @@ void CalibrationPresetPage::init_selection_values() } m_left_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT); - m_left_comboBox_nozzle_volume->Clear(); - const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); - if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { - m_left_comboBox_nozzle_volume->AppendString(_L(item)); - } - } - - m_left_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard)); + fill_nozzle_volume_combo(m_left_comboBox_nozzle_volume); } // right @@ -885,15 +892,7 @@ void CalibrationPresetPage::init_selection_values() } m_right_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT); - m_right_comboBox_nozzle_volume->Clear(); - const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); - if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { - m_right_comboBox_nozzle_volume->AppendString(_L(item)); - } - } - - m_right_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard)); + fill_nozzle_volume_combo(m_right_comboBox_nozzle_volume); } } @@ -980,13 +979,13 @@ NozzleVolumeType CalibrationPresetPage::get_nozzle_volume_type(int extruder_id) if (curr_obj) { if (curr_obj->is_multi_extruders()) { if (extruder_id == LEFT_EXTRUDER_ID) { - return NozzleVolumeType(m_left_comboBox_nozzle_volume->GetSelection()); + return NozzleVolumeType(intptr_t(m_left_comboBox_nozzle_volume->GetClientData(m_left_comboBox_nozzle_volume->GetSelection()))); } else if (extruder_id == RIGHT_EXTRUDER_ID) { - return NozzleVolumeType(m_right_comboBox_nozzle_volume->GetSelection()); + return NozzleVolumeType(intptr_t(m_right_comboBox_nozzle_volume->GetClientData(m_right_comboBox_nozzle_volume->GetSelection()))); } } else - return NozzleVolumeType(m_comboBox_nozzle_volume->GetSelection()); + return NozzleVolumeType(intptr_t(m_comboBox_nozzle_volume->GetClientData(m_comboBox_nozzle_volume->GetSelection()))); } return NozzleVolumeType::nvtStandard; } @@ -2139,7 +2138,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) } if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) { - m_left_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)))); + select_nozzle_volume(m_left_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))); } else { m_left_comboBox_nozzle_volume->SetSelection(0); } @@ -2159,7 +2158,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) } if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) { - m_right_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)))); + select_nozzle_volume(m_right_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))); } else { m_right_comboBox_nozzle_volume->SetSelection(0); } @@ -2197,7 +2196,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) else { if ((obj->GetExtderSystem()->GetTotalExtderCount() > 0) && (obj->GetExtderSystem()->GetNozzleFlowType(0) != NozzleFlowType::NONE_FLOWTYPE)) { - m_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)))); + select_nozzle_volume(m_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0))); } else { m_comboBox_nozzle_volume->SetSelection(0); } diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.cpp b/src/slic3r/GUI/CalibrationWizardSavePage.cpp index 427f022d1c..fdff760078 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.cpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.cpp @@ -732,6 +732,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< case NozzleVolumeType::nvtStandard: nozzle_id_str += _L("Standard Flow"); break; case NozzleVolumeType::nvtHighFlow: nozzle_id_str += _L("High Flow"); break; case NozzleVolumeType::nvtTPUHighFlow: nozzle_id_str += _L("TPU High Flow"); break; + case NozzleVolumeType::nvtE3DHighFlow: nozzle_id_str += _L("E3D High Flow"); break; default: break; } nozzle_id_value->SetLabel(nozzle_id_str); diff --git a/src/slic3r/GUI/DeviceCore/DevCalib.cpp b/src/slic3r/GUI/DeviceCore/DevCalib.cpp index ddf71e0c62..6385fa77c6 100644 --- a/src/slic3r/GUI/DeviceCore/DevCalib.cpp +++ b/src/slic3r/GUI/DeviceCore/DevCalib.cpp @@ -36,7 +36,8 @@ static NozzleVolumeType convert_to_nozzle_type(const std::string &str) return NozzleVolumeType::nvtHighFlow; else if (str[1] == 'U') return NozzleVolumeType::nvtTPUHighFlow; - // Orca: no nvtE3DHighFlow in Orca's NozzleVolumeType; map 'B' to Standard + else if (str[1] == 'B') + return NozzleVolumeType::nvtE3DHighFlow; else return NozzleVolumeType::nvtStandard; } diff --git a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp index 578149b9d3..9f1b5311c2 100644 --- a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp @@ -13,7 +13,7 @@ namespace Slic3r { -// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow) ---------------------- +// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow) ------ // Device-reported U_FLOW nozzles map to nvtTPUHighFlow so a synced TPU-HF rack activates the H2C // change_filament_gcode TPU-kit branch. nvtHybrid is a slicer-only sentinel with no device // representation, so ToNozzleFlowType(nvtHybrid) falls through to NONE_FLOWTYPE. @@ -24,6 +24,7 @@ NozzleFlowType DevNozzle::ToNozzleFlowType(const NozzleVolumeType& type) case NozzleVolumeType::nvtStandard: return NozzleFlowType::S_FLOW; case NozzleVolumeType::nvtHighFlow: return NozzleFlowType::H_FLOW; case NozzleVolumeType::nvtTPUHighFlow: return NozzleFlowType::U_FLOW; + case NozzleVolumeType::nvtE3DHighFlow: return NozzleFlowType::E_FLOW; default: return NozzleFlowType::NONE_FLOWTYPE; } } @@ -34,6 +35,7 @@ NozzleVolumeType DevNozzle::ToNozzleVolumeType(const NozzleFlowType& type) case NozzleFlowType::S_FLOW: return NozzleVolumeType::nvtStandard; case NozzleFlowType::H_FLOW: return NozzleVolumeType::nvtHighFlow; case NozzleFlowType::U_FLOW: return NozzleVolumeType::nvtTPUHighFlow; + case NozzleFlowType::E_FLOW: return NozzleVolumeType::nvtE3DHighFlow; default: { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "nozzle flow type None convert to nozzle volume type Standard"; return NozzleVolumeType::nvtStandard; @@ -47,6 +49,7 @@ wxString DevNozzle::GetNozzleFlowTypeStr(NozzleFlowType type) case NozzleFlowType::H_FLOW: return _L("High Flow"); case NozzleFlowType::S_FLOW: return _L("Standard"); case NozzleFlowType::U_FLOW: return _L("TPU High Flow"); + case NozzleFlowType::E_FLOW: return _L("E3D High Flow"); default: break; } @@ -61,6 +64,7 @@ std::string DevNozzle::GetNozzleFlowTypeString(NozzleFlowType type) case NozzleFlowType::H_FLOW: return "High Flow"; case NozzleFlowType::S_FLOW: return "Standard"; case NozzleFlowType::U_FLOW: return "TPU High Flow"; + case NozzleFlowType::E_FLOW: return "E3D High Flow"; default: return "Unknown"; } } @@ -73,6 +77,7 @@ std::string DevNozzle::ToNozzleFlowString(const NozzleFlowType& type) case NozzleFlowType::S_FLOW: return "Standard"; case NozzleFlowType::H_FLOW: return "High Flow"; case NozzleFlowType::U_FLOW: return "TPU High Flow"; + case NozzleFlowType::E_FLOW: return "E3D High Flow"; default: return std::string(); } } @@ -408,6 +413,7 @@ static unordered_map _str2_nozzle_flow_type = { {"X", NozzleFlowType::S_FLOW}, {"E", NozzleFlowType::H_FLOW}, // E3D high-flow {"U", NozzleFlowType::U_FLOW}, // TPU 1.75 high-flow -> nvtTPUHighFlow + {"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow }; static unordered_map _str2_nozzle_type = { diff --git a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h index 8603baeaaf..168cb94116 100644 --- a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h @@ -12,8 +12,9 @@ // Device flow-type mapping note (nozzle rack): the device U_FLOW value maps to nvtTPUHighFlow // (TPU High Flow), so a device-synced TPU-HF rack resolves to nvtTPUHighFlow and the H2C -// change_filament_gcode TPU-kit branch can activate. nvtHybrid is a slicer-only sentinel with no -// device representation, so it stays out of these device flow-type conversions. +// change_filament_gcode TPU-kit branch can activate. E_FLOW maps to nvtE3DHighFlow. nvtHybrid is a +// slicer-only sentinel with no device representation, so it stays out of these device flow-type +// conversions. // // GetExtruderNozzleInfo (and its ExtruderNozzleInfos aggregate) is intentionally omitted here; // the SelectMachine slicing-vs-installed nozzle comparison collects its per-extruder NozzleDef @@ -36,13 +37,13 @@ namespace Slic3r float m_diameter = 0.0f;// unknown until reported by the printer public: - // flow/volume conversions (Standard / High Flow / TPU High Flow) + // flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow) static NozzleFlowType ToNozzleFlowType(const NozzleVolumeType& type); static NozzleVolumeType ToNozzleVolumeType(const NozzleFlowType& type); static wxString GetNozzleFlowTypeStr(NozzleFlowType type); - static std::string GetNozzleFlowTypeString(NozzleFlowType type);// untranslated literal ("High Flow"/"Standard"/"TPU High Flow") — the filament blacklist JSON matches these raw strings, so it must NOT use the translated GetNozzleFlowTypeStr (would break non-English locales) - static std::string ToNozzleFlowString(const NozzleFlowType& type);// untranslated "Standard"/"High Flow"/"TPU High Flow" ("" for none) — the raw literal serialized into the get_auto_nozzle_mapping payload + static std::string GetNozzleFlowTypeString(NozzleFlowType type);// untranslated literal ("High Flow"/"Standard"/"TPU High Flow"/"E3D High Flow") — the filament blacklist JSON matches these raw strings, so it must NOT use the translated GetNozzleFlowTypeStr (would break non-English locales) + static std::string ToNozzleFlowString(const NozzleFlowType& type);// untranslated "Standard"/"High Flow"/"TPU High Flow"/"E3D High Flow" ("" for none) — the raw literal serialized into the get_auto_nozzle_mapping payload static wxString GetNozzleTypeStr(NozzleType type); public: diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 6bd690e9a6..6c09e05f48 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -347,6 +347,10 @@ static wxString _generate_nozzle_id(NozzleVolumeType nozzle_type, const std::str nozzle_id += "H"; break; } + case NozzleVolumeType::nvtE3DHighFlow: { + nozzle_id += "B"; + break; + } default: nozzle_id += "H"; break; @@ -368,6 +372,8 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str) res = NozzleVolumeType::nvtStandard; else if (str[1] == 'H') res = NozzleVolumeType::nvtHighFlow; + else if (str[1] == 'B') + res = NozzleVolumeType::nvtE3DHighFlow; return res; } diff --git a/src/slic3r/GUI/DragDropPanel.cpp b/src/slic3r/GUI/DragDropPanel.cpp index 0c1371c307..41c31d927b 100644 --- a/src/slic3r/GUI/DragDropPanel.cpp +++ b/src/slic3r/GUI/DragDropPanel.cpp @@ -566,7 +566,8 @@ std::vector SeparatedDragDropPanel::GetHighFlowFilaments() const const int right_eid = 1; if (nozzle_volumes->values.size() > right_eid) { int volume_type = nozzle_volumes->values[right_eid]; - if (volume_type == static_cast(NozzleVolumeType::nvtHighFlow)) { + if (volume_type == static_cast(NozzleVolumeType::nvtHighFlow) || + volume_type == static_cast(NozzleVolumeType::nvtE3DHighFlow)) { return m_unified_panel->GetAllFilaments(); } } diff --git a/src/slic3r/GUI/FilamentMapPanel.cpp b/src/slic3r/GUI/FilamentMapPanel.cpp index 0f3cc7217d..bd7d42333b 100644 --- a/src/slic3r/GUI/FilamentMapPanel.cpp +++ b/src/slic3r/GUI/FilamentMapPanel.cpp @@ -185,6 +185,12 @@ std::vector FilamentMapManualPanel::GetFilamentVolumeMaps() const auto preset_bundle = wxGetApp().preset_bundle; auto proj_config = preset_bundle->project_config; auto nozzle_volume_values = proj_config.option("nozzle_volume_type")->values; + // The high-flow panel of the right extruder also holds an E3D High Flow nozzle; keep that type + // instead of writing plain High Flow, which the extruder does not have. + const int right_extruder_id = 1; + const bool right_e3d_high_flow = nozzle_volume_values.size() > right_extruder_id && + nozzle_volume_values[right_extruder_id] == static_cast(NozzleVolumeType::nvtE3DHighFlow); + const int right_high_flow_volume = static_cast(right_e3d_high_flow ? NozzleVolumeType::nvtE3DHighFlow : NozzleVolumeType::nvtHighFlow); for (int i = 0; i < (int) volume_map.size(); ++i) { int filament_id = i + 1; @@ -199,7 +205,7 @@ std::vector FilamentMapManualPanel::GetFilamentVolumeMaps() const } } else if (std::find(right_high_flow_filaments.begin(), right_high_flow_filaments.end(), filament_id) != right_high_flow_filaments.end()) { - volume_map[i] = static_cast(NozzleVolumeType::nvtHighFlow); + volume_map[i] = right_high_flow_volume; } else if (std::find(right_standard_filaments.begin(), right_standard_filaments.end(), filament_id) != right_standard_filaments.end()) { volume_map[i] = static_cast(NozzleVolumeType::nvtStandard); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 951739e213..3a61a95dfd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -605,15 +605,50 @@ private: bool m_enabled{false}; }; +// The nozzle rows are set in the small body font their "T1" prefix and diameter readout use, so the +// combos of a row drop from the Body_14 they are created with to match, instead of towering over +// the labels next to them. +static void use_nozzle_row_font(ComboBox *combo) +{ + // A read-only combo draws its value with the font of the TextInput it is built on, and its + // dropped list carries a font of its own. + combo->SetFont(Label::Body_10); + combo->GetDropDown().SetFont(Label::Body_10); + + // A row is as tall as the tags of the variant switch: a line of Body_10 text plus the 3 points of + // padding they put above and below it. TextInput::messureSize() instead sizes a combo as its + // (hidden) text control plus 8, and on macOS that control's best height is a fixed 16 points + // whatever font it carries, so the box would keep the height of the Body_14 it was created with. + // Handing the control the height the row asks for, less those 8 points, keeps messureSize()'s own + // arithmetic landing on the small box however often it re-measures. + const int row_height = combo->GetTextExtent("0.4 mm").y + 2 * combo->FromDIP(3); + auto *text_ctrl = combo->GetTextCtrl(); + text_ctrl->SetFont(Label::Body_10); + text_ctrl->SetInitialSize(wxSize(text_ctrl->GetBestSize().x, row_height - 8)); + + combo->SetMinSize(wxSize(combo->GetMinSize().x, row_height)); + combo->SetSize(wxSize(combo->GetSize().x, row_height)); +} + struct ExtruderGroup : StaticBox { + // One nozzle row: the tool prefix ("T1"…"Tn", shown only when the group lists several + // extruders) plus the nozzle readout and flow combo of a single extruder. Rows are appended on + // demand and never destroyed, so an event handler may capture its row index for the lifetime + // of the group. + struct NozzleRow + { + Label * prefix = nullptr; + Label * diameter_label = nullptr; // read-only nozzle diameter of this extruder + ComboBox * flow = nullptr; + }; + ExtruderGroup(wxWindow * parent, int index, wxString const &title); wxBoxSizer * sizer = nullptr; HoverLabel * hover_label = nullptr; wxStaticText* ams_label{nullptr}; ScalableButton * btn_edit = nullptr; ComboBox * combo_diameter = nullptr; - ComboBox * combo_flow = nullptr; AMSPreview * ams[4] = {nullptr}; wxStaticText *ams_not_installed_msg{nullptr}; ScalableButton * btn_up{nullptr}; @@ -627,6 +662,23 @@ struct ExtruderGroup : StaticBox std::vector ams_1; wxString diameter; + std::vector rows; // rows[0] always exists, one row per extruder when multi + size_t nozzle_row_count = 0; // rows currently shown + wxBoxSizer * row_columns[2] = {}; // row i is dealt into column i % 2 + int group_index = -1; // ctor index, the extruder a single-row group edits + + // Show `count` nozzle rows, one per extruder, each prefixed with T when count > 1. + void SetNozzleRowCount(size_t count); + size_t NozzleRowCount() const { return nozzle_row_count; } + // Nozzle diameter readout of the shown rows, one value per extruder. A listing card cannot edit + // it: the diameter belongs to the machine variant the printer preset selects, not to the extruder. + void SetRowDiameters(const std::vector &diameters); + + NozzleRow create_nozzle_row(); + // Widths the prefix and diameter labels of the visible rows so that the flow combos of a column + // line up. + void update_row_widths(); + void set_ams_count(int n4, int n1) { if (n4 == ams_n4 && n1 == ams_n1) @@ -657,7 +709,12 @@ struct ExtruderGroup : StaticBox btn_up->msw_rescale(); btn_down->msw_rescale(); combo_diameter->Rescale(); - combo_flow->Rescale(); + for (const NozzleRow &row : rows) { + row.prefix->SetFont(Label::Body_10.Bold()); + row.diameter_label->SetFont(Label::Body_10); + row.flow->Rescale(); + } + update_row_widths(); for (int i = 0; i < 4; ++i) ams[i]->msw_rescale(); } @@ -668,6 +725,10 @@ struct ExtruderGroup : StaticBox hover_label->sys_color_changed(); if (btn_edit) btn_edit->SetBackgroundColour(extruder_group_chip_bg()); + for (const NozzleRow &row : rows) { + row.prefix->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row.diameter_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + } Refresh(); } }; @@ -691,11 +752,12 @@ struct Sidebar::priv ScalableButton * btn_edit_printer = nullptr; ScalableButton * btn_connect_printer = nullptr; - // Nozzle diameter + // Printer variant switch, titled "Variant": the combo lists the variants (nozzle sizes) of the + // current machine model and picking one switches the printer preset. StaticBox * panel_nozzle_dia = nullptr; Label * label_nozzle_title= nullptr; ComboBox * combo_nozzle_dia = nullptr; - Label * label_nozzle_type = nullptr; + Label * label_nozzle_type = nullptr; // read-only nozzle material, one-row layouts only // Printer - bed StaticBox * panel_printer_bed = nullptr; @@ -809,6 +871,8 @@ struct Sidebar::priv // otherwise reuses the app_config-cached option when the machine's nozzle config is unchanged. std::optional get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual); bool switch_diameter(bool single); + // Switch to the printer preset offering `diameter`. + bool switch_diameter_to(const wxString &diameter); void update_sync_status(const MachineObject* obj); // Filament Track Switch (H2-family accessory): true only when the connected printer is the @@ -918,11 +982,19 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) int extruder_count = 0; const bool has_flow_variant = cfg.support_different_extruders(extruder_count); - panel_nozzle_dia->Show(!has_flow_variant); - extruder_single_sizer->Show(has_flow_variant); + // ORCA: a non-Bambu printer with several extruders lists one nozzle row per tool; Bambu's own + // multi-extruder printers keep the left/right pair above. + const bool multi_extruder_rows = !preset_bundle.is_bbl_vendor() && extruder_count > 1; + + // The variant box switches the machine variant. The card below lists the nozzles when there + // are flow variants to choose or several extruders to show; otherwise the box says it all. + panel_nozzle_dia->Show(true); + extruder_single_sizer->Show(has_flow_variant || multi_extruder_rows); + single_extruder->SetNozzleRowCount(multi_extruder_rows ? size_t(extruder_count) : 1); } else { panel_nozzle_dia->Show(false); extruder_single_sizer->Show(false); + single_extruder->SetNozzleRowCount(1); } // ORCA ensure printer section is visible after changing printer from printer selection dialog @@ -1319,7 +1391,7 @@ public: }; ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) - : StaticBox(parent) + : StaticBox(parent), group_index(index) { SetFont(Label::Body_10); SetForegroundColour(wxColour("#CECECE")); @@ -1332,29 +1404,13 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title hover_label = new HoverLabel(this, title); hover_label->SetPosition(wxPoint(FromDIP(PRINTER_PANEL_RADIUS), 0)); // position it without putting in a sizer so it will look like title - // Nozzle + // Nozzle. Only the dual left/right cards show this combo: it is how they ask which nozzle the + // project should print with. Every other card reads the diameter out as text. auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); this->combo_diameter = combo_diameter; combo_diameter->SetToolTip(_L("Diameter")); - - // Flow - auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); - combo_flow->GetDropDown().SetUseContentWidth(true); - combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) { - auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); - NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))); - printer_tab->set_extruder_volume_type(index, volume_type); - auto plater = GUI::wxGetApp().plater(); - if (plater) { - // A new Flow type invalidates the per-filament volume choices stored on the - // plates for this extruder; rewrite them so the next apply/grouping sees the - // selected volume instead of a stale one. - plater->update_filament_volume_map(index, static_cast(volume_type)); - plater->update_machine_sync_status(); - } - }); - this->combo_flow = combo_flow; - combo_flow->SetToolTip(_L("Flow")); + combo_diameter->Show(index >= 0); + use_nozzle_row_font(combo_diameter); // AMS auto ams_panel = new wxPanel(this, wxID_ANY); @@ -1440,10 +1496,6 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title btn_down->Hide(); wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL); - wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); - - hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5)); - hsizer->Add(combo_flow , 1); vsizer->AddSpacer(FromDIP(16)); // spacing for title and control if (index < 0) { @@ -1452,14 +1504,137 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5)); vsizer->AddSpacer(FromDIP(2)); } - vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); + // Nozzle rows. The first one is the only one for a printer whose nozzles are not listed + // individually; a multi-extruder printer grows one row per tool (see SetNozzleRowCount). + // The rows are dealt into two columns, which halves the height of a long tool list; the second + // column empties itself when there is a single row, so a one-row card still spans the width. + auto rows_sizer = new wxBoxSizer(wxHORIZONTAL); + for (wxBoxSizer *&column : row_columns) { + column = new wxBoxSizer(wxVERTICAL); + rows_sizer->Add(column, 1, wxEXPAND); + } + vsizer->Add(rows_sizer, 0, wxEXPAND); + sizer = vsizer; // the floating filament-switch icon positions itself against this card SetSizer(vsizer); + SetNozzleRowCount(1); + Layout(); AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this); } +ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row() +{ + const size_t row_index = rows.size(); + NozzleRow row; + + row.prefix = new Label(this, Label::Body_10.Bold()); + row.prefix->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row.prefix->SetLabelText(wxString::Format("T%d", int(row_index) + 1)); + row.prefix->Hide(); + + row.diameter_label = new Label(this, Label::Body_10); + row.diameter_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row.diameter_label->Hide(); + + auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + combo_flow->GetDropDown().SetUseContentWidth(true); + combo_flow->Bind(wxEVT_COMBOBOX, [this, row_index, combo_flow](wxCommandEvent &evt) { + // A one-row card edits the extruder it was created for, a multi-row card one extruder per row. + const int index = nozzle_row_count > 1 ? int(row_index) : group_index; + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))); + printer_tab->set_extruder_volume_type(index, volume_type); + auto plater = GUI::wxGetApp().plater(); + if (plater) { + // A new Flow type invalidates the per-filament volume choices stored on the + // plates for this extruder; rewrite them so the next apply/grouping sees the + // selected volume instead of a stale one. + plater->update_filament_volume_map(index, static_cast(volume_type)); + plater->update_machine_sync_status(); + } + }); + combo_flow->SetToolTip(_L("Flow")); + row.flow = combo_flow; + use_nozzle_row_font(combo_flow); + + auto row_sizer = new wxBoxSizer(wxHORIZONTAL); + row_sizer->Add(row.prefix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3)); + if (row_index == 0) + row_sizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5)); + row_sizer->Add(row.diameter_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + row_sizer->Add(row.flow, 1); + // Row-major: T1 T2 on the first line, T3 T4 on the second, and so on. + row_columns[row_index % 2]->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); + + return row; +} + +void ExtruderGroup::update_row_widths() +{ + // Measured from the rows on screen rather than fixed, so no gap is reserved for a "T10" or a + // "0.25 mm" that no row carries while the flow combos of a column still line up. + wxSize prefix_width(0, -1); + wxSize diameter_width(0, -1); + for (const NozzleRow &row : rows) { + if (row.prefix->IsShown()) + prefix_width.x = std::max(prefix_width.x, row.prefix->GetTextExtent(row.prefix->GetLabelText()).x); + if (row.diameter_label->IsShown()) + diameter_width.x = std::max(diameter_width.x, row.diameter_label->GetTextExtent(row.diameter_label->GetLabelText()).x); + } + for (const NozzleRow &row : rows) { + row.prefix->SetMinSize(prefix_width); + row.diameter_label->SetMinSize(diameter_width); + } +} + +void ExtruderGroup::SetRowDiameters(const std::vector &diameters) +{ + for (size_t i = 0; i < std::min(nozzle_row_count, diameters.size()); ++i) { + // Spelled like the calibration dialogs spell a nozzle diameter, "0.4 mm". + const wxString text = from_u8(get_diameter_string(diameters[i])) + " mm"; + rows[i].diameter_label->SetLabelText(text); + rows[i].diameter_label->SetToolTip(text); + } + update_row_widths(); + Layout(); +} + +void ExtruderGroup::SetNozzleRowCount(size_t count) +{ + const size_t row_count = std::min(count, size_t(MAXIMUM_EXTRUDER_NUMBER)); + if (row_count == nozzle_row_count) + return; + nozzle_row_count = row_count; + + // Every row names its tool as soon as there is more than one, so a single-nozzle card keeps the + // plain "Nozzle" look. A row is shown by showing its widgets: a sizer item holding a sub-sizer + // reports itself as shown while any of its windows is, so hiding the sub-sizer alone would + // leave the row in the layout. + for (size_t i = 0; i < row_count; ++i) { + if (i >= rows.size()) + rows.push_back(create_nozzle_row()); + rows[i].prefix->Show(row_count > 1); + rows[i].diameter_label->Show(group_index < 0); // the dual left/right cards show combo_diameter + // The flow combo's own visibility is decided with its choices, see update_extruder_variant. + } + for (size_t i = row_count; i < rows.size(); ++i) { + rows[i].prefix->Hide(); + rows[i].diameter_label->Hide(); + rows[i].flow->Hide(); + } + + // The rows just changed: re-measure them, then let the card and its containers give it the new room. + update_row_widths(); + Layout(); + if (wxWindow *parent = GetParent()) { + parent->Layout(); + if (wxWindow *grand_parent = parent->GetParent()) + grand_parent->Layout(); + } +} + void ExtruderGroup::update_ams() { static AMSinfo info4; @@ -1588,9 +1763,19 @@ bool Sidebar::priv::switch_diameter(bool single) diameter = diameter_left; } } - + + return switch_diameter_to(diameter); +} + +bool Sidebar::priv::switch_diameter_to(const wxString &diameter) +{ // ORCA: Check if the selected diameter matches the current nozzle diameter in the config Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + // The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no + // single extruder's diameter, so the preset's own variant answers first. + if (printer_preset.config.opt_string("printer_variant") == diameter.ToStdString()) { + return true; + } auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); if (nozzle_diameter && nozzle_diameter->size() > 0) { auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]); @@ -2679,7 +2864,8 @@ Sidebar::Sidebar(Plater *parent) p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); }); - p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Nozzle"), LB_PROPAGATE_MOUSE_EVENT); + // "Variant", not "Nozzle": picking one switches the printer preset (see panel_nozzle_dia). + p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Variant"), LB_PROPAGATE_MOUSE_EVENT); p->label_nozzle_title->SetFont(Label::Body_10); p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); @@ -2688,12 +2874,15 @@ Sidebar::Sidebar(Plater *parent) p->combo_nozzle_dia->SetMinSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // requires a static value in here p->combo_nozzle_dia->SetMaxSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // using -1 with wxEXPAND has issues p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto &e) { - auto evt_combo = (*p->single_extruder).combo_diameter; - evt_combo->SetSelection(e.GetSelection()); - wxCommandEvent evt(wxEVT_COMBOBOX, evt_combo->GetId()); - evt.SetEventObject(evt_combo); - evt.SetInt(e.GetSelection()); - wxPostEvent(evt_combo, evt); + // This box is the machine-variant selector, so it switches the printer preset directly + // instead of feeding an extruder card's combo. Deferred, because switching preset + // rebuilds this very combo. + const wxString diameter = p->combo_nozzle_dia->GetValue(); + p->combo_nozzle_dia->CallAfter([this, diameter]() { + p->is_switching_diameter = true; + p->switch_diameter_to(diameter); + p->is_switching_diameter = false; + }); e.Skip(); }); // ORCA paint whole combobox on focus @@ -3673,8 +3862,16 @@ void Sidebar::update_presets(Preset::Type preset_type) auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); auto diameter = printer_preset.config.opt_string("printer_variant"); auto extruder_max_nozzle_count = printer_preset.config.option("extruder_max_nozzle_count"); - auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, int index) { - extruder.combo_flow->Clear(); + auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, size_t row, int index) { + ComboBox *combo_flow = extruder.rows[row].flow; + combo_flow->Clear(); + // A profile may leave the per-extruder lists shorter than the nozzle count (they are + // padded when the printer is loaded, but a hand-written preset need not be). + if (index >= int(extruder_variants->values.size()) || index >= int(extruders->values.size()) || + index >= int(nozzle_volumes->values.size())) { + combo_flow->Hide(); + return; + } auto type = extruders_def->enum_labels[extruders->values[index]]; int select = -1; for (size_t i = 0; i < nozzle_volumes_def->enum_labels.size(); ++i) { @@ -3684,26 +3881,36 @@ void Sidebar::update_presets(Preset::Type preset_type) if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) || (extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid)) { - if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" || + auto cur_volume_type = nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]); + // Defensive: profiles restrict E3D to 0.4 / 0.6; keep it out elsewhere. + if (cur_volume_type == NozzleVolumeType::nvtE3DHighFlow && diameter != "0.4" && diameter != "0.6") + continue; + if (cur_volume_type == NozzleVolumeType::nvtHighFlow && (diameter == "0.2" || is_skip_high_flow_printer(printer_model))) continue; - if (nozzle_volumes->values[index] == i) - select = extruder.combo_flow->GetCount(); - extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)i); + // The client data is the enum value, not the label position: E3D High Flow is 5 + // but sits at position 4, and a position would write an invalid type. + if (nozzle_volumes->values[index] == cur_volume_type) + select = combo_flow->GetCount(); + combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)(intptr_t)cur_volume_type); } } if (select == -1) - select = extruder.combo_flow->GetCount() - 1; - extruder.combo_flow->SetSelection(select); + select = combo_flow->GetCount() - 1; + combo_flow->SetSelection(select); + // No flow variant for this extruder: the diameter keeps the whole row. + combo_flow->Show(combo_flow->GetCount() > 0); }; auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { extruder.combo_diameter->Clear(); + if (extruder_index >= int(nozzle_diameter->values.size())) + return; int select = -1; // ORCA get the actual nozzle diameter from printer config auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias - if(diameters[0].empty() && !nozzle_dia.empty()){ + if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){ diameters[0] = nozzle_dia; } // Orca: Check if the actual nozzle diameter exists in the list, if not add it as a custom option @@ -3719,14 +3926,15 @@ void Sidebar::update_presets(Preset::Type preset_type) extruder.diameter = nozzle_dia; }; auto image_path = get_cur_select_bed_image(); - if (is_dual_extruder) { + const bool multi_extruder_rows = p->single_extruder->NozzleRowCount() > 1; + if (is_dual_extruder && !multi_extruder_rows) { std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder); AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder); - update_extruder_variant(*p->left_extruder, 0); - update_extruder_variant(*p->right_extruder, 1); + update_extruder_variant(*p->left_extruder, 0, 0); + update_extruder_variant(*p->right_extruder, 0, 1); //if (!p->is_switching_diameter) { update_extruder_diameter(0, *p->left_extruder); update_extruder_diameter(1, *p->right_extruder); @@ -3734,15 +3942,27 @@ void Sidebar::update_presets(Preset::Type preset_type) p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); } else { AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder); - update_extruder_variant(*p->single_extruder, 0); + for (size_t row = 0; row < p->single_extruder->NozzleRowCount(); ++row) + update_extruder_variant(*p->single_extruder, row, int(row)); //if (!p->is_switching_diameter) update_extruder_diameter(0, *p->single_extruder); + // The card reads the nozzles out, the variant box above is what switches the machine. + p->single_extruder->SetRowDiameters(nozzle_diameter->values); + if (multi_extruder_rows) { + // The hidden left/right cards keep mirroring the first extruders' diameters: the + // device sync path (a connected Bambu printer or a printer agent) still reads them. + update_extruder_diameter(0, *p->left_extruder); + update_extruder_diameter(1, *p->right_extruder); + } // ORCA sync unified nozzle combo box p->combo_nozzle_dia->Clear(); for (size_t i = 0; i < diameters.size(); ++i) p->combo_nozzle_dia->Append(diameters[i], {}); - p->combo_nozzle_dia->SetSelection((*p->single_extruder).combo_diameter->GetSelection()); + // Prefer the variant the preset names: a mixed-nozzle machine reads "0.4+0.6", which is + // no single extruder's diameter. + const int variant = p->combo_nozzle_dia->FindString(diameter); + p->combo_nozzle_dia->SetSelection(variant != wxNOT_FOUND ? variant : (*p->single_extruder).combo_diameter->GetSelection()); // ORCA update nozzle type const auto& full_config = wxGetApp().preset_bundle->full_config(); diff --git a/src/slic3r/GUI/PrintOptionsDialog.cpp b/src/slic3r/GUI/PrintOptionsDialog.cpp index d68bd24ccc..f03bcb1d29 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.cpp +++ b/src/slic3r/GUI/PrintOptionsDialog.cpp @@ -1793,6 +1793,7 @@ wxString PrinterPartsDialog::GetString(NozzleFlowType nozzle_flow_type) const { case Slic3r::S_FLOW: return _L("Standard"); case Slic3r::H_FLOW: return _L("High flow"); case Slic3r::U_FLOW: return _L("TPU High flow"); + case Slic3r::E_FLOW: return _L("E3D High Flow"); default: break; } diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index a3b83bd643..04eb2b1612 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -75,6 +75,9 @@ std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_t else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { return "tpu_high_flow"; } + else if (nozzle_volume_type == NozzleVolumeType::nvtE3DHighFlow) { + return "e3d_high_flow"; + } else if (nozzle_volume_type == NozzleVolumeType::nvtHybrid) { // to be supported return "hybrid_flow"; diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 19b1ddc568..07c75e809a 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1516,6 +1516,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err used_extruders_flow[used_extruders[i]] = "Standard"; } else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { used_extruders_flow[used_extruders[i]] = "TPU High Flow"; + } else if (nozzle_volume_type == NozzleVolumeType::nvtE3DHighFlow) { + used_extruders_flow[used_extruders[i]] = "E3D High Flow"; } else { used_extruders_flow[used_extruders[i]] = "High Flow"; } @@ -1533,6 +1535,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err flow_type_of_machine.push_back("Standard"); } else if (it->GetNozzleFlowType() == NozzleFlowType::U_FLOW) { flow_type_of_machine.push_back("TPU High Flow"); + } else if (it->GetNozzleFlowType() == NozzleFlowType::E_FLOW) { + flow_type_of_machine.push_back("E3D High Flow"); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bf348f856f..67e899c40e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -506,7 +506,7 @@ void Tab::create_preset_tab() if (dynamic_cast(this) || dynamic_cast(this)) { m_extruder_switch = new MultiSwitchButton(panel); - m_extruder_switch->SetMaxSize({em_unit(this) * 40, -1}); + m_extruder_switch->SetFitToOptions(); m_extruder_switch->Bind(wxCUSTOMEVT_MULTISWITCH_SELECTION, [this](auto &evt) { evt.Skip(); int selection = evt.GetInt(); @@ -539,7 +539,8 @@ void Tab::create_preset_tab() auto right_sizer = new wxBoxSizer(wxHORIZONTAL); m_variant_sizer->AddStretchSpacer(1); - m_variant_sizer->Add(m_extruder_switch, 0, wxALIGN_CENTER, 0); + // Orca: proportion 1 lets a narrow row squeeze the switch, which then scrolls its buttons. + m_variant_sizer->Add(m_extruder_switch, 1, wxALIGN_CENTER, 0); m_variant_sizer->Add(right_sizer, 1, wxALIGN_CENTER); right_sizer->AddStretchSpacer(1); right_sizer->Add(m_extruder_sync_box, 0, wxALIGN_CENTER | wxRIGHT, m_em_unit); @@ -8172,7 +8173,7 @@ void Tab::update_extruder_variants(int extruder_id, bool reload) // Orca: a non-Bambu dual-nozzle printer has two extruders but a single variant column, so // the nozzle switch and sync button have nothing to act on. Only enable with real variants. - if (extruder_nums == 2 && m_preset_bundle->support_different_extruders()) { + if (extruder_nums >= 2 && m_preset_bundle->support_different_extruders()) { auto options = generate_extruder_options(); m_extruder_switch->SetOptions(options); @@ -8219,6 +8220,23 @@ void Tab::update_extruder_variants(int extruder_id, bool reload) } } +// The variant switch tags are the narrowest place a volume type is named, so they abbreviate it; +// the flow combo boxes and the Printer tab's parameter labels keep the full names. +static wxString short_nozzle_volume_name(const std::string &volume_name) +{ + // Hybrid has no entry on purpose: it is never a variant string, and the switch lists its two + // sub-nozzle types as separate tags (see generate_extruder_options). + if (volume_name == "Standard") + return "SF"; + if (volume_name == "High Flow") + return "HF"; + if (volume_name == "TPU High Flow") + return "TPU HF"; + if (volume_name == "E3D High Flow") + return "E3D HF"; + return from_u8(volume_name); +} + std::vector Tab::generate_extruder_options() { std::vector options; @@ -8259,7 +8277,7 @@ std::vector Tab::generate_extruder_options() nozzle = ""; } } - options.push_back(wxString::Format(_L("%s: %s"), _L(drive), _L(nozzle))); + options.push_back(wxString::Format(_L("%s: %s"), _L(drive), short_nozzle_volume_name(nozzle))); } return options; } @@ -8273,18 +8291,22 @@ std::vector Tab::generate_extruder_options() } std::string pt = m_preset_bundle->printers.get_edited_preset().get_printer_type(m_preset_bundle); + // Orca: the main/deputy toolhead names describe a dual-nozzle printer, where extruder 0 is the + // left (deputy) and extruder 1 the right (main) nozzle. From three extruders on the tools are + // interchangeable, so name them by index instead of repeating one side. for (int i = 0; i < extruder_nums; ++i) { - int ext_id = (i == 0) ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID; - wxString extruder_name = _L(DevPrinterConfigUtil::get_toolhead_display_name( - pt, ext_id, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true)); + wxString extruder_name = extruder_nums > 2 ? wxString::Format("T%d", i + 1) : + _L(DevPrinterConfigUtil::get_toolhead_display_name( + pt, (i == 0) ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID, + ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true)); NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]); if (volume_type == NozzleVolumeType::nvtHybrid) { - options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("Standard"))); - options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("High Flow"))); + options.push_back(wxString::Format(_L("%s: %s"), extruder_name, short_nozzle_volume_name(get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard)))); + options.push_back(wxString::Format(_L("%s: %s"), extruder_name, short_nozzle_volume_name(get_nozzle_volume_type_string(NozzleVolumeType::nvtHighFlow)))); } else { - wxString volume_name = get_nozzle_volume_type_name(volume_type); - options.push_back(wxString::Format(_L("%s: %s"), extruder_name, volume_name)); + options.push_back(wxString::Format(_L("%s: %s"), extruder_name, + short_nozzle_volume_name(get_nozzle_volume_type_string(volume_type)))); } } return options; @@ -8315,7 +8337,10 @@ bool Tab::get_extruder_sync_enable_state(int extruder_id) Preset& printer_preset = m_preset_bundle->printers.get_edited_preset(); auto nozzle_volumes = m_preset_bundle->project_config.option("nozzle_volume_type"); auto extruders = printer_preset.config.option("extruder_type"); - if (nozzle_volumes->values.size() < 2 || extruders->values.size() < 2) { + // Orca: every rule below describes the two toolheads of a dual-nozzle printer as left/right. + // A printer with any other extruder count has no single counterpart to copy to, so it gets no + // sync button (a toolchanger would need a target to be chosen). + if (nozzle_volumes->values.size() != 2 || extruders->values.size() != 2) { return false; } @@ -8456,6 +8481,10 @@ void Tab::sync_excluder() ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second, stride); }; int active_index = get_current_active_extruder(); + // The button copies to the other toolhead, so it is only offered when that other one exists; + // without this the `1 - active_index` below would index an extruder that is not there. + if (!get_extruder_sync_enable_state(active_index)) + return; auto active_nozzle = get_actual_nozzle_volume_type(active_index); int from_index = get_index_for_extruder(active_index, active_nozzle); int dest_index = get_index_for_extruder(1 - active_index, active_nozzle); diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 316389d65c..42659431f0 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -70,8 +70,8 @@ int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id) // ---- ManualNozzleCountDialog ---------------------------------------------------------------------------------- -ManualNozzleCountDialog::ManualNozzleCountDialog( - wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero) +ManualNozzleCountDialog::ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int e3d_count, + int max_nozzle_count, bool force_no_zero, const std::set &supported_types) : DPIDialog(parent, wxID_ANY, _L("Set nozzle count"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) { SetBackgroundColour(*wxWHITE); @@ -93,43 +93,39 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( for (int i = 0; i <= max_nozzle_count; ++i) nozzle_choices.Add(wxString::Format("%d", i)); - // A Hybrid extruder mixes Standard and High Flow nozzles, so it gets both count choices; the concrete - // types get exactly one. - if (volume_type == nvtStandard || volume_type == nvtHybrid) { - wxBoxSizer *standard_sizer = new wxBoxSizer(wxHORIZONTAL); - auto standard_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtStandard)), wxDefaultPosition, wxSize(FromDIP(100), -1)); - standard_label->SetFont(Label::Body_14); - standard_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); - standard_sizer->Add(standard_label, 0, wxALIGN_CENTER_VERTICAL); - m_standard_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY); - std::vector::iterator iter; - for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++) - m_standard_choice->Append(*iter); - m_standard_choice->SetSelection(standard_count); - standard_sizer->Add(m_standard_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); - choice_sizer->Add(standard_sizer, 0, wxTOP | wxRIGHT, FromDIP(15)); - } - if (volume_type == nvtHighFlow || volume_type == nvtHybrid) { - wxBoxSizer *highflow_sizer = new wxBoxSizer(wxHORIZONTAL); - auto highflow_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtHighFlow)), wxDefaultPosition, wxSize(FromDIP(100), -1)); - highflow_label->SetFont(Label::Body_14); - highflow_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); - highflow_sizer->Add(highflow_label, 0, wxALIGN_CENTER_VERTICAL); - m_highflow_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY); - std::vector::iterator iter; - for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++) - m_highflow_choice->Append(*iter); - m_highflow_choice->SetSelection(highflow_count); - highflow_sizer->Add(m_highflow_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); - choice_sizer->Add(highflow_sizer, 0, wxTOP | wxRIGHT, FromDIP(15)); - } + // A Hybrid extruder mixes several nozzle volume types, so it gets a count choice for each type the + // extruder actually has; a concrete type gets exactly its own, which it supports by definition. + auto shows = [volume_type, &supported_types](NozzleVolumeType type) { + return volume_type == type || (volume_type == nvtHybrid && (supported_types.empty() || supported_types.count(type) > 0)); + }; + auto add_count_choice = [this, content, choice_sizer, &nozzle_choices](NozzleVolumeType type, int count) { + wxBoxSizer *row_sizer = new wxBoxSizer(wxHORIZONTAL); + auto label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(type)), wxDefaultPosition, wxSize(FromDIP(100), -1)); + label->SetFont(Label::Body_14); + label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL); + auto choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY); + for (const wxString &item : nozzle_choices) + choice->Append(item); + choice->SetSelection(count); + row_sizer->Add(choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + choice_sizer->Add(row_sizer, 0, wxTOP | wxRIGHT, FromDIP(15)); + return choice; + }; + if (shows(nvtStandard)) + m_standard_choice = add_count_choice(nvtStandard, standard_count); + if (shows(nvtHighFlow)) + m_highflow_choice = add_count_choice(nvtHighFlow, highflow_count); + if (shows(nvtE3DHighFlow)) + m_e3d_choice = add_count_choice(nvtE3DHighFlow, e3d_count); m_error_label = new wxStaticText(this, wxID_ANY, ""); m_error_label->SetForegroundColour(wxColour("#E14747")); m_error_label->Hide(); - auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](int standard_count, int highflow_count) { - const int total_count = standard_count + highflow_count; + auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](wxCommandEvent &e) { + // The Hybrid count is the sum over every choice on screen, whatever the extruder's own type. + const int total_count = GetNozzleCount(nvtHybrid); if (0 < total_count && total_count <= max_nozzle_count && m_error_label->IsShown()) { m_error_label->Hide(); m_confirm_btn->Enable(); @@ -145,18 +141,12 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( Layout(); Fit(); } + e.Skip(); }; - if (m_standard_choice) - m_standard_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) { - update_nozzle_error(m_standard_choice->GetSelection(), m_highflow_choice ? m_highflow_choice->GetSelection() : 0); - e.Skip(); - }); - if (m_highflow_choice) - m_highflow_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) { - update_nozzle_error(m_standard_choice ? m_standard_choice->GetSelection() : 0, m_highflow_choice->GetSelection()); - e.Skip(); - }); + for (ComboBox *choice : {m_standard_choice, m_highflow_choice, m_e3d_choice}) + if (choice) + choice->Bind(wxEVT_COMBOBOX, update_nozzle_error); content_sizer->Add(nozzle_icon, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(15)); content_sizer->Add(choice_sizer, 0, wxALIGN_CENTRE_VERTICAL); @@ -181,9 +171,10 @@ int ManualNozzleCountDialog::GetNozzleCount(NozzleVolumeType volume_type) const return m_standard_choice ? m_standard_choice->GetSelection() : 0; if (volume_type == nvtHighFlow) return m_highflow_choice ? m_highflow_choice->GetSelection() : 0; + if (volume_type == nvtE3DHighFlow) + return m_e3d_choice ? m_e3d_choice->GetSelection() : 0; if (volume_type == nvtHybrid) - return (m_standard_choice ? m_standard_choice->GetSelection() : 0) + - (m_highflow_choice ? m_highflow_choice->GetSelection() : 0); + return GetNozzleCount(nvtStandard) + GetNozzleCount(nvtHighFlow) + GetNozzleCount(nvtE3DHighFlow); return 0; } @@ -318,6 +309,7 @@ void manuallySetNozzleCount(int extruder_id) const NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id]); const int standard_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard); const int highflow_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow); + const int e3d_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtE3DHighFlow); // Require at least one nozzle for a Hybrid extruder (an empty mix is meaningless) and when the other // extruder currently has none. @@ -325,11 +317,19 @@ void manuallySetNozzleCount(int extruder_id) if (nozzle_volume_type_opt->values.size() > 1) force_no_zero |= getExtruderNozzleCountTotal(preset_bundle, 1 - extruder_id) == 0; - ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, max_nozzle_count->values[extruder_id], force_no_zero); + // The printer profile's variant list is the authority on which volume types an extruder provides. An + // empty set means the profile could not be read, and the dialog then skips filtering instead of hiding + // everything. + const std::set supported_types = + get_extruder_supported_nozzle_volume_types(preset_bundle->printers.get_edited_preset().config, extruder_id); + + ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, e3d_count, + max_nozzle_count->values[extruder_id], force_no_zero, supported_types); if (dialog.ShowModal() == wxID_OK) { if (volume_type == nvtHybrid) { setExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard, dialog.GetNozzleCount(nvtStandard), true); setExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow, dialog.GetNozzleCount(nvtHighFlow), false); + setExtruderNozzleCount(preset_bundle, extruder_id, nvtE3DHighFlow, dialog.GetNozzleCount(nvtE3DHighFlow), false); } else { setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, dialog.GetNozzleCount(volume_type), true); } diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp index 5a3e9374a7..50bd534c72 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -64,7 +65,9 @@ struct NozzleOption class ManualNozzleCountDialog : public DPIDialog { public: - ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero); + // supported_types lists the volume types the extruder physically provides; an empty set means "unknown", in which case no filtering is applied. + ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int e3d_count, int max_nozzle_count, bool force_no_zero, + const std::set &supported_types); ~ManualNozzleCountDialog() override = default; void on_dpi_changed(const wxRect &suggested_rect) override {} int GetNozzleCount(NozzleVolumeType volume_type) const; @@ -72,6 +75,7 @@ public: private: ComboBox *m_standard_choice{nullptr}; ComboBox *m_highflow_choice{nullptr}; + ComboBox *m_e3d_choice{nullptr}; Button *m_confirm_btn{nullptr}; wxStaticText *m_error_label{nullptr}; }; diff --git a/src/slic3r/GUI/Widgets/SwitchButton.cpp b/src/slic3r/GUI/Widgets/SwitchButton.cpp index c391ddd017..f9fa3405d3 100644 --- a/src/slic3r/GUI/Widgets/SwitchButton.cpp +++ b/src/slic3r/GUI/Widgets/SwitchButton.cpp @@ -4,6 +4,8 @@ #include "StaticBox.hpp" #include "../wxExtensions.hpp" + +#include #include "../GUI_App.hpp" #include "slic3r/GUI/I18N.hpp" #include "../Utils/MacDarkMode.hpp" @@ -620,19 +622,34 @@ MultiSwitchButton::MultiSwitchButton(wxWindow *parent, wxWindowID id, const wxPo std::make_pair(0x6B6B6B, (int) StateColor::NotChecked), std::make_pair(0xFFFFFE, (int) StateColor::Normal))) , m_button_radius(10.0) - , m_button_padding(10, 6) + , m_button_padding(FromDIP(wxSize(11, 3))) { SetCornerRadius(m_button_radius); SetBorderWidth(0); - sizer = new wxBoxSizer(wxHORIZONTAL); - auto *hsizer = new wxBoxSizer(wxVERTICAL); - hsizer->Add(sizer, 1, wxEXPAND); - SetSizer(hsizer); - SetMinSize(wxSize(-1, 20)); + // Orca: a switch can hold more buttons than the layout has room for (a toolchanger lists one per + // tool), so they live in a scrolled area: the caller caps the switch at its natural width and + // this scrolls horizontally instead of clipping the last buttons. + m_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxBORDER_NONE); + m_scroll->SetBackgroundColour(GetBackgroundColour()); + // The buttons are a single row, so only the horizontal bar may ever appear: a vertical one would + // eat into the row's height. + m_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_NEVER); + m_scroll->EnableScrolling(true, false); + m_scroll->SetScrollRate(FromDIP(10), 0); + sizer = new wxBoxSizer(wxHORIZONTAL); + m_scroll->SetSizer(sizer); + + auto *hsizer = new wxBoxSizer(wxVERTICAL); + hsizer->Add(m_scroll, 1, wxEXPAND); + SetSizer(hsizer); + SetMinSize(wxSize(-1, options_height())); + + Bind(wxEVT_SIZE, &MultiSwitchButton::on_size, this); Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MultiSwitchButton::button_clicked, this); - SetFont(Label::Body_12); + // The tags name a tool and its volume type only, so they stay compact. + SetFont(Label::Body_10); } MultiSwitchButton::~MultiSwitchButton() @@ -640,10 +657,88 @@ MultiSwitchButton::~MultiSwitchButton() DeleteAllOptions(); } +int MultiSwitchButton::options_height() const +{ + // With no button to measure yet, keep the placeholder height the switch starts with. + return btns.empty() ? FromDIP(20) : btns.front()->GetMinSize().y; +} + +void MultiSwitchButton::update_scroll_range() +{ + // The scrollbar range is measured against the virtual size, so it has to follow the buttons + // whenever their labels or count change. + m_scroll->InvalidateBestSize(); + m_scroll->FitInside(); + + // A scrolled window reports its min size plus a scrollbar as its best size, never the width of + // the buttons it holds, so the layout has to be given that width explicitly. It is also the + // widest this switch wants to be: a row with less room squeezes it below this and it scrolls. + const wxSize content = sizer->CalcMin(); + SetMinSize(wxSize(content.x, options_height() + scrollbar_height(content.x))); + SetMaxSize(m_fit_to_options ? wxSize(content.x, -1) : wxDefaultSize); + InvalidateBestSize(); +} + +int MultiSwitchButton::scrollbar_height(int options_width) const +{ + // The bar is drawn inside the switch, so while the buttons need more width than the row gave us + // the switch has to be taller by the bar's height, or the bar sits on top of the buttons. + const int width = GetClientSize().x; + if (width <= 0 || options_width <= width) + return 0; + + const int bar = wxSystemSettings::GetMetric(wxSYS_HSCROLL_Y, this); + return bar > 0 ? bar : 0; +} + +void MultiSwitchButton::on_size(wxSizeEvent &evt) +{ + evt.Skip(); + + // The row resized, so the buttons may now overflow it (or no longer fit in it) and the room the + // bar needs changed with that. Width does not depend on height, so this settles in one pass. + const int height = options_height() + scrollbar_height(sizer->CalcMin().x); + if (GetMinSize().y != height) { + SetMinSize(wxSize(GetMinSize().x, height)); + // The switch sits in a row of this tab, and the tab in a panel that shares its height + // with the page view, so both have to lay out again for the taller row to get its room. + if (wxWindow *tab = GetParent()) { + tab->Layout(); + if (wxWindow *panel = tab->GetParent()) + panel->Layout(); + } + } +} + +void MultiSwitchButton::scroll_option_into_view(Button *btn) +{ + const int width = m_scroll->GetClientSize().x; + const int view_x = m_scroll->GetViewStartPixels().x; + if (width <= 0) // not laid out yet: there is no view to scroll + return; + + const wxRect rect = btn->GetRect(); + const int right = rect.GetRight() + 1; + int target = view_x; + if (rect.x < view_x) + target = rect.x; + else if (right > view_x + width) + target = right - width; + + if (target == view_x) + return; + + // Scroll() counts scroll units; round up so the whole button ends up inside the view rather + // than a few pixels short of it. + int step = 1; + m_scroll->GetScrollPixelsPerUnit(&step, nullptr); + m_scroll->Scroll((target + step - 1) / step, -1); +} + int MultiSwitchButton::AppendOption(const wxString &option, void *clientData) { Button *btn = new Button(); - btn->Create(this, option, "", wxBORDER_NONE); + btn->Create(m_scroll, option, "", wxBORDER_NONE); btn->SetFont(GetFont()); btn->SetBackgroundColor(m_bg_color); btn->SetTextColor(m_text_color); @@ -654,9 +749,6 @@ int MultiSwitchButton::AppendOption(const wxString &option, void *clientData) btns.push_back(btn); sizer->Add(btn, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL); - wxSize text_size = btn->GetTextExtent(option); - btn->SetMinSize(wxSize(text_size.x + m_button_padding.x * 2 + 6, -1)); - return int(btns.size()) - 1; } @@ -666,6 +758,7 @@ void MultiSwitchButton::SetOptions(const std::vector &options) for (const auto &option : options) AppendOption(option); + update_scroll_range(); Layout(); Refresh(); } @@ -700,6 +793,8 @@ void MultiSwitchButton::SetSelection(int index) sel = index; update_button_styles(); send_selection_event(); + // The selected button may be scrolled out of sight, e.g. when the tab restores the active tool. + scroll_option_into_view(btns[sel]); Refresh(); } @@ -718,6 +813,7 @@ void MultiSwitchButton::SetOptionText(unsigned int index, const wxString &text) if (index >= btns.size()) return; btns[index]->SetLabel(text); + update_scroll_range(); } void *MultiSwitchButton::GetOptionData(unsigned int index) const @@ -769,6 +865,7 @@ void MultiSwitchButton::SetButtonPadding(const wxSize &padding) m_button_padding = padding; for (auto *btn : btns) btn->SetPaddingSize(padding); + update_scroll_range(); Layout(); Refresh(); } @@ -777,6 +874,8 @@ void MultiSwitchButton::Rescale() { for (auto *btn : btns) btn->Rescale(); + // Rescaling can change how the labels measure, and the scrollbar range follows the buttons. + update_scroll_range(); } void MultiSwitchButton::button_clicked(wxCommandEvent &event) diff --git a/src/slic3r/GUI/Widgets/SwitchButton.hpp b/src/slic3r/GUI/Widgets/SwitchButton.hpp index 6e271abfaf..decff25919 100644 --- a/src/slic3r/GUI/Widgets/SwitchButton.hpp +++ b/src/slic3r/GUI/Widgets/SwitchButton.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "Button.hpp" @@ -164,6 +165,10 @@ public: void SetButtonCornerRadius(double radius); void SetButtonPadding(const wxSize &padding); + // Keep the switch exactly as wide as the buttons need instead of letting the layout stretch it. + // A layout with less room than that still squeezes it, and it scrolls its buttons then. + void SetFitToOptions(bool fit = true) { m_fit_to_options = fit; update_scroll_range(); } + void Rescale(); protected: @@ -173,9 +178,20 @@ protected: bool send_selection_event(); private: + // Height of the single button row, measured from the buttons themselves. + int options_height() const; + void update_scroll_range(); + int scrollbar_height(int options_width) const; + void scroll_option_into_view(Button *btn); + void on_size(wxSizeEvent &evt); + std::vector