feat(gui): per-filament nozzle volume selection and persistence

- FilamentMapDialog's manual page understands volume types: a mixed
  (Hybrid) extruder shows separate Standard / High Flow drop zones
  with live sub-nozzle counts, a validation timer with an inline
  error + "set nozzle count" suggestion, and composes a per-filament
  volume map on OK (persisted to the plate or globally)
- switching an extruder's Flow type rewrites the affected plate map
  entries; plate maps stay sized across filament add/delete/count
  changes (values keep their filament, no index shift)
- CLI: manual mapping on multi-nozzle printers synthesizes the volume
  map from extruder flow types when absent; nozzle-manual mode
  requires explicit maps; computed maps land on the plate so exported
  projects carry filament_volume_maps
- filament_nozzle_map joins the project options (selection seeding +
  filament-count resizing)
- pot entries for the new dialog strings

All 19 reference fixtures byte-identical; slicing an exported Hybrid
project reproduces its g-code byte-for-byte with the volume map
round-tripped through model_settings.config.
This commit is contained in:
SoftFever
2026-07-11 16:37:50 +08:00
parent 21b46044d0
commit 75c8d0775a
14 changed files with 1131 additions and 83 deletions

View File

@@ -1,9 +1,14 @@
#include "DragDropPanel.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/StateColor.hpp"
#include <slic3r/GUI/wxExtensions.hpp>
namespace Slic3r { namespace GUI {
wxDEFINE_EVENT(wxEVT_DRAG_DROP_COMPLETED, wxCommandEvent);
struct CustomData
{
int filament_id;
@@ -201,7 +206,7 @@ wxDragResult ColorDropTarget::OnData(wxCoord x, wxCoord y, wxDragResult def)
/////////////// ColorDropTarget end ////////////////////////
DragDropPanel::DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto)
DragDropPanel::DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto, bool has_title, bool is_sub)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
, m_is_auto(is_auto)
{
@@ -209,22 +214,31 @@ DragDropPanel::DragDropPanel(wxWindow *parent, const wxString &label, bool is_au
m_sizer = new wxBoxSizer(wxVERTICAL);
auto title_panel = new wxPanel(this);
title_panel->SetBackgroundColour(0xEEEEEE);
auto title_sizer = new wxBoxSizer(wxHORIZONTAL);
title_panel->SetSizer(title_sizer);
if (has_title) {
auto title_panel = new wxPanel(this);
title_panel->SetBackgroundColour(is_sub ? wxColour(0xF8F8F8) : wxColour(0xEEEEEE));
auto title_sizer = new wxBoxSizer(wxHORIZONTAL);
title_panel->SetSizer(title_sizer);
Label* static_text = new Label(this, label);
static_text->SetFont(Label::Head_13);
static_text->SetBackgroundColour(0xEEEEEE);
m_title_label = new Label(this, label);
m_title_label->SetFont(is_sub ? Label::Body_12 : Label::Head_13);
if (is_sub)
m_title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
m_title_label->SetBackgroundColour(is_sub ? wxColour(0xF8F8F8) : wxColour(0xEEEEEE));
title_sizer->Add(static_text, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
title_sizer->Add(m_title_label, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
m_sizer->Add(title_panel, 0, wxEXPAND);
m_sizer->AddSpacer(10);
m_sizer->Add(title_panel, 0, wxEXPAND);
m_sizer->AddSpacer(10);
}
m_grid_item_sizer = new wxGridSizer(0, 6, FromDIP(8), FromDIP(8)); // row = 0, col = 3, 10 10 is space
m_sizer->Add(m_grid_item_sizer, 1, wxEXPAND | wxALL, FromDIP(8));
if (is_sub) {
m_grid_item_sizer = new wxGridSizer(0, 3, FromDIP(6), FromDIP(6));
m_sizer->Add(m_grid_item_sizer, 0, wxEXPAND);
} else {
m_grid_item_sizer = new wxGridSizer(0, 6, FromDIP(8), FromDIP(8)); // row = 0, col = 3, 10 10 is space
m_sizer->Add(m_grid_item_sizer, 1, wxEXPAND | wxALL, FromDIP(8));
}
// set droptarget
auto drop_target = new ColorDropTarget(this);
@@ -242,20 +256,35 @@ void DragDropPanel::AddColorBlock(const wxColour &color, const std::string &type
m_grid_item_sizer->Add(panel, 0);
m_filament_blocks.push_back(panel);
if (update_ui) {
m_filament_blocks.front()->Refresh(); // FIX BUG: STUDIO-8467
Freeze();
m_grid_item_sizer->Layout();
Layout();
Fit();
GetParent()->GetParent()->Layout();
GetParent()->GetParent()->Fit();
m_filament_blocks.front()->Refresh(); // FIX BUG: STUDIO-8467
Thaw();
NotifyDragDropCompleted();
}
}
void DragDropPanel::RemoveColorBlock(ColorPanel *panel, bool update_ui)
{
m_sizer->Detach(panel);
m_grid_item_sizer->Detach(panel);
panel->Destroy();
m_filament_blocks.erase(std::remove(m_filament_blocks.begin(), m_filament_blocks.end(), panel), m_filament_blocks.end());
if (update_ui) {
Freeze();
Layout();
Fit();
GetParent()->GetParent()->Layout();
GetParent()->GetParent()->Fit();
Thaw();
NotifyDragDropCompleted();
}
}
@@ -270,6 +299,15 @@ void DragDropPanel::DoDragDrop(ColorPanel *panel, const wxColour &color, const s
}
}
void DragDropPanel::UpdateLabel(const wxString &label)
{
if (m_title_label) {
m_title_label->SetLabel(label);
m_title_label->Refresh();
Layout();
}
}
std::vector<int> DragDropPanel::GetAllFilaments() const
{
std::vector<int> filaments;
@@ -287,4 +325,327 @@ std::vector<int> DragDropPanel::GetAllFilaments() const
return filaments;
}
void DragDropPanel::NotifyDragDropCompleted()
{
wxCommandEvent event(wxEVT_DRAG_DROP_COMPLETED);
event.SetEventObject(this);
wxWindow *parent = GetParent();
while (parent) {
auto name = parent->GetName();
if (name == wxT("FilamentMapManualPanel")) {
parent->GetEventHandler()->ProcessEvent(event);
break;
}
parent = parent->GetParent();
}
}
class SeparatedColorDropTarget : public wxDropTarget
{
public:
SeparatedColorDropTarget(SeparatedDragDropPanel *panel) : wxDropTarget(), m_panel(panel)
{
m_data = new ColorDataObject();
SetDataObject(m_data);
}
virtual wxDragResult OnData(wxCoord x, wxCoord y, wxDragResult def) override;
virtual bool OnDrop(wxCoord x, wxCoord y) override { return true; }
private:
SeparatedDragDropPanel *m_panel;
ColorDataObject *m_data;
};
wxDragResult SeparatedColorDropTarget::OnData(wxCoord x, wxCoord y, wxDragResult def)
{
if (!GetData()) return wxDragNone;
m_panel->AddColorBlock(m_data->GetColor(), m_data->GetType(), m_data->GetFilament(), false);
return wxDragCopy;
}
SeparatedDragDropPanel::SeparatedDragDropPanel(wxWindow *parent, const wxString &label, bool use_separation) : wxPanel(parent), m_use_separation(use_separation)
{
SetBackgroundColour(0xF8F8F8);
m_main_sizer = new wxBoxSizer(wxVERTICAL);
auto title_panel = new wxPanel(this);
title_panel->SetBackgroundColour(0xEEEEEE);
auto title_sizer = new wxBoxSizer(wxHORIZONTAL);
title_panel->SetSizer(title_sizer);
m_label = new Label(title_panel, label);
m_label->SetFont(Label::Head_13);
m_label->SetBackgroundColour(0xEEEEEE);
title_sizer->Add(m_label, 0, wxALIGN_CENTER | wxALL, FromDIP(5));
m_main_sizer->Add(title_panel, 0, wxEXPAND);
m_main_sizer->AddSpacer(10);
m_content_panel = new wxPanel(this);
m_content_panel->SetBackgroundColour(0xF8F8F8);
m_content_sizer = new wxBoxSizer(wxHORIZONTAL);
m_content_panel->SetSizer(m_content_sizer);
m_high_flow_panel = new DragDropPanel(m_content_panel, _L("High Flow"), false, true, true);
m_standard_panel = new DragDropPanel(m_content_panel, _L("Standard"), false, true, true);
m_unified_panel = new DragDropPanel(m_content_panel, wxEmptyString, false, false);
m_high_flow_panel->SetBackgroundColour(0xF8F8F8);
m_standard_panel->SetBackgroundColour(0xF8F8F8);
m_unified_panel->SetBackgroundColour(0xF8F8F8);
m_unified_panel->SetMinSize({FromDIP(260), -1});
m_main_sizer->Add(m_content_panel, 1, wxEXPAND);
UpdateLayout();
auto drop_target = new SeparatedColorDropTarget(this);
SetDropTarget(drop_target);
SetSizer(m_main_sizer);
Layout();
wxGetApp().UpdateDarkUIWin(this);
}
void SeparatedDragDropPanel::UpdateLayout()
{
m_content_sizer->Clear(false);
if (m_use_separation) {
m_unified_panel->Hide();
m_high_flow_panel->Show();
m_standard_panel->Show();
wxSize content_size = m_content_panel->GetSize();
int panel_width = (content_size.GetWidth() - FromDIP(1) - FromDIP(8)) / 2;
if (panel_width > 0) {
m_high_flow_panel->SetMinSize(wxSize(panel_width, -1));
m_standard_panel->SetMinSize(wxSize(panel_width, -1));
}
m_content_sizer->Add(m_high_flow_panel, 1, wxEXPAND | wxLEFT, FromDIP(8));
// Create the separator once and re-attach it on relayout; Clear(false) above only
// detaches, so allocating here would leak a window per relayout.
if (!m_separator) {
m_separator = new wxPanel(m_content_panel, wxID_ANY);
m_separator->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(0xCE, 0xCE, 0xCE)));
m_separator->SetMinSize(wxSize(FromDIP(1), -1));
}
m_separator->Show();
m_content_sizer->Add(m_separator, 0, wxEXPAND | wxALL, FromDIP(4));
m_content_sizer->Add(m_standard_panel, 1, wxEXPAND | wxRIGHT, FromDIP(8));
} else {
m_high_flow_panel->Hide();
m_standard_panel->Hide();
if (m_separator)
m_separator->Hide();
m_unified_panel->Show();
m_content_sizer->Add(m_unified_panel, 1, wxEXPAND);
}
m_content_sizer->Layout();
Layout();
Fit();
if (GetParent()) {
GetParent()->Layout();
if (GetParent()->GetParent()) {
GetParent()->GetParent()->Layout();
}
}
}
void SeparatedDragDropPanel::UpdateLabel(const wxString &label)
{
if (m_label) {
m_label->SetLabel(label);
m_label->Refresh();
Layout();
}
}
void SeparatedDragDropPanel::SetUseSeparation(bool use_separation)
{
if (m_use_separation != use_separation) {
m_use_separation = use_separation;
if (use_separation) {
auto blocks = m_unified_panel->get_filament_blocks();
for (auto &block : blocks) {
m_standard_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false);
m_unified_panel->RemoveColorBlock(block, false);
}
} else {
auto high_flow_blocks = m_high_flow_panel->get_filament_blocks();
auto standard_blocks = m_standard_panel->get_filament_blocks();
for (auto &block : high_flow_blocks) {
m_unified_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false);
m_high_flow_panel->RemoveColorBlock(block, false);
}
for (auto &block : standard_blocks) {
m_unified_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false);
m_standard_panel->RemoveColorBlock(block, false);
}
}
UpdateLayout();
}
}
void SeparatedDragDropPanel::AddColorBlock(const wxColour &color, const std::string &type, int filament_id, bool is_high_flow, bool update_ui)
{
if (m_use_separation) {
if (is_high_flow) {
m_high_flow_panel->AddColorBlock(color, type, filament_id, update_ui);
} else {
m_standard_panel->AddColorBlock(color, type, filament_id, update_ui);
}
if (update_ui) {
CallAfter([this]() {
Layout();
GetParent()->Layout();
});
}
} else {
m_unified_panel->AddColorBlock(color, type, filament_id, update_ui);
}
}
void SeparatedDragDropPanel::RemoveColorBlock(ColorPanel *panel, bool update_ui)
{
auto high_flow_blocks = m_high_flow_panel->get_filament_blocks();
auto standard_blocks = m_standard_panel->get_filament_blocks();
auto unified_blocks = m_unified_panel->get_filament_blocks();
if (std::find(high_flow_blocks.begin(), high_flow_blocks.end(), panel) != high_flow_blocks.end()) {
m_high_flow_panel->RemoveColorBlock(panel, update_ui);
} else if (std::find(standard_blocks.begin(), standard_blocks.end(), panel) != standard_blocks.end()) {
m_standard_panel->RemoveColorBlock(panel, update_ui);
} else if (std::find(unified_blocks.begin(), unified_blocks.end(), panel) != unified_blocks.end()) {
m_unified_panel->RemoveColorBlock(panel, update_ui);
}
if (update_ui && m_use_separation) {
CallAfter([this]() {
Layout();
GetParent()->Layout();
});
}
}
std::vector<int> SeparatedDragDropPanel::GetAllFilaments() const
{
if (m_use_separation) {
auto high_flow = m_high_flow_panel->GetAllFilaments();
auto standard = m_standard_panel->GetAllFilaments();
std::vector<int> result;
result.insert(result.end(), high_flow.begin(), high_flow.end());
result.insert(result.end(), standard.begin(), standard.end());
return result;
} else {
return m_unified_panel->GetAllFilaments();
}
}
std::vector<int> SeparatedDragDropPanel::GetHighFlowFilaments() const
{
if (m_use_separation) {
return m_high_flow_panel->GetAllFilaments();
}
auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type");
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<int>(NozzleVolumeType::nvtHighFlow)) {
return m_unified_panel->GetAllFilaments();
}
}
return {};
}
std::vector<int> SeparatedDragDropPanel::GetStandardFilaments() const
{
if (m_use_separation) {
return m_standard_panel->GetAllFilaments();
}
auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type");
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<int>(NozzleVolumeType::nvtStandard)) {
return m_unified_panel->GetAllFilaments();
}
}
return {};
}
std::vector<int> SeparatedDragDropPanel::GetTPUHighFlowFilaments() const
{
if (m_use_separation) {
return {};
}
auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type");
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<int>(NozzleVolumeType::nvtTPUHighFlow)) {
return m_unified_panel->GetAllFilaments();
}
}
return {};
}
std::vector<ColorPanel *> SeparatedDragDropPanel::get_filament_blocks() const
{
if (m_use_separation) {
auto high_flow = m_high_flow_panel->get_filament_blocks();
auto standard = m_standard_panel->get_filament_blocks();
std::vector<ColorPanel *> result;
result.insert(result.end(), high_flow.begin(), high_flow.end());
result.insert(result.end(), standard.begin(), standard.end());
return result;
} else {
return m_unified_panel->get_filament_blocks();
}
}
std::vector<ColorPanel *> SeparatedDragDropPanel::get_high_flow_blocks() const
{
return m_high_flow_panel->get_filament_blocks();
}
std::vector<ColorPanel *> SeparatedDragDropPanel::get_standard_blocks() const
{
return m_standard_panel->get_filament_blocks();
}
void SeparatedDragDropPanel::ClearAllBlocks()
{
auto high_flow_blocks = m_high_flow_panel->get_filament_blocks();
for (auto &block : high_flow_blocks) {
m_high_flow_panel->RemoveColorBlock(block, false);
}
auto standard_blocks = m_standard_panel->get_filament_blocks();
for (auto &block : standard_blocks) {
m_standard_panel->RemoveColorBlock(block, false);
}
auto unified_blocks = m_unified_panel->get_filament_blocks();
for (auto &block : unified_blocks) {
m_unified_panel->RemoveColorBlock(block, false);
}
}
}} // namespace Slic3r::GUI

