X2D Support (#13388)

# Description

Adresses #13294 

- Adds the X2D printer definition, machine presets, process presets,
filament presets, BBL profile index entries, CLI config entries,
filament blacklist updates, and printer/load/calibration/cover assets.
- Updates dual-nozzle handling to use configured toolhead labels and
match Bambu X2D hotend placeholders.
- Adds X2D-specific wipe tower cooling placeholder support and 3MF
filament/nozzle change sequence metadata import/export plumbing.

# Note

I own a P2S and an X2D. That's all. I frankly have no idea if my changes
cause regression on other printers, and have no capability to test. I
know that for my X2D, which runs an AMS, .2mm nozzles, SuperTack, and in
LAN mode, this has been working without issue.

# Screenshots/Recordings/Graphs

<img width="606" height="380" alt="Dual nozzle control"
src="https://github.com/user-attachments/assets/0d1c1063-4621-4097-b97c-d739557bf18c"
/>

*Dual nozzle control*

<img width="726" height="260" alt="image"
src="https://github.com/user-attachments/assets/270355b7-ca67-4ca3-ad19-582b8f11411b"
/>

*Multi nozzle filament override*

<img width="416" height="202" alt="X2D Machine config and dual nozzle
support"
src="https://github.com/user-attachments/assets/6a5c07b2-0d20-4819-8f42-d60731313249"
/>

*X2D Machine config and dual nozzle support*

<img width="397" height="142" alt="Filament for Supports test prints"
src="https://github.com/user-attachments/assets/3c7546bd-0e27-4d56-89b7-d9ca18c976f9"
/>

*Filament for Supports has been used in over 20 hours of test prints*

<img width="210" height="263" alt="Left vs Right filament distinction"
src="https://github.com/user-attachments/assets/03322268-b669-4f14-8d77-c4d96843d219"
/>

*Left vs Right filament distinction*

<img width="557" height="327" alt="Custom filament mapping"
src="https://github.com/user-attachments/assets/c1c4396f-7359-474e-80bd-78fec22f9c82"
/>

*Custom filament mapping*

<img width="556" height="314" alt="Auto map"
src="https://github.com/user-attachments/assets/d83e3217-edce-4340-886e-043962003a30"
/>

*Auto map*

<img width="689" height="664" alt="LAN mode send print with X2D preview
and no errors"
src="https://github.com/user-attachments/assets/76009bbf-31d3-4a6c-979c-8643b487c824"
/>

*LAN mode send print with X2D preview and no errors, dual nozzle
selection*


## Tests

- 20 hours of dual-nozzle printing.
- 100% CTest tests passed
- Validated 208 changed JSON files.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
Fix #13294
This commit is contained in:
glowstab
2026-05-09 13:21:13 -05:00
committed by GitHub
parent 7bef75b2cd
commit 9956ad5b48
242 changed files with 53863 additions and 83 deletions

View File

@@ -61,6 +61,7 @@
#endif // WIN32
#include <algorithm>
#include <cstdint>
namespace Slic3r {
@@ -499,6 +500,22 @@ void Tab::create_preset_tab()
}
#endif
if (dynamic_cast<TabFilament *>(this)) {
m_variant_combo = new MultiSwitchButton(panel);
m_variant_combo->Bind(wxCUSTOMEVT_MULTISWITCH_SELECTION, [this](auto &evt) {
evt.Skip();
switch_excluder(evt.GetInt());
reload_config();
update_changed_ui();
toggle_options();
if (m_active_page)
m_active_page->update_visibility(m_mode, true);
m_page_view->GetParent()->Layout();
});
m_variant_combo->Hide();
m_main_sizer->Add(m_variant_combo, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, m_em_unit);
}
this->SetSizer(m_main_sizer);
//this->Layout();
m_page_view = m_parent->get_paged_view();
@@ -1295,6 +1312,8 @@ void Tab::msw_rescale()
{
m_mode_view->Rescale();
}
if (m_variant_combo)
m_variant_combo->Rescale();
if (m_detach_preset_btn)
m_detach_preset_btn->msw_rescale();
@@ -1360,6 +1379,8 @@ void Tab::sys_color_changed()
m_active_page->sys_color_changed();
if (m_extruder_switch)
m_extruder_switch->Rescale();
if (m_variant_combo)
m_variant_combo->Rescale();
//BBS: GUI refactor
//Layout();
@@ -3747,7 +3768,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
// "filament_seam_gap"
};
const int selection = 0; //m_variant_combo->GetSelection(); // TODO: Orca hack
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
auto opt = dynamic_cast<ConfigOptionVectorBase *>(m_config->option("filament_retraction_length"));
const int extruder_idx = selection < 0 || selection >= static_cast<int>(opt->size()) ? 0 : selection;
@@ -4277,9 +4298,11 @@ void TabFilament::toggle_options()
toggle_line("activate_chamber_temp_control", printer_cfg.opt_bool("support_chamber_temp_control"));
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", 0u);
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
const unsigned int variant_idx = (unsigned int) std::max(selection, 0);
std::string volumetric_speed_cos = m_config->opt_string("volumetric_speed_coefficients", variant_idx);
bool enable_fit = volumetric_speed_cos != "0 0 0 0 0 0";
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + 0u);
toggle_option("filament_adaptive_volumetric_speed", enable_fit, 256 + variant_idx);
}
if (m_active_page->title() == L("Setting Overrides"))
@@ -4297,7 +4320,8 @@ void TabFilament::toggle_options()
toggle_option("filament_multitool_ramming_flow", multitool_ramming);
bool is_BBL_multi_extruder = is_BBL_printer && printer_cfg.option<ConfigOptionFloats>("nozzle_diameter")->size() > 1;
const int extruder_idx = 0; // m_variant_combo->GetSelection(); // TODO: Orca hack
const int selection = m_variant_combo ? m_variant_combo->GetSelection() : 0;
const int extruder_idx = std::max(selection, 0);
toggle_line("long_retractions_when_ec", is_BBL_multi_extruder, 256 + extruder_idx);
toggle_line("retraction_distances_when_ec", is_BBL_multi_extruder && m_config->opt_bool("long_retractions_when_ec", extruder_idx), 256 + extruder_idx);
}
@@ -6306,6 +6330,9 @@ bool Tab::tree_sel_change_delayed(wxCommandEvent& event)
if (m_extruder_switch) {
m_main_sizer->Show(m_extruder_switch, !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
} else if (m_variant_combo) {
m_main_sizer->Show(m_variant_combo, m_variant_combo->IsEnabled() && !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
}
auto throw_if_canceled = std::function<void()>([this](){
@@ -6976,6 +7003,41 @@ void Tab::set_just_edit(bool just_edit)
/// </summary>
/// <param name="extruder_id"></param>
std::vector<wxString> Tab::generate_extruder_options()
{
std::vector<wxString> options;
if (m_type != Preset::TYPE_FILAMENT)
return options;
auto *variants = m_config->option<ConfigOptionStrings>("filament_extruder_variant");
if (!variants)
return options;
const std::vector<std::string> known_nozzle_types = {
get_nozzle_volume_type_string(NozzleVolumeType::nvtHighFlow),
get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard),
};
for (const std::string &variant : variants->values) {
std::string drive;
std::string nozzle;
for (const std::string &nozzle_type : known_nozzle_types) {
if (variant.size() > nozzle_type.size() &&
variant.substr(variant.size() - nozzle_type.size()) == nozzle_type &&
variant[variant.size() - nozzle_type.size() - 1] == ' ') {
drive = variant.substr(0, variant.size() - nozzle_type.size() - 1);
nozzle = nozzle_type;
break;
}
}
options.push_back(nozzle.empty() ? from_u8(variant) : wxString::Format(wxT("%s: %s"), from_u8(drive), from_u8(nozzle)));
}
return options;
}
void Tab::update_extruder_variants(int extruder_id)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << extruder_id;
@@ -7000,11 +7062,26 @@ void Tab::update_extruder_variants(int extruder_id)
GetParent()->Layout();
return;
}
} else if (m_variant_combo) {
if (extruder_id >= 0)
return;
const int selection = m_variant_combo->GetSelection();
auto options = generate_extruder_options();
m_variant_combo->SetOptions(options);
if (!options.empty())
m_variant_combo->SetSelection(selection < 0 || selection >= (int) options.size() ? 0 : selection);
m_variant_combo->Enable(options.size() > 1);
}
switch_excluder(extruder_id);
if (m_extruder_switch) {
m_main_sizer->Show(m_extruder_switch, m_active_page && !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
} else if (m_variant_combo) {
m_main_sizer->Show(m_variant_combo, m_variant_combo->IsEnabled() && m_active_page && !m_active_page->m_opt_id_map.empty());
GetParent()->Layout();
}
}
@@ -7018,7 +7095,7 @@ void Tab::switch_excluder(int extruder_id)
{}, {"", "filament_extruder_variant"}, // Preset::TYPE_FILAMENT filament don't use id anymore
{}, {"printer_extruder_id", "printer_extruder_variant"}, // Preset::TYPE_PRINTER
};
if (extruder_id >= nozzle_volumes->size() || extruder_id >= extruders->size())
if (!m_variant_combo && (extruder_id >= nozzle_volumes->size() || extruder_id >= extruders->size()))
extruder_id = 0;
if (m_extruder_switch && m_type != Preset::TYPE_PRINTER) {
int current_extruder = m_extruder_switch->GetValue() ? 1 : 0;
@@ -7026,15 +7103,25 @@ void Tab::switch_excluder(int extruder_id)
extruder_id = current_extruder;
else if (extruder_id != current_extruder)
return;
} else if (m_variant_combo) {
int current_variant = m_variant_combo->GetSelection();
if (current_variant < 0)
current_variant = 0;
if (extruder_id == -1)
extruder_id = current_variant;
else if (extruder_id != current_variant)
return;
}
auto get_index_for_extruder =
[this, &extruders, &nozzle_volumes, variant_keys = variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, int stride = 1) {
return m_config->get_index_for_extruder(extruder_id + 1, variant_keys.first,
ExtruderType(extruders->values[extruder_id]), NozzleVolumeType(nozzle_volumes->values[extruder_id]), variant_keys.second, stride);
};
auto index = get_index_for_extruder(extruder_id == -1 ? 0 : extruder_id);
auto index = m_variant_combo ? extruder_id : get_index_for_extruder(extruder_id == -1 ? 0 : extruder_id);
if (index < 0)
return;
if (m_variant_combo)
m_variant_combo->SetClientData(reinterpret_cast<void *>(static_cast<std::uintptr_t>(index)));
for (auto page : m_pages) {
bool is_extruder = false;
if (m_type == Preset::TYPE_PRINTER) {