diff --git a/.claude/skills/orca-profiles/references/extruder-variants.md b/.claude/skills/orca-profiles/references/extruder-variants.md index 5059fd5c6b..633443cbab 100644 --- a/.claude/skills/orca-profiles/references/extruder-variants.md +++ b/.claude/skills/orca-profiles/references/extruder-variants.md @@ -105,11 +105,12 @@ Rules: `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 rebuilt pair, padded with their first value or cut. The `machine_max_*` limits are padded the + same way, so extruder 2 and up of a list-less printer take extruder 1's normal limit as their + silent one too. The resize skips `hotend_heating_rate` / `hotend_cooling_rate`: they keep their + width, and an extruder beyond it reads their first value. 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. diff --git a/scripts/appimage_lib_policy.sh b/scripts/appimage_lib_policy.sh index 35925227a0..81936d83c6 100755 --- a/scripts/appimage_lib_policy.sh +++ b/scripts/appimage_lib_policy.sh @@ -23,7 +23,13 @@ appimage_is_host_library() { } appimage_is_elf_file() { - file -b "$1" 2>/dev/null | grep -q '^ELF ' + # Read the four-byte ELF magic rather than asking file(1): this runs once per candidate, and an + # AppDir holds thousands of them (the bundled Python runtime alone is ~5k files, none of them + # ELF). Two processes per call made this ~14 s of a ~95 s image build on its own. + local magic + [[ -f "$1" && -r "$1" ]] || return 1 + IFS= read -r -n 4 -d '' magic < "$1" 2>/dev/null + [[ $magic == $'\177ELF' ]] } appimage_list_direct_dependencies() { diff --git a/src/dev-utils/platform/unix/build_linux_image.sh.in b/src/dev-utils/platform/unix/build_linux_image.sh.in index 873cf2e1b1..8466597d34 100755 --- a/src/dev-utils/platform/unix/build_linux_image.sh.in +++ b/src/dev-utils/platform/unix/build_linux_image.sh.in @@ -118,9 +118,13 @@ bundle_dependency_closure() { # (scripts/check_appimage_libs.sh). local -a search_dirs=("$dst_dir") - while [ ${#queue[@]} -gt 0 ]; do - target="${queue[0]}" - queue=("${queue[@]:1}") + # Walked with a read index rather than by reslicing the array: "${queue[@]:1}" rebuilds the whole + # queue on every pop, so draining it costs O(n^2). At the ~5k entries this bundle reaches that was + # ~22 s of the image build spent copying an array around. + local head=0 + while [ "$head" -lt ${#queue[@]} ]; do + target="${queue[$head]}" + head=$((head + 1)) if [ ! -e "$target" ] || ! appimage_is_elf_file "$target"; then continue diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 59a01bc5ff..cd60a8f471 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10030,6 +10030,13 @@ static void extend_extruder_variant(DynamicPrintConfig& config, const unsigned i printer_extruder_variant_opt->values.insert(printer_extruder_variant_opt->values.end(), variants_list.begin(), variants_list.end()); } } + + // 3. Size the machine limits to the rebuilt variants, padded with their first value like the other variant keys. + // They are not extruder option keys, so the resize loop in set_num_extruders skips them. + const auto &defaults = FullPrintConfig::defaults(); + for (const std::string &key : printer_options_with_variant_2) + if (auto *opt = config.option(key)) + opt->resize(config.get_parameter_size(key, num_extruders), defaults.option(key)); } void DynamicPrintConfig::set_num_extruders(unsigned int num_extruders) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 8d192cfcf0..7beb3bd9dd 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -14,6 +14,7 @@ #include "Widgets/DialogButtons.hpp" #include "Widgets/StaticLine.hpp" #include "Widgets/StateColor.hpp" +#include "Widgets/SwitchButton.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Preset.hpp" @@ -864,34 +865,25 @@ void PublishSettingsDialog::build_option_model() }; // --- Phase 1: printer per-extruder retraction settings (first, mirroring the sidebar's - // Printer group), from the printer tab's "Extruder"/"Extruder N" pages. One inner tab per - // extruder (e.g. "Left Extruder"/"Right Extruder" via Tab::translate_category), each holding - // that extruder's Retraction and Z-Hop rows with per-extruder "#N" values. + // Printer group), from the printer tab's "Extruder" page. One inner tab per extruder (named + // as on the printer tab's switch, e.g. "Left Extruder"/"Right Extruder" via + // Tab::translate_category), each holding that extruder's Retraction and Z-Hop rows with + // per-extruder "#N" values. { size_t g = section_group_for(Section::Printer); std::set printer_added; for (Tab* tab : wxGetApp().tabs_list) { - if (tab->m_type != Preset::TYPE_PRINTER) + // The page's controls edit the extruder chosen on the printer tab's switch, so its + // option list is read once per extruder. + auto* printer_tab = dynamic_cast(tab); + const Page* page = printer_tab ? printer_tab->extruder_page() : nullptr; + if (page == nullptr) continue; - for (const PageShp& page : tab->m_pages) { - if (!page->title().StartsWith("Extruder")) - continue; - // The extruder index of this page: its options are appended with the same - // "#N" opt_index (opt.second.second), so derive the tab's index from the first - // allowlisted option; skip the page when none is found (defensive). - int extruder_idx = -1; - for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { - if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop") - continue; - for (const auto& opt : optgroup->opt_map()) - if (extruder_idx < 0) - extruder_idx = opt.second.second; - if (extruder_idx >= 0) - break; - } - if (extruder_idx < 0) - continue; - const wxString page_title = Tab::translate_category(page->title(), tab->m_type); + const size_t extruders_count = printer_tab->m_extruders_count; + for (size_t extruder_idx = 0; extruder_idx < extruders_count; ++extruder_idx) { + const wxString page_title = Tab::translate_category(extruders_count > 1 ? wxString::Format("Extruder %d", int(extruder_idx + 1)) : wxString("Extruder"), tab->m_type); + // Retraction and Z-Hop values are stored per variant column, not per extruder. + const int variant_index = printer_tab->extruder_variant_index(int(extruder_idx)); for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { // Allowlist on the untranslated optgroup title; the "Retraction when // switching material" group is intentionally skipped. @@ -899,17 +891,17 @@ void PublishSettingsDialog::build_option_model() continue; const wxString subcategory = _(optgroup->title); for (const auto& opt : optgroup->opt_map()) { - const std::string& opt_id = opt.first; const std::string& pure_key = opt.second.first; // Rows are keyed by the full per-extruder "#N" opt_id so each extruder // tab publishes its own value; GetPublishedKeys() emits the checked rows // as-is. + const std::string opt_id = pure_key + "#" + std::to_string(variant_index); if (!printer_added.insert(opt_id).second) continue; wxString label, value, unit; if (!option_text(opt_id, pure_key, label, value, unit)) continue; - size_t cat_index = category_index_for(page_title, Section::Printer, g, size_t(extruder_idx)); + size_t cat_index = category_index_for(page_title, Section::Printer, g, extruder_idx); size_t sub_index = subcategory_index_for(cat_index, subcategory, optgroup->icon); add_row_ui(opt_id, label, value, unit, cat_index, sub_index); } @@ -1098,6 +1090,10 @@ void PublishSettingsDialog::build_option_model() for (SectionGroup& section : m_sections) if (!section.categories.empty()) section.tabs->SelectItem(0); + // Orca: the Printer section shows its extruders on the same switch as the printer tab's Extruder page. + for (size_t s = 0; s < m_sections.size(); ++s) + if (m_sections[s].kind == Section::Printer && m_sections[s].categories.size() > 1) + setup_variant_switch(s); if (!m_sections.empty()) { m_outer_tabs->SelectItem(0); show_outer_page(0); @@ -1630,6 +1626,8 @@ void PublishSettingsDialog::show_inner_page(size_t section_index, int inner_inde section.selected_mixed = -1; } section.selected_inner = inner_index; + if (section.variant_switch != nullptr) + section.variant_switch->SetSelection(inner_index); // fires its event, which ignores the shown page Category& category = m_categories[section.categories[inner_index]]; category.page->Show(); category.scroll->FitInside(); @@ -1639,6 +1637,32 @@ void PublishSettingsDialog::show_inner_page(size_t section_index, int inner_inde section.page_host_sizer->Layout(); } +void PublishSettingsDialog::setup_variant_switch(size_t section_index) +{ + SectionGroup& section = m_sections[section_index]; + std::vector titles; + for (size_t category : section.categories) + titles.push_back(m_categories[category].title); + + section.variant_switch = new MultiSwitchButton(section.page); + section.variant_switch->SetFitToOptions(); + section.variant_switch->SetOptions(titles); + section.variant_switch->SetSelection(section.selected_inner); + section.variant_switch->Bind(wxCUSTOMEVT_MULTISWITCH_SELECTION, [this, section_index](wxCommandEvent& evt) { + evt.Skip(); + // The hidden tab strip stays the selection model; its event shows the page. + SectionGroup& sec = m_sections[section_index]; + if (evt.GetInt() != sec.selected_inner) + sec.tabs->SelectItem(evt.GetInt()); + }); + + // The switch takes the place of the tab strip, centered like on the printer tab. + wxSizer* page_sizer = section.page->GetSizer(); + page_sizer->Insert(1, section.variant_switch, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4)); + section.tabs->Hide(); + section.page->Layout(); +} + void PublishSettingsDialog::show_mixed_page(size_t section_index, int mixed_index) { if (section_index >= m_sections.size()) @@ -2184,6 +2208,13 @@ void PublishSettingsDialog::refresh_tab_indicators() for (size_t i = 0; i < section.categories.size(); ++i) { const bool on = category_has_selection(m_categories[section.categories[i]]); section.tabs->SetItemIndicator(static_cast(i), on); + // The switch has no indicator dot; mark its option text instead. + if (section.variant_switch != nullptr) { + const wxString& title = m_categories[section.categories[i]].title; + const wxString text = on ? title + wxString(" ") + wxString(wxUniChar(0x2022)) : title; + if (section.variant_switch->GetOptionText(static_cast(i)) != text) + section.variant_switch->SetOptionText(static_cast(i), text); + } any = any || on; } if (section.mixed_tabs != nullptr) @@ -2248,6 +2279,8 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) section.tabs->Rescale(); if (section.mixed_tabs != nullptr) section.mixed_tabs->Rescale(); + if (section.variant_switch != nullptr) + section.variant_switch->Rescale(); } // Refresh the per-row Color chips at the new DPI (they carry the slot number too). diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index b7f537b4a5..5e3ef48617 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -37,6 +37,7 @@ namespace Slic3r { class DynamicPrintConfig; } class TextInput; class StaticLine; class CheckBox; +class MultiSwitchButton; namespace Slic3r { namespace GUI { @@ -195,6 +196,10 @@ private: ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change wxPanel* page{nullptr}; TabCtrl* tabs{nullptr}; + // Printer section with several extruders: the extruder switch shown instead of `tabs`, the + // same MultiSwitchButton (and option names) as the printer tab's Extruder page. One option + // per entry of `categories`, named by its title; `tabs` stays as the hidden selection model. + MultiSwitchButton* variant_switch{nullptr}; // Second tab strip, below the main one, listing only the mixed-color filament slots. // Present on the Material section only (null elsewhere). TabCtrl* mixed_tabs{nullptr}; @@ -277,6 +282,8 @@ private: bool row_is_visible(const Row& row) const; void apply_visibility(); void bind_tab_events(); + // Replaces a section's inner tab strip with a variant switch (see SectionGroup::variant_switch). + void setup_variant_switch(size_t section_index); TabCtrl* m_outer_tabs{nullptr}; wxPanel* m_outer_host{nullptr}; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 143025399f..5e5761c6fe 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -811,6 +811,12 @@ wxString Tab::translate_category(const wxString& title, Preset::Type preset_type } return _("Extruder") + title.SubString(8, title.Last()); } + // Orca: one "Extruder" page serves all extruders; name it "Extruders" when there are several. + if (preset_type == Preset::TYPE_PRINTER && title == "Extruder") { + auto preset = wxGetApp().preset_bundle; + if (preset && preset->get_printer_extruder_count() > 1) + return _("Extruders"); + } return _(title); } @@ -1153,6 +1159,38 @@ std::string Tab::options_list_storage_key(const std::string& opt_key) const return (serialized || is_plugin_field) ? opt_key : opt_key + "#0"; } +// Orca: deep_diff() flags every vector entry at or past the reference vector's length as changed, +// whatever its value (e.g. the values of an extruder added by raising the extruder count). A vector +// grows by copying its first entry (ConfigOptionVector::resize), so such an entry only counts as +// changed when it differs from the reference's first entry, as before Orca's deep_diff() change. +// The change of the count itself shows on "extruders_count". +static void drop_unchanged_added_entries(std::vector &options, const DynamicPrintConfig ¤t, const Preset *reference) +{ + if (reference == nullptr) + return; + // deep_diff() lists a key's entries one after another, so serialize each key's vectors once. + std::string serialized_key; + std::vector cur_values; + std::string ref_first; + options.erase(std::remove_if(options.begin(), options.end(), [&](const std::string &opt) { + const auto pos = opt.find('#'); + if (pos == std::string::npos) + return false; + const std::string key = opt.substr(0, pos); + const size_t idx = size_t(std::atoi(opt.c_str() + pos + 1)); + auto ref = dynamic_cast(reference->config.option(key)); + auto cur = dynamic_cast(current.option(key)); + if (ref == nullptr || cur == nullptr || idx < ref->size() || ref->size() == 0 || idx >= cur->size()) + return false; + if (key != serialized_key) { + serialized_key = key; + cur_values = cur->vserialize(); + ref_first = ref->vserialize().front(); + } + return cur_values[idx] == ref_first; + }), options.end()); +} + void Tab::update_all_extruder_options_status() { if (!m_extruder_switch && !m_variant_combo) { @@ -1183,6 +1221,8 @@ void Tab::update_all_extruder_options_status() auto dirty_options = m_presets->current_dirty_options(true); auto nonsys_options = m_presets->current_different_from_parent_options(true); + if (m_type == Preset::TYPE_PRINTER) + update_custom_dirty(dirty_options, nonsys_options); auto filter_extruder_options = [](const std::vector& options) { std::vector filtered_options; for (const auto& opt : options) { @@ -1231,7 +1271,7 @@ void Tab::update_extruder_switch_colors() if (m_active_page) { if (m_active_page->title() == "Speed" || m_active_page->title() == "Motion ability" || m_active_page->title() == "Filament" || - m_active_page->title() == "Setting Overrides" || m_active_page->title() == "Multimaterial") { + m_active_page->title() == "Setting Overrides" || m_active_page->title() == "Multimaterial" || is_printer_extruder_page(m_active_page)) { for (auto page_ptr : m_pages) { if (page_ptr.get() == m_active_page) { pages_to_check.push_back(page_ptr); @@ -1262,8 +1302,8 @@ void Tab::update_extruder_switch_colors() void Tab::check_extruder_options_status(int index, bool &sys_extruder, bool &modified_extruder, const std::vector& pages_to_check) { int config_index = index; + int extruder_id = index; if (m_type == Preset::TYPE_PRINT || m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_MODEL) { - int extruder_id; NozzleVolumeType nozzle_type; parse_extruder_selection(index, extruder_id, nozzle_type); @@ -1296,6 +1336,10 @@ void Tab::check_extruder_options_status(int index, bool &sys_extruder, bool &mod } std::string target_opt_key = base_opt_key + "#" + std::to_string(config_index * stride); + // Orca: on the printer's Extruder page only per-variant options use the variant column, + // the others (nozzle_diameter, extruder_offset, ...) are indexed by the extruder. + if (is_printer_extruder_page(page.get()) && printer_options_with_variant_1.count(base_opt_key) == 0) + target_opt_key = base_opt_key + "#" + std::to_string(extruder_id); auto status_iter = m_all_extruder_options_status.find(target_opt_key); if (status_iter != m_all_extruder_options_status.end()) { @@ -1336,18 +1380,6 @@ void TabPrinter::init_options_list() Tab::init_options_list(); if (m_printer_technology == ptFFF) m_options_list.emplace("extruders_count", m_opt_status_value); - for (size_t i = 1; i < m_extruders_count; ++i) { - wxString target_title = wxString::Format("Extruder %d", int(i + 1)); - for (auto &page : m_pages) { - if (page->title() == target_title) { - for (auto group : page->m_optgroups) { - for (auto &opt : group->opt_map()) - m_options_list.emplace(opt.first, m_opt_status_value); - } - break; - } - } - } } void TabPrinter::msw_rescale() @@ -1437,7 +1469,7 @@ void Tab::update_changed_tree_ui() get_sys_and_mod_flags("compatible_printers", sys_page, modified_page); } } - if (page->title() == "Speed" || page->title() == "Motion ability" || page->title() == "Filament" || page->title() == "Setting Overrides" || page->title() == "Multimaterial") { + if (page->title() == "Speed" || page->title() == "Motion ability" || page->title() == "Filament" || page->title() == "Setting Overrides" || page->title() == "Multimaterial" || is_printer_extruder_page(page.get())) { auto options = generate_extruder_options(); for (size_t switch_index = 0; switch_index < options.size(); ++switch_index) { std::vector pages_to_check = { page }; @@ -1540,6 +1572,10 @@ void Tab::on_roll_back_value(const bool to_sys /*= true*/) m_postpone_update_ui = false; + // Orca: the restored config may have another extruder count than the tab shows. + if (auto printer_tab = dynamic_cast(this)) + printer_tab->sync_extruders_count(); + // When all values are rolled, then we have to update whole tab in respect to the reverted values update(); if (m_active_page) @@ -1877,6 +1913,17 @@ static wxString pad_combo_value_for_config(const DynamicPrintConfig &config) return config.opt_bool("pad_enable") ? (config.opt_bool("pad_around_object") ? _("Around object") : _("Below object")) : _("None"); } +// Rebuilds the variant switch of every tab that has one, e.g. after the extruder count or a nozzle volume type changed. +static void update_all_extruder_variants(int extruder_idx = -1) +{ + for (auto tab : wxGetApp().tabs_list) + tab->update_extruder_variants(extruder_idx); + if (auto tab = wxGetApp().plate_tab) + tab->update_extruder_variants(extruder_idx); + for (auto tab : wxGetApp().model_tabs_list) + tab->update_extruder_variants(extruder_idx); +} + void Tab::on_value_change(const std::string& opt_key, const boost::any& value) { // Orca: @@ -2319,15 +2366,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if (opt_key.find("nozzle_volume_type") != std::string::npos) { int extruder_idx = std::atoi(opt_key.substr(opt_key.find_last_of('#') + 1).c_str()); - for (auto tab : wxGetApp().tabs_list) { - tab->update_extruder_variants(extruder_idx); - } - if (auto tab = wxGetApp().plate_tab) { - tab->update_extruder_variants(extruder_idx); - } - for (auto tab : wxGetApp().model_tabs_list) { - tab->update_extruder_variants(extruder_idx); - } + update_all_extruder_variants(extruder_idx); if (wxGetApp().app_config->get("auto_calculate_flush") == "all") { wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(-1,extruder_idx); } @@ -5685,9 +5724,13 @@ if (is_marlin_flavor) size_t extruders_count = size_t(boost::any_cast(v)); wxTheApp->CallAfter([this, opt_key, value, extruders_count]() { if (opt_key == "extruders_count" || opt_key == "single_extruder_multi_material") { + const size_t old_extruders_count = m_extruders_count; extruders_count_changed(extruders_count); init_options_list(); // m_options_list should be updated before UI updating update_dirty(); + // Orca: the variant switches (here, Process in the sidebar, ...) list one option per extruder. + if (m_extruders_count != old_extruders_count) + update_all_extruder_variants(); if (opt_key == "single_extruder_multi_material") { // the single_extruder_multimaterial was added to force pages on_value_change(opt_key, value); // rebuild - let's make sure the on_value_change is not skipped @@ -5697,6 +5740,7 @@ if (is_marlin_flavor) // Orca: we use a different logic here. If SEMM is enabled, we set extruder count to 1. #if 1 extruders_count_changed(1); + update_all_extruder_variants(); #else std::vector nozzle_diameters = @@ -5768,14 +5812,13 @@ if (is_marlin_flavor) m_pages.insert(m_pages.end() - n_after_single_extruder_MM, page); } - // Orca: build missed extruder pages - for (auto extruder_idx = m_extruders_count_old; extruder_idx < m_extruders_count; ++extruder_idx) { - const wxString& page_name = (m_extruders_count > 1) ? wxString::Format("Extruder %d", int(extruder_idx + 1)) : wxString::Format("Extruder"); - - //# build page - //const wxString& page_name = wxString::Format("Extruder %d", int(extruder_idx + 1)); - auto page = add_options_page(page_name, "custom-gcode_extruder", true); // ORCA: icon only visible on placeholders - m_pages.insert(m_pages.begin() + n_before_extruders + extruder_idx, page); + // Orca: a single "Extruder" page serves all extruders. Its controls are created once for extruder 0; + // switch_excluder() re-targets them to the extruder selected on the variant switch (m_extruder_switch), + // which the printer tab shows on this page too. + if (extruder_page() == nullptr && m_extruders_count > 0) { + const size_t extruder_idx = 0; + auto page = add_options_page(L("Extruder"), "custom-gcode_extruder", true); // ORCA: icon only visible on placeholders + m_pages.insert(m_pages.begin() + n_before_extruders, page); auto optgroup = page->new_optgroup(L("Basic information"), L"param_information", -1, true); optgroup->append_single_option_line("nozzle_diameter", "printer_extruder_basic_information#nozzle-diameter", extruder_idx); @@ -5788,8 +5831,10 @@ if (is_marlin_flavor) option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_extruder_basic_information#extruder-offset-position"); - optgroup->m_on_change = [this, extruder_idx](const t_config_option_key& opt_key, boost::any value) + optgroup->m_on_change = [this](const t_config_option_key& opt_key, boost::any value) { + // The page edits the extruder selected on the variant switch. + const size_t extruder_idx = size_t(get_current_active_extruder()); bool is_SEMM = m_config->opt_bool("single_extruder_multi_material"); if (is_SEMM && m_extruders_count > 1 && boost::starts_with(opt_key, "nozzle_diameter")) { @@ -5884,22 +5929,22 @@ if (is_marlin_flavor) //optgroup->append_line(line); #endif } - // BBS. No extra extruder page for single physical extruder machine - // # remove extra pages - auto &first_extruder_title = const_cast(m_pages[n_before_extruders]->title()); - if (m_extruders_count < m_extruders_count_old) { - m_pages.erase( m_pages.begin() + n_before_extruders + m_extruders_count, - m_pages.begin() + n_before_extruders + m_extruders_count_old); - if (m_extruders_count == 1) - first_extruder_title = wxString::Format("Extruder"); - } else if (m_extruders_count_old == 1) { - first_extruder_title = wxString::Format("Extruder %d", 1); - } - auto & index = wxGetApp().sidebar().settings_index(); - for (auto &group : m_pages[n_before_extruders]->m_optgroups) { - group->set_config_category_and_type(first_extruder_title, m_type); - for (auto &opt : group->opt_map()) - index.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon); + + // The page holds only the "#0" controls, so register every extruder's options with the search + // index under "Extruder N"; a search hit selects that extruder (TabPrinter::activate_option()). + if (Page *page = extruder_page()) { + auto &index = wxGetApp().sidebar().settings_index(); + for (auto &group : page->m_optgroups) { + group->set_config_category_and_type(page->title(), m_type); + for (auto &opt : group->opt_map()) { + if (opt.second.second < 0) + continue; + for (size_t i = 0; i < m_extruders_count; ++i) { + const wxString category = m_extruders_count > 1 ? wxString::Format("Extruder %d", int(i + 1)) : wxString("Extruder"); + index.add_key(opt.second.first + "#" + std::to_string(i), m_type, group->title, category, group->icon); + } + } + } } Thaw(); @@ -6180,12 +6225,6 @@ void TabPrinter::toggle_options() return; auto nozzle_volumes = m_preset_bundle->project_config.option("nozzle_volume_type"); - auto extruders = m_config->option("extruder_type"); - auto get_index_for_extruder = - [this, &extruders](int extruder_id, int stride = 1) { - return m_config->get_index_for_extruder(extruder_id + 1, "printer_extruder_id", - ExtruderType(extruders->values[extruder_id]), get_actual_nozzle_volume_type(extruder_id), "printer_extruder_variant", stride); - }; //BBS: whether the preset is Bambu Lab printer bool is_BBL_printer = false; @@ -6260,14 +6299,13 @@ void TabPrinter::toggle_options() toggle_option("tool_change_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1); toggle_option("wait_for_temp_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1); } - wxString extruder_number; - long val = 1; - if ( m_active_page->title().IsSameAs(L("Extruder")) || - (m_active_page->title().StartsWith("Extruder ", &extruder_number) && extruder_number.ToLong(&val) && - val > 0 && (size_t)val <= m_extruders_count)) + if (m_active_page->title() == L("Extruder") && m_extruders_count > 0) { - size_t i = size_t(val - 1); - int variant_index = get_index_for_extruder(i); + // Orca: the single Extruder page edits the extruder selected on the variant switch; its + // controls carry field index 0 (i), the values are read for `extruder`. + const size_t i = 0; + const size_t extruder = std::min(size_t(get_current_active_extruder()), m_extruders_count - 1); + const int variant_index = extruder_variant_index(int(extruder)); bool have_retract_length = m_config->opt_float("retraction_length", variant_index) > 0; toggle_option("extruder_printable_area", false, i); // disable @@ -6294,7 +6332,7 @@ void TabPrinter::toggle_options() vec.resize(0); vec = {"retract_lift_above", "retract_lift_below", "retract_lift_enforce"}; for (auto el : vec) - toggle_option(el, retraction && (m_config->opt_float("z_hop", i) > 0), i); + toggle_option(el, retraction && (m_config->opt_float("z_hop", variant_index) > 0), i); // some options only apply when not using firmware retraction vec.resize(0); @@ -6347,7 +6385,7 @@ void TabPrinter::toggle_options() toggle_option("long_retractions_when_cut", !use_firmware_retraction && m_config->opt_int("enable_long_retraction_when_cut"), i); toggle_line("retraction_distances_when_cut", m_config->opt_bool("long_retractions_when_cut", variant_index), i); - toggle_option("travel_slope", m_config->opt_enum("z_hop_types", i) != ZHopType::zhtNormal, i); + toggle_option("travel_slope", m_config->opt_enum("z_hop_types", variant_index) != ZHopType::zhtNormal, i); } if (m_active_page->title() == L("Motion ability")) { @@ -6421,12 +6459,16 @@ void TabPrinter::on_value_change(const std::string& opt_key, const boost::any& v return; const int pos = opt_key.find("#"); + // Orca: fields of the single Extruder page keep index 0 but edit the selected extruder. + const int data_idx = pos > 0 ? extruder_page_data_index(opt_key) : -1; if (pos > 0) { std::string temp_str = opt_key; boost::erase_head(temp_str, pos + 1); int orig_opt_idx = static_cast(atoi(temp_str.c_str())); int opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0; + if (data_idx >= 0) + opt_idx = data_idx; std::string opt_key_pure = opt_key; boost::erase_tail(opt_key_pure, opt_key_pure.size() - pos); @@ -6461,7 +6503,12 @@ void TabPrinter::on_value_change(const std::string& opt_key, const boost::any& v } } - Tab::on_value_change(opt_key, value); + // Orca: report Extruder page changes as "key#", as the former "Extruder N" pages did, + // so per-extruder handling in Tab::on_value_change() sees the extruder that was edited. + if (data_idx >= 0) + Tab::on_value_change(opt_key.substr(0, pos) + "#" + std::to_string(get_current_active_extruder()), value); + else + Tab::on_value_change(opt_key, value); } void TabPrinter::update() @@ -7393,12 +7440,7 @@ bool Tab::tree_sel_change_delayed(wxCommandEvent& event) // update_undo_buttons(); this->OnActivate(); m_parent->set_active_tab(this); - if (m_variant_sizer) { - wxWindow *variant_ctrl = m_extruder_switch ? (wxWindow *) m_extruder_switch : m_variant_combo; - m_main_sizer->Show(m_variant_sizer, variant_ctrl->IsThisEnabled() && !m_active_page->m_opt_id_map.empty() && !m_active_page->title().StartsWith("Extruder ")); - if (m_extruder_sync) m_extruder_sync->Show(variant_ctrl->IsShown()); - GetParent()->Layout(); - } + update_variant_sizer_visibility(); m_page_view->Thaw(); return false; @@ -7409,12 +7451,7 @@ bool Tab::tree_sel_change_delayed(wxCommandEvent& event) return false; m_active_page = page; - if (m_variant_sizer) { - wxWindow *variant_ctrl = m_extruder_switch ? (wxWindow *) m_extruder_switch : m_variant_combo; - m_main_sizer->Show(m_variant_sizer, variant_ctrl->IsThisEnabled() && !m_active_page->m_opt_id_map.empty() && !m_active_page->title().StartsWith("Extruder")); - if (m_extruder_sync) m_extruder_sync->Show(variant_ctrl->IsShown()); - GetParent()->Layout(); - } + update_variant_sizer_visibility(); auto throw_if_canceled = std::function([this](){ #ifdef WIN32 @@ -8240,9 +8277,11 @@ void Tab::update_extruder_variants(int extruder_id, bool reload) m_actual_nozzle_volumes.resize(extruder_nums, NozzleVolumeType::nvtStandard); for (int i = 0; i < extruder_nums; i++) m_actual_nozzle_volumes[i] = (NozzleVolumeType)nozzle_volumes->values[i]; - // 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()) { + // Orca: when every extruder uses the same variant (e.g. a non-Bambu dual-nozzle printer), the + // switch has no nozzle variants to select and nothing to sync. The printer tab still enables it + // to choose the extruder its Extruder and Motion ability pages edit. + m_extruder_switch_variants = extruder_nums >= 2 && m_preset_bundle->support_different_extruders(); + if (m_extruder_switch_variants || (m_type == Preset::TYPE_PRINTER && extruder_nums >= 2)) { auto options = generate_extruder_options(); m_extruder_switch->SetOptions(options); @@ -8281,12 +8320,8 @@ void Tab::update_extruder_variants(int extruder_id, bool reload) if (m_type == Preset::TYPE_PRINT) { update_pages_with_multi_variant(); } - if (m_variant_sizer) { - wxWindow *variant_ctrl = m_extruder_switch ? (wxWindow *) m_extruder_switch : m_variant_combo; - m_main_sizer->Show(m_variant_sizer, variant_ctrl->IsThisEnabled() && m_active_page && !m_active_page->m_opt_id_map.empty() && !m_active_page->title().StartsWith("Extruder ")); - if (m_extruder_sync) m_extruder_sync->Show(variant_ctrl->IsShown()); - GetParent()->Layout(); - } + update_extruder_switch_colors(); + update_variant_sizer_visibility(); } // The variant switch tags are the narrowest place a volume type is named, so they abbreviate it; @@ -8361,12 +8396,21 @@ std::vector Tab::generate_extruder_options() return options; } + // Orca: the printer tab has one tag per extruder, named as its former "Extruder N" pages were. + // parse_extruder_selection() counts a hybrid extruder as two tags; that still lines up because + // only the last extruder can be hybrid. + if (m_type == Preset::TYPE_PRINTER) { + for (int i = 0; i < extruder_nums; ++i) + options.push_back(translate_category(wxString::Format("Extruder %d", i + 1), m_type)); + return 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. + // Orca: the main/deputy toolhead names describe a Bambu dual-nozzle printer, where extruder 0 is + // the left (deputy) and extruder 1 the right (main) nozzle. Other printers number their tools. + const bool toolhead_names = extruder_nums == 2 && m_preset_bundle->is_bbl_vendor(); for (int i = 0; i < extruder_nums; ++i) { - wxString extruder_name = extruder_nums > 2 ? wxString::Format("T%d", i + 1) : + wxString extruder_name = !toolhead_names ? 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)); @@ -8383,6 +8427,71 @@ std::vector Tab::generate_extruder_options() return options; } +Page *TabPrinter::extruder_page() const +{ + for (const PageShp &page : m_pages) + if (page->title() == "Extruder") + return page.get(); + return nullptr; +} + +int TabPrinter::extruder_page_data_index(const std::string &field_id) const +{ + if (Page *page = extruder_page()) + for (const auto &group : page->m_optgroups) + if (auto it = group->opt_map().find(field_id); it != group->opt_map().end()) + return it->second.second; + return -1; +} + +int TabPrinter::extruder_variant_index(int extruder) +{ + const auto *extruders = m_config->option("extruder_type"); + const int index = extruder < int(extruders->size()) ? + m_config->get_index_for_extruder(extruder + 1, "printer_extruder_id", ExtruderType(extruders->values[extruder]), + get_actual_nozzle_volume_type(extruder), "printer_extruder_variant") : + -1; + return index < 0 ? extruder : index; +} + +void TabPrinter::update_custom_dirty(std::vector &dirty_options, std::vector &nonsys_options) +{ + drop_unchanged_added_entries(dirty_options, *m_config, &m_presets->get_selected_preset()); + drop_unchanged_added_entries(nonsys_options, *m_config, m_presets->get_selected_preset_parent()); +} + +void TabPrinter::sync_extruders_count() +{ + if (m_printer_technology != ptFFF) + return; + const auto *nozzle_diameter = m_config->option("nozzle_diameter"); + if (nozzle_diameter == nullptr || nozzle_diameter->size() == m_extruders_count) + return; + extruders_count_changed(nozzle_diameter->size()); + init_options_list(); + update_all_extruder_variants(); +} + +void TabPrinter::activate_option(const std::string &opt_key, const wxString &category) +{ + wxString number; + long n = 0; + const bool numbered = category.StartsWith("Extruder ", &number) && number.ToLong(&n); + if (extruder_page() == nullptr || (!numbered && category != "Extruder")) { + Tab::activate_option(opt_key, category); + return; + } + + // Selecting fires the switch's event, which re-targets the page (switch_excluder()). + const int extruder_idx = (n >= 1 && n <= long(m_extruders_count)) ? int(n - 1) : 0; + if (m_extruder_switch && m_extruder_switch->IsThisEnabled() && extruder_idx != get_current_active_extruder()) + m_extruder_switch->SetSelection(calculate_selection_index_for_extruder(extruder_idx, get_actual_nozzle_volume_type(extruder_idx))); + + // The page's controls are created for index 0. + const auto pos = opt_key.find('#'); + Tab::activate_option(pos == std::string::npos ? opt_key : opt_key.substr(0, pos) + "#0", "Extruder"); +} + NozzleVolumeType Tab::get_actual_nozzle_volume_type(int extruder_id) { int extruder_count = m_preset_bundle->get_printer_extruder_count(); @@ -8459,18 +8568,44 @@ bool Tab::get_extruder_sync_enable_state(int extruder_id) return false; } +bool Tab::variant_switch_active() const +{ + if (m_extruder_switch) + return m_extruder_switch->IsThisEnabled(); + return m_variant_combo && m_variant_combo->IsThisEnabled(); +} + +void Tab::update_variant_sizer_visibility() +{ + if (!m_variant_sizer) + return; + const bool show = variant_switch_active() && m_active_page && !m_active_page->m_opt_id_map.empty(); + m_main_sizer->Show(m_variant_sizer, show); + if (m_extruder_sync) { + m_extruder_sync->Show(show); + // Orca: copying between extruders is offered only between nozzle variants, and not on the Extruder page. + m_extruder_sync->Enable(m_extruder_switch_variants && !is_printer_extruder_page(m_active_page) && + get_extruder_sync_enable_state(get_current_active_extruder())); + } + GetParent()->Layout(); +} + void Tab::switch_excluder(int extruder_id, bool reload) { 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"); + // Orca: the printer tab re-targets its pages to the extruder selected on the switch. Resolved before + // the range check below, which keeps the extruder_type read in get_index_for_extruder in range. + if (m_type == Preset::TYPE_PRINTER && extruder_id == -1) + extruder_id = get_current_active_extruder(); if (!m_variant_combo && (extruder_id >= (int)nozzle_volumes->size() || extruder_id >= (int)extruders->size())) extruder_id = 0; if (m_extruder_switch) { int current_extruder = get_current_active_extruder(); bool sync_enable = get_extruder_sync_enable_state(current_extruder); - m_extruder_sync->Enable(m_extruder_switch->IsThisEnabled() && sync_enable); + m_extruder_sync->Enable(m_extruder_switch_variants && sync_enable && !is_printer_extruder_page(m_active_page)); m_extruder_sync->Show(); if (m_type != Preset::TYPE_PRINTER) { if (extruder_id == -1) @@ -8497,24 +8632,26 @@ void Tab::switch_excluder(int extruder_id, bool reload) return; if (m_extruder_switch) m_extruder_switch->SetClientData(reinterpret_cast(static_cast(index))); if (m_variant_combo) m_variant_combo->SetClientData(reinterpret_cast(static_cast(index))); - wxWindow *variant_ctrl = m_extruder_switch ? (wxWindow *) m_extruder_switch : m_variant_combo; for (auto page : m_pages) { bool is_extruder = false; + int page_index = index; if (m_type == Preset::TYPE_PRINTER) { - if (page->title().StartsWith("Extruder")) { - int extruder_id2 = std::atoi(page->title().Mid(9).ToUTF8()) - 1; - if (extruder_id >= 0 && extruder_id2 != extruder_id) - continue; - if (extruder_id2 > 0) - index = get_index_for_extruder(extruder_id2); + if (page->title() == "Extruder") is_extruder = true; - } else if (page->title().StartsWith("Motion ability")) { - index = get_index_for_extruder(extruder_id == -1 ? 0 : extruder_id, 2); - } + else if (page->title().StartsWith("Motion ability")) + page_index = get_index_for_extruder(extruder_id == -1 ? 0 : extruder_id, 2); } page->m_opt_id_map.clear(); for (auto group : page->m_optgroups) { for (auto &opt : group->opt_map()) { + if (is_extruder && opt.second.second >= 0) { + // Per-variant options use the variant column, the others (nozzle_diameter, + // extruder_offset, ...) are sized by the extruder count and use the extruder. + const int idx = printer_options_with_variant_1.count(opt.second.first) > 0 ? page_index : extruder_id; + const_cast(opt.second.second) = idx; + page->m_opt_id_map.insert({opt.second.first + "#" + std::to_string(idx), opt.first}); + continue; + } auto iter = std::find(printer_extruder_options.begin(), printer_extruder_options.end(), opt.second.first); if (iter != printer_extruder_options.end()) { page->m_opt_id_map.insert({opt.first, opt.first}); @@ -8522,9 +8659,9 @@ void Tab::switch_excluder(int extruder_id, bool reload) } if (opt.second.second >= 0) { - const_cast(opt.second.second) = index; - page->m_opt_id_map.insert({opt.second.first + "#" + std::to_string(index), opt.first}); - group->draw_multi_extruder = !is_extruder && variant_ctrl->IsThisEnabled(); + const_cast(opt.second.second) = page_index; + page->m_opt_id_map.insert({opt.second.first + "#" + std::to_string(page_index), opt.first}); + group->draw_multi_extruder = variant_switch_active(); } } } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 0ace0d7b9f..e916dd6f7d 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -331,6 +331,10 @@ public: MultiSwitchButton * m_variant_combo = nullptr; ScalableButton *m_extruder_sync = nullptr; wxPanel * m_extruder_sync_box = nullptr; + // Orca: whether m_extruder_switch switches nozzle variants (it then offers sync between them). + // The printer tab also enables the switch for printers without variants, to choose the extruder + // its Extruder and Motion ability pages edit. + bool m_extruder_switch_variants = false; std::vector m_actual_nozzle_volumes; public: @@ -387,6 +391,13 @@ public: void update_changed_tree_ui(); void update_undo_buttons(); void update_extruder_switch_colors(); + // Whether the variant switch (m_extruder_switch / m_variant_combo) is enabled: on the printer tab for + // any multi-extruder printer, on the other tabs when it switches nozzle variants. + bool variant_switch_active() const; + // Orca: whether `page` is the printer tab's single "Extruder" page, which edits the extruder selected on the switch. + bool is_printer_extruder_page(const Page* page) const { return m_type == Preset::TYPE_PRINTER && page && page->title() == "Extruder"; } + // Shows the variant switch row on pages with options that follow it. + void update_variant_sizer_visibility(); void update_all_extruder_options_status(); void check_extruder_options_status(int index, bool &sys_extruder, bool &modified_extruder, const std::vector& pages_to_check); @@ -444,7 +455,7 @@ public: virtual void on_value_change(const std::string& opt_key, const boost::any& value); void update_wiping_button_visibility(); - void activate_option(const std::string& opt_key, const wxString& category); + virtual void activate_option(const std::string& opt_key, const wxString& category); void apply_searcher(); void cache_config_diff(const std::vector& selected_options, const DynamicPrintConfig* config = nullptr); void apply_config_from_cache(); @@ -710,6 +721,25 @@ public: void cache_extruder_cnt(const DynamicPrintConfig* config = nullptr); bool apply_extruder_cnt_from_cache(); void refresh_printer_agent_dropdown() const; + + // Orca: a single "Extruder" page for all extruders. Its controls are created once (index 0) and + // switch_excluder() re-targets them to the extruder selected on m_extruder_switch. + Page* extruder_page() const; + // Config index an "Extruder" page field (e.g. "retraction_length#0") currently edits, -1 if not on that page. + int extruder_page_data_index(const std::string& field_id) const; + // Config index of an extruder's per-variant options (Retraction, Z-Hop, ...): its variant column + // for the nozzle selected on the switch, or the extruder index on a printer without variants. + int extruder_variant_index(int extruder); + // After the config was restored (roll back): follow its extruder count, if it differs. + void sync_extruders_count(); + // Search jump to "Extruder N" / "key#N": selects extruder N on the switch, then activates the + // page's own field on the "Extruder" page. + void activate_option(const std::string& opt_key, const wxString& category) override; + +protected: + // Orca: values of extruders added by raising the extruder count have no saved / system value to + // revert to; the change is shown on "extruders_count", not on each of their parameters. + void update_custom_dirty(std::vector &dirty_options, std::vector &nonsys_options) override; }; class TabSLAMaterial : public Tab diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index 0df6eb976f..40fdce384a 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -800,6 +800,43 @@ TEST_CASE("A per-variant filament option read with a single value gives it to ev REQUIRE(config.option("pressure_advance")->values == std::vector({0.021, 0.021, 0.021})); } +// Machine limits (printer_options_with_variant_2) hold a (normal, silent) pair per printer variant, so the +// printer Tab's Motion ability page can edit each extruder's own limits. +TEST_CASE("set_num_extruders gives every printer variant its own pair of machine limits", "[Config]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + auto speed_x = [&config]() { return config.option("machine_max_speed_x")->values; }; + + SECTION("a new extruder's pair is padded with the first value, like the other variant keys") { + config.option("machine_max_speed_x")->values = {500., 200.}; + config.set_num_extruders(3); + REQUIRE(config.option("printer_extruder_variant")->size() == 3); + REQUIRE(speed_x() == std::vector({500., 200., 500., 500., 500., 500.})); + } + + SECTION("per-extruder pairs are kept, and removing an extruder removes its pair") { + config.option("machine_max_speed_x")->values = {500., 200., 400., 150., 300., 100.}; + config.set_num_extruders(3); + REQUIRE(speed_x() == std::vector({500., 200., 400., 150., 300., 100.})); + config.set_num_extruders(2); + REQUIRE(speed_x() == std::vector({500., 200., 400., 150.})); + } + + SECTION("a printer with nozzle variants gets a pair per variant") { + // 2 extruders x 2 variants each = 4 variants + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow"}; + const std::vector per_variant = {500., 200., 510., 210., 520., 220., 530., 230.}; + config.option("machine_max_speed_x")->values = per_variant; + config.set_num_extruders(2); + REQUIRE(speed_x() == per_variant); + + config.option("machine_max_speed_x")->values = {500., 200.}; + config.set_num_extruders(2); + REQUIRE(speed_x() == std::vector({500., 200., 500., 500., 500., 500., 500., 500.})); + } +} + // The device drying options hold several values per filament, as many as each filament preset gives. TEST_CASE("The device drying options are rebuilt as each filament's values in slot order", "[Config]") {