View File

@@ -3,6 +3,7 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "Widgets/Label.hpp"
#include <wx/simplebook.h>
#include <wx/dialog.h>
@@ -13,17 +14,20 @@
namespace Slic3r { namespace GUI {
wxDECLARE_EVENT(wxEVT_DRAG_DROP_COMPLETED, wxCommandEvent);
wxColor Hex2Color(const std::string& str);
class ColorPanel;
class DragDropPanel : public wxPanel
{
public:
DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto);
DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto, bool has_title = true, bool is_sub = false);
void AddColorBlock(const wxColour &color, const std::string &type, int filament_id, bool update_ui = true);
void RemoveColorBlock(ColorPanel *panel, bool update_ui = true);
void DoDragDrop(ColorPanel *panel, const wxColour &color, const std::string &type, int filament_id);
void UpdateLabel(const wxString &label);
std::vector<int> GetAllFilaments() const;
@@ -35,9 +39,12 @@ public:
private:
wxBoxSizer *m_sizer;
wxGridSizer *m_grid_item_sizer;
Label *m_title_label = nullptr;
bool m_is_auto;
std::vector<ColorPanel *> m_filament_blocks;
void NotifyDragDropCompleted();
private:
bool m_is_draging = false;
};
@@ -64,6 +71,49 @@ private:
int m_filament_id;
};
// Drop zone for one extruder that can split into two sub-zones (High Flow / Standard)
// when the extruder mixes nozzle volume types (Hybrid flow), and collapses back to a
// single unified zone otherwise.
class SeparatedDragDropPanel : public wxPanel
{
public:
SeparatedDragDropPanel(wxWindow *parent, const wxString &label, bool use_separation = false);
void AddColorBlock(const wxColour &color, const std::string &type, int filament_id, bool is_high_flow = false, bool update_ui = true);
void RemoveColorBlock(ColorPanel *panel, bool update_ui = true);
std::vector<int> GetAllFilaments() const;
std::vector<int> GetHighFlowFilaments() const;
std::vector<int> GetStandardFilaments() const;
std::vector<int> GetTPUHighFlowFilaments() const;
std::vector<ColorPanel *> get_filament_blocks() const;
std::vector<ColorPanel *> get_high_flow_blocks() const;
std::vector<ColorPanel *> get_standard_blocks() const;
void SetUseSeparation(bool use_separation);
bool IsUseSeparation() const { return m_use_separation; }
void ClearAllBlocks();
void UpdateLabel(const wxString &label);
private:
void UpdateLayout();
wxBoxSizer *m_main_sizer;
wxPanel *m_content_panel;
wxBoxSizer *m_content_sizer;
wxPanel *m_separator = nullptr;
Label *m_label;
DragDropPanel *m_high_flow_panel;
DragDropPanel *m_standard_panel;
DragDropPanel *m_unified_panel;
bool m_use_separation;
};
}} // namespace Slic3r::GUI
#endif /* slic3r_DragDropPanel_hpp_ */

