Add Flashforge Adventurer 5 series local send workflow with IFS mapping (#12991)

* Add Flashforge AD5X local send dialog, IFS mapping, and LAN discovery

* Refine Flashforge AD5X IFS dialog behavior

* Refine Flashforge IFS slot selection dialog

* Fix Flashforge printer selection and print mapping

* Use 3MF for Flashforge local uploads

* Generalize Flashforge local API handling

* Handle Flashforge local API IFS support more robustly

* Use selected plate filament info for Flashforge IFS mapping

* Fix Flashforge current-plate mapping and widget sizing

* Improve Flashforge IFS contrast and color matching

* Fix Flashforge legacy plate export and upload naming

Resolve PLATE_CURRENT_IDX before the legacy send-to-printhost path calls send_gcode so single-plate Flashforge 3MF exports target the selected plate instead of leaking the sentinel into export_3mf.

Sanitize Flashforge upload names in one shared utility reused by both the dialog and the backend client. This keeps the UI-visible filename and the actual uploaded filename consistent and replaces printer-problematic characters such as '=' without scattering Flashforge-specific logic through the generic Plater flow.

* Keep Flashforge upload filename sanitization in the backend only

Drop the PrintHostSendDialog API changes and keep filename sanitization inside the Flashforge backend paths that actually talk to the printer. This keeps the generic send dialog flow untouched while still normalizing problematic upload names for both serial and local API uploads.

* Only use the Flashforge IFS dialog for local API uploads

* Use reported Flashforge IFS support without model fallback

* Remove unused Flashforge slot uniqueness tracking

* Include <array> for Flashforge discovery message
This commit is contained in:
Mariano Dupont
2026-05-27 12:03:44 -03:00
committed by GitHub
parent 9c63aee9f8
commit f118b6b337
9 changed files with 1626 additions and 25 deletions

View File

@@ -1,7 +1,9 @@
#include "PrintHostDialogs.hpp"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <limits>
#include <wx/frame.h>
#include <wx/progdlg.h>
@@ -11,6 +13,9 @@
#include <wx/checkbox.h>
#include <wx/button.h>
#include <wx/dataview.h>
#include <wx/dcbuffer.h>
#include <wx/graphics.h>
#include <wx/wrapsizer.h>
#include <wx/wupdlock.h>
#include <wx/debug.h>
#include <wx/msgdlg.h>
@@ -18,6 +23,8 @@
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/algorithm/string.hpp>
#include <nlohmann/json.hpp>
#include "GUI.hpp"
#include "GUI_App.hpp"
@@ -30,10 +37,383 @@
#include "format.hpp"
namespace fs = boost::filesystem;
using json = nlohmann::json;
namespace Slic3r {
namespace GUI {
namespace {
wxColour contrasting_text_color(const wxColour& background)
{
return background.GetLuminance() < 0.60 ? *wxWHITE : wxColour("#303030");
}
long long color_distance_sq(const wxColour& lhs, const wxColour& rhs)
{
const long long dr = static_cast<long long>(lhs.Red()) - static_cast<long long>(rhs.Red());
const long long dg = static_cast<long long>(lhs.Green()) - static_cast<long long>(rhs.Green());
const long long db = static_cast<long long>(lhs.Blue()) - static_cast<long long>(rhs.Blue());
return dr * dr + dg * dg + db * db;
}
class FlashforgeSlotCard : public wxPanel
{
public:
FlashforgeSlotCard(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
{
SetDoubleBuffered(true);
SetMinSize(wxSize(FromDIP(68), FromDIP(92)));
SetMaxSize(GetMinSize());
Bind(wxEVT_PAINT, &FlashforgeSlotCard::on_paint, this);
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) {
m_hover = true;
SetCursor(wxCursor(m_enabled ? wxCURSOR_HAND : wxCURSOR_NO_ENTRY));
Refresh();
e.Skip();
});
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) {
m_hover = false;
SetCursor(wxCursor(wxCURSOR_ARROW));
Refresh();
e.Skip();
});
Bind(wxEVT_LEFT_DOWN, &FlashforgeSlotCard::on_left_down, this);
}
void set_slot(const Slic3r::FlashforgeMaterialSlot& slot, bool enabled)
{
m_slot_id = slot.slot_id;
m_color = parse_color(slot.material_color);
m_name = slot.material_name.empty() ? _L("Unknown") : from_u8(slot.material_name);
m_empty = !slot.has_filament;
m_enabled = enabled && !m_empty;
Refresh();
}
private:
static wxColour parse_color(const std::string& raw)
{
wxColour color(from_u8(raw));
if (color.IsOk())
return color;
std::string value = raw;
boost::trim(value);
if (!value.empty() && value.front() != '#')
value.insert(value.begin(), '#');
color = wxColour(from_u8(value));
return color.IsOk() ? color : wxColour("#D0D0D0");
}
void on_left_down(wxMouseEvent& e)
{
if (!m_enabled)
return;
wxCommandEvent evt(wxEVT_BUTTON, GetId());
evt.SetInt(m_slot_id);
evt.SetString(m_color.GetAsString(wxC2S_HTML_SYNTAX));
wxPostEvent(this, evt);
e.Skip();
}
void on_paint(wxPaintEvent&)
{
wxPaintDC dc(this);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc == nullptr)
return;
const wxSize size = GetSize();
const int circle_size = FromDIP(24);
const int outline_width = (m_hover && m_enabled) ? FromDIP(2) : FromDIP(1);
const wxRect body_rect(FromDIP(7), FromDIP(26), size.x - FromDIP(14), size.y - FromDIP(32));
const wxColour badge_color = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)
: wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT);
const wxColour body_border = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)
: wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT);
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush((m_hover && m_enabled) ? badge_color.ChangeLightness(130) : badge_color));
gc->DrawEllipse((size.x - circle_size) / 2.0, 0, circle_size, circle_size);
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(*wxWHITE);
const wxString slot_txt = wxString::Format("%d", m_slot_id);
const wxSize slot_size = dc.GetTextExtent(slot_txt);
dc.DrawText(slot_txt, (size.x - slot_size.x) / 2, (circle_size - slot_size.y) / 2);
const wxColour bg_color = m_empty ? wxColour("#F6F6F6") : m_color;
gc->SetPen(wxPen(body_border, outline_width));
gc->SetBrush(wxBrush(bg_color));
gc->DrawRoundedRectangle(body_rect.x, body_rect.y, body_rect.width, body_rect.height, FromDIP(8));
dc.SetFont(::Label::Body_12);
dc.SetTextForeground(contrasting_text_color(bg_color));
wxString label = m_empty ? _L("Empty") : m_name;
if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8))
dc.SetFont(::Label::Body_10);
if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8)) {
while (!label.empty() && dc.GetTextExtent(label + "...").x > body_rect.width - FromDIP(8))
label.RemoveLast();
label += "...";
}
const wxSize label_size = dc.GetTextExtent(label);
dc.DrawText(label, body_rect.x + (body_rect.width - label_size.x) / 2, body_rect.y + (body_rect.height - label_size.y) / 2);
}
private:
int m_slot_id {0};
wxColour m_color {*wxWHITE};
wxString m_name;
bool m_empty {true};
bool m_enabled {false};
bool m_hover {false};
};
class FlashforgeSlotDialog : public DPIDialog
{
public:
FlashforgeSlotDialog(wxWindow* parent, const wxString& material_name)
: DPIDialog(parent, wxID_ANY, _L("Choose a slot for the selected color"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_material_name(material_name)
{
SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
auto* root = new wxBoxSizer(wxVERTICAL);
auto* title = new wxStaticText(this, wxID_ANY, _L("Material in the material station"));
title->SetFont(::Label::Head_13);
root->Add(title, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12));
m_grid = new wxGridSizer(1, 4, FromDIP(10), FromDIP(12));
auto* grid_row = new wxBoxSizer(wxHORIZONTAL);
grid_row->AddStretchSpacer();
grid_row->Add(m_grid, 0);
grid_row->AddStretchSpacer();
root->Add(grid_row, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(18));
auto* tip = new wxStaticText(this, wxID_ANY, _L("Only materials of the same type can be selected."));
tip->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT));
root->Add(tip, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12));
SetSizer(root);
SetMinSize(wxSize(FromDIP(460), FromDIP(240)));
for (int i = 0; i < 4; ++i) {
auto* card = new FlashforgeSlotCard(this);
card->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
m_selected_slot_id = e.GetInt();
m_selected_color = wxColour(e.GetString());
EndModal(wxID_OK);
});
m_cards.push_back(card);
m_grid->Add(card, 0);
}
wxGetApp().UpdateDlgDarkUI(this);
Layout();
Fit();
CenterOnParent();
Refresh();
}
void update_slots(const std::vector<Slic3r::FlashforgeMaterialSlot>& slots, const std::function<bool(const Slic3r::FlashforgeMaterialSlot&)>& matcher)
{
for (size_t i = 0; i < m_cards.size(); ++i) {
Slic3r::FlashforgeMaterialSlot slot;
slot.slot_id = static_cast<int>(i) + 1;
if (const auto it = std::find_if(slots.begin(), slots.end(), [&](const Slic3r::FlashforgeMaterialSlot& item) { return item.slot_id == slot.slot_id; }); it != slots.end())
slot = *it;
const bool enabled = slot.has_filament && matcher(slot);
m_cards[i]->set_slot(slot, enabled);
}
Layout();
Fit();
}
bool has_selection() const { return m_selected_slot_id > 0; }
int selected_slot_id() const { return m_selected_slot_id; }
wxColour selected_color() const { return m_selected_color; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override
{
Fit();
Refresh();
if (suggested_rect.IsEmpty())
return;
SetSize(suggested_rect.GetSize());
}
private:
wxString m_material_name;
wxGridSizer* m_grid {nullptr};
std::vector<FlashforgeSlotCard*> m_cards;
int m_selected_slot_id {0};
wxColour m_selected_color;
};
class FlashforgeMaterialMapWidget : public wxPanel
{
public:
using SelectFn = std::function<void(FlashforgeMaterialMapWidget*)>;
FlashforgeMaterialMapWidget(wxWindow* parent, int tool_id, const wxColour& color, const wxString& material_name, SelectFn on_select)
: wxPanel(parent, wxID_ANY)
, m_tool_id(tool_id)
, m_color(color)
, m_name(material_name.Strip())
, m_select_fn(std::move(on_select))
{
SetDoubleBuffered(true);
const wxSize size(FromDIP(72), FromDIP(58));
SetSize(size);
SetMinSize(size);
SetMaxSize(size);
Bind(wxEVT_PAINT, &FlashforgeMaterialMapWidget::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &FlashforgeMaterialMapWidget::on_left_down, this);
}
int tool_id() const { return m_tool_id; }
int selected_slot_id() const { return m_slot_id; }
bool is_slot_selected() const { return m_slot_id > 0; }
wxString material_name() const { return m_name; }
void set_enable_mapping(bool enable)
{
if (m_mapping_enabled == enable)
return;
m_mapping_enabled = enable;
if (!enable)
reset_slot();
Enable(enable);
Refresh();
}
void set_slot_selection(int slot_id, const wxColour& slot_color)
{
m_slot_id = slot_id;
m_slot_color = slot_color;
Refresh();
}
void reset_slot()
{
m_slot_id = 0;
m_slot_color = wxColour("#DDDDDD");
Refresh();
}
void update_popup_slots(const std::vector<Slic3r::FlashforgeMaterialSlot>& slots, const std::function<bool(const Slic3r::FlashforgeMaterialSlot&)>& matcher)
{
m_slots_snapshot = slots;
m_matcher = matcher;
}
wxSize DoGetBestSize() const override
{
return wxSize(FromDIP(72), FromDIP(58));
}
private:
void on_left_down(wxMouseEvent& e)
{
if (!m_mapping_enabled)
return;
FlashforgeSlotDialog dlg(this, m_name);
dlg.update_slots(m_slots_snapshot, m_matcher);
m_selected = true;
Refresh();
if (dlg.ShowModal() == wxID_OK && dlg.has_selection()) {
m_slot_id = dlg.selected_slot_id();
m_slot_color = dlg.selected_color();
if (m_select_fn)
m_select_fn(this);
}
m_selected = false;
Refresh();
e.Skip();
}
void on_paint(wxPaintEvent&)
{
wxPaintDC dc(this);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc == nullptr)
return;
const wxSize size = GetSize();
const int half_h = size.y / 2;
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush(m_color));
gc->DrawRoundedRectangle(0, 0, size.x, half_h, FromDIP(3));
gc->DrawRectangle(0, half_h - FromDIP(3), size.x, FromDIP(3));
gc->SetBrush(wxBrush(m_mapping_enabled ? m_slot_color : wxColour("#DDDDDD")));
gc->DrawRoundedRectangle(0, half_h, size.x, half_h, FromDIP(3));
gc->DrawRectangle(0, half_h, size.x, FromDIP(3));
if (m_selected) {
gc->SetPen(wxPen(wxColour("#00AE42"), FromDIP(2)));
gc->SetBrush(*wxTRANSPARENT_BRUSH);
gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3));
} else if (m_color.GetLuminance() > 0.95 || m_slot_color.GetLuminance() > 0.95) {
gc->SetPen(wxPen(wxColour("#ACACAC"), FromDIP(1)));
gc->SetBrush(*wxTRANSPARENT_BRUSH);
gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3));
}
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(contrasting_text_color(m_color));
wxString top_text = m_name;
if (dc.GetTextExtent(top_text).x > size.x - FromDIP(10)) {
dc.SetFont(::Label::Body_10);
}
wxSize top_size = dc.GetTextExtent(top_text);
dc.DrawText(top_text, (size.x - top_size.x) / 2, (half_h - top_size.y) / 2);
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(contrasting_text_color(m_slot_color));
const wxString bottom_text = m_slot_id > 0 ? wxString::Format("%d", m_slot_id) : "-";
const wxSize bottom_size = dc.GetTextExtent(bottom_text);
dc.DrawText(bottom_text, (size.x - bottom_size.x - FromDIP(10)) / 2, half_h + (half_h - bottom_size.y) / 2);
wxPoint pts[3] = {
wxPoint(size.x - FromDIP(18), half_h + half_h / 2 - FromDIP(2)),
wxPoint(size.x - FromDIP(10), half_h + half_h / 2 - FromDIP(2)),
wxPoint(size.x - FromDIP(14), half_h + half_h / 2 + FromDIP(3))
};
dc.SetBrush(wxBrush(contrasting_text_color(m_slot_color)));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawPolygon(3, pts);
}
private:
int m_tool_id {-1};
wxColour m_color;
wxString m_name;
wxColour m_slot_color {wxColour("#DDDDDD")};
int m_slot_id {0};
bool m_selected {false};
bool m_mapping_enabled {true};
SelectFn m_select_fn;
std::vector<Slic3r::FlashforgeMaterialSlot> m_slots_snapshot;
std::function<bool(const Slic3r::FlashforgeMaterialSlot&)> m_matcher;
};
static FlashforgeMaterialMapWidget* as_ff_map_widget(wxWindow* window)
{
return dynamic_cast<FlashforgeMaterialMapWidget*>(window);
}
} // namespace
static const char *CONFIG_KEY_PATH = "printhost_path";
static const char *CONFIG_KEY_GROUP = "printhost_group";
static const char* CONFIG_KEY_STORAGE = "printhost_storage";
@@ -257,6 +637,528 @@ void PrintHostSendDialog::EndModal(int ret)
MsgDialog::EndModal(ret);
}
FlashforgePrintHostSendDialog::FlashforgePrintHostSendDialog(const fs::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
const Slic3r::Flashforge* host,
bool supports_material_station,
std::vector<Slic3r::FlashforgeMaterialSlot> slots,
const std::vector<FilamentInfo>& project_filaments)
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
, m_host(host)
, m_slots(std::move(slots))
, m_project_filaments(project_filaments)
{
m_supports_material_station = supports_material_station;
m_slots_loaded = !m_slots.empty();
}
void FlashforgePrintHostSendDialog::init()
{
const AppConfig* app_config = wxGetApp().app_config;
const auto& path = m_path;
std::string leveling = app_config->get("recent", CONFIG_KEY_LEVELING);
if (!leveling.empty())
m_leveling_before_print = leveling == "1";
std::string timelapse = app_config->get("recent", CONFIG_KEY_TIMELAPSE);
if (!timelapse.empty())
m_time_lapse_video = timelapse == "1";
// Flashforge local printing should default to IFS enabled when supported.
// We don't revive an old stale "0" here.
m_use_material_station = m_supports_material_station;
if (m_supports_material_station && !app_config->has("recent", CONFIG_KEY_IFS))
const_cast<AppConfig*>(app_config)->set("recent", CONFIG_KEY_IFS, "1");
this->SetMinSize(wxSize(560, 420));
auto* label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed."));
label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
content_sizer->Add(txt_filename, 0, wxEXPAND);
content_sizer->Add(label_dir_hint);
content_sizer->AddSpacer(VERT_SPACING);
wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH));
if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/')
recent_path += '/';
const auto recent_path_len = recent_path.Length();
recent_path += path.filename().wstring();
wxString stem(path.stem().wstring());
const auto stem_len = stem.Length();
txt_filename->SetValue(recent_path);
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this, wxID_APPLY);
checkbox->SetValue(m_switch_to_device_tab);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject());
if (source != nullptr)
source->SetValue(e.IsChecked());
m_switch_to_device_tab = e.IsChecked();
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Switch to Device tab after upload."));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
content_sizer->Add(checkbox_sizer);
content_sizer->AddSpacer(VERT_SPACING);
}
m_flashforge_options_sizer = new wxBoxSizer(wxVERTICAL);
auto add_option_checkbox = [this](wxBoxSizer* parent, const wxString& label, bool value, std::function<void(bool)> setter, ::CheckBox** out = nullptr) {
auto row = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(value);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [setter](wxCommandEvent& e) {
auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject());
if (source != nullptr)
source->SetValue(e.IsChecked());
setter(e.IsChecked());
e.Skip();
});
row->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto text = new wxStaticText(this, wxID_ANY, label);
text->SetFont(::Label::Body_13);
text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
row->Add(text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
parent->Add(row);
parent->AddSpacer(FromDIP(6));
if (out != nullptr)
*out = checkbox;
};
add_option_checkbox(m_flashforge_options_sizer, _L("Leveling before print"), m_leveling_before_print,
[this](bool checked) { m_leveling_before_print = checked; }, &m_checkbox_leveling);
add_option_checkbox(m_flashforge_options_sizer, _L("Time-lapse"), m_time_lapse_video,
[this](bool checked) { m_time_lapse_video = checked; }, &m_checkbox_timelapse);
add_option_checkbox(m_flashforge_options_sizer, _L("Enable IFS"), m_use_material_station,
[this](bool checked) {
m_use_material_station = checked;
if (checked) {
ensure_slots_loaded();
rebuild_mapping_rows();
}
sync_mapping_section_visibility();
}, &m_checkbox_ifs);
if (m_checkbox_ifs != nullptr && !m_supports_material_station)
m_checkbox_ifs->Enable(false);
m_status_text = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_status_text->SetFont(::Label::Body_12);
m_flashforge_options_sizer->Add(m_status_text, 0, wxTOP | wxBOTTOM, FromDIP(4));
m_mapping_section_sizer = new wxBoxSizer(wxVERTICAL);
m_mapping_wrap_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS);
m_mapping_section_sizer->Add(m_mapping_wrap_sizer, 0, wxTOP | wxALIGN_LEFT, FromDIP(10));
m_flashforge_options_sizer->Add(m_mapping_section_sizer, 0, wxEXPAND);
content_sizer->Add(m_flashforge_options_sizer, 0, wxEXPAND);
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
rebuild_mapping_rows();
sync_mapping_section_visibility();
if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos)
m_valid_suffix = recent_path.substr(extension_start);
auto validate_path = [this](const wxString& filename) -> bool {
if (!filename.Lower().EndsWith(m_valid_suffix.Lower())) {
MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix),
wxString(SLIC3R_APP_NAME), wxYES | wxNO);
if (msg_wingow.ShowModal() == wxID_NO)
return false;
}
return validate_before_close();
};
auto* btn_ok = add_button(wxID_OK, true, _L("Upload"));
btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::None;
EndDialog(wxID_OK);
}
});
if (m_post_actions.has(PrintHostPostUploadAction::StartPrint)) {
auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print"));
btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::StartPrint;
EndDialog(wxID_OK);
}
});
}
add_button(wxID_CANCEL, false, _L("Cancel"));
finalize();
txt_filename->SetFocus();
#ifdef __linux__
txt_filename->Bind(wxEVT_KILL_FOCUS, [this](wxEvent& e) {
e.Skip();
txt_filename->SetInsertionPoint(txt_filename->GetLastPosition());
}, txt_filename->GetId());
#endif /* __linux__ */
Bind(wxEVT_SHOW, [=](const wxShowEvent&) {
CallAfter([=]() {
txt_filename->SetInsertionPoint(0);
txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
});
});
}
void FlashforgePrintHostSendDialog::EndModal(int ret)
{
if (ret == wxID_OK) {
AppConfig* app_config = wxGetApp().app_config;
app_config->set("recent", CONFIG_KEY_LEVELING, m_leveling_before_print ? "1" : "0");
app_config->set("recent", CONFIG_KEY_TIMELAPSE, m_time_lapse_video ? "1" : "0");
app_config->set("recent", CONFIG_KEY_IFS, m_use_material_station ? "1" : "0");
}
PrintHostSendDialog::EndModal(ret);
}
std::map<std::string, std::string> FlashforgePrintHostSendDialog::extendedInfo() const
{
json mappings = json::array();
int mapped_count = 0;
if (m_use_material_station) {
for (const auto& row : m_mapping_rows) {
auto* card = as_ff_map_widget(row.card);
if (card == nullptr || row.tool_id < 0)
continue;
const int slot_id = card->selected_slot_id();
if (slot_id <= 0)
continue;
const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& item) { return item.id == row.tool_id; });
const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return slot.slot_id == slot_id; });
if (filament_it == m_project_filaments.end() || slot_it == m_slots.end())
continue;
mappings.push_back({
{"toolId", filament_it->id},
{"slotId", slot_it->slot_id},
{"materialName", slot_it->material_name},
{"toolMaterialColor", filament_it->color},
{"slotMaterialColor", slot_it->material_color}
});
++mapped_count;
}
}
return {
{"levelingBeforePrint", m_leveling_before_print ? "1" : "0"},
{"timeLapseVideo", m_time_lapse_video ? "1" : "0"},
{"useMatlStation", m_use_material_station ? "1" : "0"},
{"gcodeToolCnt", std::to_string(mapped_count)},
{"materialMappings", mappings.dump()}
};
}
void FlashforgePrintHostSendDialog::load_slots()
{
m_slots.clear();
m_slots_loaded = false;
m_supports_material_station = false;
if (m_host == nullptr) {
m_status_text->SetLabel(_L("Flashforge host is not available."));
return;
}
wxString msg;
bool supports_material_station = false;
if (!m_host->fetch_material_slots(m_slots, &supports_material_station, msg)) {
m_status_text->SetLabel(msg.empty() ? _L("Unable to read IFS slots from printer.") : msg);
return;
}
m_supports_material_station = supports_material_station;
m_slots_loaded = !m_slots.empty();
m_use_material_station = m_supports_material_station;
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
}
bool FlashforgePrintHostSendDialog::ensure_slots_loaded(bool force_reload)
{
if (!force_reload && (m_slots_loaded || !m_supports_material_station))
return m_slots_loaded;
if (m_status_text != nullptr)
m_status_text->SetLabel(_L("Loading IFS slots from printer..."));
wxBusyCursor wait;
load_slots();
return m_slots_loaded;
}
void FlashforgePrintHostSendDialog::rebuild_mapping_rows()
{
if (m_mapping_wrap_sizer == nullptr)
return;
m_mapping_wrap_sizer->Clear(true);
m_mapping_rows.clear();
if (m_project_filaments.empty()) {
m_mapping_wrap_sizer->Add(new wxStaticText(this, wxID_ANY, _L("Slice the plate first to get project material information.")), 0, wxALL, FromDIP(2));
return;
}
for (const auto& filament : m_project_filaments) {
auto* card = new FlashforgeMaterialMapWidget(this, filament.id, to_wx_colour(filament.color), from_u8(filament.get_display_filament_type()),
[this](FlashforgeMaterialMapWidget* changed_card) {
if (changed_card == nullptr)
return;
for (auto& row : m_mapping_rows) {
if (row.card == changed_card) {
refresh_mapping_card(row);
break;
}
}
});
m_mapping_wrap_sizer->Add(card, 0, wxRIGHT | wxBOTTOM | wxFIXED_MINSIZE, FromDIP(10));
MappingRow row;
row.tool_id = filament.id;
row.card = card;
m_mapping_rows.push_back(row);
}
auto_assign_mappings();
}
void FlashforgePrintHostSendDialog::auto_assign_mappings()
{
for (size_t idx = 0; idx < m_project_filaments.size() && idx < m_mapping_rows.size(); ++idx) {
auto& filament = m_project_filaments[idx];
auto* card = as_ff_map_widget(m_mapping_rows[idx].card);
if (card == nullptr)
continue;
const wxColour filament_color = to_wx_colour(filament.color);
const Slic3r::FlashforgeMaterialSlot* best_slot = nullptr;
long long best_distance = std::numeric_limits<long long>::max();
for (const auto& slot : m_slots) {
if (!slot.has_filament || !slot_matches_filament(slot, filament))
continue;
const long long distance = color_distance_sq(filament_color, to_wx_colour(slot.material_color));
if (best_slot == nullptr || distance < best_distance) {
best_slot = &slot;
best_distance = distance;
}
}
if (best_slot != nullptr)
card->set_slot_selection(best_slot->slot_id, to_wx_colour(best_slot->material_color));
else
card->reset_slot();
refresh_mapping_card(m_mapping_rows[idx]);
}
}
void FlashforgePrintHostSendDialog::refresh_mapping_card(MappingRow& row)
{
auto* card = as_ff_map_widget(row.card);
if (card == nullptr)
return;
const auto* filament = find_filament_by_tool_id(row.tool_id);
card->set_enable_mapping(m_use_material_station);
card->update_popup_slots(m_slots, [this, filament](const FlashforgeMaterialSlot& slot) {
return filament != nullptr && slot_matches_filament(slot, *filament);
});
if (card->selected_slot_id() <= 0) {
card->reset_slot();
return;
}
const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id()));
if (slot == nullptr) {
card->reset_slot();
return;
}
card->set_slot_selection(slot->slot_id, to_wx_colour(slot->material_color));
}
void FlashforgePrintHostSendDialog::sync_mapping_section_visibility()
{
if (m_mapping_section_sizer == nullptr)
return;
m_mapping_section_sizer->ShowItems(m_use_material_station && m_supports_material_station);
if (wxSizer* sizer = GetSizer(); sizer != nullptr) {
sizer->Layout();
sizer->Fit(this);
SetMinSize(GetBestSize());
}
Layout();
Fit();
}
const Slic3r::FlashforgeMaterialSlot* FlashforgePrintHostSendDialog::find_slot_by_id(const std::string& slot_id_text) const
{
const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return std::to_string(slot.slot_id) == slot_id_text; });
return slot_it == m_slots.end() ? nullptr : &(*slot_it);
}
const FilamentInfo* FlashforgePrintHostSendDialog::find_filament_by_tool_id(int tool_id) const
{
const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& filament) { return filament.id == tool_id; });
return filament_it == m_project_filaments.end() ? nullptr : &(*filament_it);
}
bool FlashforgePrintHostSendDialog::slot_matches_filament(const Slic3r::FlashforgeMaterialSlot& slot, const FilamentInfo& filament) const
{
if (!slot.has_filament)
return false;
const std::string project_material = normalize_material(!filament.type.empty() ? filament.type : filament.get_display_filament_type());
const std::string slot_material = normalize_material(slot.material_name);
return !project_material.empty() && !slot_material.empty() && project_material == slot_material;
}
bool FlashforgePrintHostSendDialog::validate_before_close()
{
if (!m_use_material_station && m_project_filaments.size() > 1) {
show_error(this, _L("This plate uses multiple materials. Enable IFS and assign each tool to a printer slot."));
return false;
}
if (!m_use_material_station)
return true;
for (const auto& row : m_mapping_rows) {
auto* card = as_ff_map_widget(row.card);
if (card == nullptr || !card->is_slot_selected()) {
show_error(this, _L("Each project material must be assigned to an IFS slot before printing."));
return false;
}
const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id()));
const auto* filament = find_filament_by_tool_id(row.tool_id);
if (slot == nullptr || filament == nullptr || !slot->has_filament) {
show_error(this, _L("Each project material must be assigned to a loaded IFS slot before printing."));
return false;
}
if (!slot_matches_filament(*slot, *filament)) {
show_error(this, _L("Each project material must match the material loaded in the selected IFS slot."));
return false;
}
}
return true;
}
std::string FlashforgePrintHostSendDialog::normalize_material(const std::string& material) const
{
std::string normalized = boost::to_upper_copy(material);
normalized.erase(std::remove_if(normalized.begin(), normalized.end(), [](unsigned char ch) { return !std::isalnum(ch); }), normalized.end());
if (normalized.empty())
return {};
if (normalized.find("SILK") != std::string::npos)
return "SILK";
if (normalized.find("PLA") != std::string::npos && normalized.find("CF") != std::string::npos)
return "PLACF";
if (normalized.find("PETG") != std::string::npos && normalized.find("CF") != std::string::npos)
return "PETGCF";
if (normalized == "PLA" || normalized == "PLA+" || normalized == "PLAPLUS")
return "PLA";
if (normalized.find("PLA") != std::string::npos)
return "PLA";
if (normalized == "ABS" || normalized.find("ABS") != std::string::npos)
return "ABS";
if (normalized == "ASA" || normalized.find("ASA") != std::string::npos)
return "ABS";
if (normalized.find("PETG") != std::string::npos)
return "PETG";
if (normalized.find("TPU") != std::string::npos || normalized.find("TPE") != std::string::npos || normalized.find("FLEX") != std::string::npos)
return "TPU";
return normalized;
}
wxColour FlashforgePrintHostSendDialog::to_wx_colour(const std::string& color) const
{
wxColour wx_color(from_u8(color));
if (wx_color.IsOk())
return wx_color;
std::string normalized = boost::trim_copy(color);
if (boost::istarts_with(normalized, "0x"))
normalized = normalized.substr(2);
if (!normalized.empty() && normalized.front() == '#')
normalized.erase(normalized.begin());
if (normalized.size() == 8) {
auto hex_to_byte = [](char hi, char lo) -> int {
auto hex_val = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
const int h = hex_val(hi);
const int l = hex_val(lo);
return (h < 0 || l < 0) ? -1 : h * 16 + l;
};
const int r = hex_to_byte(normalized[0], normalized[1]);
const int g = hex_to_byte(normalized[2], normalized[3]);
const int b = hex_to_byte(normalized[4], normalized[5]);
const int a = hex_to_byte(normalized[6], normalized[7]);
if (r >= 0 && g >= 0 && b >= 0 && a >= 0)
return wxColour(r, g, b, a);
}
if (normalized.size() == 6) {
wx_color = wxColour("#" + from_u8(normalized));
if (wx_color.IsOk())
return wx_color;
}
return wxColour("#999999");
}
wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event);