View File

@@ -36,6 +36,13 @@ static std::vector<int> get_applied_map(DynamicConfig& proj_config, const Plater
return plater_ref->get_global_filament_map();
}
static std::vector<int> get_applied_volume_map(DynamicConfig& proj_config, const Plater* plater_ref, const PartPlate* partplate_ref, const bool sync_plate)
{
if (sync_plate)
return partplate_ref->get_real_filament_volume_maps(proj_config);
return plater_ref->get_global_filament_volume_map();
}
extern std::string& get_left_extruder_unprintable_text();
extern std::string& get_right_extruder_unprintable_text();
@@ -123,7 +130,9 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p
std::vector<std::string> filament_types = full_config.option<ConfigOptionStrings>("filament_type")->values;
FilamentMapMode applied_mode = get_applied_map_mode(full_config, plater_ref,partplate_ref, sync_plate);
std::vector<int> applied_maps = get_applied_map(full_config, plater_ref, partplate_ref, sync_plate);
std::vector<int> applied_volume_maps = get_applied_volume_map(full_config, plater_ref, partplate_ref, sync_plate);
applied_maps.resize(filament_colors.size(), 1);
applied_volume_maps.resize(filament_colors.size(), 0);
if (!force_pop_up && applied_mode != fmmManual)
return true;
@@ -141,6 +150,7 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p
filament_colors,
filament_types,
applied_maps,
applied_volume_maps,
filament_lists,
applied_mode,
plater_ref->get_machine_sync_status(),
@@ -152,25 +162,32 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p
if (ret == wxID_OK) {
FilamentMapMode new_mode = map_dlg.get_mode();
std::vector<int> new_maps = map_dlg.get_filament_maps();
std::vector<int> new_volume_maps = map_dlg.get_filament_volume_maps();
if (sync_plate) {
if (is_slice_all) {
auto plate_list = plater_ref->get_partplate_list().get_plate_list();
for (int i = 0; i < plate_list.size(); ++i) {
plate_list[i]->set_filament_map_mode(new_mode);
if(new_mode == fmmManual)
if (new_mode == fmmManual) {
plate_list[i]->set_filament_maps(new_maps);
plate_list[i]->set_filament_volume_maps(new_volume_maps);
}
}
}
else {
partplate_ref->set_filament_map_mode(new_mode);
if (new_mode == fmmManual)
if (new_mode == fmmManual) {
partplate_ref->set_filament_maps(new_maps);
partplate_ref->set_filament_volume_maps(new_volume_maps);
}
}
}
else {
plater_ref->set_global_filament_map_mode(new_mode);
if (new_mode == fmmManual)
if (new_mode == fmmManual) {
plater_ref->set_global_filament_map(new_maps);
plater_ref->set_global_filament_volume_map(new_volume_maps);
}
}
plater_ref->update();
// check whether able to slice, if not, return false
@@ -186,12 +203,17 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
const std::vector<std::string> &filament_color,
const std::vector<std::string> &filament_type,
const std::vector<int> &filament_map,
const std::vector<int> &filament_volume_map,
const std::vector<int> &filaments,
const FilamentMapMode mode,
bool machine_synced,
bool show_default,
bool with_checkbox)
: wxDialog(parent, wxID_ANY, _L("Filament grouping"), wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE), m_filament_color(filament_color), m_filament_type(filament_type), m_filament_map(filament_map)
: wxDialog(parent, wxID_ANY, _L("Filament grouping"), wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE)
, m_filament_color(filament_color)
, m_filament_type(filament_type)
, m_filament_map(filament_map)
, m_filament_volume_map(filament_volume_map)
{
SetBackgroundColour(*wxWHITE);
@@ -244,7 +266,20 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
mode == fmmAutoForMatch && !auto_match_available ? fmmAutoForFlush :
mode;
m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map);
m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map, filament_volume_map);
// A manual grouping can point filaments at a nozzle volume the extruder does not physically
// carry; the panel's validation timer reports that here so OK is gated on a printable map.
m_manual_map_panel->Bind(wxEVT_INVALID_MANUAL_MAP, [this](wxCommandEvent &event) {
if (m_page_type != PageType::ptManual) {
if (!m_ok_btn->IsEnabled()) { m_ok_btn->Enable(); }
return;
}
if (event.GetInt()) {
if (!m_ok_btn->IsEnabled()) { m_ok_btn->Enable(); }
} else {
if (m_ok_btn->IsEnabled()) { m_ok_btn->Disable(); }
}
});
m_auto_map_panel = new FilamentMapAutoPanel(this, default_auto_mode, auto_match_available);
if (show_default)
m_default_map_panel = new FilamentMapDefaultPanel(this);
@@ -351,16 +386,8 @@ void FilamentMapDialog::on_checkbox(wxCommandEvent &event)
void FilamentMapDialog::on_ok(wxCommandEvent &event)
{
if (m_page_type == PageType::ptManual) {
std::vector<int> left_filaments = m_manual_map_panel->GetLeftFilaments();
std::vector<int> right_filaments = m_manual_map_panel->GetRightFilaments();
for (int i = 0; i < m_filament_map.size(); ++i) {
if (std::find(left_filaments.begin(), left_filaments.end(), i + 1) != left_filaments.end()) {
m_filament_map[i] = 1;
} else if (std::find(right_filaments.begin(), right_filaments.end(), i + 1) != right_filaments.end()) {
m_filament_map[i] = 2;
}
}
m_filament_map = m_manual_map_panel->GetFilamentMaps();
m_filament_volume_map = m_manual_map_panel->GetFilamentVolumeMaps();
}
EndModal(wxID_OK);
@@ -398,6 +425,11 @@ void FilamentMapDialog::update_panel_status(PageType page)
m_auto_map_panel->Show();
}
// The nozzle-availability gate only constrains manual grouping; every other page must
// leave OK usable even if the manual page had disabled it.
if (page != PageType::ptManual && m_ok_btn && !m_ok_btn->IsEnabled())
m_ok_btn->Enable();
if (m_smart_filament)
m_smart_filament->Show(get_mode() == fmmAutoForFlush);

View File

@@ -43,6 +43,7 @@ public:
const std::vector<std::string>& filament_color,
const std::vector<std::string>& filament_type,
const std::vector<int> &filament_map,
const std::vector<int> &filament_volume_map,
const std::vector<int> &filaments,
const FilamentMapMode mode,
bool machine_synced,
@@ -57,6 +58,12 @@ public:
return {};
}
std::vector<int> get_filament_volume_maps() const {
if (m_page_type == PageType::ptManual)
return m_filament_volume_map;
return {};
}
int ShowModal();
void set_modal_btn_labels(const wxString& left_label, const wxString& right_label);
private:
@@ -87,6 +94,7 @@ private:
private:
std::vector<int> m_filament_map;
std::vector<int> m_filament_volume_map;
std::vector<std::string> m_filament_color;
std::vector<std::string> m_filament_type;
};

View File

@@ -1,5 +1,6 @@
#include "FilamentMapPanel.hpp"
#include "GUI_App.hpp"
#include "Plater.hpp"
#include "Widgets/MultiNozzleSync.hpp" // manuallySetNozzleCount producer for extruder_nozzle_stats
#include <algorithm>
#include <wx/dcbuffer.h>
@@ -19,28 +20,278 @@ static const wxColour BorderDisableColor = wxColour("#EEEEEE");
static const wxColour TextNormalBlackColor = wxColour("#262E30");
static const wxColour TextNormalGreyColor = wxColour("#6B6B6B");
static const wxColour TextDisableColor = wxColour("#CECECE");
static const wxColour TextErrorColor = wxColour("#E14747");
wxDEFINE_EVENT(wxEVT_INVALID_MANUAL_MAP, wxCommandEvent);
// Orca: whether the edited printer has an extruder that can physically carry several nozzles
// (only such extruders track a per-volume-type nozzle inventory worth validating against).
static bool printer_has_multi_nozzle_extruder()
{
auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count");
// Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate.
return max_nozzle_counts_opt &&
std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(),
[](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); });
}
void FilamentMapManualPanel::OnTimer(wxTimerEvent &)
{
bool valid = true;
int invalid_eid = -1;
NozzleVolumeType invalid_nozzle = NozzleVolumeType::nvtStandard;
auto preset_bundle = wxGetApp().preset_bundle;
auto proj_config = preset_bundle->project_config;
auto nozzle_volume_values = proj_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
std::vector<int> filament_map = GetFilamentMaps();
std::vector<int> filament_volume_map = GetFilamentVolumeMaps();
// Orca: only multi-nozzle extruders carry a meaningful nozzle inventory; validating a plain
// dual-extruder printer against it would flag every grouping whenever the stats are stale.
if (printer_has_multi_nozzle_extruder()) {
for (size_t eid = 0; eid < nozzle_volume_values.size(); ++eid) {
NozzleVolumeType extruder_volume_type = NozzleVolumeType(nozzle_volume_values[eid]);
bool extruder_used = std::find_if(m_filament_list.begin(), m_filament_list.end(),
[&filament_map, eid](int fid) {
return fid - 1 < (int) filament_map.size() && (filament_map[fid - 1] - 1) == (int) eid;
}) != m_filament_list.end();
if (!extruder_used) {
continue;
}
if (extruder_volume_type == nvtHybrid) {
int standard_count = getExtruderNozzleCount(preset_bundle, eid, NozzleVolumeType::nvtStandard);
int highflow_count = getExtruderNozzleCount(preset_bundle, eid, NozzleVolumeType::nvtHighFlow);
auto has_material_of_type = [this, eid, &filament_map, &filament_volume_map](NozzleVolumeType volume_type) {
return std::find_if(m_filament_list.begin(), m_filament_list.end(),
[eid, &filament_map, &filament_volume_map, volume_type](int fid) {
return fid - 1 < (int) filament_map.size() && (filament_map[fid - 1] - 1) == (int) eid &&
fid - 1 < (int) filament_volume_map.size() &&
filament_volume_map[fid - 1] == static_cast<int>(volume_type);
}) != m_filament_list.end();
};
bool has_standard = has_material_of_type(NozzleVolumeType::nvtStandard);
bool has_highflow = has_material_of_type(NozzleVolumeType::nvtHighFlow);
if ((has_standard && standard_count == 0) ||
(has_highflow && highflow_count == 0)) {
valid = false;
invalid_eid = eid;
invalid_nozzle = (has_standard && standard_count == 0) ? NozzleVolumeType::nvtStandard : NozzleVolumeType::nvtHighFlow;
break;
}
} else {
int count = getExtruderNozzleCount(preset_bundle, eid, extruder_volume_type);
if (count == 0) {
valid = false;
invalid_eid = eid;
invalid_nozzle = extruder_volume_type;
break;
}
}
}
}
bool update_ui = m_invalid_id != invalid_eid;
bool send_event = update_ui || m_force_validation;
m_invalid_id = invalid_eid;
if (update_ui) {
if (valid) {
m_errors->Hide();
m_suggestion_panel->Hide();
} else {
m_errors->SetLabel(wxString::Format(_L("Error: %s extruder has no available %s nozzle, current group result is invalid."),
invalid_eid == 0 ? _L("Left") : _L("Right"),
invalid_nozzle == NozzleVolumeType::nvtStandard ? _L("Standard") : _L("High Flow")));
// Re-wrap: wrapping only applies to the label text present when Wrap is called,
// and the label was empty at construction time.
m_errors->Wrap(FromDIP(520));
m_errors->Show();
m_suggestion_panel->Show();
}
m_left_panel->Freeze();
m_right_panel->Freeze();
m_tips->Freeze();
m_description->Freeze();
Layout();
Fit();
this->GetParent()->Layout();
this->GetParent()->Fit();
m_left_panel->Thaw();
m_right_panel->Thaw();
m_tips->Thaw();
m_description->Thaw();
}
if (send_event) {
wxCommandEvent event(wxEVT_INVALID_MANUAL_MAP);
event.SetInt(valid);
ProcessEvent(event);
m_force_validation = false;
}
}
void FilamentMapManualPanel::OnSuggestionClicked(wxCommandEvent &event)
{
wxWindow *current = this;
while (current && !wxDynamicCast(current, wxDialog)) {
current = current->GetParent();
}
if (current) {
wxDialog *dlg = wxDynamicCast(current, wxDialog);
if (dlg) {
int invalid_eid = m_invalid_id;
dlg->EndModal(wxID_CANCEL);
if (invalid_eid >= 0) {
manuallySetNozzleCount(invalid_eid);
}
wxGetApp().plater()->update();
}
}
}
std::vector<int> FilamentMapManualPanel::GetFilamentMaps() const
{
std::vector<int> new_filament_map = m_filament_map;
std::vector<int> left_filaments = this->GetLeftFilaments();
std::vector<int> right_filaments = this->GetRightFilaments();
for (int i = 0; i < (int) new_filament_map.size(); ++i) {
if (std::find(left_filaments.begin(), left_filaments.end(), i + 1) != left_filaments.end()) {
new_filament_map[i] = 1;
} else if (std::find(right_filaments.begin(), right_filaments.end(), i + 1) != right_filaments.end()) {
new_filament_map[i] = 2;
}
}
return new_filament_map;
}
std::vector<int> FilamentMapManualPanel::GetFilamentVolumeMaps() const
{
std::vector<int> volume_map(m_filament_map.size(), 0);
std::vector<int> left_filaments = this->GetLeftFilaments();
std::vector<int> right_high_flow_filaments = this->GetRightHighFlowFilaments();
std::vector<int> right_standard_filaments = this->GetRightStandardFilaments();
std::vector<int> right_tpu_high_flow_filaments = this->GetRightTPUHighFlowFilaments();
auto preset_bundle = wxGetApp().preset_bundle;
auto proj_config = preset_bundle->project_config;
auto nozzle_volume_values = proj_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
for (int i = 0; i < (int) volume_map.size(); ++i) {
int filament_id = i + 1;
if (std::find(left_filaments.begin(), left_filaments.end(), filament_id) != left_filaments.end()) {
if (nozzle_volume_values.size() > 0) {
// Orca: never emit the Hybrid marker as a per-filament value; on a Hybrid
// extruder each filament prints with a concrete flow, defaulting to Standard.
volume_map[i] = nozzle_volume_values[0] == static_cast<int>(NozzleVolumeType::nvtHybrid) ?
static_cast<int>(NozzleVolumeType::nvtStandard) :
nozzle_volume_values[0];
}
}
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<int>(NozzleVolumeType::nvtHighFlow);
}
else if (std::find(right_standard_filaments.begin(), right_standard_filaments.end(), filament_id) != right_standard_filaments.end()) {
volume_map[i] = static_cast<int>(NozzleVolumeType::nvtStandard);
}
else if (std::find(right_tpu_high_flow_filaments.begin(), right_tpu_high_flow_filaments.end(), filament_id) != right_tpu_high_flow_filaments.end()) {
volume_map[i] = static_cast<int>(NozzleVolumeType::nvtTPUHighFlow);
}
}
return volume_map;
}
void FilamentMapManualPanel::SyncPanelHeights()
{
if (!m_left_panel || !m_right_panel) return;
auto curr_left = m_left_panel->GetMinSize();
auto curr_right = m_right_panel->GetMinSize();
m_left_panel->SetMinSize(wxSize(FromDIP(260), FromDIP(110)));
m_right_panel->SetMinSize(wxSize(FromDIP(260), FromDIP(110)));
m_left_panel->Layout();
m_left_panel->Fit();
m_right_panel->Layout();
m_right_panel->Fit();
wxSize left_best_size = m_left_panel->GetBestSize();
wxSize right_best_size = m_right_panel->GetBestSize();
int max_height = std::max(left_best_size.GetHeight(), right_best_size.GetHeight());
bool height_changed = curr_left.GetHeight() != max_height || curr_right.GetHeight() != max_height;
if (!height_changed) {
if (curr_left.GetHeight() > 0)
m_left_panel->SetMinSize(curr_left);
if (curr_right.GetHeight() > 0)
m_right_panel->SetMinSize(curr_right);
if (GetParent()) {
GetParent()->Layout();
GetParent()->Fit();
}
return;
}
m_left_panel->SetMinSize(wxSize(FromDIP(260), max_height));
m_right_panel->SetMinSize(wxSize(FromDIP(260), max_height));
Layout();
Fit();
if (GetParent()) {
GetParent()->Layout();
GetParent()->Fit();
}
}
void FilamentMapManualPanel::OnDragDropCompleted(wxCommandEvent &event)
{
SyncPanelHeights();
event.Skip();
}
FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *parent,
const std::vector<std::string> &color,
const std::vector<std::string> &type,
const std::vector<int> &filament_list,
const std::vector<int> &filament_map)
: wxPanel(parent), m_filament_map(filament_map), m_filament_color(color), m_filament_type(type), m_filament_list(filament_list)
const std::vector<int> &filament_map,
const std::vector<int> &filament_volume_map)
: wxPanel(parent)
, m_filament_map(filament_map)
, m_filament_volume_map(filament_volume_map)
, m_filament_list(filament_list)
, m_filament_color(color)
, m_filament_type(type)
{
SetName(wxT("FilamentMapManualPanel"));
SetBackgroundColour(BgNormalColor);
auto top_sizer = new wxBoxSizer(wxVERTICAL);
m_description = new Label(this, _L("We will slice according to this grouping method:"));
top_sizer->Add(m_description, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15));
m_description->Wrap(FromDIP(520));
top_sizer->AddSpacer(FromDIP(8));
auto drag_sizer = new wxBoxSizer(wxHORIZONTAL);
m_left_panel = new DragDropPanel(this, _L("Left Nozzle"), false);
m_right_panel = new DragDropPanel(this, _L("Right Nozzle"), false);
m_right_panel = new SeparatedDragDropPanel(this, _L("Right Nozzle"), false);
m_switch_btn = new ScalableButton(this, wxID_ANY, "switch_filament_maps");
UpdateNozzleVolumeType();
for (size_t idx = 0; idx < m_filament_map.size(); ++idx) {
auto iter = std::find(m_filament_list.begin(), m_filament_list.end(), idx + 1);
if (iter == m_filament_list.end()) continue;
@@ -50,29 +301,55 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p
m_left_panel->AddColorBlock(color, type, idx + 1);
} else {
assert(m_filament_map[idx] == 2);
m_right_panel->AddColorBlock(color, type, idx + 1);
bool is_high_flow = (idx < m_filament_volume_map.size()) && (m_filament_volume_map[idx] == static_cast<int>(NozzleVolumeType::nvtHighFlow));
m_right_panel->AddColorBlock(color, type, idx + 1, is_high_flow);
}
}
m_left_panel->SetMinSize({ FromDIP(260),-1 });
m_right_panel->SetMinSize({ FromDIP(260),-1 });
m_left_panel->SetMinSize({FromDIP(260), FromDIP(110)});
m_right_panel->SetMinSize({FromDIP(260), FromDIP(110)});
drag_sizer->AddStretchSpacer();
drag_sizer->Add(m_left_panel, 1, wxEXPAND);
drag_sizer->AddSpacer(FromDIP(7));
drag_sizer->Add(m_switch_btn, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(1));
drag_sizer->AddSpacer(FromDIP(7));
drag_sizer->Add(m_switch_btn, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(8));
drag_sizer->Add(m_right_panel, 1, wxEXPAND);
drag_sizer->AddStretchSpacer();
top_sizer->Add(drag_sizer, 0, wxEXPAND);
m_tips = new Label(this, _L("Tip: You can drag the filaments to reassign them to different nozzles."));
m_tips->SetFont(Label::Body_14);
m_tips->SetForegroundColour(TextNormalGreyColor);
m_tips->Wrap(FromDIP(520));
top_sizer->AddSpacer(FromDIP(20));
top_sizer->Add(m_tips, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15));
m_switch_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSwitchFilament, this);
m_errors = new Label(this, "");
m_errors->SetFont(Label::Body_13);
m_errors->SetForegroundColour(TextErrorColor);
m_errors->Wrap(FromDIP(520));
top_sizer->AddSpacer(FromDIP(10));
top_sizer->Add(m_errors, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15));
m_errors->Hide();
m_suggestion_panel = new wxPanel(this, wxID_ANY);
m_suggestion_panel->SetBackgroundColour(*wxWHITE);
auto suggestion_sizer = new wxBoxSizer(wxHORIZONTAL);
auto suggestion_text = new Label(m_suggestion_panel, _L("Please adjust your grouping or click "));
suggestion_text->SetFont(Label::Body_13);
suggestion_text->SetForegroundColour(TextErrorColor);
suggestion_text->SetBackgroundColour(*wxWHITE);
auto suggestion_btn = new ScalableButton(m_suggestion_panel, wxID_ANY, "edit", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 14);
suggestion_btn->SetBackgroundColour(*wxWHITE);
auto suggestion_text2 = new Label(m_suggestion_panel, _L(" to set nozzle count"));
suggestion_text2->SetFont(Label::Body_13);
suggestion_text2->SetForegroundColour(TextErrorColor);
suggestion_text2->SetBackgroundColour(*wxWHITE);
suggestion_sizer->Add(suggestion_text, 0, wxALIGN_CENTER_VERTICAL);
suggestion_sizer->Add(suggestion_btn, 0, wxALIGN_CENTER_VERTICAL);
suggestion_sizer->Add(suggestion_text2, 0, wxALIGN_CENTER_VERTICAL);
m_suggestion_panel->SetSizer(suggestion_sizer);
top_sizer->Add(m_suggestion_panel, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15));
m_suggestion_panel->Hide();
suggestion_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSuggestionClicked, this);
// Multi-nozzle: give the user a reachable way to declare, per extruder, how many
// physical nozzles of each volume type a multi-nozzle extruder carries. This is the
@@ -80,11 +357,8 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p
// auto-sync is deferred). Gated on the edited printer preset having an extruder with
// extruder_max_nozzle_count > 1, so the trigger is not even created for any single-nozzle or
// dual-extruder ({1,1}, H2D) printer - zero UI change for every existing profile.
auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count");
// Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate.
if (max_nozzle_counts_opt &&
std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(),
[](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); })) {
if (printer_has_multi_nozzle_extruder()) {
auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionIntsNullable>("extruder_max_nozzle_count");
auto *set_count_link = new Label(this, _L("Set the physical nozzle count..."));
set_count_link->SetFont(Label::Body_14);
set_count_link->SetForegroundColour(BorderSelectedColor);
@@ -94,26 +368,88 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p
const std::vector<int> max_counts = max_nozzle_counts_opt->values;
set_count_link->Bind(wxEVT_LEFT_DOWN, [max_counts](wxMouseEvent &evt) {
for (int extruder_id = 0; extruder_id < (int) max_counts.size(); ++extruder_id) {
if (max_counts[extruder_id] > 1)
if (max_counts[extruder_id] > 1 && max_counts[extruder_id] != ConfigOptionIntsNullable::nil_value())
GUI::manuallySetNozzleCount(extruder_id);
}
evt.Skip();
});
}
m_timer = new wxTimer(this);
Bind(wxEVT_TIMER, &FilamentMapManualPanel::OnTimer, this);
Bind(wxEVT_DRAG_DROP_COMPLETED, &FilamentMapManualPanel::OnDragDropCompleted, this);
m_switch_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSwitchFilament, this);
SetSizer(top_sizer);
SetMinSize(wxSize(FromDIP(580), -1));
Layout();
Fit();
GUI::wxGetApp().UpdateDarkUIWin(this);
}
void FilamentMapManualPanel::UpdateNozzleVolumeType()
{
auto check_separation = []() {
auto preset_bundle = wxGetApp().preset_bundle;
auto nozzle_volume_values = preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
if (nozzle_volume_values.size() <= 1)
return false;
return nozzle_volume_values[1] == static_cast<int>(NozzleVolumeType::nvtHybrid);
};
bool should_separate = check_separation();
m_right_panel->SetUseSeparation(should_separate);
UpdateNozzleCountDisplay();
Layout();
Fit();
}
void FilamentMapManualPanel::UpdateNozzleCountDisplay()
{
auto preset_bundle = wxGetApp().preset_bundle;
// Orca: nozzle counts are only tracked (and only meaningful) for multi-nozzle extruders;
// plain dual-extruder printers keep their unadorned zone titles.
if (!printer_has_multi_nozzle_extruder()) {
m_left_panel->UpdateLabel(_L("Left Nozzle"));
m_right_panel->UpdateLabel(_L("Right Nozzle"));
return;
}
// Format the count suffix separately so a translation containing '%' cannot
// corrupt the wxString::Format output.
int left_count = getExtruderNozzleCountTotal(preset_bundle, 0);
wxString left_title = _L("Left Nozzle") + wxString::Format("(%d)", left_count);
m_left_panel->UpdateLabel(left_title);
if (m_right_panel->IsUseSeparation()) {
int standard_count = getExtruderNozzleCount(preset_bundle, 1, NozzleVolumeType::nvtStandard);
int highflow_count = getExtruderNozzleCount(preset_bundle, 1, NozzleVolumeType::nvtHighFlow);
wxString right_title = _L("Right Nozzle") + wxString::Format("(Std: %d, HF: %d)", standard_count, highflow_count);
m_right_panel->UpdateLabel(right_title);
} else {
int right_count = getExtruderNozzleCountTotal(preset_bundle, 1);
wxString right_title = _L("Right Nozzle") + wxString::Format("(%d)", right_count);
m_right_panel->UpdateLabel(right_title);
}
}
FilamentMapManualPanel::~FilamentMapManualPanel()
{
m_timer->Stop();
delete m_timer;
}
void FilamentMapManualPanel::OnSwitchFilament(wxCommandEvent &)
{
auto left_blocks = m_left_panel->get_filament_blocks();
auto right_blocks = m_right_panel->get_filament_blocks();
for (auto &block : left_blocks) {
m_right_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false);
m_right_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false, false);
m_left_panel->RemoveColorBlock(block, false);
}
@@ -123,22 +459,27 @@ void FilamentMapManualPanel::OnSwitchFilament(wxCommandEvent &)
}
this->GetParent()->Layout();
this->GetParent()->Fit();
if (m_right_panel->IsUseSeparation()) {
m_left_panel->Layout();
m_left_panel->Fit();
m_right_panel->Layout();
m_right_panel->Fit();
SyncPanelHeights();
}
}
void FilamentMapManualPanel::Hide()
bool FilamentMapManualPanel::Show(bool show)
{
m_left_panel->Hide();
m_right_panel->Hide();
m_switch_btn->Hide();
wxPanel::Hide();
}
m_force_validation = show;
if (show) {
m_timer->Start(500);
SyncPanelHeights();
} else {
m_timer->Stop();
}
void FilamentMapManualPanel::Show()
{
m_left_panel->Show();
m_right_panel->Show();
m_switch_btn->Show();
wxPanel::Show();
return wxPanel::Show(show);
}
GUI::FilamentMapBtnPanel::FilamentMapBtnPanel(wxWindow *parent, const wxString &label, const wxString &detail, const std::string &icon) : wxPanel(parent)

View File

@@ -9,32 +9,60 @@
namespace Slic3r { namespace GUI {
// Fired by the manual panel's validation timer: the event int is 1 when the current manual
// grouping is printable with the installed nozzles, 0 when some zone has no matching nozzle.
wxDECLARE_EVENT(wxEVT_INVALID_MANUAL_MAP, wxCommandEvent);
class FilamentMapManualPanel : public wxPanel
{
public:
FilamentMapManualPanel(wxWindow *parent, const std::vector<std::string> &color, const std::vector<std::string> &type, const std::vector<int> &filament_list, const std::vector<int> &filament_map);
FilamentMapManualPanel(wxWindow *parent,
const std::vector<std::string> &color,
const std::vector<std::string> &type,
const std::vector<int> &filament_list,
const std::vector<int> &filament_map,
const std::vector<int> &filament_volume_map);
~FilamentMapManualPanel();
std::vector<int> GetFilamentMaps() const { return m_filament_map; }
std::vector<int> GetFilamentMaps() const;
std::vector<int> GetFilamentVolumeMaps() const;
std::vector<int> GetLeftFilaments() const { return m_left_panel->GetAllFilaments(); }
std::vector<int> GetRightFilaments() const { return m_right_panel->GetAllFilaments(); }
void Hide();
void Show();
std::vector<int> GetRightHighFlowFilaments() const { return m_right_panel->GetHighFlowFilaments(); }
std::vector<int> GetRightStandardFilaments() const { return m_right_panel->GetStandardFilaments(); }
std::vector<int> GetRightTPUHighFlowFilaments() const { return m_right_panel->GetTPUHighFlowFilaments(); }
void UpdateNozzleVolumeType();
void UpdateNozzleCountDisplay();
bool Show(bool show = true) override;
private:
void OnSwitchFilament(wxCommandEvent &);
DragDropPanel *m_left_panel;
DragDropPanel *m_right_panel;
void OnTimer(wxTimerEvent &evt);
void OnSwitchFilament(wxCommandEvent &);
void SyncPanelHeights();
void OnDragDropCompleted(wxCommandEvent &evt);
void OnSuggestionClicked(wxCommandEvent &event);
Label *m_description;
Label *m_tips;
DragDropPanel *m_left_panel;
SeparatedDragDropPanel *m_right_panel;
Label *m_description;
Label *m_tips;
Label *m_errors;
wxPanel *m_suggestion_panel;
ScalableButton *m_switch_btn;
std::vector<int> m_filament_map;
std::vector<int> m_filament_volume_map;
std::vector<int> m_filament_list;
std::vector<std::string> m_filament_color;
std::vector<std::string> m_filament_type;
wxTimer *m_timer;
int m_invalid_id{-1};
bool m_force_validation{false};
};
class FilamentMapBtnPanel : public wxPanel

View File

@@ -3894,6 +3894,16 @@ void PartPlate::set_filament_count(int filament_count)
std::vector<int>& filament_maps = m_config.option<ConfigOptionInts>("filament_map")->values;
filament_maps.resize(filament_count, 1);
}
if (m_config.has("filament_nozzle_map")) {
std::vector<int>& filament_nozzle_map = m_config.option<ConfigOptionInts>("filament_nozzle_map")->values;
filament_nozzle_map.resize(filament_count, 0);
}
if (m_config.has("filament_volume_map")) {
std::vector<int>& filament_volume_map = m_config.option<ConfigOptionInts>("filament_volume_map")->values;
filament_volume_map.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
}
void PartPlate::on_filament_added()
@@ -3902,6 +3912,27 @@ void PartPlate::on_filament_added()
std::vector<int>& filament_maps = m_config.option<ConfigOptionInts>("filament_map")->values;
filament_maps.push_back(1);
}
if (m_config.has("filament_nozzle_map")) {
std::vector<int>& filament_nozzle_map = m_config.option<ConfigOptionInts>("filament_nozzle_map")->values;
filament_nozzle_map.push_back(0);
}
if (m_config.has("filament_volume_map")) {
std::vector<int>& filament_volume_map = m_config.option<ConfigOptionInts>("filament_volume_map")->values;
// A new filament defaults onto the first extruder, so seed its volume value from
// that extruder's flow type.
int volume_type = static_cast<int>(NozzleVolumeType::nvtStandard);
auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type");
if (nozzle_volumes && !nozzle_volumes->values.empty())
volume_type = nozzle_volumes->values[0];
// Orca: never store the Hybrid marker as a per-filament value; on a Hybrid extruder
// each filament still prints with a concrete flow, defaulting to Standard.
if (volume_type == static_cast<int>(NozzleVolumeType::nvtHybrid))
volume_type = static_cast<int>(NozzleVolumeType::nvtStandard);
filament_volume_map.push_back(volume_type);
}
}
void PartPlate::on_filament_deleted(int filament_count, int filament_id)
@@ -3914,6 +3945,19 @@ void PartPlate::on_filament_deleted(int filament_count, int filament_id)
if (filament_id >= 0 && filament_id < (int) filament_maps.size())
filament_maps.erase(filament_maps.begin() + filament_id);
}
if (m_config.has("filament_nozzle_map")) {
std::vector<int>& filament_nozzle_map = m_config.option<ConfigOptionInts>("filament_nozzle_map")->values;
if (filament_id >= 0 && filament_id < (int) filament_nozzle_map.size())
filament_nozzle_map.erase(filament_nozzle_map.begin() + filament_id);
}
if (m_config.has("filament_volume_map")) {
std::vector<int>& filament_volume_map = m_config.option<ConfigOptionInts>("filament_volume_map")->values;
if (filament_id >= 0 && filament_id < (int) filament_volume_map.size())
filament_volume_map.erase(filament_volume_map.begin() + filament_id);
}
update_first_layer_print_sequence_when_delete_filament(filament_id);
}

View File

@@ -1213,9 +1213,16 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
combo_flow->GetDropDown().SetUseContentWidth(true);
combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent &evt) {
auto printer_tab = dynamic_cast<TabPrinter *>(wxGetApp().get_tab(Preset::TYPE_PRINTER));
printer_tab->set_extruder_volume_type(index, NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))));
if (GUI::wxGetApp().plater())
GUI::wxGetApp().plater()->update_machine_sync_status();
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<int>(volume_type));
plater->update_machine_sync_status();
}
});
this->combo_flow = combo_flow;
@@ -7328,6 +7335,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
if (filament_nozzle_map->size() != filament_count) {
filament_nozzle_map->values.resize(filament_count, 0);
}
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
if (filament_multi_color->size() != filament_count) {
@@ -17799,12 +17816,24 @@ void Plater::set_global_filament_map(const std::vector<int>& filament_map)
project_config.option<ConfigOptionInts>("filament_map")->values = filament_map;
}
void Plater::set_global_filament_volume_map(const std::vector<int>& filament_volume_map)
{
auto& project_config = wxGetApp().preset_bundle->project_config;
project_config.option<ConfigOptionInts>("filament_volume_map")->values = filament_volume_map;
}
std::vector<int> Plater::get_global_filament_map() const
{
auto& project_config = wxGetApp().preset_bundle->project_config;
return project_config.option<ConfigOptionInts>("filament_map")->values;
}
std::vector<int> Plater::get_global_filament_volume_map() const
{
auto& project_config = wxGetApp().preset_bundle->project_config;
return project_config.option<ConfigOptionInts>("filament_volume_map")->values;
}
FilamentMapMode Plater::get_global_filament_map_mode() const
{
@@ -18721,13 +18750,17 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt)
auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config);
auto plate_filament_map_mode = curr_plate->get_filament_map_mode();
auto plate_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config);
if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config
plate_filament_maps.resize(filament_colors.size(), 1);
if (plate_filament_volume_maps.size() != filament_colors.size())
plate_filament_volume_maps.resize(filament_colors.size(), 0);
FilamentMapDialog filament_dlg(this,
filament_colors,
filament_types,
plate_filament_maps,
plate_filament_volume_maps,
curr_plate->get_extruders(true),
plate_filament_map_mode,
this->get_machine_sync_status(),
@@ -18741,16 +18774,21 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt)
FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode();
FilamentMapMode new_map_mode = filament_dlg.get_mode();
std::vector<int> new_filament_volume_maps = filament_dlg.get_filament_volume_maps();
std::vector<int> old_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config);
if (new_map_mode != old_map_mode) {
curr_plate->set_filament_map_mode(new_map_mode);
}
if (new_map_mode == fmmManual){
curr_plate->set_filament_maps(new_filament_maps);
curr_plate->set_filament_volume_maps(new_filament_volume_maps);
}
bool need_invalidate = (old_map_mode != new_map_mode ||
old_filament_maps != new_filament_maps);
old_filament_maps != new_filament_maps ||
old_filament_volume_maps != new_filament_volume_maps);
if (need_invalidate) {
if (need_slice) {
@@ -19216,6 +19254,30 @@ bool Plater::get_machine_sync_status()
return p->get_machine_sync_status();
}
void Plater::update_filament_volume_map(int extruder_id, int volume_type)
{
// Hybrid is a per-extruder mix, not a per-filament volume; reset the affected filaments
// to Standard so the manual grouping dialog starts from a concrete assignment.
int selected_volume_type = volume_type == static_cast<int>(NozzleVolumeType::nvtHybrid) ? static_cast<int>(NozzleVolumeType::nvtStandard) : volume_type;
auto& partplate_list = get_partplate_list();
for (int idx = 0; idx < partplate_list.get_plate_count(); ++idx) {
auto plate = partplate_list.get_plate(idx);
if (!plate) continue;
auto filament_map = plate->get_filament_maps();
auto filament_volume_map = plate->get_filament_volume_maps();
if (filament_map.empty() || filament_volume_map.empty()) continue;
if (filament_volume_map.size() < filament_map.size()) {
filament_volume_map.resize(filament_map.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
}
for (size_t i = 0; i < filament_map.size(); ++i) {
if (filament_map[i] == extruder_id + 1) {
filament_volume_map[i] = selected_volume_type;
}
}
plate->set_filament_volume_maps(filament_volume_map);
}
}
#if ENABLE_ENVIRONMENT_MAP
void Plater::init_environment_texture()
{

View File

@@ -586,7 +586,9 @@ public:
void set_global_filament_map_mode(FilamentMapMode mode);
void set_global_filament_map(const std::vector<int>& filament_map);
void set_global_filament_volume_map(const std::vector<int>& filament_volume_map);
std::vector<int> get_global_filament_map() const;
std::vector<int> get_global_filament_volume_map() const;
FilamentMapMode get_global_filament_map_mode() const;
void update_menus();
@@ -748,6 +750,11 @@ public:
void update_machine_sync_status();
// Rewrite every plate's per-filament volume choice for the filaments grouped onto this
// extruder after its Flow type changed (Hybrid resets them to Standard so the user
// re-assigns concrete volumes in the grouping dialog).
void update_filament_volume_map(int extruder_id, int volume_type);
#if ENABLE_ENVIRONMENT_MAP
void init_environment_texture();
unsigned int get_environment_texture_id() const;

View File

@@ -35,7 +35,7 @@ static const int RightExtruderIdx = 1;
// config key (ConfigOptionStrings, one encoded map per extruder) via get_extruder_nozzle_stats /
// save_extruder_nozzle_stats_to_string. These helpers read and write that config.
static int extruder_nozzle_count(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type)
int getExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type)
{
if (!preset_bundle)
return 0;
@@ -49,7 +49,7 @@ static int extruder_nozzle_count(PresetBundle *preset_bundle, int extruder_id, N
return it == stats[extruder_id].end() ? 0 : it->second;
}
static int extruder_nozzle_count_total(PresetBundle *preset_bundle, int extruder_id)
int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id)
{
if (!preset_bundle)
return 0;
@@ -230,8 +230,8 @@ void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, Nozz
int display_count = -1; // hides the badge
if (support_multi_nozzle)
display_count = volume_type == nvtHybrid ? extruder_nozzle_count_total(preset_bundle, extruder_id)
: extruder_nozzle_count(preset_bundle, extruder_id, volume_type);
display_count = volume_type == nvtHybrid ? getExtruderNozzleCountTotal(preset_bundle, extruder_id)
: getExtruderNozzleCount(preset_bundle, extruder_id, volume_type);
plater->sidebar().set_extruder_nozzle_count(extruder_id, display_count);
}
@@ -269,7 +269,7 @@ void onNozzleVolumeTypeSwitch(PresetBundle *preset_bundle, int extruder_id, Nozz
// Hybrid is a mix, not a type every nozzle shares, so there is nothing to carry over.
if (type == nvtHybrid)
return;
const int total = extruder_nozzle_count_total(preset_bundle, extruder_id);
const int total = getExtruderNozzleCountTotal(preset_bundle, extruder_id);
setExtruderNozzleCount(preset_bundle, extruder_id, type, total, true);
}
@@ -294,14 +294,14 @@ void manuallySetNozzleCount(int extruder_id)
return;
const NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id]);
const int standard_count = extruder_nozzle_count(preset_bundle, extruder_id, nvtStandard);
const int highflow_count = extruder_nozzle_count(preset_bundle, extruder_id, nvtHighFlow);
const int standard_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard);
const int highflow_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow);
// Require at least one nozzle for a Hybrid extruder (an empty mix is meaningless) and when the other
// extruder currently has none.
bool force_no_zero = volume_type == nvtHybrid;
if (nozzle_volume_type_opt->values.size() > 1)
force_no_zero |= extruder_nozzle_count_total(preset_bundle, 1 - extruder_id) == 0;
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);
if (dialog.ShowModal() == wxID_OK) {

View File

@@ -219,6 +219,11 @@ std::optional<NozzleOption> tryPopUpMultiNozzleDialog(MachineObject* obj);
// counts are reset first.
void setExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType type, int nozzle_count, bool clear_all);
// Read one extruder's nozzle count for a volume type (or its total) from the edited printer preset's
// `extruder_nozzle_stats` config key. Returns 0 when the stats are absent or the extruder is unknown.
int getExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type);
int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id);
// Refresh the sidebar nozzle-count badge for one extruder: shows the extruder's physical nozzle count on
// multi-nozzle printers, hides it (count -1) everywhere else.
void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type);