mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 01:40:57 +00:00
Merge branch 'main' into dev/ffmpeg-player
This commit is contained in:
@@ -355,6 +355,16 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Monitor.hpp
|
||||
GUI/MonitorPage.cpp
|
||||
GUI/MonitorPage.hpp
|
||||
GUI/MixedFilamentDialog.cpp
|
||||
GUI/MixedFilamentDialog.hpp
|
||||
GUI/GradientCurveEditor.cpp
|
||||
GUI/GradientCurveEditor.hpp
|
||||
GUI/ColorDecomposeDialog.cpp
|
||||
GUI/ColorDecomposeDialog.hpp
|
||||
GUI/ColorDecomposeSupport.cpp
|
||||
GUI/ColorDecomposeSupport.hpp
|
||||
GUI/TextureImportDialog.cpp
|
||||
GUI/TextureImportDialog.hpp
|
||||
GUI/Mouse3DController.cpp
|
||||
GUI/Mouse3DController.hpp
|
||||
GUI/MsgDialog.cpp
|
||||
@@ -624,6 +634,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/host/PluginHostSlicing.cpp
|
||||
plugin/host/PluginHostUi.cpp
|
||||
plugin/host/PluginHostUi.hpp
|
||||
plugin/host/PluginPages.cpp
|
||||
plugin/host/PluginPages.hpp
|
||||
plugin/CloudPluginService.cpp
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
@@ -644,6 +656,9 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.hpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.cpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.hpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.cpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
|
||||
|
||||
@@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot:
|
||||
cfg.models_variants_installed.erase(it ++);
|
||||
else
|
||||
++ it;
|
||||
// Read the active config bundle, parse the config version.
|
||||
PresetBundle bundle;
|
||||
//BBS: change directoties by design
|
||||
//bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
for (const auto &vp : bundle.vendors)
|
||||
if (vp.second.id == cfg.name)
|
||||
cfg.version.config_version = vp.second.config_version;
|
||||
// Orca: the version the vendor is installed at, read from its profile or —
|
||||
// where the cache is the whole installation — from the cache's own stamp.
|
||||
cfg.version.config_version = installed_vendor_version(cfg.name);
|
||||
snapshot.vendor_configs.emplace_back(std::move(cfg));
|
||||
}
|
||||
|
||||
|
||||
@@ -682,13 +682,19 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
if (shader) {
|
||||
if (idx == 0) {
|
||||
int extruder_id = model_volume->extruder_id();
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - (extruder_id - 1))/255.0f;
|
||||
// ORCA: extruder_id may be 0 (unset) or point past the colour list after a
|
||||
// filament is deleted/remapped, so clamp the index instead of reading out of
|
||||
// bounds.
|
||||
if (!extruder_colors.empty()) {
|
||||
int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1);
|
||||
//to make black not too hard too see
|
||||
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]);
|
||||
if (ban_light) {
|
||||
new_color[3] = (255 - color_idx)/255.0f;
|
||||
}
|
||||
m.set_color(new_color);
|
||||
// shader->set_uniform("uniform_color", new_color);
|
||||
}
|
||||
m.set_color(new_color);
|
||||
// shader->set_uniform("uniform_color", new_color);
|
||||
}
|
||||
else {
|
||||
if (idx <= extruder_colors.size()) {
|
||||
|
||||
@@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel()
|
||||
m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE);
|
||||
m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS);
|
||||
|
||||
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true);
|
||||
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false);
|
||||
m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), "", false);
|
||||
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false);
|
||||
m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false);
|
||||
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true);
|
||||
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false);
|
||||
m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false);
|
||||
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false);
|
||||
m_tabpanel->AddPage(m_others_panel, _L("Others"), false);
|
||||
}
|
||||
|
||||
wxWindow *AuxiliaryPanel::create_side_tools()
|
||||
|
||||
@@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() {
|
||||
selected = true;
|
||||
m_tabpanel->AddPage(m_cali_panels[i],
|
||||
get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()),
|
||||
"",
|
||||
selected);
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ public:
|
||||
|
||||
void show_panels(CalibrationMethod method, const PrinterSeries printer_ser);
|
||||
|
||||
void on_device_connected(MachineObject* obj);
|
||||
void on_device_connected(MachineObject* obj) override;
|
||||
|
||||
void update(MachineObject* obj) override;
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ public:
|
||||
|
||||
void create_page(wxWindow* parent);
|
||||
|
||||
void on_reset_page();
|
||||
void on_device_connected(MachineObject* obj);
|
||||
void on_reset_page() override;
|
||||
void on_device_connected(MachineObject* obj) override;
|
||||
void msw_rescale() override;
|
||||
};
|
||||
|
||||
@@ -63,8 +63,8 @@ public:
|
||||
long style = wxTAB_TRAVERSAL);
|
||||
|
||||
void create_page(wxWindow* parent);
|
||||
void on_reset_page();
|
||||
void on_device_connected(MachineObject* obj);
|
||||
void on_reset_page() override;
|
||||
void on_device_connected(MachineObject* obj) override;
|
||||
void msw_rescale() override;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,951 @@
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include "wx/graphics.h"
|
||||
|
||||
#include "I18N.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "format.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "Widgets/DropDown.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "libslic3r/ColorDecomposeRecipe.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static const wxColour COLOR_BRAND("#009688");
|
||||
static const wxColour COLOR_BORDER_NORMAL("#EEEEEE");
|
||||
static const wxColour COLOR_BG_CARD("#F8F8F8");
|
||||
static const wxColour COLOR_LABEL_GREY("#ACACAC");
|
||||
static const wxColour COLOR_TEXT_DARK("#262E30");
|
||||
static const wxColour COLOR_DIVIDER("#EEEEEE");
|
||||
|
||||
// Standard CMYW base colors
|
||||
static const wxColour CMYW_CYAN(0, 255, 255);
|
||||
static const wxColour CMYW_MAGENTA(255, 0, 255);
|
||||
static const wxColour CMYW_YELLOW(255, 255, 0);
|
||||
static const wxColour CMYW_WHITE(255, 255, 255);
|
||||
|
||||
// Standard RYBW base colors
|
||||
static const wxColour RYBW_RED(255, 0, 0);
|
||||
static const wxColour RYBW_YELLOW(255, 255, 0);
|
||||
static const wxColour RYBW_BLUE(0, 0, 255);
|
||||
static const wxColour RYBW_WHITE(255, 255, 255);
|
||||
|
||||
static size_t mode_index(DecomposeMode mode)
|
||||
{
|
||||
return static_cast<size_t>(mode);
|
||||
}
|
||||
|
||||
static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color)
|
||||
{
|
||||
return {
|
||||
static_cast<unsigned char>(color.Red()),
|
||||
static_cast<unsigned char>(color.Green()),
|
||||
static_cast<unsigned char>(color.Blue())
|
||||
};
|
||||
}
|
||||
|
||||
static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback)
|
||||
{
|
||||
wxColour color(hex);
|
||||
return color.IsOk() ? color : fallback;
|
||||
}
|
||||
|
||||
static bool same_rgb(const wxColour& lhs, const wxColour& rhs)
|
||||
{
|
||||
return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue();
|
||||
}
|
||||
|
||||
static DecomposeBaseColor standard_base_color_from_key(const std::string& key)
|
||||
{
|
||||
if (key == "Cyan") return DecomposeBaseColor::Cyan;
|
||||
if (key == "Magenta") return DecomposeBaseColor::Magenta;
|
||||
if (key == "Yellow") return DecomposeBaseColor::Yellow;
|
||||
if (key == "White") return DecomposeBaseColor::White;
|
||||
if (key == "Red") return DecomposeBaseColor::Red;
|
||||
if (key == "Green") return DecomposeBaseColor::Green;
|
||||
if (key == "Blue") return DecomposeBaseColor::Blue;
|
||||
return DecomposeBaseColor::None;
|
||||
}
|
||||
|
||||
static wxColour pure_color_for_base(DecomposeBaseColor base)
|
||||
{
|
||||
switch (base) {
|
||||
case DecomposeBaseColor::Cyan: return CMYW_CYAN;
|
||||
case DecomposeBaseColor::Magenta: return CMYW_MAGENTA;
|
||||
case DecomposeBaseColor::Yellow: return CMYW_YELLOW;
|
||||
case DecomposeBaseColor::White: return CMYW_WHITE;
|
||||
case DecomposeBaseColor::Red: return RYBW_RED;
|
||||
case DecomposeBaseColor::Blue: return RYBW_BLUE;
|
||||
default: return *wxBLACK;
|
||||
}
|
||||
}
|
||||
|
||||
static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color)
|
||||
{
|
||||
if (mode == DecomposeMode::CMYW) {
|
||||
if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan;
|
||||
if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta;
|
||||
if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow;
|
||||
if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White;
|
||||
} else if (mode == DecomposeMode::RYBW) {
|
||||
if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red;
|
||||
if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow;
|
||||
if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue;
|
||||
if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White;
|
||||
}
|
||||
return DecomposeBaseColor::None;
|
||||
}
|
||||
|
||||
static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe,
|
||||
const wxColour& fallback)
|
||||
{
|
||||
ColorDecomposeResult result;
|
||||
result.mode = recipe.mode;
|
||||
result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback);
|
||||
for (const auto& comp_recipe : recipe.components) {
|
||||
DecomposeComponent comp;
|
||||
comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback);
|
||||
comp.ratio = comp_recipe.ratio;
|
||||
comp.filament_index = static_cast<int>(comp_recipe.filament_index);
|
||||
comp.base_color = standard_base_color_from_key(comp_recipe.base_color);
|
||||
if (comp.base_color == DecomposeBaseColor::None)
|
||||
comp.base_color = standard_base_color_for(recipe.mode, comp.colour);
|
||||
result.components.push_back(comp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1)
|
||||
{
|
||||
const int h = parent->FromDIP(1);
|
||||
int w = fixed_width > 0 ? fixed_width : -1;
|
||||
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h));
|
||||
panel->SetMinSize(wxSize(w, h));
|
||||
if (fixed_width > 0)
|
||||
panel->SetMaxSize(wxSize(fixed_width, h));
|
||||
panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER));
|
||||
return panel;
|
||||
}
|
||||
|
||||
static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text)
|
||||
{
|
||||
auto* label = new wxStaticText(parent, wxID_ANY, text);
|
||||
label->SetFont(Label::Body_11);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY));
|
||||
return label;
|
||||
}
|
||||
|
||||
static void match_parent_bg(wxWindow* w, const wxColour& bg)
|
||||
{
|
||||
w->SetBackgroundColour(bg);
|
||||
}
|
||||
|
||||
static bool material_type_matches(const std::string& a, const std::string& b)
|
||||
{
|
||||
if (a.empty() || b.empty())
|
||||
return false;
|
||||
return a == b || a == b + " Basic" || b == a + " Basic";
|
||||
}
|
||||
|
||||
|
||||
ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent,
|
||||
int filament_idx,
|
||||
const wxColour& target_color,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& filament_names,
|
||||
const std::vector<std::string>& filament_types,
|
||||
size_t current_filament_count,
|
||||
size_t max_filament_count,
|
||||
std::vector<size_t> physical_config_indices)
|
||||
: DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition,
|
||||
wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
, m_filament_idx(filament_idx)
|
||||
, m_target_color(target_color)
|
||||
, m_physical_colors(physical_colors)
|
||||
, m_filament_names(filament_names)
|
||||
, m_filament_types(filament_types)
|
||||
, m_current_filament_count(current_filament_count)
|
||||
, m_max_filament_count(max_filament_count)
|
||||
, m_physical_config_indices(std::move(physical_config_indices))
|
||||
{
|
||||
for (const auto& t : m_filament_types) {
|
||||
if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end())
|
||||
m_project_types.push_back(t);
|
||||
}
|
||||
|
||||
if (m_filament_idx >= 0 && static_cast<size_t>(m_filament_idx) < m_filament_types.size())
|
||||
m_preferred_type = m_filament_types[m_filament_idx];
|
||||
else if (!m_project_types.empty())
|
||||
m_preferred_type = m_project_types.front();
|
||||
|
||||
build_ui();
|
||||
wxGetApp().UpdateDlgDarkUI(this);
|
||||
// Restore target swatch after dark mode color remapping
|
||||
if (m_target_swatch) {
|
||||
m_target_swatch->SetBackgroundColour(m_target_color);
|
||||
m_target_swatch->Refresh();
|
||||
}
|
||||
|
||||
update_card_visibility();
|
||||
Fit();
|
||||
compute_decomposition();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
{
|
||||
(void)suggested_rect;
|
||||
Fit();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::build_ui()
|
||||
{
|
||||
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
|
||||
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
const int selector_side_margin = FromDIP(26);
|
||||
const int selector_top_gap = FromDIP(22);
|
||||
const int content_side_margin = FromDIP(30);
|
||||
const int target_section_top_gap = FromDIP(18);
|
||||
|
||||
main_sizer->AddSpacer(selector_top_gap);
|
||||
main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin);
|
||||
main_sizer->AddSpacer(target_section_top_gap);
|
||||
main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin);
|
||||
|
||||
SetSizer(main_sizer);
|
||||
SetMinSize(wxSize(FromDIP(477), FromDIP(380)));
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_filament_selector()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
|
||||
wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY);
|
||||
m_type_combo->SetFont(Label::Body_13);
|
||||
|
||||
m_combo_item_types.clear();
|
||||
int default_sel = -1;
|
||||
|
||||
// --- Group 1: Project filament list (deduplicated by type) ---
|
||||
m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
|
||||
m_combo_item_types.push_back(std::string());
|
||||
|
||||
std::set<std::string> seen_types;
|
||||
for (size_t i = 0; i < m_filament_names.size(); ++i) {
|
||||
const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA";
|
||||
if (!seen_types.insert(type).second)
|
||||
continue;
|
||||
int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i]));
|
||||
m_combo_item_types.push_back(type);
|
||||
if (type == m_preferred_type && default_sel < 0)
|
||||
default_sel = idx;
|
||||
}
|
||||
|
||||
// --- Group 2: Standard mode material recommendations ---
|
||||
static const char* kStandardTypes[] = {
|
||||
kDecomposePlaBasicType
|
||||
};
|
||||
|
||||
m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
|
||||
m_combo_item_types.push_back(std::string());
|
||||
|
||||
for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) {
|
||||
// Always show standard recommendations, even if the same type already
|
||||
// appears in the project filament list above.
|
||||
const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s];
|
||||
int idx = m_type_combo->Append(wxString::FromUTF8(label));
|
||||
m_combo_item_types.push_back(kStandardTypes[s]);
|
||||
if (kStandardTypes[s] == m_preferred_type && default_sel < 0)
|
||||
default_sel = idx;
|
||||
}
|
||||
|
||||
if (default_sel < 0) {
|
||||
for (int i = 0; i < static_cast<int>(m_combo_item_types.size()); ++i) {
|
||||
if (!m_combo_item_types[i].empty()) {
|
||||
default_sel = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (default_sel >= 0) {
|
||||
m_type_combo->SetSelection(default_sel);
|
||||
if (!m_combo_item_types[default_sel].empty())
|
||||
m_preferred_type = m_combo_item_types[default_sel];
|
||||
}
|
||||
|
||||
m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) {
|
||||
evt.StopPropagation();
|
||||
int sel = m_type_combo->GetSelection();
|
||||
if (sel >= 0 && static_cast<size_t>(sel) < m_combo_item_types.size()
|
||||
&& !m_combo_item_types[sel].empty()) {
|
||||
m_preferred_type = m_combo_item_types[sel];
|
||||
}
|
||||
update_card_visibility();
|
||||
compute_decomposition();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
});
|
||||
|
||||
sizer->Add(m_type_combo, 1, wxEXPAND);
|
||||
return sizer;
|
||||
}
|
||||
|
||||
static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size)
|
||||
{
|
||||
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size));
|
||||
panel->SetBackgroundColour(color);
|
||||
panel->SetMinSize(wxSize(size, size));
|
||||
panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
panel->Bind(wxEVT_PAINT, [panel](wxPaintEvent&) {
|
||||
wxAutoBufferedPaintDC dc(panel);
|
||||
wxSize sz = panel->GetClientSize();
|
||||
wxColour c = panel->GetBackgroundColour();
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(c));
|
||||
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
|
||||
// Mirror sidebar (FilamentBitmapUtils::create_single_filament_bitmap):
|
||||
// gray border for near-white in light mode so white swatches stay
|
||||
// visible on a white background; light border for near-black in dark mode.
|
||||
const bool light_mode = !wxGetApp().dark_mode();
|
||||
if ((light_mode && c.Red() > 224 && c.Green() > 224 && c.Blue() > 224) ||
|
||||
(!light_mode && c.Red() < 45 && c.Green() < 45 && c.Blue() < 45)) {
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
dc.SetPen(wxPen(light_mode ? wxColour(130, 130, 128) : wxColour(207, 207, 207),
|
||||
1, wxPENSTYLE_SOLID));
|
||||
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
|
||||
}
|
||||
});
|
||||
return panel;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_target_color_section()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color"));
|
||||
label->SetFont(Label::Head_14);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19));
|
||||
|
||||
m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
|
||||
sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_target_rgb_text = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
|
||||
m_target_rgb_text->SetFont(Label::Body_13);
|
||||
m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
|
||||
arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
|
||||
sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_matched_rgb_text = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
|
||||
m_matched_rgb_text->SetFont(Label::Head_13);
|
||||
m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode,
|
||||
const wxString& title)
|
||||
{
|
||||
const int pad = FromDIP(12);
|
||||
|
||||
auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
card->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
auto* card_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* title_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* title_label = new wxStaticText(card, wxID_ANY, title);
|
||||
title_label->SetFont(Label::Body_14);
|
||||
title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
|
||||
match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
auto* chk = new ::CheckBox(card);
|
||||
chk->SetValue(mode == m_selected_mode);
|
||||
match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
switch (mode) {
|
||||
case DecomposeMode::MaterialList: m_chk_material_list = chk; break;
|
||||
case DecomposeMode::CMYW: m_chk_cmyw = chk; break;
|
||||
case DecomposeMode::RYBW: m_chk_rybw = chk; break;
|
||||
}
|
||||
chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) {
|
||||
select_mode(mode);
|
||||
e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue()
|
||||
});
|
||||
title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad);
|
||||
|
||||
card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8));
|
||||
|
||||
auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
|
||||
|
||||
auto& controls = m_mode_cards[mode_index(mode)];
|
||||
controls.card = card;
|
||||
controls.components_sizer = colors_sizer;
|
||||
|
||||
card->SetSizer(card_sizer);
|
||||
card->SetMinSize(wxSize(FromDIP(128), FromDIP(111)));
|
||||
card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111)));
|
||||
|
||||
card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) {
|
||||
wxBufferedPaintDC dc(card);
|
||||
wxSize sz = card->GetClientSize();
|
||||
dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE)));
|
||||
dc.Clear();
|
||||
|
||||
bool selected = (m_selected_mode == mode);
|
||||
wxColour border_col = selected
|
||||
? StateColor::darkModeColorFor(COLOR_BRAND)
|
||||
: StateColor::darkModeColorFor(COLOR_BORDER_NORMAL);
|
||||
const int border_width = FromDIP(selected ? 2 : 1);
|
||||
const double inset = border_width / 2.0;
|
||||
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
|
||||
if (gc) {
|
||||
gc->SetPen(wxPen(border_col, border_width));
|
||||
gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
|
||||
gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8));
|
||||
} else {
|
||||
const int fallback_inset = (border_width + 1) / 2;
|
||||
dc.SetPen(wxPen(border_col, border_width));
|
||||
dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
|
||||
dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8));
|
||||
}
|
||||
});
|
||||
|
||||
std::function<void(wxWindow*)> bind_click;
|
||||
bind_click = [this, mode, chk, &bind_click](wxWindow* w) {
|
||||
if (w == chk || dynamic_cast<::CheckBox*>(w))
|
||||
return;
|
||||
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
|
||||
select_mode(mode);
|
||||
});
|
||||
w->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
for (auto* child : w->GetChildren())
|
||||
bind_click(child);
|
||||
};
|
||||
bind_click(card);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition"));
|
||||
section_label->SetFont(Label::Head_14);
|
||||
section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4));
|
||||
|
||||
auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// --- Arbitrary mode column (wrapped in a panel so the whole column hides together) ---
|
||||
m_arb_column_panel = new wxPanel(this, wxID_ANY);
|
||||
m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
auto* arb_col = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL);
|
||||
arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
||||
|
||||
m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList,
|
||||
_L("Material List"));
|
||||
arb_col->Add(m_card_material_list, 0, wxEXPAND);
|
||||
}
|
||||
m_arb_column_panel->SetSizer(arb_col);
|
||||
modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16));
|
||||
|
||||
// --- Standard mode column ---
|
||||
auto* std_col = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL);
|
||||
std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
||||
|
||||
auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW");
|
||||
cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12));
|
||||
|
||||
m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW");
|
||||
cards_sizer->Add(m_card_rybw, 0);
|
||||
|
||||
std_col->Add(cards_sizer, 0, wxEXPAND);
|
||||
}
|
||||
modes_sizer->Add(std_col, 0, wxEXPAND);
|
||||
|
||||
sizer->Add(modes_sizer, 0, wxEXPAND);
|
||||
|
||||
m_no_card_hint = new wxStaticText(this, wxID_ANY,
|
||||
_L("At least two filaments of the same material type are required for decomposition"));
|
||||
m_no_card_hint->SetFont(Label::Body_13);
|
||||
m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6A6A")));
|
||||
m_no_card_hint->Wrap(FromDIP(400));
|
||||
m_no_card_hint->Hide();
|
||||
sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8));
|
||||
|
||||
m_limit_warning_panel = new wxPanel(this, wxID_ANY);
|
||||
m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY,
|
||||
create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16),
|
||||
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
|
||||
m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString);
|
||||
m_limit_warning_text->SetFont(Label::Body_13);
|
||||
m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B")));
|
||||
m_limit_warning_text->Wrap(FromDIP(400));
|
||||
warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6));
|
||||
warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND);
|
||||
m_limit_warning_panel->SetSizer(warning_sizer);
|
||||
m_limit_warning_panel->Hide();
|
||||
sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8));
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_button_panel()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer->AddStretchSpacer();
|
||||
|
||||
m_btn_cancel = new Button(this, _L("Cancel"));
|
||||
m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
|
||||
m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
|
||||
m_btn_ok = new Button(this, _L("OK"));
|
||||
m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
|
||||
m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
EndModal(wxID_OK);
|
||||
});
|
||||
|
||||
sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12));
|
||||
sizer->Add(m_btn_ok, 0);
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::select_mode(DecomposeMode mode)
|
||||
{
|
||||
m_selected_mode = mode;
|
||||
m_result = m_mode_results[mode_index(mode)];
|
||||
update_card_styles();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_card_styles()
|
||||
{
|
||||
if (m_card_material_list) m_card_material_list->Refresh();
|
||||
if (m_card_cmyw) m_card_cmyw->Refresh();
|
||||
if (m_card_rybw) m_card_rybw->Refresh();
|
||||
|
||||
if (m_chk_material_list)
|
||||
m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList);
|
||||
if (m_chk_cmyw)
|
||||
m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW);
|
||||
if (m_chk_rybw)
|
||||
m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW);
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_card_visibility()
|
||||
{
|
||||
// Count physical filaments of the same type (excluding the source filament)
|
||||
int same_type_count = 0;
|
||||
for (size_t i = 0; i < m_filament_types.size(); ++i) {
|
||||
if (static_cast<int>(i) == m_filament_idx)
|
||||
continue;
|
||||
if (material_type_matches(m_filament_types[i], m_preferred_type))
|
||||
++same_type_count;
|
||||
}
|
||||
|
||||
bool show_arb = (same_type_count >= 2);
|
||||
bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType);
|
||||
bool show_rybw = (m_preferred_type == kDecomposePlaBasicType);
|
||||
|
||||
if (m_arb_column_panel) m_arb_column_panel->Show(show_arb);
|
||||
if (m_card_material_list) m_card_material_list->Show(show_arb);
|
||||
if (m_card_cmyw) m_card_cmyw->Show(show_cmyw);
|
||||
if (m_card_rybw) m_card_rybw->Show(show_rybw);
|
||||
|
||||
bool any_visible = show_arb || show_cmyw || show_rybw;
|
||||
if (m_no_card_hint)
|
||||
m_no_card_hint->Show(!any_visible);
|
||||
|
||||
// Auto-select a visible mode when current selection becomes hidden
|
||||
if (any_visible) {
|
||||
bool cur_visible = false;
|
||||
if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true;
|
||||
if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true;
|
||||
if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true;
|
||||
if (!cur_visible) {
|
||||
if (show_arb) select_mode(DecomposeMode::MaterialList);
|
||||
else if (show_cmyw) select_mode(DecomposeMode::CMYW);
|
||||
else select_mode(DecomposeMode::RYBW);
|
||||
}
|
||||
}
|
||||
|
||||
Layout();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
{
|
||||
if (!m_limit_warning_panel || !m_limit_warning_text)
|
||||
return;
|
||||
|
||||
size_t missing_new = 0;
|
||||
if (m_missing_calculator) {
|
||||
missing_new = m_missing_calculator(m_result);
|
||||
} else {
|
||||
const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast<size_t>(m_filament_idx) : size_t(-1);
|
||||
const std::vector<size_t>* indices =
|
||||
m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices;
|
||||
missing_new = count_decompose_new_physical_filaments(
|
||||
m_result, m_physical_colors, m_filament_types, source_physical_idx, indices);
|
||||
}
|
||||
// A result with fewer than 2 components (e.g. target color is already a
|
||||
// standard base color shown as "100%") creates no mixed filament and no new
|
||||
// physical filament, so it can never exceed the limit.
|
||||
const bool creates_mixed = m_result.components.size() >= 2;
|
||||
// +1 for the mixed filament slot that will be created after decomposition.
|
||||
const size_t needed = m_current_filament_count + missing_new + 1;
|
||||
const bool blocked = creates_mixed && needed > m_max_filament_count;
|
||||
|
||||
const bool was_shown = m_limit_warning_panel->IsShown();
|
||||
|
||||
if (!blocked) {
|
||||
if (was_shown) {
|
||||
m_limit_warning_panel->Hide();
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wxString mode_name;
|
||||
switch (m_selected_mode) {
|
||||
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
|
||||
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
|
||||
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
|
||||
}
|
||||
|
||||
const wxString warning_text = format_wxstr(
|
||||
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
|
||||
m_max_filament_count, mode_name);
|
||||
|
||||
// Show first so the panel is laid out and the text control gets its real
|
||||
// width, then wrap to that width so the paragraph fills the content area.
|
||||
m_limit_warning_panel->Show();
|
||||
Layout();
|
||||
const int avail = m_limit_warning_text->GetClientSize().x;
|
||||
m_limit_warning_text->SetLabel(warning_text);
|
||||
if (avail > FromDIP(50))
|
||||
m_limit_warning_text->Wrap(avail);
|
||||
|
||||
Layout();
|
||||
// Only resize when the warning panel actually toggled from hidden to shown.
|
||||
// While already visible, switching modes must not re-Fit the dialog, which
|
||||
// would make it jump on every card switch. Fit keeps the user-moved position.
|
||||
if (!was_shown) {
|
||||
Fit();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn)
|
||||
{
|
||||
m_missing_calculator = std::move(fn);
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_ok_button_state()
|
||||
{
|
||||
if (!m_btn_ok) return;
|
||||
update_filament_limit_warning();
|
||||
bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown())
|
||||
|| (m_card_cmyw && m_card_cmyw->IsShown())
|
||||
|| (m_card_rybw && m_card_rybw->IsShown());
|
||||
const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown();
|
||||
m_btn_ok->Enable(any_card_visible && !blocked);
|
||||
Layout();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode)
|
||||
{
|
||||
auto& controls = m_mode_cards[mode_index(mode)];
|
||||
auto* sizer = controls.components_sizer;
|
||||
auto* card = controls.card;
|
||||
if (!sizer || !card)
|
||||
return;
|
||||
|
||||
sizer->Clear(true);
|
||||
const auto& components = m_mode_results[mode_index(mode)].components;
|
||||
const size_t count = components.size();
|
||||
if (count == 0) {
|
||||
card->Layout();
|
||||
card->Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const int swatch_sz = FromDIP(24);
|
||||
const int plus_gap = FromDIP(24);
|
||||
const wxFont& ratio_font = Label::Body_13;
|
||||
auto bind_select = [this, mode](wxWindow* w) {
|
||||
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
|
||||
select_mode(mode);
|
||||
});
|
||||
w->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto* col = new wxBoxSizer(wxVERTICAL);
|
||||
auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz);
|
||||
bind_select(swatch);
|
||||
col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL);
|
||||
auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio));
|
||||
ratio_text->SetFont(ratio_font);
|
||||
ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
bind_select(ratio_text);
|
||||
col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4));
|
||||
sizer->Add(col, 0, wxALIGN_TOP);
|
||||
|
||||
if (i + 1 < count) {
|
||||
sizer->AddStretchSpacer();
|
||||
auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
auto* plus_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+");
|
||||
plus_label->SetFont(Label::Body_13);
|
||||
plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
bind_select(plus_panel);
|
||||
bind_select(plus_label);
|
||||
plus_sizer->AddStretchSpacer();
|
||||
plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL);
|
||||
plus_sizer->AddStretchSpacer();
|
||||
plus_panel->SetSizer(plus_sizer);
|
||||
sizer->Add(plus_panel, 0, wxALIGN_TOP);
|
||||
sizer->AddStretchSpacer();
|
||||
}
|
||||
}
|
||||
|
||||
const int card_width = FromDIP(128 + (count > 2 ? static_cast<int>(count - 2) * 31 : 0));
|
||||
card->SetMinSize(wxSize(card_width, FromDIP(111)));
|
||||
card->SetMaxSize(wxSize(card_width, FromDIP(111)));
|
||||
|
||||
card->Layout();
|
||||
card->Refresh();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_mode_card_contents()
|
||||
{
|
||||
update_mode_card_content(DecomposeMode::MaterialList);
|
||||
update_mode_card_content(DecomposeMode::CMYW);
|
||||
update_mode_card_content(DecomposeMode::RYBW);
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_matched_color_display()
|
||||
{
|
||||
if (!m_result.matched_color.IsOk())
|
||||
m_result.matched_color = m_target_color;
|
||||
|
||||
if (m_matched_swatch) {
|
||||
m_matched_swatch->SetBackgroundColour(m_result.matched_color);
|
||||
m_matched_swatch->Refresh();
|
||||
}
|
||||
if (m_matched_rgb_text) {
|
||||
m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d",
|
||||
m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue()));
|
||||
}
|
||||
}
|
||||
|
||||
bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const
|
||||
{
|
||||
// Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic.
|
||||
if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) {
|
||||
if (m_preferred_type != kDecomposePlaBasicType)
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const DecomposeBaseColor cmyw_bases[] = {
|
||||
DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta,
|
||||
DecomposeBaseColor::Yellow, DecomposeBaseColor::White
|
||||
};
|
||||
static const DecomposeBaseColor rybw_bases[] = {
|
||||
DecomposeBaseColor::Red, DecomposeBaseColor::Yellow,
|
||||
DecomposeBaseColor::Blue, DecomposeBaseColor::White
|
||||
};
|
||||
const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases;
|
||||
const size_t base_count = (mode == DecomposeMode::CMYW)
|
||||
? sizeof(cmyw_bases) / sizeof(cmyw_bases[0])
|
||||
: sizeof(rybw_bases) / sizeof(rybw_bases[0]);
|
||||
|
||||
const std::string target_hex = decompose_normalize_color_hex(
|
||||
m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
|
||||
for (size_t i = 0; i < base_count; ++i) {
|
||||
const DecomposeBaseColor base = bases[i];
|
||||
DecomposeOfficialComponent official =
|
||||
lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base));
|
||||
if (decompose_normalize_color_hex(official.color_hex) != target_hex)
|
||||
continue;
|
||||
|
||||
out = ColorDecomposeResult{};
|
||||
out.mode = mode;
|
||||
out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color);
|
||||
DecomposeComponent comp;
|
||||
comp.colour = out.matched_color;
|
||||
comp.ratio = 100;
|
||||
comp.filament_index = -1;
|
||||
comp.base_color = base;
|
||||
out.components.push_back(comp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::compute_decomposition()
|
||||
{
|
||||
auto fallback_result = [this](DecomposeMode mode, const std::vector<DecomposeComponent>& components) {
|
||||
ColorDecomposeResult result;
|
||||
result.mode = mode;
|
||||
result.components = components;
|
||||
int total = 0;
|
||||
double r = 0.0, g = 0.0, b = 0.0;
|
||||
for (const auto& comp : result.components)
|
||||
total += comp.ratio;
|
||||
if (total <= 0)
|
||||
total = 100;
|
||||
for (const auto& comp : result.components) {
|
||||
const double w = static_cast<double>(comp.ratio) / total;
|
||||
r += comp.colour.Red() * w;
|
||||
g += comp.colour.Green() * w;
|
||||
b += comp.colour.Blue() * w;
|
||||
}
|
||||
result.matched_color = result.components.empty()
|
||||
? m_target_color
|
||||
: wxColour(static_cast<unsigned char>(std::clamp(r, 0.0, 255.0)),
|
||||
static_cast<unsigned char>(std::clamp(g, 0.0, 255.0)),
|
||||
static_cast<unsigned char>(std::clamp(b, 0.0, 255.0)));
|
||||
return result;
|
||||
};
|
||||
|
||||
std::vector<ColorDecomposePhysicalFilament> physical_filaments;
|
||||
physical_filaments.reserve(m_physical_colors.size());
|
||||
for (size_t i = 0; i < m_physical_colors.size(); ++i) {
|
||||
if (m_filament_idx >= 0 && i == static_cast<size_t>(m_filament_idx))
|
||||
continue;
|
||||
ColorDecomposePhysicalFilament filament;
|
||||
filament.color_hex = m_physical_colors[i];
|
||||
filament.name = i < m_filament_names.size() ? m_filament_names[i] : "";
|
||||
filament.type = i < m_filament_types.size() ? m_filament_types[i] : "";
|
||||
filament.filament_index = static_cast<unsigned int>(i + 1);
|
||||
physical_filaments.push_back(std::move(filament));
|
||||
}
|
||||
|
||||
const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color);
|
||||
|
||||
auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type);
|
||||
if (material_recipe.valid) {
|
||||
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
|
||||
to_dialog_result(material_recipe, m_target_color);
|
||||
} else {
|
||||
std::vector<DecomposeComponent> components;
|
||||
for (size_t i = 0; i < std::min<size_t>(2, physical_filaments.size()); ++i) {
|
||||
DecomposeComponent comp;
|
||||
comp.colour = wxColour(physical_filaments[i].color_hex);
|
||||
comp.ratio = 50;
|
||||
comp.filament_index = static_cast<int>(physical_filaments[i].filament_index);
|
||||
components.push_back(comp);
|
||||
}
|
||||
if (components.empty()) {
|
||||
components.push_back({m_target_color, 100, -1});
|
||||
} else if (components.size() == 1) {
|
||||
components.front().ratio = 100;
|
||||
}
|
||||
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
|
||||
fallback_result(DecomposeMode::MaterialList, components);
|
||||
}
|
||||
|
||||
ColorDecomposeResult single_base;
|
||||
if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) {
|
||||
m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base;
|
||||
} else {
|
||||
auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type);
|
||||
m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid
|
||||
? to_dialog_result(cmyw_recipe, m_target_color)
|
||||
: fallback_result(DecomposeMode::CMYW, {
|
||||
{CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
|
||||
{CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan}
|
||||
});
|
||||
}
|
||||
|
||||
if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) {
|
||||
m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base;
|
||||
} else {
|
||||
auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type);
|
||||
m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid
|
||||
? to_dialog_result(rybw_recipe, m_target_color)
|
||||
: fallback_result(DecomposeMode::RYBW, {
|
||||
{RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
|
||||
{RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue}
|
||||
});
|
||||
}
|
||||
|
||||
m_result = m_mode_results[mode_index(m_selected_mode)];
|
||||
update_mode_card_contents();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,152 @@
|
||||
#ifndef slic3r_ColorDecomposeDialog_hpp_
|
||||
#define slic3r_ColorDecomposeDialog_hpp_
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/statbmp.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "libslic3r/ColorDecomposeRecipe.hpp"
|
||||
|
||||
class Button;
|
||||
class CheckBox;
|
||||
class ComboBox;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
using DecomposeMode = ColorDecomposeRecipeMode;
|
||||
|
||||
enum class DecomposeBaseColor {
|
||||
None,
|
||||
Cyan,
|
||||
Magenta,
|
||||
Yellow,
|
||||
White,
|
||||
Red,
|
||||
Green,
|
||||
Blue
|
||||
};
|
||||
|
||||
struct DecomposeComponent {
|
||||
wxColour colour;
|
||||
int ratio{50}; // percentage
|
||||
int filament_index{-1}; // 1-based physical filament index, -1 if standard base color
|
||||
DecomposeBaseColor base_color{DecomposeBaseColor::None};
|
||||
};
|
||||
|
||||
struct ColorDecomposeResult {
|
||||
DecomposeMode mode{DecomposeMode::MaterialList};
|
||||
wxColour matched_color;
|
||||
std::vector<DecomposeComponent> components;
|
||||
};
|
||||
|
||||
class ColorDecomposeDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
ColorDecomposeDialog(wxWindow* parent,
|
||||
int filament_idx,
|
||||
const wxColour& target_color,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& filament_names,
|
||||
const std::vector<std::string>& filament_types,
|
||||
size_t current_filament_count = 0,
|
||||
size_t max_filament_count = 32,
|
||||
std::vector<size_t> physical_config_indices = {});
|
||||
|
||||
ColorDecomposeResult get_result() const { return m_result; }
|
||||
|
||||
// Override the "new physical filaments" count used by the filament-limit
|
||||
// warning. The Texture import path supplies its own calculator so the
|
||||
// pre-check shares the exact reuse rule as its write-back (existing +
|
||||
// virtual physical filaments), instead of the project-config based default
|
||||
// that cannot see not-yet-committed virtual base colors.
|
||||
void set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn);
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
private:
|
||||
void build_ui();
|
||||
wxBoxSizer* create_filament_selector();
|
||||
wxBoxSizer* create_target_color_section();
|
||||
wxBoxSizer* create_mode_selection_section();
|
||||
wxPanel* create_mode_card(wxWindow* parent, DecomposeMode mode, const wxString& title);
|
||||
wxBoxSizer* create_button_panel();
|
||||
|
||||
void select_mode(DecomposeMode mode);
|
||||
void update_card_styles();
|
||||
void update_card_visibility();
|
||||
void update_mode_card_content(DecomposeMode mode);
|
||||
void update_mode_card_contents();
|
||||
void update_matched_color_display();
|
||||
void update_ok_button_state();
|
||||
void update_filament_limit_warning();
|
||||
|
||||
void compute_decomposition();
|
||||
|
||||
// When the target color is exactly one of the standard base colors for the
|
||||
// preferred type, the standard card should show that base at 100% instead of
|
||||
// a mix. PLA Basic covers CMYW and RYBW.
|
||||
bool try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const;
|
||||
|
||||
struct ModeCardControls {
|
||||
wxPanel* card{nullptr};
|
||||
wxBoxSizer* components_sizer{nullptr};
|
||||
};
|
||||
|
||||
ColorDecomposeResult m_result;
|
||||
std::array<ColorDecomposeResult, 3> m_mode_results;
|
||||
std::array<ModeCardControls, 3> m_mode_cards;
|
||||
int m_filament_idx{-1};
|
||||
wxColour m_target_color;
|
||||
std::vector<std::string> m_physical_colors;
|
||||
std::vector<std::string> m_filament_names;
|
||||
std::vector<std::string> m_filament_types;
|
||||
std::vector<std::string> m_project_types;
|
||||
std::string m_preferred_type;
|
||||
// Dropdown selectable item index -> material type string
|
||||
std::vector<std::string> m_combo_item_types;
|
||||
size_t m_current_filament_count{0};
|
||||
size_t m_max_filament_count{32};
|
||||
std::vector<size_t> m_physical_config_indices;
|
||||
std::function<size_t(const ColorDecomposeResult&)> m_missing_calculator;
|
||||
|
||||
// UI controls
|
||||
ComboBox* m_type_combo{nullptr};
|
||||
wxPanel* m_target_swatch{nullptr};
|
||||
wxStaticText* m_target_rgb_text{nullptr};
|
||||
wxPanel* m_matched_swatch{nullptr};
|
||||
wxStaticText* m_matched_rgb_text{nullptr};
|
||||
|
||||
// Mode cards
|
||||
wxPanel* m_card_material_list{nullptr};
|
||||
wxPanel* m_card_cmyw{nullptr};
|
||||
wxPanel* m_card_rybw{nullptr};
|
||||
wxPanel* m_arb_column_panel{nullptr};
|
||||
CheckBox* m_chk_material_list{nullptr};
|
||||
CheckBox* m_chk_cmyw{nullptr};
|
||||
CheckBox* m_chk_rybw{nullptr};
|
||||
DecomposeMode m_selected_mode{DecomposeMode::MaterialList};
|
||||
|
||||
// Hint shown when no mode card is visible
|
||||
wxStaticText* m_no_card_hint{nullptr};
|
||||
|
||||
// Warning shown when decomposition would exceed filament limit
|
||||
wxPanel* m_limit_warning_panel{nullptr};
|
||||
wxStaticText* m_limit_warning_text{nullptr};
|
||||
|
||||
Button* m_btn_ok{nullptr};
|
||||
Button* m_btn_cancel{nullptr};
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_ColorDecomposeDialog_hpp_
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "MixedFilamentDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
std::string decompose_normalize_color_hex(std::string color)
|
||||
{
|
||||
if (color.size() >= 7)
|
||||
color = color.substr(0, 7);
|
||||
std::transform(color.begin(), color.end(), color.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::toupper(c));
|
||||
});
|
||||
return color;
|
||||
}
|
||||
|
||||
const char* decompose_base_color_en(DecomposeBaseColor color)
|
||||
{
|
||||
switch (color) {
|
||||
case DecomposeBaseColor::Cyan: return "Cyan";
|
||||
case DecomposeBaseColor::Magenta: return "Magenta";
|
||||
case DecomposeBaseColor::Yellow: return "Yellow";
|
||||
case DecomposeBaseColor::White: return "White";
|
||||
case DecomposeBaseColor::Red: return "Red";
|
||||
case DecomposeBaseColor::Green: return "Green";
|
||||
case DecomposeBaseColor::Blue: return "Blue";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
wxString decompose_base_color_display(DecomposeBaseColor color)
|
||||
{
|
||||
switch (color) {
|
||||
case DecomposeBaseColor::Cyan: return _L("Cyan");
|
||||
case DecomposeBaseColor::Magenta: return _L("Magenta");
|
||||
case DecomposeBaseColor::Yellow: return _L("Yellow");
|
||||
case DecomposeBaseColor::White: return _L("White");
|
||||
case DecomposeBaseColor::Red: return _L("Red");
|
||||
case DecomposeBaseColor::Green: return _L("Green");
|
||||
case DecomposeBaseColor::Blue: return _L("Blue");
|
||||
default: return wxString();
|
||||
}
|
||||
}
|
||||
|
||||
std::string decompose_basic_type_from_source(size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_types)
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
|
||||
if (source_config_idx < filament_id_opt->values.size()) {
|
||||
const std::string& filament_id = filament_id_opt->values[source_config_idx];
|
||||
if (filament_id == kDecomposePetgFilamentId)
|
||||
return kDecomposePetgBasicType;
|
||||
if (filament_id == kDecomposePlaFilamentId)
|
||||
return kDecomposePlaBasicType;
|
||||
}
|
||||
}
|
||||
|
||||
if (source_physical_idx < physical_types.size()) {
|
||||
const std::string& type = physical_types[source_physical_idx];
|
||||
if (type == kDecomposePetgShortType || type == kDecomposePetgBasicType)
|
||||
return kDecomposePetgBasicType;
|
||||
if (type == kDecomposePlaShortType || type == kDecomposePlaBasicType)
|
||||
return kDecomposePlaBasicType;
|
||||
}
|
||||
return kDecomposePlaBasicType;
|
||||
}
|
||||
|
||||
std::string decompose_basic_filament_id(const std::string& basic_type)
|
||||
{
|
||||
if (basic_type == kDecomposePetgBasicType)
|
||||
return kDecomposePetgFilamentId;
|
||||
return kDecomposePlaFilamentId;
|
||||
}
|
||||
|
||||
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component)
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
if (!component.filament_id.empty()) {
|
||||
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
|
||||
while (filament_id_opt->values.size() <= config_idx)
|
||||
filament_id_opt->values.push_back("");
|
||||
filament_id_opt->values[config_idx] = component.filament_id;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType :
|
||||
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : "";
|
||||
if (!type.empty()) {
|
||||
if (auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type")) {
|
||||
while (type_opt->values.size() <= config_idx)
|
||||
type_opt->values.push_back("");
|
||||
type_opt->values[config_idx] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecomposeOfficialComponent lookup_decompose_official_component(
|
||||
const std::string& basic_type,
|
||||
DecomposeBaseColor base_color,
|
||||
const wxColour& fallback)
|
||||
{
|
||||
DecomposeOfficialComponent result;
|
||||
result.base_color = base_color;
|
||||
result.color_hex = decompose_normalize_color_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
result.filament_id = decompose_basic_filament_id(basic_type);
|
||||
|
||||
const char* color_name = decompose_base_color_en(base_color);
|
||||
if (color_name[0] == '\0')
|
||||
return result;
|
||||
|
||||
// Some materials name a standard base color differently in the color-code
|
||||
// table. PETG Basic's RYBW blue base is "Reflex Blue" (deep blue, B00,
|
||||
// #001489), not "Blue". Match by an ordered list of exact English names so
|
||||
// "Navy Blue" (B01, #0086D6) is never picked up by mistake.
|
||||
std::vector<std::string> candidate_names;
|
||||
candidate_names.emplace_back(color_name);
|
||||
if (base_color == DecomposeBaseColor::Blue && basic_type == kDecomposePetgBasicType)
|
||||
candidate_names.emplace_back("Reflex Blue");
|
||||
|
||||
std::ifstream ifs(resources_dir() + "/profiles/BBL/filament/filaments_color_codes.json");
|
||||
if (!ifs)
|
||||
return result;
|
||||
|
||||
json root = json::parse(ifs, nullptr, false);
|
||||
if (root.is_discarded() || !root.contains("data") || !root["data"].is_array())
|
||||
return result;
|
||||
|
||||
for (const std::string& candidate : candidate_names) {
|
||||
for (const auto& item : root["data"]) {
|
||||
if (!item.is_object() || item.value("fila_type", "") != basic_type)
|
||||
continue;
|
||||
if (!item.contains("fila_color_name"))
|
||||
continue;
|
||||
const auto& names = item["fila_color_name"];
|
||||
if (!names.is_object() || names.value("en", "") != candidate)
|
||||
continue;
|
||||
if (item.contains("fila_color") && item["fila_color"].is_array() && !item["fila_color"].empty())
|
||||
result.color_hex = decompose_normalize_color_hex(item["fila_color"][0].get<std::string>());
|
||||
result.filament_id = item.value("fila_id", result.filament_id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type)
|
||||
{
|
||||
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
if (source_config_idx < preset_bundle.filament_presets.size()) {
|
||||
const std::string& source_name = preset_bundle.filament_presets[source_config_idx];
|
||||
if (source_name.find(std::string(kDecomposeBambuPresetPrefix) + basic_type) != std::string::npos)
|
||||
return source_name;
|
||||
}
|
||||
|
||||
const std::string prefix = std::string(kDecomposeBambuPresetPrefix) + basic_type + " @BBL ";
|
||||
for (const std::string& preset_name : preset_bundle.filament_presets) {
|
||||
if (preset_name.find(prefix) == 0)
|
||||
return preset_name;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string official_basic_type_from_preset_name(const std::string& preset_name)
|
||||
{
|
||||
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePlaBasicType) != std::string::npos)
|
||||
return kDecomposePlaBasicType;
|
||||
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePetgBasicType) != std::string::npos)
|
||||
return kDecomposePetgBasicType;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string filament_type_for_color_decompose(Preset* preset)
|
||||
{
|
||||
if (!preset)
|
||||
return kDecomposePlaShortType;
|
||||
|
||||
std::string display_type;
|
||||
std::string ft = preset->config.get_filament_type(display_type);
|
||||
const std::string basic = official_basic_type_from_preset_name(preset->name);
|
||||
if (!basic.empty())
|
||||
ft = basic;
|
||||
if (ft.empty())
|
||||
ft = kDecomposePlaShortType;
|
||||
return ft;
|
||||
}
|
||||
|
||||
int find_existing_decompose_component(
|
||||
const DecomposeOfficialComponent& component,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
size_t source_config_idx)
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id");
|
||||
auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type");
|
||||
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
const size_t num_physical = physical_colors.size();
|
||||
const std::string expected_basic_type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType :
|
||||
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : "";
|
||||
const std::string expected_short_type = expected_basic_type == kDecomposePetgBasicType ? kDecomposePetgShortType :
|
||||
expected_basic_type == kDecomposePlaBasicType ? kDecomposePlaShortType : "";
|
||||
const std::string expected_preset_part = expected_basic_type.empty() ? "" : std::string(kDecomposeBambuPresetPrefix) + expected_basic_type;
|
||||
for (size_t i = 0; i < num_physical && i < physical_config_indices.size(); ++i) {
|
||||
const size_t config_idx = physical_config_indices[i];
|
||||
const std::string slot_color = decompose_normalize_color_hex(physical_colors[i]);
|
||||
const std::string slot_filament_id = (filament_id_opt && config_idx < filament_id_opt->values.size()) ? filament_id_opt->values[config_idx] : "";
|
||||
const std::string slot_type = (type_opt && config_idx < type_opt->values.size()) ? type_opt->values[config_idx] : "";
|
||||
const std::string preset_name = config_idx < preset_bundle.filament_presets.size() ? preset_bundle.filament_presets[config_idx] : "";
|
||||
if (config_idx == source_config_idx) {
|
||||
continue;
|
||||
}
|
||||
if (slot_color != component.color_hex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!component.filament_id.empty() && slot_filament_id == component.filament_id) {
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
|
||||
if (!expected_basic_type.empty() && (slot_type == expected_basic_type || slot_type == expected_short_type)) {
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
|
||||
if (!expected_preset_part.empty() && preset_name.find(expected_preset_part) != std::string::npos) {
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
|
||||
const bool has_material_hint = !slot_filament_id.empty() || !slot_type.empty() || !preset_name.empty();
|
||||
if (!expected_basic_type.empty() && has_material_hint)
|
||||
continue;
|
||||
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool prepare_decompose_mixed_result(
|
||||
const ColorDecomposeResult& result,
|
||||
size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
MixedFilamentResult& out_result,
|
||||
std::vector<DecomposeMissingComponent>& missing)
|
||||
{
|
||||
out_result = {};
|
||||
missing.clear();
|
||||
if (result.components.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool standard_mode = result.mode == DecomposeMode::CMYW || result.mode == DecomposeMode::RYBW;
|
||||
std::string basic_type;
|
||||
std::string preset_name;
|
||||
if (standard_mode) {
|
||||
basic_type = decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
|
||||
preset_name = find_decompose_standard_preset_name(source_config_idx, basic_type);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < result.components.size(); ++i) {
|
||||
const DecomposeComponent& comp = result.components[i];
|
||||
out_result.ratios.push_back(comp.ratio);
|
||||
if (!standard_mode) {
|
||||
if (comp.filament_index <= 0) {
|
||||
return false;
|
||||
}
|
||||
const size_t physical_idx = static_cast<size_t>(comp.filament_index - 1);
|
||||
if (physical_idx >= physical_config_indices.size()) {
|
||||
return false;
|
||||
}
|
||||
out_result.components.push_back(static_cast<unsigned int>(physical_config_indices[physical_idx] + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (comp.base_color == DecomposeBaseColor::None) {
|
||||
return false;
|
||||
}
|
||||
DecomposeOfficialComponent official_component =
|
||||
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
|
||||
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
|
||||
physical_config_indices, source_config_idx);
|
||||
if (existing_idx > 0) {
|
||||
out_result.components.push_back(static_cast<unsigned int>(existing_idx));
|
||||
continue;
|
||||
}
|
||||
|
||||
DecomposeMissingComponent missing_comp;
|
||||
missing_comp.component_idx = out_result.components.size();
|
||||
missing_comp.official_component = official_component;
|
||||
missing_comp.preset_name = preset_name;
|
||||
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
|
||||
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
|
||||
missing.push_back(std::move(missing_comp));
|
||||
out_result.components.push_back(0);
|
||||
}
|
||||
|
||||
const bool ok = out_result.components.size() == out_result.ratios.size() && out_result.components.size() >= 2;
|
||||
return ok;
|
||||
}
|
||||
|
||||
size_t count_decompose_new_physical_filaments(
|
||||
const ColorDecomposeResult& result,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<size_t>* physical_config_indices)
|
||||
{
|
||||
if (result.mode != DecomposeMode::CMYW && result.mode != DecomposeMode::RYBW)
|
||||
return 0;
|
||||
|
||||
std::vector<size_t> fallback_indices;
|
||||
const std::vector<size_t>* indices = physical_config_indices;
|
||||
if (!indices) {
|
||||
fallback_indices.resize(physical_colors.size());
|
||||
for (size_t i = 0; i < fallback_indices.size(); ++i)
|
||||
fallback_indices[i] = i;
|
||||
indices = &fallback_indices;
|
||||
}
|
||||
|
||||
size_t source_config_idx = size_t(-1);
|
||||
if (source_physical_idx < indices->size())
|
||||
source_config_idx = (*indices)[source_physical_idx];
|
||||
|
||||
const std::string basic_type =
|
||||
decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
|
||||
|
||||
size_t missing_count = 0;
|
||||
for (const DecomposeComponent& comp : result.components) {
|
||||
if (comp.base_color == DecomposeBaseColor::None)
|
||||
continue;
|
||||
DecomposeOfficialComponent official_component =
|
||||
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
|
||||
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
|
||||
*indices, source_config_idx);
|
||||
if (existing_idx <= 0)
|
||||
++missing_count;
|
||||
}
|
||||
return missing_count;
|
||||
}
|
||||
|
||||
bool confirm_create_decompose_missing_components(wxWindow* parent, const std::vector<DecomposeMissingComponent>& missing)
|
||||
{
|
||||
if (missing.empty())
|
||||
return true;
|
||||
|
||||
static const char* config_key = "not_show_color_decompose_missing_component_tip";
|
||||
if (wxGetApp().app_config->get(config_key) == "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
wxString missing_text;
|
||||
for (size_t i = 0; i < missing.size(); ++i) {
|
||||
if (i > 0)
|
||||
missing_text += _L(", ");
|
||||
missing_text += missing[i].display_name;
|
||||
}
|
||||
|
||||
wxString message = _L("The current filament list does not contain ") + missing_text +
|
||||
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
|
||||
|
||||
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
||||
dlg.show_dsa_button();
|
||||
int res = dlg.ShowModal();
|
||||
if (res == wxID_OK && dlg.get_checkbox_state())
|
||||
wxGetApp().app_config->set(config_key, "1");
|
||||
return res == wxID_OK;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,104 @@
|
||||
#ifndef slic3r_GUI_ColorDecomposeSupport_hpp_
|
||||
#define slic3r_GUI_ColorDecomposeSupport_hpp_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/string.h>
|
||||
#include <wx/colour.h>
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r {
|
||||
class Preset;
|
||||
namespace GUI {
|
||||
|
||||
// ---- Constants ----
|
||||
|
||||
inline constexpr const char* kDecomposePlaBasicType = "PLA Basic";
|
||||
inline constexpr const char* kDecomposePetgBasicType = "PETG Basic";
|
||||
inline constexpr const char* kDecomposePlaShortType = "PLA";
|
||||
inline constexpr const char* kDecomposePetgShortType = "PETG";
|
||||
inline constexpr const char* kDecomposePlaFilamentId = "GFA00";
|
||||
inline constexpr const char* kDecomposePetgFilamentId = "GFG00";
|
||||
inline constexpr const char* kDecomposeBambuPresetPrefix = "Bambu ";
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
struct DecomposeOfficialComponent {
|
||||
DecomposeBaseColor base_color{DecomposeBaseColor::None};
|
||||
std::string color_hex;
|
||||
std::string filament_id;
|
||||
};
|
||||
|
||||
struct DecomposeMissingComponent {
|
||||
size_t component_idx{0};
|
||||
DecomposeOfficialComponent official_component;
|
||||
std::string preset_name;
|
||||
wxString display_name;
|
||||
};
|
||||
|
||||
struct MixedFilamentResult;
|
||||
|
||||
// ---- Functions ----
|
||||
|
||||
std::string decompose_normalize_color_hex(std::string color);
|
||||
|
||||
const char* decompose_base_color_en(DecomposeBaseColor color);
|
||||
|
||||
wxString decompose_base_color_display(DecomposeBaseColor color);
|
||||
|
||||
std::string decompose_basic_type_from_source(size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_types);
|
||||
|
||||
std::string decompose_basic_filament_id(const std::string& basic_type);
|
||||
|
||||
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component);
|
||||
|
||||
DecomposeOfficialComponent lookup_decompose_official_component(
|
||||
const std::string& basic_type,
|
||||
DecomposeBaseColor base_color,
|
||||
const wxColour& fallback);
|
||||
|
||||
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type);
|
||||
|
||||
// Returns "PLA Basic" / "PETG Basic" when preset_name names an official Bambu
|
||||
// basic filament, else an empty string.
|
||||
std::string official_basic_type_from_preset_name(const std::string& preset_name);
|
||||
|
||||
// Resolve display type for color-decompose: official Bambu Basic overrides
|
||||
// get_filament_type when preset name matches; empty/missing -> "PLA".
|
||||
std::string filament_type_for_color_decompose(Preset* preset);
|
||||
|
||||
int find_existing_decompose_component(
|
||||
const DecomposeOfficialComponent& component,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
size_t source_config_idx);
|
||||
|
||||
bool prepare_decompose_mixed_result(
|
||||
const ColorDecomposeResult& result,
|
||||
size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
MixedFilamentResult& out_result,
|
||||
std::vector<DecomposeMissingComponent>& missing);
|
||||
|
||||
// For standard modes: how many base colors are not reusable from physical list.
|
||||
// MaterialList returns 0. When physical_config_indices is null, indices are 0..n-1.
|
||||
size_t count_decompose_new_physical_filaments(
|
||||
const ColorDecomposeResult& result,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<size_t>* physical_config_indices);
|
||||
|
||||
bool confirm_create_decompose_missing_components(wxWindow* parent,
|
||||
const std::vector<DecomposeMissingComponent>& missing);
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_ColorDecomposeSupport_hpp_
|
||||
@@ -577,22 +577,67 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
|
||||
}
|
||||
|
||||
// BBS
|
||||
static const char* keys[] = { "support_filament", "support_interface_filament"};
|
||||
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
||||
std::string key = std::string(keys[i]);
|
||||
// Reset filament overrides pointing at a slot that no longer exists. Support and the wipe
|
||||
// tower additionally reject mixed slots: the engine consumes those keys directly, so a virtual
|
||||
// slot would reach the G-code unresolved, while the per-feature keys are resolved per layer.
|
||||
static const char* physical_only_keys[] = { "support_filament", "support_interface_filament", "wipe_tower_filament" };
|
||||
static const char* feature_keys[] = { "outer_wall_filament_id", "inner_wall_filament_id",
|
||||
"sparse_infill_filament_id", "internal_solid_filament_id",
|
||||
"top_surface_filament_id", "bottom_surface_filament_id" };
|
||||
auto reset_invalid_filament = [this, config, filament_cnt](const char* key, bool allow_mixed) {
|
||||
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
|
||||
if (opt != nullptr) {
|
||||
if (opt->getInt() > filament_cnt) {
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
|
||||
int new_value = 0;
|
||||
if (conf_temp != nullptr && conf_temp->has(key)) {
|
||||
new_value = conf_temp->opt_int(key);
|
||||
if (opt == nullptr)
|
||||
return;
|
||||
const int val = opt->getInt();
|
||||
const bool out_of_range = val > filament_cnt;
|
||||
const bool is_mixed = !allow_mixed && val > 0 && val <= filament_cnt &&
|
||||
wxGetApp().preset_bundle->is_mixed_filament(val - 1);
|
||||
if (!out_of_range && !is_mixed)
|
||||
return;
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
int new_value = 0;
|
||||
if (out_of_range) {
|
||||
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
|
||||
if (conf_temp != nullptr && conf_temp->has(key))
|
||||
new_value = conf_temp->opt_int(key);
|
||||
}
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
|
||||
apply(config, &new_conf);
|
||||
};
|
||||
for (const char* key : physical_only_keys)
|
||||
reset_invalid_filament(key, false);
|
||||
for (const char* key : feature_keys)
|
||||
reset_invalid_filament(key, true);
|
||||
|
||||
// Sub-layer splitting divides each layer by the mix ratio; an adaptive layer profile makes
|
||||
// those sub-layer heights vary per layer, which degrades the blend. Warn once per enable.
|
||||
{
|
||||
static bool s_mixed_sublayer_warned = false;
|
||||
bool sublayer_on = config->opt_bool("enable_mixed_color_sublayer");
|
||||
if (sublayer_on && !s_mixed_sublayer_warned &&
|
||||
wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
|
||||
bool has_variable_layer = false;
|
||||
for (const auto* obj : wxGetApp().model().objects) {
|
||||
if (obj->layer_height_profile.get().size() > 4) {
|
||||
has_variable_layer = true;
|
||||
break;
|
||||
}
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
|
||||
apply(config, &new_conf);
|
||||
}
|
||||
if (has_variable_layer) {
|
||||
MessageDialog dialog(m_msg_dlg_parent,
|
||||
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
|
||||
"", wxICON_WARNING | wxOK);
|
||||
dialog.show_dsa_button();
|
||||
is_msg_dlg_already_exist = true;
|
||||
dialog.ShowModal();
|
||||
is_msg_dlg_already_exist = false;
|
||||
if (dialog.get_checkbox_state())
|
||||
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
|
||||
s_mixed_sublayer_warned = true;
|
||||
}
|
||||
}
|
||||
if (!sublayer_on)
|
||||
s_mixed_sublayer_warned = false;
|
||||
}
|
||||
|
||||
if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None &&
|
||||
@@ -752,7 +797,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
bool has_top_shell = has_top_shell_layers && config->option<ConfigOptionPercent>("top_surface_density")->value > 0;
|
||||
bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0;
|
||||
bool has_solid_infill = has_top_shell_layers || has_bottom_shell;
|
||||
toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve);
|
||||
toggle_line("sparse_infill_smooth_factor", is_smoothable_infill_pattern(pattern, config->opt_int("fill_multiline")));
|
||||
toggle_field("top_surface_pattern", has_top_shell);
|
||||
toggle_field("bottom_surface_pattern", has_bottom_shell);
|
||||
toggle_field("top_surface_density", has_top_shell_layers);
|
||||
|
||||
@@ -66,41 +66,41 @@ using Config::SnapshotDB;
|
||||
|
||||
// Configuration data structures extensions needed for the wizard
|
||||
//BBS: set BBL as default
|
||||
bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_bbl_bundle)
|
||||
bool Bundle::load(fs::path dir, const std::string &vendor_name, bool ais_in_resources, bool ais_bbl_bundle)
|
||||
{
|
||||
this->preset_bundle = std::make_unique<PresetBundle>();
|
||||
this->is_in_resources = ais_in_resources;
|
||||
this->is_bbl_bundle = ais_bbl_bundle;
|
||||
|
||||
std::string path_string = source_path.string();
|
||||
std::string parent_path = source_path.parent_path().string();
|
||||
//BBS: add json logic for vendor bundles
|
||||
std::string vendor_name = source_path.filename().string();
|
||||
if (Slic3r::is_json_file(path_string)) {
|
||||
// Remove the .json suffix.
|
||||
vendor_name.erase(vendor_name.size() - 5);
|
||||
}
|
||||
else
|
||||
// Orca: served from the vendor's preset cache where one covers it — which is
|
||||
// how a shipped build carries its vendors — and parsed from the JSONs otherwise.
|
||||
// A vendor that can be neither read nor parsed — a cache the build cannot use
|
||||
// with the preset JSONs behind it pruned, say — is one the wizard cannot offer.
|
||||
// Every other vendor still can be, so it is left out rather than thrown over.
|
||||
size_t presets_loaded = 0;
|
||||
try {
|
||||
auto [config_substitutions, loaded] = preset_bundle->load_vendor_configs_from_json(
|
||||
dir.string(), vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
UNUSED(config_substitutions);
|
||||
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
|
||||
assert(config_substitutions.empty());
|
||||
presets_loaded = loaded;
|
||||
} catch (const std::exception &e) {
|
||||
BOOST_LOG_TRIVIAL(fatal) << boost::format("Vendor bundle: `%1%`: cannot be loaded, leaving it out: %2%") % vendor_name % e.what();
|
||||
return false;
|
||||
|
||||
// Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air.
|
||||
//BBS: add json logic for vendor bundles
|
||||
auto [config_substitutions, presets_loaded] = preset_bundle->load_vendor_configs_from_json(
|
||||
parent_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
|
||||
UNUSED(config_substitutions);
|
||||
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
|
||||
assert(config_substitutions.empty());
|
||||
}
|
||||
auto first_vendor = preset_bundle->vendors.begin();
|
||||
if (first_vendor == preset_bundle->vendors.end()) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % vendor_name;
|
||||
return false;
|
||||
}
|
||||
if (presets_loaded == 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string;
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % vendor_name;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded;
|
||||
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % vendor_name % presets_loaded;
|
||||
this->vendor_profile = &first_vendor->second;
|
||||
return true;
|
||||
}
|
||||
@@ -125,15 +125,10 @@ BundleMap BundleMap::load()
|
||||
|
||||
//Orca: add custom as default
|
||||
//Orca: add json logic for vendor bundle
|
||||
auto orca_bundle_path = (vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
|
||||
auto orca_bundle_rsrc = false;
|
||||
if (!boost::filesystem::exists(orca_bundle_path)) {
|
||||
orca_bundle_path = (rsrc_vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
|
||||
orca_bundle_rsrc = true;
|
||||
}
|
||||
{
|
||||
const bool from_rsrc = ! is_vendor_installed(PresetBundle::ORCA_DEFAULT_BUNDLE);
|
||||
Bundle bbl_bundle;
|
||||
if (bbl_bundle.load(std::move(orca_bundle_path), orca_bundle_rsrc, true))
|
||||
if (bbl_bundle.load(from_rsrc ? rsrc_vendor_dir : vendor_dir, PresetBundle::ORCA_DEFAULT_BUNDLE, from_rsrc, true))
|
||||
res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle));
|
||||
}
|
||||
|
||||
@@ -141,18 +136,13 @@ BundleMap BundleMap::load()
|
||||
// and then additionally from resources/profiles.
|
||||
bool is_in_resources = false;
|
||||
for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) {
|
||||
for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) {
|
||||
//BBS: add json logic for vendor bundle
|
||||
if (Slic3r::is_json_file(dir_entry.path().string())) {
|
||||
std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".json" part
|
||||
for (const std::string &id : vendor_names_in(*dir)) {
|
||||
// Don't load this bundle if we've already loaded it.
|
||||
if (res.find(id) != res.end()) { continue; }
|
||||
|
||||
// Don't load this bundle if we've already loaded it.
|
||||
if (res.find(id) != res.end()) { continue; }
|
||||
|
||||
Bundle bundle;
|
||||
if (bundle.load(dir_entry.path(), is_in_resources))
|
||||
res.emplace(std::move(id), std::move(bundle));
|
||||
}
|
||||
Bundle bundle;
|
||||
if (bundle.load(*dir, id, is_in_resources))
|
||||
res.emplace(id, std::move(bundle));
|
||||
}
|
||||
|
||||
is_in_resources = true;
|
||||
|
||||
@@ -71,9 +71,11 @@ struct Bundle
|
||||
Bundle() = default;
|
||||
Bundle(Bundle&& other);
|
||||
|
||||
// Load the vendor `vendor_name` as it is installed in `dir`, from its preset
|
||||
// cache or its profile JSONs, whichever is usable.
|
||||
// Returns false if not loaded. Reason for that is logged as boost::log error.
|
||||
//BBS: set BBL as default
|
||||
bool load(fs::path source_path, bool is_in_resources, bool is_bbl_bundle = false);
|
||||
bool load(fs::path dir, const std::string &vendor_name, bool is_in_resources, bool is_bbl_bundle = false);
|
||||
|
||||
const std::string& vendor_id() const { return vendor_profile->id; }
|
||||
};
|
||||
|
||||
@@ -156,6 +156,8 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
|
||||
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
|
||||
{
|
||||
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
|
||||
if (code.empty())
|
||||
code = "88888888";
|
||||
for (char c : code) {
|
||||
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
|
||||
show_error(this, _L("Invalid input"));
|
||||
@@ -163,7 +165,7 @@ void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
|
||||
}
|
||||
}
|
||||
if (m_obj) {
|
||||
m_obj->set_user_access_code(code.ToStdString());
|
||||
m_obj->set_access_code(code.ToStdString());
|
||||
}
|
||||
EndModal(wxID_OK);
|
||||
}
|
||||
|
||||
@@ -2201,25 +2201,14 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre
|
||||
} else {
|
||||
selected_vendor_id = m_printer_preset_vendor_selected.id;
|
||||
|
||||
if (boost::filesystem::exists(boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR / selected_vendor_id)) {
|
||||
preset_path = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string();
|
||||
} else if (boost::filesystem::exists(boost::filesystem::path(Slic3r::resources_dir()) / "profiles" / selected_vendor_id)) {
|
||||
preset_path = (boost::filesystem::path(Slic3r::resources_dir()) / "profiles").string();
|
||||
}
|
||||
|
||||
if (preset_path.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "Preset path was not found";
|
||||
MessageDialog dlg(this, _L("Preset path was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
|
||||
wxYES_NO | wxYES_DEFAULT | wxCENTRE);
|
||||
dlg.ShowModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base
|
||||
// bundle so vendor filaments that inherit OFL bases resolve via the existing
|
||||
// cross-vendor inheritance path.
|
||||
temp_preset_bundle.load_vendor_configs_from_json(preset_path, selected_vendor_id,
|
||||
// Orca: served from the vendor's preset cache where one covers it — a shipped
|
||||
// build carries that instead of the raw preset JSONs — and parsed otherwise.
|
||||
temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(),
|
||||
selected_vendor_id,
|
||||
PresetBundle::LoadConfigBundleAttribute::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||
wxGetApp().preset_bundle);
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
DevFirmware(MachineObject* obj) : m_owner(obj) {}
|
||||
|
||||
private:
|
||||
MachineObject* m_owner = nullptr;
|
||||
[[maybe_unused]] MachineObject* m_owner = nullptr;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -10,11 +10,39 @@
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
|
||||
#include "libslic3r/Time.hpp"
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
namespace {
|
||||
// Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via
|
||||
// get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a
|
||||
// printer under one agent doesn't silently appear as already-bound under a different,
|
||||
// independent agent. This only covers LAN devices (BBLocalMachine's own scope); access_code
|
||||
// and user_access_code used to be the only, flat dev_id-only AppConfig keys before
|
||||
// BBLocalMachine::access_code existed, and codes saved back then are still stored flat (no
|
||||
// agent association at all). Since BBL was the only agent that existed at the time, honor
|
||||
// those flat legacy keys as implicitly BBL's - but only for the BBL agent, so they aren't
|
||||
// leaked to other agents that never bound the device themselves.
|
||||
std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id, const std::string& agent_id)
|
||||
{
|
||||
const auto& machines = config->get_local_machines();
|
||||
auto it = machines.find(dev_id);
|
||||
if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty())
|
||||
return it->second.access_code;
|
||||
|
||||
if (agent_id == Slic3r::BBL_PRINTER_AGENT_ID || agent_id.empty()) {
|
||||
std::string code = config->get("access_code", dev_id);
|
||||
if (code.empty())
|
||||
code = config->get("user_access_code", dev_id);
|
||||
return code;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
DeviceManager::DeviceManager(NetworkAgent* agent)
|
||||
@@ -43,13 +71,13 @@ namespace Slic3r
|
||||
continue;
|
||||
MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip);
|
||||
obj->printer_type = m.printer_type;
|
||||
obj->printer_agent_id = m.printer_agent_id;
|
||||
obj->dev_connection_type = "lan";
|
||||
obj->bind_state = "free";
|
||||
obj->bind_sec_link = "secure";
|
||||
obj->m_is_online = true;
|
||||
obj->last_alive = Slic3r::Utils::get_current_time_utc();
|
||||
obj->set_access_code(config->get("access_code", m.dev_id), false);
|
||||
obj->set_user_access_code(config->get("user_access_code", m.dev_id), false);
|
||||
obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id, obj->printer_agent_id), false);
|
||||
if (obj->has_access_right()) {
|
||||
localMachineList.insert(std::make_pair(m.dev_id, obj));
|
||||
} else {
|
||||
@@ -66,10 +94,12 @@ namespace Slic3r
|
||||
if (m.is_lan_mode_printer()) {
|
||||
if (m.has_access_right()) {
|
||||
BBLocalMachine local_machine;
|
||||
local_machine.dev_id = m.get_dev_id();
|
||||
local_machine.dev_name = m.get_dev_name();
|
||||
local_machine.dev_ip = m.get_dev_ip();
|
||||
local_machine.printer_type = m.printer_type;
|
||||
local_machine.dev_id = m.get_dev_id();
|
||||
local_machine.dev_name = m.get_dev_name();
|
||||
local_machine.dev_ip = m.get_dev_ip();
|
||||
local_machine.printer_type = m.printer_type;
|
||||
local_machine.printer_agent_id = m.printer_agent_id;
|
||||
local_machine.access_code = m.get_access_code();
|
||||
config->update_local_machine(local_machine);
|
||||
}
|
||||
} else {
|
||||
@@ -132,6 +162,14 @@ namespace Slic3r
|
||||
}
|
||||
}
|
||||
|
||||
std::string DeviceManager::get_current_printer_agent_id() const
|
||||
{
|
||||
if (!m_agent)
|
||||
return "";
|
||||
auto printer_agent = m_agent->get_printer_agent();
|
||||
return printer_agent ? printer_agent->get_agent_info().id : "";
|
||||
}
|
||||
|
||||
void DeviceManager::EnableMultiMachine(bool enable)
|
||||
{
|
||||
m_agent->enable_multi_machine(enable);
|
||||
@@ -328,6 +366,7 @@ namespace Slic3r
|
||||
/* insert a new machine */
|
||||
obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip);
|
||||
obj->printer_type = _parse_printer_type(printer_type_str);
|
||||
obj->printer_agent_id = get_current_printer_agent_id();
|
||||
obj->wifi_signal = printer_signal;
|
||||
obj->dev_connection_type = connect_type;
|
||||
obj->bind_state = bind_state;
|
||||
@@ -339,8 +378,7 @@ namespace Slic3r
|
||||
//load access code
|
||||
AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
obj->set_access_code(Slic3r::GUI::wxGetApp().app_config->get("access_code", dev_id), false);
|
||||
obj->set_user_access_code(Slic3r::GUI::wxGetApp().app_config->get("user_access_code", dev_id), false);
|
||||
obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false);
|
||||
}
|
||||
localMachineList.insert(std::make_pair(dev_id, obj));
|
||||
|
||||
@@ -369,6 +407,7 @@ namespace Slic3r
|
||||
obj = it->second;
|
||||
} else {
|
||||
obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip);
|
||||
obj->printer_agent_id = get_current_printer_agent_id();
|
||||
localMachineList.insert(std::make_pair(machine.dev_id, obj));
|
||||
}
|
||||
if (machine.printer_type.empty())
|
||||
@@ -382,7 +421,6 @@ namespace Slic3r
|
||||
obj->m_is_online = true;
|
||||
obj->last_alive = Slic3r::Utils::get_current_time_utc();
|
||||
obj->set_access_code(access_code, false);
|
||||
obj->set_user_access_code(access_code, false);
|
||||
|
||||
update_local_machine(*obj);
|
||||
|
||||
@@ -496,16 +534,26 @@ namespace Slic3r
|
||||
OnSelectedMachineChanged(previous_selected_machine, selected_machine);
|
||||
}
|
||||
|
||||
void DeviceManager::clear_other_devices()
|
||||
void DeviceManager::clear_other_devices(const std::string& target_agent_id)
|
||||
{
|
||||
// why: on agent swap, keep "My Devices" but drop the transient "Other Devices"
|
||||
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own.
|
||||
//
|
||||
// Also drop "My Devices" stamped by a different agent than the one we're swapping to
|
||||
// (target_agent_id, passed by the caller since the live agent hasn't been repointed yet
|
||||
// at this point): otherwise a device first discovered under agent A survives every swap
|
||||
// with a stale printer_agent_id, stays hidden from every agent's filtered list, and only
|
||||
// gets re-tagged if something happens to delete and re-create it (e.g. account logout).
|
||||
// Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it
|
||||
// like any other fresh device.
|
||||
const auto my = get_my_machine_list();
|
||||
for (auto it = localMachineList.begin(); it != localMachineList.end();)
|
||||
{
|
||||
if (my.find(it->first) == my.end())
|
||||
const bool is_my_device = my.find(it->first) != my.end();
|
||||
const bool agent_mismatch = !target_agent_id.empty() && it->second &&
|
||||
it->second->printer_agent_id != target_agent_id;
|
||||
if (!is_my_device || agent_mismatch)
|
||||
{
|
||||
// not a "My Device" -> an "Other Device"
|
||||
delete it->second;
|
||||
it = localMachineList.erase(it);
|
||||
}
|
||||
@@ -688,13 +736,16 @@ namespace Slic3r
|
||||
m_agent->add_subscribe(subscribe_list_cache);
|
||||
}
|
||||
|
||||
std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list()
|
||||
std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list(const std::string& agent_id)
|
||||
{
|
||||
std::map<std::string, MachineObject*> result;
|
||||
|
||||
for (auto it = userMachineList.begin(); it != userMachineList.end(); it++)
|
||||
{
|
||||
if (it->second && !it->second->is_lan_mode_printer())
|
||||
if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
|
||||
continue;
|
||||
|
||||
if (!it->second->is_lan_mode_printer())
|
||||
{
|
||||
result.insert(std::make_pair(it->first, it->second));
|
||||
}
|
||||
@@ -702,7 +753,10 @@ namespace Slic3r
|
||||
|
||||
for (auto it = localMachineList.begin(); it != localMachineList.end(); it++)
|
||||
{
|
||||
if (it->second && it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer())
|
||||
if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
|
||||
continue;
|
||||
|
||||
if (it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer())
|
||||
{
|
||||
// remove redundant in userMachineList
|
||||
if (result.find(it->first) == result.end())
|
||||
@@ -714,12 +768,15 @@ namespace Slic3r
|
||||
return result;
|
||||
}
|
||||
|
||||
std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list()
|
||||
std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list(const std::string& agent_id)
|
||||
{
|
||||
std::map<std::string, MachineObject*> result;
|
||||
for (auto it = userMachineList.begin(); it != userMachineList.end(); it++)
|
||||
{
|
||||
if (it->second && !it->second->is_lan_mode_printer()) { result.emplace(*it); }
|
||||
if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
|
||||
continue;
|
||||
|
||||
if (!it->second->is_lan_mode_printer()) { result.emplace(*it); }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -792,6 +849,7 @@ namespace Slic3r
|
||||
else
|
||||
{
|
||||
obj = new MachineObject(this, m_agent, "", "", "");
|
||||
obj->printer_agent_id = get_current_printer_agent_id();
|
||||
if (m_agent)
|
||||
{
|
||||
obj->set_bind_status(m_agent->get_user_name(provider));
|
||||
|
||||
@@ -74,7 +74,10 @@ public:
|
||||
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
|
||||
void clean_user_info(bool keep_local_selection = false);
|
||||
|
||||
void clear_other_devices();
|
||||
// target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check,
|
||||
// just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the
|
||||
// live one - this runs before the live agent is repointed.
|
||||
void clear_other_devices(const std::string& target_agent_id = "");
|
||||
|
||||
void load_last_machine();
|
||||
void update_user_machine_list_info(const std::string& provider);
|
||||
@@ -90,10 +93,15 @@ public:
|
||||
|
||||
/* my machine*/
|
||||
MachineObject* get_my_machine(std::string dev_id);
|
||||
std::map<std::string, MachineObject*> get_my_machine_list();
|
||||
std::map<std::string, MachineObject*> get_my_cloud_machine_list();
|
||||
std::map<std::string, MachineObject*> get_my_machine_list(const std::string& agent_id = "");
|
||||
std::map<std::string, MachineObject*> get_my_cloud_machine_list(const std::string& agent_id = "");
|
||||
void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider);
|
||||
|
||||
// id of the currently live IPrinterAgent (IPrinterAgent::get_agent_info().id), or empty if
|
||||
// m_agent has no printer agent set yet. Pass to get_my_machine_list()/get_my_cloud_machine_list()
|
||||
// to scope results to the active agent.
|
||||
std::string get_current_printer_agent_id() const;
|
||||
|
||||
/* create machine or update machine properties */
|
||||
void on_machine_alive(std::string json_str);
|
||||
int query_bind_status(std::string& msg, const std::string& provider);
|
||||
|
||||
@@ -27,6 +27,7 @@ void DevStatus::ParseStatus(const nlohmann::json& print_jj)
|
||||
#else
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get exception=" << e.what();
|
||||
#endif
|
||||
(void)e; // suppress C4101 when BBL_RELEASE_TO_PUBLIC
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "libslic3r/Time.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
@@ -449,9 +450,7 @@ bool MachineObject::HasRecentLanMessage()
|
||||
|
||||
std::string MachineObject::get_access_code() const
|
||||
{
|
||||
if (get_user_access_code().empty())
|
||||
return access_code;
|
||||
return get_user_access_code();
|
||||
return access_code;
|
||||
}
|
||||
|
||||
void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
@@ -460,47 +459,46 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
if (only_refresh) {
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
if (!code.empty()) {
|
||||
GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code);
|
||||
DeviceManager::update_local_machine(*this);
|
||||
if (is_lan_mode_printer()) {
|
||||
// why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
|
||||
// scoped by that record's own printer_agent_id field - see the matching comment
|
||||
// on get_access_code_with_legacy_fallback() in DevManager.cpp - so binding this
|
||||
// device under one printer agent doesn't silently read as already-bound under a
|
||||
// different, independent one. Cloud devices (the else branch below) aren't
|
||||
// scoped this way: they're never recalled from a stale local cache across a
|
||||
// session boundary, since parse_user_print_info() always overwrites their code
|
||||
// fresh from the cloud API's current response, so there's no cross-agent leakage
|
||||
// risk to guard against there.
|
||||
if (!code.empty()) {
|
||||
DeviceManager::update_local_machine(*this);
|
||||
} else {
|
||||
// Only patch an existing record's code - don't persist a brand-new
|
||||
// never-bound entry just because set_access_code("") was called on it.
|
||||
const auto& machines = config->get_local_machines();
|
||||
auto it = machines.find(get_dev_id());
|
||||
if (it != machines.end()) {
|
||||
BBLocalMachine local_machine = it->second;
|
||||
local_machine.access_code = "";
|
||||
config->update_local_machine(local_machine);
|
||||
}
|
||||
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
|
||||
// old BBL-era code can't silently "re-bind" this device again via
|
||||
// get_access_code_with_legacy_fallback()'s legacy fallback.
|
||||
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
|
||||
config->erase("access_code", get_dev_id());
|
||||
config->erase("user_access_code", get_dev_id());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GUI::wxGetApp().app_config->erase("access_code", get_dev_id());
|
||||
if (!code.empty())
|
||||
config->set_str("access_code", get_dev_id(), code);
|
||||
else
|
||||
config->erase("access_code", get_dev_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MachineObject::erase_user_access_code()
|
||||
{
|
||||
this->user_access_code = "";
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
GUI::wxGetApp().app_config->erase("user_access_code", get_dev_id());
|
||||
//GUI::wxGetApp().app_config->save();
|
||||
}
|
||||
}
|
||||
|
||||
void MachineObject::set_user_access_code(std::string code, bool only_refresh)
|
||||
{
|
||||
this->user_access_code = code;
|
||||
if (only_refresh && !code.empty()) {
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config && !code.empty()) {
|
||||
GUI::wxGetApp().app_config->set_str("user_access_code", get_dev_id(), code);
|
||||
DeviceManager::update_local_machine(*this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string MachineObject::get_user_access_code() const
|
||||
{
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
return GUI::wxGetApp().app_config->get("user_access_code", get_dev_id());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string MachineObject::get_show_printer_type() const
|
||||
{
|
||||
std::string printer_type = this->printer_type;
|
||||
@@ -2907,7 +2905,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
|
||||
std::string access_code = j_pre["system"]["access_code"].get<std::string>();
|
||||
if (!access_code.empty()) {
|
||||
set_access_code(access_code);
|
||||
set_user_access_code(access_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ private:
|
||||
std::string dev_name;
|
||||
std::string dev_ip;
|
||||
std::string access_code;
|
||||
std::string user_access_code;
|
||||
|
||||
// type, time stamp, delay
|
||||
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
|
||||
@@ -228,13 +227,18 @@ public:
|
||||
std::string get_access_code() const;
|
||||
void set_access_code(std::string code, bool only_refresh = true);
|
||||
|
||||
/*user access code*/
|
||||
void set_user_access_code(std::string code, bool only_refresh = true);
|
||||
void erase_user_access_code();
|
||||
std::string get_user_access_code() const;
|
||||
|
||||
//PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN;
|
||||
std::string printer_type; /* model_id */
|
||||
|
||||
// id of the IPrinterAgent that was used to discover or bind this device (IPrinterAgent::get_agent_info().id,
|
||||
// e.g. "bbl"), stamped at creation time — not derived from get_agent(), since m_agent is a single
|
||||
// process-wide NetworkAgent shared by every MachineObject and gets repointed on agent swap
|
||||
// (see DeviceManager::set_agent()), so it can't tell which agent originally found this device.
|
||||
// We persist this as well so that when the printer agent is swapped, we don't show unrelated devices,
|
||||
// e.g. if the current printer agent is elegoo, we shouldn't show printers connected by BBL printer agent
|
||||
// under local machines.
|
||||
std::string printer_agent_id;
|
||||
|
||||
std::string get_show_printer_type() const;
|
||||
PrinterSeries get_printer_series() const;
|
||||
PrinterArch get_printer_arch() const;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* File: uiAMSBestPositionPopup.hpp
|
||||
* Description: The popup with suggest best ams position
|
||||
*
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#include "uiAMSBestPositionPopup.hpp"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* File: uiAMSBestPositionPopup.hpp
|
||||
* Description: The popup with suggest best ams position
|
||||
*
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "slic3r/GUI/Widgets/AMSItem.hpp"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* \n class wgtDeviceNozzleRackNozzleItem;
|
||||
* \n class wgtDeviceNozzleRackToolHead;
|
||||
* \n class wgtDeviceNozzleRackPos;
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#include "wgtDeviceNozzleRack.h"
|
||||
#include "wgtDeviceNozzleRackUpdate.h"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* \n class wgtDeviceNozzleRackNozzleItem;
|
||||
* \n class wgtDeviceNozzleRackToolHead;
|
||||
* \n class wgtDeviceNozzleRackPos;
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "slic3r/GUI/DeviceCore/DevNozzleRack.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Description: The panel with rack updating
|
||||
*
|
||||
* \n class wgtDeviceNozzleRackUpdate
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#include "wgtDeviceNozzleRackUpdate.h"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Description: The panel for updating hotends
|
||||
*
|
||||
* \n class wgtDeviceNozzleRackUpdate
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "slic3r/GUI/DeviceCore/DevNozzleRack.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Description: The panel to select nozzle
|
||||
*
|
||||
* \n class wgtDeviceNozzleSelect;
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#include "wgtDeviceNozzleSelect.h"
|
||||
#include "wgtDeviceNozzleRack.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Description: The panel to select nozzle
|
||||
*
|
||||
* \n class wgtDeviceNozzleSelect;
|
||||
//**********************************************************/
|
||||
************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
|
||||
#include "Widgets/HyperLink.hpp" // ORCA
|
||||
|
||||
#define DESIGN_INPUT_SIZE wxSize(FromDIP(100), -1)
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url)
|
||||
Plater* plater = wxGetApp().plater();
|
||||
|
||||
mainframe->Freeze();
|
||||
mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
|
||||
mainframe->select_tab(TAB_ID_PREPARE);
|
||||
plater->select_view_3D("3D");
|
||||
plater->select_view("plate");
|
||||
plater->get_current_canvas3D()->zoom_to_bed();
|
||||
|
||||
@@ -331,8 +331,10 @@ void Field::PostInitialize()
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
if (tab_id >= 0)
|
||||
wxGetApp().mainframe->select_tab(tab_id);
|
||||
if (tab_id >= 0) {
|
||||
static constexpr const char* kShortcutTabIds[] = {TAB_ID_HOME, TAB_ID_PREPARE, TAB_ID_PREVIEW, TAB_ID_MONITOR};
|
||||
wxGetApp().mainframe->select_tab(kShortcutTabIds[tab_id]);
|
||||
}
|
||||
if (tab_id > 0)
|
||||
// tab panel should be focused for correct navigation between tabs
|
||||
wxGetApp().tab_panel()->SetFocus();
|
||||
|
||||
@@ -385,7 +385,7 @@ public:
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
/// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
|
||||
void propagate_value() ;
|
||||
void propagate_value() override;
|
||||
|
||||
void set_value(const std::string& value, bool change_event = false) {
|
||||
m_disable_change_event = !change_event;
|
||||
@@ -440,7 +440,7 @@ public:
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
// Propagate value from field to the OptionGroupe and Config after kill_focus/ENTER
|
||||
void propagate_value();
|
||||
void propagate_value() override;
|
||||
|
||||
/* Under OSX: wxBitmapComboBox->GetWindowStyle() returns some weard value,
|
||||
* so let use a flag, which has TRUE value for a control without wxCB_READONLY style
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
@@ -28,6 +31,113 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from,
|
||||
}
|
||||
}
|
||||
|
||||
static std::string to_hex(const wxColour& c)
|
||||
{
|
||||
return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString();
|
||||
}
|
||||
|
||||
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights)
|
||||
{
|
||||
const size_t n = std::min(cols.size(), weights.size());
|
||||
std::vector<std::string> hex_colors;
|
||||
std::vector<int> int_weights;
|
||||
hex_colors.reserve(n);
|
||||
int_weights.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
hex_colors.push_back(to_hex(cols[i]));
|
||||
// Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi;
|
||||
// only relative magnitude matters.
|
||||
int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000.0)));
|
||||
}
|
||||
wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights));
|
||||
return blended.IsOk() ? blended : wxColour(128, 128, 128);
|
||||
}
|
||||
|
||||
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
||||
const wxColour& second,
|
||||
const Slic3r::GradientCurve& curve,
|
||||
int steps)
|
||||
{
|
||||
std::vector<wxColour> ramp;
|
||||
if (steps <= 0 || curve.points.size() < 2) return ramp;
|
||||
|
||||
ramp.reserve(steps);
|
||||
for (int i = 0; i < steps; ++i) {
|
||||
const double t = (steps > 1) ? (i + 0.5) / steps : 0.5;
|
||||
const double r1 = Slic3r::sample_gradient_curve(curve, t);
|
||||
ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1}));
|
||||
}
|
||||
return ramp;
|
||||
}
|
||||
|
||||
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
|
||||
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
|
||||
// endpoints, otherwise the 0.10 -> 0.90 default.
|
||||
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
|
||||
{
|
||||
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
|
||||
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
|
||||
Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]);
|
||||
if (custom.points.size() >= 2) return custom;
|
||||
}
|
||||
|
||||
double start = kGradientMinRatio, end = kGradientMaxRatio;
|
||||
const auto* range_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_range");
|
||||
if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) {
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
float v0 = 0, v1 = 0;
|
||||
if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 &&
|
||||
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
|
||||
start = v0;
|
||||
end = v1;
|
||||
}
|
||||
}
|
||||
|
||||
Slic3r::GradientCurve curve;
|
||||
curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}};
|
||||
return curve;
|
||||
}
|
||||
|
||||
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps)
|
||||
{
|
||||
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
|
||||
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
|
||||
if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {};
|
||||
if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {};
|
||||
if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {};
|
||||
if (slot >= comp_opt->values.size()) return {};
|
||||
|
||||
// Only two-component slots fade; anything else stays on the plain blended swatch.
|
||||
const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]);
|
||||
if (comp_ids.size() != 2) return {};
|
||||
|
||||
auto component_colour = [&](unsigned int id) {
|
||||
wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour();
|
||||
return c.IsOk() ? c : wxColour("#D9D9D9");
|
||||
};
|
||||
|
||||
// Both gradient_range and the curve express the *first* component's ratio over Z, so
|
||||
// the components stay in config order and the curve alone decides which end is which.
|
||||
return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]),
|
||||
mixed_gradient_curve(cfg, slot), steps);
|
||||
}
|
||||
|
||||
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp)
|
||||
{
|
||||
if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return;
|
||||
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
for (int y = 0; y < rect.height; ++y) {
|
||||
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
|
||||
// Mapping over height - 1 puts both ends of the ramp on screen even in a short swatch.
|
||||
const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5;
|
||||
dc.SetBrush(wxBrush(ramp[static_cast<size_t>(t * (ramp.size() - 1) + 0.5)]));
|
||||
dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper struct to hold bitmap and DC
|
||||
struct BitmapDC {
|
||||
wxBitmap bitmap;
|
||||
@@ -47,6 +157,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) {
|
||||
return BitmapDC(size);
|
||||
}
|
||||
|
||||
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size)
|
||||
{
|
||||
if (ramp.empty()) return wxNullBitmap;
|
||||
|
||||
BitmapDC bdc = init_bitmap_dc(size);
|
||||
if (!bdc.dc.IsOk()) return wxNullBitmap;
|
||||
|
||||
fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp);
|
||||
|
||||
bdc.dc.SelectObject(wxNullBitmap);
|
||||
return bdc.bitmap;
|
||||
}
|
||||
|
||||
// Check if a color is transparent (alpha == 0)
|
||||
static bool is_transparent_color(const wxColour& color) {
|
||||
return color.Alpha() == 0;
|
||||
@@ -265,4 +388,65 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
|
||||
}
|
||||
}
|
||||
|
||||
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
|
||||
const Slic3r::DynamicPrintConfig& cfg)
|
||||
{
|
||||
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
const auto* ratio_opt = cfg.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
|
||||
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
|
||||
if (!is_mixed_opt || !comp_opt) return;
|
||||
|
||||
const size_t n = is_mixed_opt->values.size();
|
||||
if (colors.size() < n) colors.resize(n);
|
||||
|
||||
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
|
||||
const auto kFallback = wxColour(128, 128, 128, 255);
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!is_mixed_opt->values[i]) continue;
|
||||
|
||||
if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; }
|
||||
auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]);
|
||||
if (comp_ids.empty()) { colors[i] = kFallback; continue; }
|
||||
|
||||
bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i];
|
||||
std::vector<unsigned int> use_ids = comp_ids;
|
||||
std::vector<int> weights;
|
||||
|
||||
if (is_gradient && comp_ids.size() >= 2) {
|
||||
use_ids = { comp_ids.front(), comp_ids.back() };
|
||||
weights = { 5000, 5000 };
|
||||
} else {
|
||||
auto ratios_d = Slic3r::parse_mixed_ratios(
|
||||
(ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{},
|
||||
comp_ids.size());
|
||||
weights.reserve(comp_ids.size());
|
||||
for (double r : ratios_d)
|
||||
weights.push_back(static_cast<int>(std::lround(r * 10000.0)));
|
||||
}
|
||||
|
||||
std::vector<std::string> hex_colors;
|
||||
hex_colors.reserve(use_ids.size());
|
||||
bool any_invalid = false;
|
||||
for (unsigned int id : use_ids) {
|
||||
if (id == 0 || id > colors.size()) { any_invalid = true; break; }
|
||||
wxColour c = colors[id - 1];
|
||||
if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) {
|
||||
hex_colors.push_back(to_hex(c));
|
||||
} else if (colour_opt && (id - 1) < colour_opt->values.size()) {
|
||||
hex_colors.push_back(colour_opt->values[id - 1]);
|
||||
} else {
|
||||
any_invalid = true; break;
|
||||
}
|
||||
}
|
||||
if (any_invalid) { colors[i] = kFallback; continue; }
|
||||
|
||||
std::string hex = Slic3r::blend_color_multi(hex_colors, weights);
|
||||
wxColour blended(hex);
|
||||
if (!blended.IsOk()) blended = kFallback;
|
||||
colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255);
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <wx/gdicmn.h>
|
||||
#include <vector>
|
||||
|
||||
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
|
||||
// force-included pch (the GUI test suite includes it directly).
|
||||
namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
|
||||
@@ -28,6 +32,37 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors,
|
||||
const wxSize& size,
|
||||
bool force_gradient = false);
|
||||
|
||||
// Blend colours at the given relative weights through blend_color_multi, so a measured
|
||||
// real-world mix is used where one exists instead of a plain channel lerp.
|
||||
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights);
|
||||
|
||||
// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the
|
||||
// model's height, the curve gives the first component's ratio at t, and the two
|
||||
// components are blended at that ratio through blend_n_colors. Entry 0 is the bottom
|
||||
// of the model, the last entry its top.
|
||||
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
||||
const wxColour& second,
|
||||
const Slic3r::GradientCurve& curve,
|
||||
int steps);
|
||||
|
||||
// Same ramp for a project config slot, resolving components, colours and curve (or the
|
||||
// linear gradient_range fallback) from cfg. Returns empty for any slot that is not a
|
||||
// two-component gradient mixed filament. steps is the ramp's resolution; pass the
|
||||
// destination's height in pixels.
|
||||
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
|
||||
|
||||
// Fill rect with a ramp, ramp.front() along the bottom edge.
|
||||
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
|
||||
|
||||
// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp.
|
||||
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size);
|
||||
|
||||
// Recompute blended representative colors for mixed (virtual) filament slots.
|
||||
// Reads mixed-filament config keys from cfg and writes back into colors[i]
|
||||
// for every slot where filament_is_mixed[i] is true.
|
||||
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
|
||||
const Slic3r::DynamicPrintConfig& cfg);
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_
|
||||
@@ -420,7 +420,7 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
|
||||
if (properties_shown) {
|
||||
float label_w = 0.0f;
|
||||
float value_w = 0.0f;
|
||||
properties_rows.reserve(13);
|
||||
properties_rows.reserve(14);
|
||||
auto add_row = [&properties_rows, &label_w, &value_w](std::string label, std::string value) {
|
||||
label_w = std::max(label_w, ImGui::CalcTextSize(label.c_str()).x);
|
||||
value_w = std::max(value_w, ImGui::CalcTextSize(value.c_str()).x);
|
||||
@@ -433,6 +433,27 @@ void GCodeViewer::SequentialView::Marker::render_position_window(const libvgcode
|
||||
add_row(_u8L("Width"), buff);
|
||||
if (is_extrusion) sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), vertex.height); else strcpy(buff, NA_CSTR);
|
||||
add_row(_u8L("Height"), buff);
|
||||
// ORCA: Length of the move ending at the current vertex. Arc moves (G2/G3) are discretized
|
||||
// into several vertices sharing the same gcode line id, so accumulate the whole run to report
|
||||
// the arc length instead of the length of a single chord.
|
||||
if (vertex_id > 0 && (is_extrusion || vertex.is_travel() || vertex.is_wipe())) {
|
||||
const size_t vertices_count = viewer->get_vertices_count();
|
||||
size_t first_id = vertex_id;
|
||||
while (first_id > 0 && viewer->get_vertex_at(first_id - 1).gcode_id == vertex.gcode_id)
|
||||
--first_id;
|
||||
size_t last_id = vertex_id;
|
||||
while (last_id + 1 < vertices_count && viewer->get_vertex_at(last_id + 1).gcode_id == vertex.gcode_id)
|
||||
++last_id;
|
||||
float length = 0.0f;
|
||||
for (size_t i = std::max<size_t>(first_id, 1); i <= last_id; ++i) {
|
||||
length += (libvgcode::convert(viewer->get_vertex_at(i).position) -
|
||||
libvgcode::convert(viewer->get_vertex_at(i - 1).position)).norm();
|
||||
}
|
||||
sprintf(buff, ("%.3f " + _u8L("mm")).c_str(), length);
|
||||
}
|
||||
else
|
||||
strcpy(buff, NA_CSTR);
|
||||
add_row(_u8L("Length"), buff);
|
||||
sprintf(buff, "%d", vertex.layer_id + 1);
|
||||
add_row(_u8L("Layer"), buff);
|
||||
sprintf(buff, ("%.1f " + _u8L("mm/s")).c_str(), vertex.feedrate);
|
||||
|
||||
@@ -155,6 +155,11 @@ std::string& get_filament_mixture_warning_text(){
|
||||
return filament_mixture_warning_text;
|
||||
}
|
||||
|
||||
std::string& get_single_extruder_mixed_filament_warning_text(){
|
||||
static std::string single_extruder_mixed_filament_warning_text;
|
||||
return single_extruder_mixed_filament_warning_text;
|
||||
}
|
||||
|
||||
|
||||
static std::string format_number(float value)
|
||||
{
|
||||
@@ -2887,7 +2892,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id);
|
||||
float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id);
|
||||
float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_tower_width"))->value;
|
||||
float a = dynamic_cast<const ConfigOptionFloat*>(proj_cfg.option("wipe_tower_rotation_angle"))->value;
|
||||
float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
|
||||
// BBS
|
||||
float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value;
|
||||
Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin();
|
||||
@@ -2984,6 +2989,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
bool mix_pla_and_petg = cur_plate->check_mixture_of_pla_and_petg(full_config_temp);
|
||||
_set_warning_notification(EWarning::MixUsePLAAndPETG, !mix_pla_and_petg);
|
||||
|
||||
bool single_extruder_mixed_risk = cur_plate->check_single_extruder_mixed_filament_risk(full_config_temp, get_single_extruder_mixed_filament_warning_text());
|
||||
_set_warning_notification(EWarning::SingleExtruderMixedFilament, single_extruder_mixed_risk);
|
||||
|
||||
bool filament_nozzle_compatible = cur_plate->check_compatible_of_nozzle_and_filament(full_config_temp, wxGetApp().preset_bundle->filament_presets, get_nozzle_filament_incompatible_text());
|
||||
_set_warning_notification(EWarning::NozzleFilamentIncompatible, !filament_nozzle_compatible);
|
||||
|
||||
@@ -3010,6 +3018,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
_set_warning_notification(EWarning::TPUPrintableError, false);
|
||||
_set_warning_notification(EWarning::FilamentPrintableError, false);
|
||||
_set_warning_notification(EWarning::MixUsePLAAndPETG, false);
|
||||
_set_warning_notification(EWarning::SingleExtruderMixedFilament, false);
|
||||
_set_warning_notification(EWarning::PrimeTowerOutside, false);
|
||||
_set_warning_notification(EWarning::MultiExtruderPrintableError,false);
|
||||
_set_warning_notification(EWarning::MultiExtruderHeightOutside,false);
|
||||
@@ -8902,7 +8911,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
|
||||
}
|
||||
else {
|
||||
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice()))
|
||||
// A plate using a mixed filament whose components are broken cannot be sliced,
|
||||
// so surface that on the plate toolbar the same way an unsliceable plate is.
|
||||
if ((!is_empty && !can_slice) || (plate_list.get_plate(i)->has_printable_instances() && !plate_list.get_plate(i)->can_slice())
|
||||
|| wxGetApp().plater()->sidebar().has_broken_mixed_filament(plate_list.get_plate(i)))
|
||||
m_sel_plate_toolbar.m_items[i]->slice_state = IMToolbarItem::SliceState::SLICE_FAILED;
|
||||
else {
|
||||
if (plate_list.get_plate(i)->get_slicing_percent() < 0.0f)
|
||||
@@ -9196,7 +9208,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos
|
||||
view3d_canvas->reload_scene(true);
|
||||
}
|
||||
app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
|
||||
app.mainframe->select_tab(TAB_ID_PREPARE);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -9669,6 +9681,13 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
}
|
||||
}
|
||||
}
|
||||
// ORCA: the loop above only labels a slot whose preset was found in the preset collection,
|
||||
// while the render loop below iterates extruder_num. Pad the label arrays so a slot without a
|
||||
// matching preset cannot index past them; a garbage std::string crashes ImGui::CalcTextSize.
|
||||
while (int(filament_text_first_line.size()) < extruder_num) {
|
||||
filament_text_first_line.emplace_back();
|
||||
filament_text_second_line.emplace_back();
|
||||
}
|
||||
|
||||
ImGuiWrapper& imgui = *wxGetApp().imgui();
|
||||
const float canvas_w = float(get_canvas_size().get_width());
|
||||
@@ -9698,6 +9717,10 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
bool disabled = !wxGetApp().plater()->can_fillcolor();
|
||||
ColorRGBA rgba;
|
||||
|
||||
// Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than
|
||||
// the single blended colour in `colors`. Every other slot's ramp is empty.
|
||||
const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
|
||||
|
||||
for (int i = 0; i < extruder_num; i++) {
|
||||
if (i > 0)
|
||||
ImGui::SameLine();
|
||||
@@ -9711,6 +9734,8 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max))
|
||||
wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1));
|
||||
}
|
||||
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty())
|
||||
ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]);
|
||||
if (ImGui::IsItemHovered() && i < 9) {
|
||||
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
|
||||
@@ -9726,7 +9751,13 @@ void GLCanvas3D::_render_paint_toolbar() const
|
||||
|
||||
const float text_offset_y = 4.0f * em_unit * f_scale;
|
||||
for (int i = 0; i < extruder_num; i++) {
|
||||
decode_color(colors[i], rgba);
|
||||
// A gradient slot's swatch shows its fade instead of the blended colour in `colors`, so the
|
||||
// labels take their contrast from the colour printed at the middle of the fade they sit on.
|
||||
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty()) {
|
||||
const wxColour& c = gradient_ramps[i][gradient_ramps[i].size() / 2];
|
||||
rgba = ColorRGBA(c.Red(), c.Green(), c.Blue(), c.Alpha());
|
||||
} else
|
||||
decode_color(colors[i], rgba);
|
||||
float gray = 0.299 * rgba.r_uchar() + 0.587 * rgba.g_uchar() + 0.114 * rgba.b_uchar();
|
||||
ImVec4 text_color = gray < 80 ? ImVec4(1.0f, 1.0f, 1.0f, 1.0f) : ImVec4(0, 0, 0, 1.0f);
|
||||
|
||||
@@ -10570,6 +10601,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
case EWarning::MixUsePLAAndPETG:
|
||||
text = _u8L("PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality.");
|
||||
break;
|
||||
case EWarning::SingleExtruderMixedFilament:
|
||||
text = get_single_extruder_mixed_filament_warning_text();
|
||||
break;
|
||||
case EWarning::PrimeTowerOutside:
|
||||
text = _u8L("The prime tower extends beyond the plate boundary.");
|
||||
break;
|
||||
@@ -10602,9 +10636,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
wxString region = L"en";
|
||||
if (language.find("zh") == 0)
|
||||
region = L"zh";
|
||||
// Use the generic dual-nozzle PLA+PETG guide rather than the H2D-specific page
|
||||
// so the link is relevant for all dual-extrusion printers, not just Bambu H2D. (#12073)
|
||||
wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/pla-and-petg-dual-extrusion", region));
|
||||
// Although this link looks like it's only for the H2D, its guidance is generic.
|
||||
wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/h2d-pla-and-petg-mutual-support", region));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -10619,6 +10652,14 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLNozzleFilamentIncompatible, NotificationLevel::WarningNotificationLevel);
|
||||
}
|
||||
}
|
||||
else if (warning == EWarning::SingleExtruderMixedFilament) {
|
||||
// Close by type: check_single_extruder_mixed_filament_risk() clears the shared text
|
||||
// buffer on every call, so a close-by-text would miss once the risk is gone.
|
||||
if (state)
|
||||
notification_manager.push_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel, text);
|
||||
else
|
||||
notification_manager.close_slicing_customize_error_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk, NotificationLevel::WarningNotificationLevel);
|
||||
}
|
||||
else {
|
||||
if (state)
|
||||
notification_manager.push_plater_warning_notification(text);
|
||||
@@ -10738,24 +10779,14 @@ bool GLCanvas3D::is_flushing_matrix_error() {
|
||||
if (!Sidebar::should_show_SEMM_buttons())
|
||||
return false;
|
||||
|
||||
std::vector<int> plate_extruders = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_extruders(true);
|
||||
if (plate_extruders.size() < 2)
|
||||
return false;
|
||||
|
||||
const auto &project_config = wxGetApp().preset_bundle->project_config;
|
||||
const std::vector<double> &config_matrix = (project_config.option<ConfigOptionFloats>("flush_volumes_matrix"))->values;
|
||||
const std::vector<double> &config_multiplier = (project_config.option<ConfigOptionFloats>("flush_multiplier"))->values;
|
||||
|
||||
for (auto multiplier : config_multiplier) {
|
||||
if (multiplier == 0) return true;
|
||||
}
|
||||
|
||||
int matrix_len = config_matrix.size() / config_multiplier.size();
|
||||
int row_len = std::sqrt(matrix_len);
|
||||
for (int i = 0; i < config_matrix.size(); i++)
|
||||
{
|
||||
int relative_id = i % matrix_len;
|
||||
int row_id = relative_id / row_len;
|
||||
int col_id = relative_id % row_len;
|
||||
if (row_id != col_id && config_matrix[i] == 0) return true;
|
||||
}
|
||||
return false;
|
||||
return has_zero_flush_volume_for_used_filaments(config_matrix, config_multiplier, plate_extruders);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_is_any_volume_outside() const
|
||||
|
||||
@@ -391,6 +391,7 @@ class GLCanvas3D
|
||||
PrimeTowerOutside,
|
||||
NozzleFilamentIncompatible,
|
||||
MixtureFilamentIncompatible,
|
||||
SingleExtruderMixedFilament,
|
||||
FlushingVolumeZero
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#elif _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include "boost/nowide/convert.hpp"
|
||||
#endif
|
||||
|
||||
+58
-24
@@ -521,10 +521,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
|
||||
/* FT_GCODE */ { L("G-code files"), { ".gcode"sv} },
|
||||
#ifdef __APPLE__
|
||||
/* FT_MODEL */
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
|
||||
#else
|
||||
/* FT_MODEL */
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}},
|
||||
{L("Supported files"), {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".gltf"sv, ".glb"sv, ".fbx"sv, ".drc"sv}},
|
||||
#endif
|
||||
/* FT_ZIP */ { L("ZIP files"), { ".zip"sv } },
|
||||
/* FT_PROJECT */ { L("Project files"), { ".3mf"sv} },
|
||||
@@ -813,12 +813,12 @@ void GUI_App::post_init()
|
||||
m_open_method = "url";
|
||||
} else {
|
||||
if (this->init_params->input_gcode) {
|
||||
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
|
||||
mainframe->select_tab(TAB_ID_PREPARE);
|
||||
plater_->select_view_3D("3D");
|
||||
this->plater()->load_gcode(from_u8(this->init_params->input_files.front()));
|
||||
m_open_method = "gcode";
|
||||
} else {
|
||||
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
|
||||
mainframe->select_tab(TAB_ID_PREPARE);
|
||||
plater_->select_view_3D("3D");
|
||||
wxArrayString input_files;
|
||||
for (auto& file : this->init_params->input_files) {
|
||||
@@ -852,7 +852,7 @@ void GUI_App::post_init()
|
||||
mainframe->Freeze();
|
||||
#endif
|
||||
plater_->canvas3D()->enable_render(false);
|
||||
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
|
||||
mainframe->select_tab(TAB_ID_PREPARE);
|
||||
plater_->select_view_3D("3D");
|
||||
//BBS init the opengl resource here
|
||||
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
|
||||
@@ -890,9 +890,9 @@ void GUI_App::post_init()
|
||||
}
|
||||
}
|
||||
if (is_editor())
|
||||
mainframe->select_tab(size_t(0));
|
||||
mainframe->select_tab(TAB_ID_HOME);
|
||||
if (app_config->get("default_page") == "1")
|
||||
mainframe->select_tab(size_t(1));
|
||||
mainframe->select_tab(TAB_ID_PREPARE);
|
||||
#ifndef __linux__
|
||||
mainframe->Thaw();
|
||||
#endif
|
||||
@@ -1829,10 +1829,10 @@ bool GUI_App::hot_reload_network_plugin()
|
||||
wxWindowDisabler disabler;
|
||||
|
||||
if (mainframe) {
|
||||
int current_tab = mainframe->m_tabpanel->GetSelection();
|
||||
if (current_tab == MainFrame::TabPosition::tpMonitor) {
|
||||
wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName();
|
||||
if (current_tab == TAB_ID_MONITOR) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload";
|
||||
mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tp3DEditor);
|
||||
mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREPARE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2166,7 +2166,6 @@ void GUI_App::init_networking_callbacks()
|
||||
obj->is_tunnel_mqtt = tunnel;
|
||||
obj->command_request_push_all(true);
|
||||
obj->command_get_version();
|
||||
obj->erase_user_access_code();
|
||||
obj->command_get_access_code();
|
||||
if (m_agent)
|
||||
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
|
||||
@@ -2216,7 +2215,6 @@ void GUI_App::init_networking_callbacks()
|
||||
wxString text;
|
||||
if (msg == "5") {
|
||||
obj->set_access_code("");
|
||||
obj->erase_user_access_code();
|
||||
text = wxString::Format(_L("Incorrect password"));
|
||||
wxGetApp().show_dialog(text);
|
||||
} else {
|
||||
@@ -2853,6 +2851,16 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
||||
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
|
||||
if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
|
||||
return;
|
||||
wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key);
|
||||
});
|
||||
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
|
||||
if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
|
||||
return;
|
||||
wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key);
|
||||
});
|
||||
plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load);
|
||||
plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload);
|
||||
plugin_mgr.subscribe_on_capability_load_callback(
|
||||
@@ -2868,11 +2876,15 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->revalidate_current_plate_if_plugins_missing();
|
||||
});
|
||||
if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
|
||||
wxGetApp().mainframe->plugin_pages().on_cap_register(capability);
|
||||
});
|
||||
plugin_mgr.subscribe_on_capability_unload_callback(
|
||||
[refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||
if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
|
||||
wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability);
|
||||
refresh_plugins_dialog();
|
||||
switch_printer_agent_after_unload(capability.plugin_key);
|
||||
});
|
||||
@@ -3275,15 +3287,12 @@ bool GUI_App::on_init_inner()
|
||||
}
|
||||
} */
|
||||
|
||||
copy_network_if_available();
|
||||
|
||||
if (scrn) {
|
||||
scrn->SetText(_L("Loading Plugins") + dots, 20);
|
||||
wxYield();
|
||||
}
|
||||
|
||||
on_init_network();
|
||||
|
||||
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically.
|
||||
// initialize() also installs the libslic3r hooks (capability resolver,
|
||||
// slicing-pipeline dispatcher) via plugin_hooks::install() -- no
|
||||
@@ -3312,6 +3321,9 @@ bool GUI_App::on_init_inner()
|
||||
}
|
||||
}
|
||||
|
||||
copy_network_if_available();
|
||||
on_init_network();
|
||||
|
||||
if (m_agent)
|
||||
plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()));
|
||||
|
||||
@@ -3384,7 +3396,7 @@ bool GUI_App::on_init_inner()
|
||||
mainframe = new MainFrame();
|
||||
// hide settings tabs after first Layout
|
||||
if (is_editor()) {
|
||||
mainframe->select_tab(size_t(0));
|
||||
mainframe->select_tab(TAB_ID_HOME);
|
||||
}
|
||||
|
||||
sidebar().obj_list()->init();
|
||||
@@ -3939,7 +3951,13 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
|
||||
m_agent->set_user_selected_machine("");
|
||||
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
|
||||
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
|
||||
dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices
|
||||
// why: drop stale LAN discoveries; keep My Devices, but only those belonging to the
|
||||
// agent we're about to swap to, so a device stamped by the outgoing agent doesn't
|
||||
// linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh.
|
||||
// agent is null when clearing the live agent entirely (e.g. plugin unload); there's no
|
||||
// target to filter against then, so fall back to the original "keep all My Devices"
|
||||
// behavior rather than guessing.
|
||||
dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string());
|
||||
}
|
||||
|
||||
m_agent->set_printer_agent(agent);
|
||||
@@ -4592,7 +4610,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
|
||||
mainframe = new MainFrame();
|
||||
if (is_editor())
|
||||
// hide settings tabs after first Layout
|
||||
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
|
||||
mainframe->select_tab(TAB_ID_PREPARE);
|
||||
// Propagate model objects to object list.
|
||||
sidebar().obj_list()->init();
|
||||
//sidebar().aux_list()->init_auxiliary();
|
||||
@@ -6799,6 +6817,12 @@ void GUI_App::add_pending_vendor_preset(const std::pair<std::string, std::map<st
|
||||
|
||||
// Add the corresponding vendor
|
||||
std::string vendor_name = PresetBundle::find_preset_vendor(inherits_name, type);
|
||||
if (vendor_name.empty()) {
|
||||
// No vendor ships this preset's parent. An unnamed entry here becomes an
|
||||
// unnamed bundle at install time, which nothing can install.
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no vendor carries " << inherits_name << ", skipping";
|
||||
return;
|
||||
}
|
||||
if (need_add_vendors.find(vendor_name) == need_add_vendors.end())
|
||||
need_add_vendors[vendor_name] = std::map<std::string, std::set<std::string>>();
|
||||
|
||||
@@ -8286,7 +8310,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title)
|
||||
wxGetApp().app_config->save();
|
||||
|
||||
obj->set_dev_ip(ip_address.ToStdString());
|
||||
obj->set_user_access_code(access_code.ToStdString());
|
||||
obj->set_access_code(access_code.ToStdString());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -8881,7 +8905,17 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch
|
||||
if (printer_technology == ptFFF && !edited_printer_preset.config.opt_bool("single_extruder_multi_material")) {
|
||||
auto* nozzle_diameter = edited_printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
if (nozzle_diameter) {
|
||||
preset_bundle->set_num_filaments(nozzle_diameter->values.size());
|
||||
// Mixed-color slots are virtual filaments kept at the tail of the list, so they have no
|
||||
// nozzle of their own and the count has to allow for them. Only ever grow: this sizes
|
||||
// the list so the combo boxes have something to bind to, and set_num_filaments() trims
|
||||
// at the raw tail, so shrinking here would eat the mixes rather than the surplus
|
||||
// physical slots. A list longer than the nozzle count is a state the app reaches
|
||||
// legitimately - raising the extruder count and not saving the printer preset leaves
|
||||
// exactly that on the next start - and losing the project's mixes to it is worse than
|
||||
// carrying a filament the printer has no nozzle for until the count is next changed.
|
||||
const size_t target = nozzle_diameter->values.size() + preset_bundle->num_mixed_filaments();
|
||||
if (target > preset_bundle->filament_presets.size())
|
||||
preset_bundle->set_num_filaments(target);
|
||||
}
|
||||
}
|
||||
this->plater()->set_printer_technology(printer_technology);
|
||||
@@ -9851,7 +9885,7 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b
|
||||
{
|
||||
reg_bin = L"";
|
||||
#ifdef WIN32
|
||||
wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
|
||||
wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
|
||||
if (!key_full.Exists()) {
|
||||
return false;
|
||||
}
|
||||
@@ -9877,8 +9911,8 @@ void GUI_App::associate_url(std::wstring url_prefix)
|
||||
|
||||
wxString key_string = "\"" + wbinary + "\" \"%1\"";
|
||||
|
||||
wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix);
|
||||
wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
|
||||
wxRegKey key_first(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix);
|
||||
wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
|
||||
if (!key_first.Exists()) {
|
||||
key_first.Create(false);
|
||||
}
|
||||
@@ -9898,7 +9932,7 @@ void GUI_App::disassociate_url(std::wstring url_prefix)
|
||||
#ifdef WIN32
|
||||
if (is_running_in_msix())
|
||||
return;
|
||||
wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
|
||||
wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
|
||||
if (!key_full.Exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1656,16 +1656,16 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
|
||||
{
|
||||
wxMenu *menu = &m_filament_action_menu;
|
||||
|
||||
if (init) {
|
||||
// ORCA rebuild menu everytime instead checking existing of every item then deleting
|
||||
while (menu->GetMenuItemCount() > 0)
|
||||
menu->Destroy(menu->FindItemByPosition(0));
|
||||
|
||||
//if (init) { //
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) {
|
||||
plater()->sidebar().edit_filament(); }, "", nullptr,
|
||||
[]() { return true; }, m_parent);
|
||||
}
|
||||
|
||||
const int item_id = menu->FindItem(_L("Merge with"));
|
||||
if (item_id != wxNOT_FOUND)
|
||||
menu->Destroy(item_id);
|
||||
//}
|
||||
|
||||
wxMenu* sub_menu = new wxMenu();
|
||||
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
|
||||
@@ -1684,11 +1684,15 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
|
||||
append_submenu(menu, sub_menu, wxID_ANY, _L("Merge with"), "", "",
|
||||
[filaments_cnt]() { return filaments_cnt > 1; }, m_parent);
|
||||
|
||||
// Decompose a target colour into a printable mix of the loaded filaments. Placed before the
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Decompose Color"), "", [](wxCommandEvent&) {
|
||||
plater()->sidebar().decompose_filament_color(kSidebarContextMenuFilamentId); }, "", nullptr,
|
||||
[]() { return plater()->sidebar().combos_filament().size() >= 2; }, m_parent);
|
||||
|
||||
menu->AppendSeparator(); // ORCA use seperator for reducing accidental clicks to delete
|
||||
|
||||
// ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS
|
||||
const int delete_id = menu->FindItem(_L("Delete"));
|
||||
if (delete_id != wxNOT_FOUND)
|
||||
menu->Destroy(delete_id);
|
||||
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Delete"), _L("Delete this filament"), [](wxCommandEvent&) {
|
||||
plater()->sidebar().delete_filament(-2); }, "", nullptr,
|
||||
|
||||
@@ -3233,6 +3233,24 @@ void ObjectList::merge(bool to_multipart_object)
|
||||
|
||||
void ObjectList::layers_editing()
|
||||
{
|
||||
// Height ranges give each range its own layer height, varying the mixed sub-layer heights just
|
||||
// like an adaptive profile, so this raises the same warning as variable layer height and shares
|
||||
// its do-not-show-again flag.
|
||||
const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
if (print_config.opt_bool("enable_mixed_color_sublayer")) {
|
||||
if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
|
||||
// Orca: parent to the plater like the sibling site in Plater::priv::on_action_layersediting
|
||||
// (BBS passes nullptr, which MsgDialog remaps to the main frame).
|
||||
MessageDialog dlg(wxGetApp().plater(),
|
||||
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
|
||||
_L("Warning"), wxICON_WARNING | wxOK);
|
||||
dlg.show_dsa_button();
|
||||
dlg.ShowModal();
|
||||
if (dlg.get_checkbox_state())
|
||||
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
|
||||
}
|
||||
}
|
||||
|
||||
const Selection& selection = scene_selection();
|
||||
const int obj_idx = selection.get_object_idx();
|
||||
wxDataViewItem item = obj_idx >= 0 && GetSelectedItemsCount() > 1 && selection.is_single_full_object() ?
|
||||
|
||||
@@ -155,6 +155,9 @@ public:
|
||||
update_dark_config();
|
||||
on_sys_color_changed();
|
||||
event.Skip();
|
||||
#else
|
||||
// Not calling Skip() is what stops the event propagating on Windows.
|
||||
(void) this;
|
||||
#endif // __WINDOWS__
|
||||
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ public:
|
||||
void update_model_object();
|
||||
//ClippingPlane get_sla_clipping_plane() const;
|
||||
|
||||
bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); }
|
||||
bool is_selection_rectangle_dragging() const override { return m_selection_rectangle.is_dragging(); }
|
||||
|
||||
bool wants_enter_leave_snapshots() const override { return true; }
|
||||
std::string get_gizmo_entering_text() const override { return _u8L("Entering Brim Ears"); }
|
||||
|
||||
@@ -597,7 +597,6 @@ void GLGizmoMeasure::on_render()
|
||||
}
|
||||
}
|
||||
Vec3d position_on_model;
|
||||
Vec3d direction_on_model;
|
||||
size_t model_facet_idx = -1;
|
||||
double closest_hit_distance = std::numeric_limits<double>::max();
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ protected:
|
||||
virtual void on_render() override;
|
||||
virtual void on_set_state() override;
|
||||
virtual CommonGizmosDataID on_get_requirements() const override;
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit);
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
void on_load(cereal::BinaryInputArchive &ar) override;
|
||||
void on_save(cereal::BinaryOutputArchive &ar) const override;
|
||||
|
||||
@@ -78,6 +78,9 @@ void GLGizmoMmuSegmentation::init_extruders_data()
|
||||
m_extruders_colors = wxGetApp().plater()->get_extruders_colors();
|
||||
m_selected_extruder_idx = 0;
|
||||
|
||||
m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
|
||||
m_gradient_ramps.resize(m_extruders_colors.size());
|
||||
|
||||
// keep remap table consistent with current extruder count
|
||||
m_extruder_remap.resize(m_extruders_colors.size());
|
||||
for (size_t i = 0; i < m_extruder_remap.size(); ++i)
|
||||
@@ -305,15 +308,32 @@ void GLGizmoMmuSegmentation::render_tooltip_button(float x, float y)
|
||||
}
|
||||
|
||||
// ORCA
|
||||
bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
|
||||
bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale)
|
||||
{
|
||||
// Inset of the frame stroked below, which is what trims the swatch down to its visible shape.
|
||||
const float frame_inset = 1.5f;
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
std::string label_id = std::to_string(idx) + id_str + std::to_string(idx);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 size = ImVec2(27.f * scale, 27.f * scale);
|
||||
ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color);
|
||||
ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1));
|
||||
bool dark_tone = (0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
|
||||
// Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade.
|
||||
const std::vector<wxColour>* gradient = gradient_of(idx - 1);
|
||||
// The centered slot number sits at the swatch's mid height, so take its contrast from the colour
|
||||
// printed there rather than from the slot's blended color.
|
||||
bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 :
|
||||
(0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
|
||||
|
||||
// Paint a gradient mixed filament's fade before the button and keep the button transparent, so
|
||||
// the slot number and the frame below stay on top of it. The bands cannot round their corners,
|
||||
// so the fade is inset to the frame, which masks it into the shape a plain color slot gets.
|
||||
if (gradient) {
|
||||
ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale},
|
||||
{pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient);
|
||||
color_vec.w = 0.f; // let the fade show through
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding , 7.f * scale);
|
||||
@@ -329,7 +349,7 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, std::string id_str, cons
|
||||
auto drawBorder = [&](float d, float r, float t, ImU32 col) {
|
||||
draw_list->AddRect({pos.x + d * scale, pos.y + d * scale}, {pos.x + size.x - d * scale , pos.y + size.y - d * scale}, col, r * scale, 0, t * scale);
|
||||
};
|
||||
drawBorder(1.5f, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
|
||||
drawBorder(frame_inset, 3.f, 4.f, ImGui::ColorConvertFloat4ToU32(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg)));
|
||||
if(active)
|
||||
drawBorder(.5f, 4.f , 2.f, br_color);
|
||||
else
|
||||
@@ -433,7 +453,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
|
||||
m_selected_extruder_idx = extruder_idx;
|
||||
}
|
||||
|
||||
if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
|
||||
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
|
||||
}
|
||||
// ORCA: Remap filaments section (Border only, Title in border).
|
||||
// Styled as a panel for visual grouping.
|
||||
@@ -731,6 +751,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
|
||||
continue;
|
||||
|
||||
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
|
||||
// A volume may be assigned to a mixed-color slot, whose index can sit past the
|
||||
// physical colour list; fall back to the first colour rather than reading OOB.
|
||||
if (extruder_idx >= (int)m_extruders_colors.size())
|
||||
extruder_idx = 0;
|
||||
std::vector<ColorRGBA> ebt_colors;
|
||||
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]);
|
||||
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
|
||||
@@ -753,6 +777,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors()
|
||||
TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
|
||||
int extruder_idx = m_volumes_extruder_idxs[i];
|
||||
int extruder_color_idx = std::max(0, extruder_idx - 1);
|
||||
// A mixed-color slot can index past the physical colour list; fall back to the first colour.
|
||||
if (extruder_color_idx >= (int)m_extruders_colors.size())
|
||||
extruder_color_idx = 0;
|
||||
std::vector<ColorRGBA> ebt_colors;
|
||||
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
|
||||
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
|
||||
|
||||
@@ -73,11 +73,10 @@ public:
|
||||
|
||||
void data_changed(bool is_serializing) override;
|
||||
|
||||
// TriangleSelector::serialization/deserialization has a limit to store 19 different states.
|
||||
// EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored.
|
||||
// When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization
|
||||
// will be also extended to support additional states, requiring at least one state to remain free out of 19 states.
|
||||
static const constexpr size_t EXTRUDERS_LIMIT = 16;
|
||||
// The paint material limit follows EnforcerBlockerType::ExtruderMax: TriangleSelector
|
||||
// serialization covers the extended (17..32) range through an escape nibble. Mixed-color
|
||||
// filaments occupy ordinary slots, so they draw from the same budget as physical ones.
|
||||
static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
|
||||
|
||||
const float get_cursor_radius_min() const override { return CursorRadiusMin; }
|
||||
|
||||
@@ -116,6 +115,10 @@ protected:
|
||||
|
||||
// Filament remap feature
|
||||
std::vector<size_t> m_extruder_remap; // index → target extruder index
|
||||
// Colours each gradient mixed filament actually prints, bottom of the model first, mirrored
|
||||
// from Plater so the extruder swatches draw the same fade the editor previews. Plain
|
||||
// filament slots keep an empty ramp.
|
||||
std::vector<std::vector<wxColour>> m_gradient_ramps;
|
||||
// ORCA: Cache used filaments to filter UI
|
||||
std::set<size_t> m_used_filaments; // Set of used filament indices (cached)
|
||||
|
||||
@@ -137,7 +140,13 @@ private:
|
||||
void init_model_triangle_selectors();
|
||||
|
||||
// ORCA
|
||||
bool draw_color_button(int idx, std::string id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
|
||||
bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
|
||||
// Gradient ramp of a filament slot, or nullptr when the slot is a plain single color
|
||||
// filament. A non-null result is never empty.
|
||||
const std::vector<wxColour>* gradient_of(int idx) const
|
||||
{
|
||||
return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr;
|
||||
}
|
||||
|
||||
// BBS
|
||||
void update_triangle_selectors_colors();
|
||||
|
||||
@@ -67,7 +67,7 @@ protected:
|
||||
void on_register_raycasters_for_picking() override;
|
||||
void on_unregister_raycasters_for_picking() override;
|
||||
//BBS: GUI refactor: add object manipulation
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit);
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
private:
|
||||
double calc_projection(const UpdateData& data) const;
|
||||
|
||||
@@ -89,7 +89,7 @@ protected:
|
||||
virtual void on_register_raycasters_for_picking() override;
|
||||
virtual void on_unregister_raycasters_for_picking() override;
|
||||
//BBS: GUI refactor: add object manipulation
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit);
|
||||
virtual void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
private:
|
||||
void render_grabbers_connection(unsigned int id_1, unsigned int id_2, const ColorRGBA& color);
|
||||
|
||||
@@ -998,16 +998,40 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
|
||||
keyCode = keyCode- WXK_NUMPAD0+'0';
|
||||
}
|
||||
if (keyCode >= '0' && keyCode <= '9') {
|
||||
if (keyCode == '1' && !m_timer_set_color.IsRunning()) {
|
||||
// The paint palette reaches EXTRUDERS_LIMIT slots (mixed-color filaments take
|
||||
// ordinary slots too), so any leading digit that can start a valid two-digit
|
||||
// number waits briefly for a second one.
|
||||
const int digit = keyCode - '0';
|
||||
const int shortcut_max = int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT);
|
||||
auto can_start_two_digit = [shortcut_max](int d) { return d > 0 && d * 10 <= shortcut_max; };
|
||||
auto select = [mmu_seg](int number) { return number > 0 && mmu_seg->on_number_key_down(number); };
|
||||
|
||||
if (m_timer_set_color.IsRunning() && m_pending_color_shortcut_tens > 0) {
|
||||
const int two_digit = m_pending_color_shortcut_tens * 10 + digit;
|
||||
const int pending = m_pending_color_shortcut_tens;
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
m_timer_set_color.Stop();
|
||||
if (two_digit <= shortcut_max) {
|
||||
processed = select(two_digit);
|
||||
} else {
|
||||
// Out of range: commit the pending digit, then treat this one as new input.
|
||||
processed = select(pending);
|
||||
if (can_start_two_digit(digit)) {
|
||||
m_pending_color_shortcut_tens = digit;
|
||||
m_timer_set_color.StartOnce(500);
|
||||
processed = true;
|
||||
} else {
|
||||
processed = select(digit) || processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (can_start_two_digit(digit)) {
|
||||
m_pending_color_shortcut_tens = digit;
|
||||
m_timer_set_color.StartOnce(500);
|
||||
processed = true;
|
||||
}
|
||||
else if (keyCode < '7' && m_timer_set_color.IsRunning()) {
|
||||
processed = mmu_seg->on_number_key_down(keyCode - '0'+10);
|
||||
m_timer_set_color.Stop();
|
||||
}
|
||||
else {
|
||||
processed = mmu_seg->on_number_key_down(keyCode - '0');
|
||||
processed = select(digit);
|
||||
}
|
||||
}
|
||||
else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') {
|
||||
@@ -1054,11 +1078,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
|
||||
|
||||
void GLGizmosManager::on_set_color_timer(wxTimerEvent& evt)
|
||||
{
|
||||
if (m_current == MmSegmentation) {
|
||||
// No second digit arrived in time: commit the pending leading digit on its own.
|
||||
if (m_current == MmSegmentation && m_pending_color_shortcut_tens > 0) {
|
||||
GLGizmoMmuSegmentation* mmu_seg = dynamic_cast<GLGizmoMmuSegmentation*>(get_current());
|
||||
mmu_seg->on_number_key_down(1);
|
||||
m_parent.set_as_dirty();
|
||||
if (mmu_seg != nullptr) {
|
||||
mmu_seg->on_number_key_down(m_pending_color_shortcut_tens);
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
}
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
}
|
||||
|
||||
void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot)
|
||||
|
||||
@@ -144,6 +144,8 @@ private:
|
||||
|
||||
//When there are more than 9 colors, shortcut key coloring
|
||||
wxTimer m_timer_set_color;
|
||||
// Leading digit of a two-digit color shortcut still waiting for its second digit.
|
||||
int m_pending_color_shortcut_tens = 0;
|
||||
void on_set_color_timer(wxTimerEvent& evt);
|
||||
|
||||
// key MENU_ICON_NAME, value = ImtextureID
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
#include "GradientCurveEditor.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/settings.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
|
||||
|
||||
namespace {
|
||||
// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing.
|
||||
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels.
|
||||
constexpr double kPlotLeftRatio = 0.0316;
|
||||
constexpr double kPlotRightRatio = 0.6766;
|
||||
constexpr double kPlotTopRatio = 0.1529;
|
||||
constexpr double kPlotBottomRatio = 0.8474;
|
||||
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
|
||||
|
||||
// Hit / stroke (DIP).
|
||||
constexpr int kHitRadius = 6;
|
||||
constexpr int kCurveHitRadius = 5;
|
||||
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
|
||||
constexpr int kStrokeUnselected = 2;
|
||||
constexpr int kStrokeSelected = 4;
|
||||
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention)
|
||||
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
|
||||
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
|
||||
|
||||
// Light-mode design tokens. Resolved through StateColor::darkModeColorFor()
|
||||
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
|
||||
// #818183, #262E30 -> #EFEFF0, #ACACAC -> #65656A, *wxWHITE -> #2D2D31). Don't read these
|
||||
// directly in paint; always go through the resolved locals declared at the top of on_paint().
|
||||
const wxColour kGridColor (238, 238, 238); // #EEEEEE grey 300
|
||||
const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700
|
||||
const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700
|
||||
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
|
||||
const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements
|
||||
|
||||
// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve
|
||||
// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than
|
||||
// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline.
|
||||
constexpr float kBgSimilarThreshold = 15.0f;
|
||||
constexpr int kOutlineExtraDip = 2;
|
||||
} // namespace
|
||||
|
||||
GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
|
||||
const wxColour& color_low,
|
||||
const wxColour& color_high)
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
|
||||
, m_color_low(color_low)
|
||||
, m_color_high(color_high)
|
||||
{
|
||||
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||
// Wide enough so the X-axis "Material Ratio" label fits past the arrow tip without overlap.
|
||||
SetMinSize(FromDIP(wxSize(260, 200)));
|
||||
|
||||
reset_to_linear(0.10, 0.90);
|
||||
|
||||
Bind(wxEVT_PAINT, &GradientCurveEditor::on_paint, this);
|
||||
Bind(wxEVT_LEFT_DOWN, &GradientCurveEditor::on_left_down, this);
|
||||
Bind(wxEVT_LEFT_UP, &GradientCurveEditor::on_left_up, this);
|
||||
Bind(wxEVT_RIGHT_DOWN, &GradientCurveEditor::on_right_down, this);
|
||||
Bind(wxEVT_MOTION, &GradientCurveEditor::on_motion, this);
|
||||
Bind(wxEVT_LEAVE_WINDOW,&GradientCurveEditor::on_leave, this);
|
||||
Bind(wxEVT_SIZE, &GradientCurveEditor::on_size, this);
|
||||
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
|
||||
m_drag_mode = DragMode::None;
|
||||
m_drag_idx = -1;
|
||||
m_dragged_moved = false;
|
||||
});
|
||||
}
|
||||
|
||||
GradientCurveEditor::~GradientCurveEditor()
|
||||
{
|
||||
// See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it
|
||||
// still holds the capture wedges mouse input for the whole application.
|
||||
if (HasCapture())
|
||||
ReleaseMouse();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::set_points(const PointList& pts)
|
||||
{
|
||||
m_points = pts;
|
||||
normalize_points();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::set_colors(const wxColour& color_low, const wxColour& color_high)
|
||||
{
|
||||
m_color_low = color_low;
|
||||
m_color_high = color_high;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::set_selected_curve(int curve_idx)
|
||||
{
|
||||
const int new_sel = (curve_idx == 0) ? 0 : 1;
|
||||
if (m_selected_curve == new_sel) return;
|
||||
m_selected_curve = new_sel;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::reset_to_linear(double y0, double y1)
|
||||
{
|
||||
auto clamp_y = [](double v) {
|
||||
return std::max(kGradientMinRatio, std::min(kGradientMaxRatio, v));
|
||||
};
|
||||
m_points.clear();
|
||||
GradientAnchor a0; a0.x = 0.0; a0.y = clamp_y(y0);
|
||||
GradientAnchor a1; a1.x = 1.0; a1.y = clamp_y(y1);
|
||||
m_points.push_back(a0);
|
||||
m_points.push_back(a1);
|
||||
m_selected_curve = 0;
|
||||
Refresh();
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::reverse()
|
||||
{
|
||||
// Mirror y around 0.5. Tangents are slopes dy/dx so they flip sign to keep the
|
||||
// local shape consistent across the mirror; NaN tangents remain "use PCHIP default".
|
||||
for (auto& p : m_points) {
|
||||
p.y = 1.0 - p.y;
|
||||
if (std::isfinite(p.m_in)) p.m_in = -p.m_in;
|
||||
if (std::isfinite(p.m_out)) p.m_out = -p.m_out;
|
||||
}
|
||||
Refresh();
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::normalize_points()
|
||||
{
|
||||
if (m_points.empty()) {
|
||||
GradientAnchor a0; a0.x = 0.0; a0.y = kGradientMinRatio;
|
||||
GradientAnchor a1; a1.x = 1.0; a1.y = kGradientMaxRatio;
|
||||
m_points.push_back(a0);
|
||||
m_points.push_back(a1);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& p : m_points) {
|
||||
p.x = std::max(0.0, std::min(1.0, p.x));
|
||||
p.y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, p.y));
|
||||
}
|
||||
std::sort(m_points.begin(), m_points.end(),
|
||||
[](const GradientAnchor& a, const GradientAnchor& b) {
|
||||
return a.x < b.x;
|
||||
});
|
||||
|
||||
if (m_points.size() < 2) {
|
||||
GradientAnchor tail; tail.x = 1.0; tail.y = m_points.front().y;
|
||||
m_points.push_back(tail);
|
||||
}
|
||||
|
||||
m_points.front().x = 0.0;
|
||||
m_points.back().x = 1.0;
|
||||
}
|
||||
|
||||
void GradientCurveEditor::emit_changed()
|
||||
{
|
||||
wxCommandEvent evt(wxEVT_GRADIENT_CURVE_CHANGED, GetId());
|
||||
evt.SetEventObject(this);
|
||||
ProcessWindowEvent(evt);
|
||||
}
|
||||
|
||||
wxRect GradientCurveEditor::plot_rect() const
|
||||
{
|
||||
const wxSize sz = GetClientSize();
|
||||
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
|
||||
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
|
||||
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
|
||||
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
|
||||
// Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at
|
||||
// the top-left so the "100%" labels on the bottom/right still align with the plot edges.
|
||||
const int side = std::max(1, std::min(x2 - x, y2 - y));
|
||||
return wxRect(x, y, side, side);
|
||||
}
|
||||
|
||||
wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const
|
||||
{
|
||||
const wxRect r = plot_rect();
|
||||
// y axis is inverted: y=1 should sit at the top.
|
||||
return wxPoint2DDouble(r.x + x * r.width, r.y + (1.0 - y) * r.height);
|
||||
}
|
||||
|
||||
wxPoint GradientCurveEditor::data_to_px(double x, double y) const
|
||||
{
|
||||
const wxPoint2DDouble p = data_to_px_f(x, y);
|
||||
return wxPoint(static_cast<int>(std::lround(p.m_x)), static_cast<int>(std::lround(p.m_y)));
|
||||
}
|
||||
|
||||
void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const
|
||||
{
|
||||
const wxRect r = plot_rect();
|
||||
const double w = std::max(1, r.width);
|
||||
const double h = std::max(1, r.height);
|
||||
x = std::max(0.0, std::min(1.0, (px - r.x) / w));
|
||||
y = std::max(0.0, std::min(1.0, 1.0 - (py - r.y) / h));
|
||||
}
|
||||
|
||||
double GradientCurveEditor::sample_curve_y(double x) const
|
||||
{
|
||||
GradientCurve gc;
|
||||
gc.points = m_points;
|
||||
return sample_gradient_curve(gc, x);
|
||||
}
|
||||
|
||||
int GradientCurveEditor::hit_test(int px, int py) const
|
||||
{
|
||||
const int tol = FromDIP(kHitRadius);
|
||||
int best_idx = -1;
|
||||
int best_d2 = tol * tol;
|
||||
for (size_t i = 0; i < m_points.size(); ++i) {
|
||||
// Anchor visual y is curve-specific: component 1's anchor sits at (x, 1 - stored_y).
|
||||
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
|
||||
const wxPoint p = data_to_px(m_points[i].x, vy);
|
||||
const int dx = px - p.x;
|
||||
const int dy = py - p.y;
|
||||
const int d2 = dx * dx + dy * dy;
|
||||
if (d2 <= best_d2) {
|
||||
best_idx = static_cast<int>(i);
|
||||
best_d2 = d2;
|
||||
}
|
||||
}
|
||||
return best_idx;
|
||||
}
|
||||
|
||||
int GradientCurveEditor::hit_test_curve(int px, int py, int* seg_out) const
|
||||
{
|
||||
if (seg_out) *seg_out = -1;
|
||||
if (m_points.size() < 2) return -1;
|
||||
const int tol = FromDIP(kCurveHitRadius);
|
||||
const int tol2 = tol * tol;
|
||||
|
||||
auto dist2_to_seg = [&](int ax, int ay, int bx, int by) -> int {
|
||||
const double dx = bx - ax;
|
||||
const double dy = by - ay;
|
||||
const double l2 = dx * dx + dy * dy;
|
||||
if (l2 == 0.0) {
|
||||
const double ddx = px - ax;
|
||||
const double ddy = py - ay;
|
||||
return static_cast<int>(ddx * ddx + ddy * ddy);
|
||||
}
|
||||
double t = ((px - ax) * dx + (py - ay) * dy) / l2;
|
||||
t = std::max(0.0, std::min(1.0, t));
|
||||
const double ex = ax + t * dx;
|
||||
const double ey = ay + t * dy;
|
||||
const double ddx = px - ex;
|
||||
const double ddy = py - ey;
|
||||
return static_cast<int>(ddx * ddx + ddy * ddy);
|
||||
};
|
||||
|
||||
// Hit-test against the same dense Hermite polyline that on_paint draws, so the
|
||||
// clickable line follows the visual curve exactly (no offset on the bent parts).
|
||||
// When a hit is found, also report the index of the left anchor of the data-space
|
||||
// segment that covers cursor x; needed by the segment-bend interaction.
|
||||
const wxRect rc = plot_rect();
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
auto seg_for_x = [&](double cursor_x) -> int {
|
||||
for (size_t i = 1; i < m_points.size(); ++i) {
|
||||
if (cursor_x <= m_points[i].x)
|
||||
return static_cast<int>(i - 1);
|
||||
}
|
||||
return static_cast<int>(m_points.size() - 2);
|
||||
};
|
||||
|
||||
auto curve_hit = [&](int curve_idx) -> bool {
|
||||
wxPoint prev;
|
||||
for (int s = 0; s <= samples; ++s) {
|
||||
const double x = double(s) / samples;
|
||||
const double y0 = sample_curve_y(x);
|
||||
const double vy = to_visual_y(curve_idx, y0);
|
||||
const wxPoint cur = data_to_px(x, vy);
|
||||
if (s > 0 && dist2_to_seg(prev.x, prev.y, cur.x, cur.y) <= tol2)
|
||||
return true;
|
||||
prev = cur;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Prefer the selected curve so overlapping segments don't unintentionally steal focus.
|
||||
if (curve_hit(m_selected_curve)) {
|
||||
if (seg_out) {
|
||||
double nx = 0, dummy = 0;
|
||||
px_to_data(px, py, nx, dummy);
|
||||
*seg_out = seg_for_x(nx);
|
||||
}
|
||||
return m_selected_curve;
|
||||
}
|
||||
const int other = 1 - m_selected_curve;
|
||||
if (curve_hit(other)) {
|
||||
if (seg_out) {
|
||||
double nx = 0, dummy = 0;
|
||||
px_to_data(px, py, nx, dummy);
|
||||
*seg_out = seg_for_x(nx);
|
||||
}
|
||||
return other;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/)
|
||||
{
|
||||
// Resolve theme colors every paint so dark-mode toggles (no re-construction) take
|
||||
// effect without an explicit listener. Window bg is read from GUI_App, not
|
||||
// GetBackgroundColour(), since the latter is snapshotted at construction time.
|
||||
const wxColour bg = wxGetApp().get_window_default_clr();
|
||||
const wxColour grid_color = StateColor::darkModeColorFor(kGridColor);
|
||||
const wxColour axis_color = StateColor::darkModeColorFor(kAxisColor);
|
||||
const wxColour label_muted = StateColor::darkModeColorFor(kLabelMuted);
|
||||
const wxColour label_strong = StateColor::darkModeColorFor(kLabelStrong);
|
||||
const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE);
|
||||
// Softer than axis_color: the curve outline only has to lift the curve off the
|
||||
// background, it must not compete with the structural axis / grid.
|
||||
const wxColour outline_color = StateColor::darkModeColorFor(kOutlineColor);
|
||||
|
||||
wxAutoBufferedPaintDC raw_dc(this);
|
||||
raw_dc.SetBackground(wxBrush(bg));
|
||||
raw_dc.Clear();
|
||||
|
||||
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered
|
||||
// DC is the actual back buffer that gets blitted to the window.
|
||||
wxGCDC dc(raw_dc);
|
||||
// The curve and its anchors are drawn straight on the graphics context so their
|
||||
// coordinates stay sub-pixel accurate (see data_to_px_f).
|
||||
wxGraphicsContext* gc = dc.GetGraphicsContext();
|
||||
|
||||
const wxRect rc = plot_rect();
|
||||
if (rc.width <= 0 || rc.height <= 0)
|
||||
return;
|
||||
|
||||
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
|
||||
dc.SetPen(wxPen(grid_color, 1));
|
||||
for (int i = 0; i <= kGridDivisions; ++i) {
|
||||
const int x = rc.x + rc.width * i / kGridDivisions;
|
||||
const int y = rc.y + rc.height * i / kGridDivisions;
|
||||
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
|
||||
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
|
||||
}
|
||||
|
||||
// Set the label font first so text width measurements drive arrow / label placement.
|
||||
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
|
||||
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
|
||||
dc.SetFont(label_font);
|
||||
|
||||
const wxString axis_y_title = _L("Material Ratio");
|
||||
const wxString axis_x_title = _L("Model Height");
|
||||
const wxString pct_text = wxT("100%");
|
||||
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
|
||||
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
|
||||
|
||||
wxFont strong_font = label_font;
|
||||
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
|
||||
dc.SetFont(strong_font);
|
||||
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
|
||||
dc.SetFont(label_font);
|
||||
|
||||
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
|
||||
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
|
||||
const int arrow_half = FromDIP(kAxisArrowHalf);
|
||||
const int arrow_len = FromDIP(kAxisArrowLen);
|
||||
const wxSize sz = GetClientSize();
|
||||
dc.SetPen(wxPen(axis_color, kStrokeAxis));
|
||||
dc.SetBrush(wxBrush(axis_color));
|
||||
|
||||
// Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom.
|
||||
const int y_axis_x = rc.x;
|
||||
const int y_title_pct_gap = FromDIP(1);
|
||||
const int y_title_bottom_pad = FromDIP(2);
|
||||
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
|
||||
const int y_arrow_tip_y = y_title_y;
|
||||
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
|
||||
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(y_axis_x, y_arrow_tip_y),
|
||||
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
|
||||
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing
|
||||
// "Material Ratio" label still fits inside the canvas without overlapping the arrow.
|
||||
const int x_axis_y = rc.y + rc.height;
|
||||
const int x_label_gap = FromDIP(4);
|
||||
const int x_edge_pad = FromDIP(6);
|
||||
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10);
|
||||
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
|
||||
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len,
|
||||
std::min(x_arrow_ideal, x_arrow_max));
|
||||
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
|
||||
const int x_title_x = x_arrow_tip_x + x_label_gap;
|
||||
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(x_arrow_tip_x, x_axis_y),
|
||||
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
|
||||
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// Labels.
|
||||
// "Model Height" and "100%" share the same left x; the gap is larger than the
|
||||
// axis-arrow half-base so the text never visually touches the Y-axis arrow.
|
||||
const int label_left_x = y_axis_x + FromDIP(10);
|
||||
dc.SetTextForeground(label_muted);
|
||||
dc.DrawText(axis_y_title, label_left_x, y_title_y);
|
||||
|
||||
dc.SetFont(strong_font);
|
||||
dc.SetTextForeground(label_strong);
|
||||
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
|
||||
|
||||
// Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the
|
||||
// X-axis arrow tip (placement was already clamped above to leave room).
|
||||
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
|
||||
dc.SetFont(label_font);
|
||||
dc.SetTextForeground(label_muted);
|
||||
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
|
||||
|
||||
if (m_points.size() < 2 || !gc)
|
||||
return;
|
||||
|
||||
auto color_for_curve = [&](int curve_idx) -> wxColour {
|
||||
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
|
||||
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
|
||||
// Lift alpha so the curve stays visible while still hinting at transparency.
|
||||
if (c.Alpha() == 0)
|
||||
c.Set(c.Red(), c.Green(), c.Blue(), 150);
|
||||
return c;
|
||||
};
|
||||
|
||||
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
std::vector<wxPoint2DDouble> poly;
|
||||
poly.reserve(samples + 1);
|
||||
for (int s = 0; s <= samples; ++s) {
|
||||
const double x = double(s) / samples;
|
||||
const double y0 = sample_curve_y(x);
|
||||
const double vy = to_visual_y(curve_idx, y0);
|
||||
poly.push_back(data_to_px_f(x, vy));
|
||||
}
|
||||
return poly;
|
||||
};
|
||||
|
||||
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
|
||||
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
|
||||
// forwards it here while keeping its own cached state in sync for later dc drawing.
|
||||
auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) {
|
||||
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
|
||||
gc->StrokeLines(poly.size(), poly.data());
|
||||
};
|
||||
|
||||
// Outline only when the curve color is perceptually close to the background; otherwise
|
||||
// the plain filament color reads fine and the extra stroke would look heavy.
|
||||
auto needs_outline = [&](const wxColour& c) {
|
||||
return calc_color_distance(c, bg) < kBgSimilarThreshold;
|
||||
};
|
||||
|
||||
auto draw_one = [&](int curve_idx, int stroke_dip) {
|
||||
const auto poly = build_polyline(curve_idx);
|
||||
const wxColour col = color_for_curve(curve_idx);
|
||||
if (needs_outline(col))
|
||||
draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip);
|
||||
draw_polyline(poly, col, stroke_dip);
|
||||
};
|
||||
|
||||
// Draw unselected first so the selected curve sits on top.
|
||||
const int other = 1 - m_selected_curve;
|
||||
draw_one(other, kStrokeUnselected);
|
||||
draw_one(m_selected_curve, kStrokeSelected);
|
||||
|
||||
// Control points (selected curve only): hollow circle with axis-color border, theme-aware fill.
|
||||
// Drawn on the graphics context with a sub-pixel center so the ring stays centered on the
|
||||
// curve instead of drifting up to half a pixel off it; pen and brush go through the dc for
|
||||
// the same reason as in draw_polyline above.
|
||||
const double r = FromDIP(kPointRadius);
|
||||
dc.SetPen(wxPen(axis_color, 1));
|
||||
dc.SetBrush(wxBrush(point_fill));
|
||||
for (size_t i = 0; i < m_points.size(); ++i) {
|
||||
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
|
||||
const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy);
|
||||
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
|
||||
}
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_left_down(wxMouseEvent& evt)
|
||||
{
|
||||
const wxPoint pos = evt.GetPosition();
|
||||
m_dragged_moved = false;
|
||||
|
||||
// 1) Anchor on the selected curve takes precedence over everything else.
|
||||
// Dragging an anchor resets its tangent overrides so the surrounding curve
|
||||
// returns to PCHIP-default shape (matches user expectation that pulling an
|
||||
// anchor "straightens out" the local mess).
|
||||
const int idx = hit_test(pos.x, pos.y);
|
||||
if (idx >= 0) {
|
||||
m_drag_mode = DragMode::Anchor;
|
||||
m_drag_idx = idx;
|
||||
// Only emit a change event when clearing the tangents actually mutates
|
||||
// the curve. A plain click on an already-default anchor must not trigger
|
||||
// re-slicing through the changed-event listener.
|
||||
const bool had_tangent = std::isfinite(m_points[idx].m_in)
|
||||
|| std::isfinite(m_points[idx].m_out);
|
||||
m_points[idx].m_in = std::numeric_limits<double>::quiet_NaN();
|
||||
m_points[idx].m_out = std::numeric_limits<double>::quiet_NaN();
|
||||
if (!HasCapture())
|
||||
CaptureMouse();
|
||||
Refresh();
|
||||
if (had_tangent)
|
||||
emit_changed();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Line-body hit. Determine which curve and which segment.
|
||||
int seg = -1;
|
||||
const int curve_hit = hit_test_curve(pos.x, pos.y, &seg);
|
||||
if (curve_hit < 0) {
|
||||
m_drag_mode = DragMode::None;
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Non-selected curve hit -> switch selection only, no drag arming.
|
||||
if (curve_hit != m_selected_curve) {
|
||||
m_selected_curve = curve_hit;
|
||||
m_drag_mode = DragMode::None;
|
||||
Refresh();
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Selected curve line body hit -> insert a new anchor at cursor x (snapped
|
||||
// to the current smooth curve so the initial click is visually invisible)
|
||||
// and immediately enter Anchor drag mode. Bending the segment without
|
||||
// inserting an anchor is not an option: a single cubic between two existing
|
||||
// anchors cannot put its peak under an off-center cursor.
|
||||
double nx = 0, dummy = 0;
|
||||
px_to_data(pos.x, pos.y, nx, dummy);
|
||||
if (nx <= 0.0 || nx >= 1.0 || seg < 0) {
|
||||
m_drag_mode = DragMode::None;
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
GradientAnchor a;
|
||||
a.x = nx;
|
||||
a.y = sample_curve_y(nx);
|
||||
const size_t insert_idx = static_cast<size_t>(seg) + 1;
|
||||
m_points.insert(m_points.begin() + insert_idx, a);
|
||||
|
||||
m_drag_mode = DragMode::Anchor;
|
||||
m_drag_idx = static_cast<int>(insert_idx);
|
||||
if (!HasCapture())
|
||||
CaptureMouse();
|
||||
Refresh();
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_left_up(wxMouseEvent& evt)
|
||||
{
|
||||
if (HasCapture())
|
||||
ReleaseMouse();
|
||||
|
||||
// Anchor mode (either an existing anchor or one freshly inserted by on_left_down)
|
||||
// already fired emit_changed on mouse_down; only fire again here if the user
|
||||
// actually dragged so the slicer doesn't re-run on a pure click.
|
||||
if (m_drag_mode == DragMode::Anchor && m_dragged_moved)
|
||||
emit_changed();
|
||||
|
||||
m_drag_mode = DragMode::None;
|
||||
m_drag_idx = -1;
|
||||
m_dragged_moved = false;
|
||||
(void)evt;
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_right_down(wxMouseEvent& evt)
|
||||
{
|
||||
const wxPoint pos = evt.GetPosition();
|
||||
const int idx = hit_test(pos.x, pos.y);
|
||||
if (idx > 0 && static_cast<size_t>(idx) + 1 < m_points.size()) {
|
||||
// Interior anchor on the selected curve -> delete it. Endpoints stay locked.
|
||||
m_points.erase(m_points.begin() + idx);
|
||||
Refresh();
|
||||
emit_changed();
|
||||
return;
|
||||
}
|
||||
// Right-click on the non-selected curve switches selection (never deletes).
|
||||
const int curve_hit = hit_test_curve(pos.x, pos.y);
|
||||
if (curve_hit >= 0 && curve_hit != m_selected_curve) {
|
||||
m_selected_curve = curve_hit;
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_motion(wxMouseEvent& evt)
|
||||
{
|
||||
if (!evt.LeftIsDown() || m_drag_mode != DragMode::Anchor) {
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
if (static_cast<size_t>(m_drag_idx) >= m_points.size())
|
||||
return;
|
||||
|
||||
const wxPoint pos = evt.GetPosition();
|
||||
double nx = 0, vy = 0;
|
||||
px_to_data(pos.x, pos.y, nx, vy);
|
||||
|
||||
auto& p = m_points[m_drag_idx];
|
||||
const bool is_first = (m_drag_idx == 0);
|
||||
const bool is_last = (static_cast<size_t>(m_drag_idx) + 1 == m_points.size());
|
||||
|
||||
// Endpoints stay locked at x=0 / x=1; interior anchors clamp into
|
||||
// (left_neighbor.x, right_neighbor.x) so they can't cross or coincide.
|
||||
if (!is_first && !is_last) {
|
||||
const double xl = m_points[m_drag_idx - 1].x;
|
||||
const double xr = m_points[m_drag_idx + 1].x;
|
||||
const double eps = 1e-4;
|
||||
nx = std::max(xl + eps, std::min(xr - eps, nx));
|
||||
p.x = nx;
|
||||
}
|
||||
// y is constrained to the reserved blend band so neither component ever
|
||||
// reaches 0% / 100%, matching the sampler's clamp.
|
||||
p.y = std::max(kGradientMinRatio,
|
||||
std::min(kGradientMaxRatio, to_stored_y(m_selected_curve, vy)));
|
||||
m_dragged_moved = true;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_leave(wxMouseEvent& evt)
|
||||
{
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_size(wxSizeEvent& evt)
|
||||
{
|
||||
Refresh();
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef slic3r_GradientCurveEditor_hpp_
|
||||
#define slic3r_GradientCurveEditor_hpp_
|
||||
|
||||
#include <vector>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/geometry.h>
|
||||
#include <wx/panel.h>
|
||||
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// Photoshop-style curve editor for "Z progress -> first-component ratio" mapping.
|
||||
// Curve evaluation uses cubic Hermite with PCHIP defaults plus optional per-anchor
|
||||
// tangent overrides (m_in / m_out, NaN = use PCHIP default). The same evaluator
|
||||
// (FilamentMixer::sample_gradient_curve) is shared with the slicing backend so what
|
||||
// the editor renders matches the G-code output 1:1.
|
||||
//
|
||||
// Interaction model (PS Curves style):
|
||||
// - Click or press-and-drag on the line body inserts a new anchor at the cursor x
|
||||
// (snapped to the current smooth curve, NaN tangents) and starts dragging it.
|
||||
// A pure click leaves an anchor sitting exactly on the previous curve shape; a
|
||||
// drag moves the new anchor freely so the bump follows the cursor 1:1.
|
||||
// - Dragging an existing anchor moves (x, y) and clears its m_in / m_out so the
|
||||
// local curve returns to the PCHIP default shape around it.
|
||||
// - Right-click on an interior anchor deletes it; endpoints stay locked.
|
||||
class GradientCurveEditor : public wxPanel
|
||||
{
|
||||
public:
|
||||
using PointList = std::vector<GradientAnchor>;
|
||||
|
||||
GradientCurveEditor(wxWindow* parent,
|
||||
const wxColour& color_low = wxColour(217, 217, 217),
|
||||
const wxColour& color_high = wxColour(217, 217, 217));
|
||||
|
||||
~GradientCurveEditor() override;
|
||||
|
||||
// Replace the entire point list. The widget enforces x in [0,1], y in [0,1],
|
||||
// sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are
|
||||
// preserved as-is (NaN entries continue to use PCHIP defaults).
|
||||
void set_points(const PointList& pts);
|
||||
const PointList& get_points() const { return m_points; }
|
||||
|
||||
void set_colors(const wxColour& color_low, const wxColour& color_high);
|
||||
|
||||
// Which curve currently responds to drag / add / delete and is drawn with the thick stroke.
|
||||
// 0 = first component (color_low), 1 = second component (color_high). Storage layer is
|
||||
// unaffected: m_points always represents component 0's ratio.
|
||||
void set_selected_curve(int curve_idx);
|
||||
int get_selected_curve() const { return m_selected_curve; }
|
||||
|
||||
// Reset to a two-point linear curve from y0 at t=0 to y1 at t=1.
|
||||
// Clears all tangent overrides.
|
||||
void reset_to_linear(double y0, double y1);
|
||||
// Flip the curve top to bottom (all y -> 1 - y; tangents negated to mirror shape).
|
||||
void reverse();
|
||||
|
||||
private:
|
||||
enum class DragMode {
|
||||
None, // nothing armed
|
||||
Anchor, // dragging an anchor (either existing or just inserted from a line hit)
|
||||
};
|
||||
|
||||
void normalize_points();
|
||||
void emit_changed();
|
||||
|
||||
void on_paint(wxPaintEvent& evt);
|
||||
void on_left_down(wxMouseEvent& evt);
|
||||
void on_left_up(wxMouseEvent& evt);
|
||||
void on_right_down(wxMouseEvent& evt);
|
||||
void on_motion(wxMouseEvent& evt);
|
||||
void on_leave(wxMouseEvent& evt);
|
||||
void on_size(wxSizeEvent& evt);
|
||||
|
||||
// Coordinate mapping between data (x, y in [0,1]) and pixels in plot area.
|
||||
wxRect plot_rect() const;
|
||||
// Sub-pixel accurate mapping, used for drawing: rounding the curve vertices to whole
|
||||
// pixels leaves a staircase that anti-aliasing cannot smooth out, and the step is
|
||||
// twice as coarse on 2x (Retina) displays.
|
||||
wxPoint2DDouble data_to_px_f(double x, double y) const;
|
||||
wxPoint data_to_px(double x, double y) const;
|
||||
void px_to_data(int px, int py, double& x, double& y) const;
|
||||
// Anchor hit test for the currently-selected curve (uses translated visual y).
|
||||
int hit_test(int px, int py) const; // returns point index or -1
|
||||
// Line-body hit test across both curves. Returns 0/1 for which curve was hit, -1 if none.
|
||||
// Prefers the selected curve when both are within threshold. seg_out (when non-null)
|
||||
// receives the left-anchor index of the segment that was hit on the returned curve;
|
||||
// on_left_down uses it to know where in m_points to insert a freshly-added anchor.
|
||||
int hit_test_curve(int px, int py, int* seg_out = nullptr) const;
|
||||
|
||||
// Sample the curve in stored space (component 0) at x.
|
||||
double sample_curve_y(double x) const;
|
||||
|
||||
// Symmetric translation between visual y (what the user sees / clicks) and stored y
|
||||
// (component 0's ratio in m_points).
|
||||
static double to_stored_y(int curve_idx, double visual_y) {
|
||||
return (curve_idx == 0) ? visual_y : (1.0 - visual_y);
|
||||
}
|
||||
static double to_visual_y(int curve_idx, double stored_y) {
|
||||
return (curve_idx == 0) ? stored_y : (1.0 - stored_y);
|
||||
}
|
||||
|
||||
PointList m_points;
|
||||
wxColour m_color_low;
|
||||
wxColour m_color_high;
|
||||
|
||||
int m_selected_curve = 0;
|
||||
DragMode m_drag_mode = DragMode::None;
|
||||
int m_drag_idx = -1; // valid when m_drag_mode == Anchor
|
||||
bool m_dragged_moved = false;
|
||||
};
|
||||
|
||||
// Custom event raised when the curve is edited (drag / add / remove / reset / reverse).
|
||||
wxDECLARE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_GradientCurveEditor_hpp_
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "HMS.hpp"
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "DeviceManager.hpp"
|
||||
#include "DeviceCore/DevManager.h"
|
||||
#include "DeviceCore/DevUtil.h"
|
||||
|
||||
+16
-13
@@ -1,7 +1,6 @@
|
||||
#ifndef slic3r_HMS_hpp_
|
||||
#define slic3r_HMS_hpp_
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
@@ -11,7 +10,11 @@
|
||||
#include "slic3r/Utils/Http.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -26,12 +29,12 @@ namespace GUI {
|
||||
class HMSQuery {
|
||||
|
||||
protected:
|
||||
std::unordered_map<string, json> m_hms_info_jsons; // key-> device id type, the first three digits of SN number
|
||||
std::unordered_map<string, json> m_hms_action_jsons;// key-> device id type
|
||||
std::unordered_map<std::string, nlohmann::json> m_hms_info_jsons; // key-> device id type, the first three digits of SN number
|
||||
std::unordered_map<std::string, nlohmann::json> m_hms_action_jsons;// key-> device id type
|
||||
std::unordered_map<wxString, wxImage> m_hms_local_images; // key-> image name
|
||||
mutable std::mutex m_hms_mutex;
|
||||
|
||||
std::unordered_map<string, time_t> m_cloud_hms_last_update_time;
|
||||
std::unordered_map<std::string, std::time_t> m_cloud_hms_last_update_time;
|
||||
|
||||
public:
|
||||
HMSQuery() { }
|
||||
@@ -61,18 +64,18 @@ private:
|
||||
// load hms
|
||||
void init_hms_info(const std::string& dev_type_id);
|
||||
void copy_from_data_dir_to_local();
|
||||
int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, json* receive_json);
|
||||
int load_from_local(const std::string& hms_type, const std::string& dev_id_type, json* receive_json, std::string& version_info);
|
||||
int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, json save_json);
|
||||
int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json);
|
||||
int load_from_local(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json, std::string& version_info);
|
||||
int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, nlohmann::json save_json);
|
||||
std::string get_hms_file(std::string hms_type, std::string lang = std::string("en"), std::string dev_id_type = "");
|
||||
|
||||
// internal query
|
||||
string get_dev_id_type(const MachineObject* obj) const;
|
||||
wxString _query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code = std::string("en"));
|
||||
std::string get_dev_id_type(const MachineObject* obj) const;
|
||||
wxString _query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
|
||||
|
||||
bool _is_internal_error(const string &dev_id_type, const string &long_error_code, const string &lang_code = std::string("en"));
|
||||
wxString _query_error_msg(const string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
|
||||
wxString _query_error_image_action(const string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action);
|
||||
bool _is_internal_error(const std::string &dev_id_type, const std::string &long_error_code, const std::string &lang_code = std::string("en"));
|
||||
wxString _query_error_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
|
||||
wxString _query_error_image_action(const std::string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action);
|
||||
};
|
||||
|
||||
int get_hms_info_version(std::string &version);
|
||||
@@ -85,4 +88,4 @@ std::string get_error_message(int error_code);
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2404,6 +2404,25 @@ void ImGuiWrapper::draw(
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiWrapper::draw_gradient_ramp(ImDrawList *draw_list, const ImVec2 &top_left, const ImVec2 &bottom_right, const std::vector<wxColour> &ramp)
|
||||
{
|
||||
if (draw_list == nullptr || ramp.empty() || bottom_right.x <= top_left.x || bottom_right.y <= top_left.y)
|
||||
return;
|
||||
|
||||
const int rows = std::max(1, (int) std::lround(bottom_right.y - top_left.y));
|
||||
const float row_h = (bottom_right.y - top_left.y) / rows;
|
||||
const size_t last = ramp.size() - 1;
|
||||
for (int r = 0; r < rows; ++r) {
|
||||
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
|
||||
const double t = (rows > 1) ? (double) (rows - 1 - r) / (rows - 1) : 0.5;
|
||||
const wxColour &c = ramp[(size_t) (t * last + 0.5)];
|
||||
// The bottom row snaps to the rect's edge so rounding never leaves a sliver uncovered.
|
||||
const float y0 = top_left.y + r * row_h;
|
||||
const float y1 = (r + 1 == rows) ? bottom_right.y : top_left.y + (r + 1) * row_h;
|
||||
draw_list->AddRectFilled({top_left.x, y0}, {bottom_right.x, y1}, IM_COL32(c.Red(), c.Green(), c.Blue(), c.Alpha()));
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) {
|
||||
auto draw_list = ImGui::GetOverlayDrawList();
|
||||
draw_list->AddCircle(position, radius, color, num_segments, thickness);
|
||||
@@ -3332,8 +3351,9 @@ const char* ImGuiWrapper::clipboard_get(void* user_data)
|
||||
wxTextDataObject data;
|
||||
wxTheClipboard->GetData(data);
|
||||
|
||||
if (data.GetTextLength() > 0) {
|
||||
self->m_clipboard_text = into_u8(data.GetText());
|
||||
const wxString text = data.GetText();
|
||||
if (text.Length() > 0) {
|
||||
self->m_clipboard_text = into_u8(text);
|
||||
res = self->m_clipboard_text.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
|
||||
#include <wx/colour.h>
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
@@ -299,6 +301,20 @@ public:
|
||||
int num_segments = 0,
|
||||
float thickness = 4.f);
|
||||
|
||||
/// <summary>
|
||||
/// Fill a rect with a filament gradient ramp, one band per pixel row, ramp.front() along
|
||||
/// the bottom edge. Bands rather than one interpolated rect, because the ramp follows the
|
||||
/// slot's gradient curve and ImGui's corner interpolation could only draw a straight fade.
|
||||
/// </summary>
|
||||
/// <param name="draw_list">Define where to draw it</param>
|
||||
/// <param name="top_left">Upper left corner of the rect</param>
|
||||
/// <param name="bottom_right">Lower right corner of the rect</param>
|
||||
/// <param name="ramp">Colours printed, bottom of the model first</param>
|
||||
static void draw_gradient_ramp(ImDrawList * draw_list,
|
||||
const ImVec2 & top_left,
|
||||
const ImVec2 & bottom_right,
|
||||
const std::vector<wxColour> &ramp);
|
||||
|
||||
/// <summary>
|
||||
/// Check that font ranges contain all chars in string
|
||||
/// (rendered Unicodes are stored in GlyphRanges)
|
||||
|
||||
+125
-96
@@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
});
|
||||
|
||||
//BBS
|
||||
Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) {
|
||||
TabPosition pos = (TabPosition)evt.GetInt();
|
||||
m_tabpanel->SetSelection(pos);
|
||||
Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) {
|
||||
m_tabpanel->SelectPageByName(evt.GetString());
|
||||
});
|
||||
|
||||
Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this);
|
||||
@@ -702,7 +701,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
}
|
||||
return;}
|
||||
#endif
|
||||
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; }
|
||||
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
|
||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
|
||||
m_plater->apply_background_progress();
|
||||
m_print_enable = get_enable_print_status();
|
||||
@@ -723,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;}
|
||||
else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;}
|
||||
if (evt.CmdDown() && evt.GetKeyCode() == 'F') {
|
||||
if (m_plater && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview)) {
|
||||
if (m_plater && is_prepare_or_preview_tab()) {
|
||||
m_plater->sidebar().can_search();
|
||||
}
|
||||
}
|
||||
@@ -1007,8 +1006,8 @@ void MainFrame::update_layout()
|
||||
m_layout = layout;
|
||||
|
||||
// From the very beginning the Print settings should be selected
|
||||
//m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1;
|
||||
m_last_selected_tab = 1;
|
||||
//m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE;
|
||||
m_last_selected_tab = TAB_ID_PREPARE;
|
||||
|
||||
// Set new settings
|
||||
switch (m_layout)
|
||||
@@ -1016,14 +1015,18 @@ void MainFrame::update_layout()
|
||||
case ESettingsLayout::Old:
|
||||
{
|
||||
m_plater->Reparent(m_tabpanel);
|
||||
m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false);
|
||||
m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false);
|
||||
// Right after Home — or first, when there is no Home tab (PositionAfter() would
|
||||
// append instead, and by now the other built-in tabs are already in place).
|
||||
const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME);
|
||||
const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
|
||||
m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active");
|
||||
m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active");
|
||||
m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
|
||||
|
||||
m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt)
|
||||
{
|
||||
// jump to 3deditor under preview_only mode
|
||||
if (evt.GetId() == tp3DEditor){
|
||||
if (evt.GetId() == m_tabpanel->FindPageByName(TAB_ID_PREPARE)) {
|
||||
Sidebar& sidebar = GUI::wxGetApp().sidebar();
|
||||
if (sidebar.need_auto_sync_after_connect_printer()) {
|
||||
sidebar.set_need_auto_sync_after_connect_printer(false);
|
||||
@@ -1107,6 +1110,9 @@ void MainFrame::update_edge_panels()
|
||||
void MainFrame::shutdown()
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter";
|
||||
if (m_project != nullptr)
|
||||
m_project->shutdown();
|
||||
m_plugin_pages.shutdown();
|
||||
#ifdef __WXGTK__
|
||||
// Edge panels are child windows — wxWidgets destroys them automatically.
|
||||
m_edge_bottom = nullptr;
|
||||
@@ -1252,15 +1258,14 @@ void MainFrame::init_tabpanel() {
|
||||
#endif
|
||||
//BBS
|
||||
wxWindow* panel = m_tabpanel->GetCurrentPage();
|
||||
int sel = m_tabpanel->GetSelection();
|
||||
//wxString page_text = m_tabpanel->GetPageText(sel);
|
||||
m_last_selected_tab = m_tabpanel->GetSelection();
|
||||
m_last_selected_tab = m_tabpanel->GetSelectedPageName();
|
||||
if (panel == m_plater) {
|
||||
if (sel == tp3DEditor) {
|
||||
if (m_last_selected_tab == TAB_ID_PREPARE) {
|
||||
wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D));
|
||||
m_param_panel->OnActivate();
|
||||
}
|
||||
else if (sel == tpPreview) {
|
||||
else if (m_last_selected_tab == TAB_ID_PREVIEW) {
|
||||
m_plater->reset_check_status();
|
||||
if (!m_plater->check_ams_status(m_slice_select == eSliceAll))
|
||||
return;
|
||||
@@ -1275,7 +1280,7 @@ void MainFrame::init_tabpanel() {
|
||||
//monitor
|
||||
}
|
||||
#ifndef __APPLE__
|
||||
if (sel == tp3DEditor) {
|
||||
if (m_last_selected_tab == TAB_ID_PREPARE) {
|
||||
m_topbar->EnableUndoRedoItems();
|
||||
}
|
||||
else {
|
||||
@@ -1285,34 +1290,16 @@ void MainFrame::init_tabpanel() {
|
||||
|
||||
if (panel)
|
||||
panel->SetFocus();
|
||||
|
||||
/*switch (sel) {
|
||||
case TabPosition::tpHome:
|
||||
show_option(false);
|
||||
break;
|
||||
case TabPosition::tp3DEditor:
|
||||
show_option(true);
|
||||
break;
|
||||
case TabPosition::tpPreview:
|
||||
show_option(true);
|
||||
break;
|
||||
case TabPosition::tpMonitor:
|
||||
show_option(false);
|
||||
break;
|
||||
default:
|
||||
show_option(false);
|
||||
break;
|
||||
}*/
|
||||
});
|
||||
|
||||
if (wxGetApp().is_editor()) {
|
||||
m_webview = new WebViewPanel(m_tabpanel);
|
||||
Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) {
|
||||
wxString url = evt.GetString();
|
||||
select_tab(MainFrame::tpHome);
|
||||
select_tab(TAB_ID_HOME);
|
||||
m_webview->load_url(url);
|
||||
});
|
||||
m_tabpanel->AddPage(m_webview, "", "tab_home_active", "tab_home_active", false);
|
||||
m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active");
|
||||
m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL);
|
||||
}
|
||||
|
||||
@@ -1327,7 +1314,7 @@ void MainFrame::init_tabpanel() {
|
||||
//BBS add pages
|
||||
m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
m_monitor->SetBackgroundColour(*wxWHITE);
|
||||
m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false);
|
||||
m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active");
|
||||
|
||||
m_printer_view = new PrinterWebView(m_tabpanel);
|
||||
Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) {
|
||||
@@ -1342,16 +1329,20 @@ void MainFrame::init_tabpanel() {
|
||||
m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
m_multi_machine->SetBackgroundColour(*wxWHITE);
|
||||
// TODO: change the bitmap
|
||||
m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false);
|
||||
m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active");
|
||||
}
|
||||
|
||||
m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
m_project->SetBackgroundColour(*wxWHITE);
|
||||
m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false);
|
||||
m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active");
|
||||
|
||||
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
m_calibration->SetBackgroundColour(*wxWHITE);
|
||||
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false);
|
||||
m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active");
|
||||
|
||||
// Plugin pages are appended after the built-in tabs; their ids are namespaced
|
||||
// (plugin.<plugin_key>.<name>) so they can't collide with the built-in TAB_ID_* constants.
|
||||
m_plugin_pages.initialize(m_tabpanel);
|
||||
|
||||
if (m_plater) {
|
||||
// load initial config
|
||||
@@ -1373,10 +1364,15 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
|
||||
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
|
||||
|
||||
// The legacy page is appended when printer agents are enabled. Remove that
|
||||
// extra page before switching back to the normal native/legacy layout.
|
||||
if (!use_printer_agents) {
|
||||
if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) {
|
||||
// The web Device page is the extra tab printer-agents mode shows alongside the native one.
|
||||
// Printers that drive the native Bambu device tab have nothing to put in it, so they don't
|
||||
// get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty.
|
||||
const bool want_web_device_tab = use_printer_agents && wxGetApp().preset_bundle != nullptr &&
|
||||
!wxGetApp().preset_bundle->use_bbl_device_tab();
|
||||
|
||||
// Remove the extra page before switching to any layout that shouldn't have it.
|
||||
if (!want_web_device_tab) {
|
||||
if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) {
|
||||
m_printer_view->Show(false);
|
||||
m_tabpanel->RemovePage(idx);
|
||||
}
|
||||
@@ -1394,8 +1390,8 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
m_tabpanel->RemovePage(idx);
|
||||
}
|
||||
m_monitor->Show(false);
|
||||
m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"),
|
||||
std::string("tab_monitor_active"));
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
|
||||
_L("Device"), "tab_monitor_active");
|
||||
}
|
||||
|
||||
if (m_printer_view == nullptr) {
|
||||
@@ -1416,28 +1412,31 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
// TODO: change the bitmap
|
||||
if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) {
|
||||
m_multi_machine->Show(false);
|
||||
m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"),
|
||||
std::string("tab_multi_active"), false);
|
||||
// Past the web Device tab when it is already there, so enabling multi-machine
|
||||
// later can't wedge this page between the two Device tabs.
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR_WEB, TAB_ID_MONITOR}),
|
||||
TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active");
|
||||
}
|
||||
}
|
||||
if (!m_calibration) {
|
||||
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
m_calibration->SetBackgroundColour(*wxWHITE);
|
||||
}
|
||||
// Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled,
|
||||
// the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position.
|
||||
if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) {
|
||||
m_calibration->Show(false);
|
||||
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"),
|
||||
std::string("tab_calibration_active"), false);
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
|
||||
_L("Calibration"), "tab_calibration_active");
|
||||
}
|
||||
|
||||
if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
|
||||
m_printer_view->Show(false);
|
||||
m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"),
|
||||
std::string("tab_monitor_active"), false);
|
||||
} else {
|
||||
m_tabpanel->SetPageText(idx, _L("Device (legacy)"));
|
||||
if (want_web_device_tab) {
|
||||
if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
|
||||
m_printer_view->Show(false);
|
||||
// Immediately right of the native Device tab, not at the end of the tab bar.
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB,
|
||||
m_printer_view, _L("Device (Web)"), "tab_monitor_active");
|
||||
} else {
|
||||
m_tabpanel->SetPageText(idx, _L("Device (Web)"));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _MSW_DARK_MODE
|
||||
@@ -1445,6 +1444,7 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
#endif // _MSW_DARK_MODE
|
||||
|
||||
fit_tab_labels(); // ORCA on printer change
|
||||
m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1466,7 +1466,8 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
m_monitor->SetBackgroundColour(*wxWHITE);
|
||||
}
|
||||
m_monitor->Show(false);
|
||||
m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"));
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
|
||||
_L("Device"), "tab_monitor_active");
|
||||
|
||||
if (wxGetApp().is_enable_multi_machine()) {
|
||||
if (!m_multi_machine) {
|
||||
@@ -1475,18 +1476,18 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
}
|
||||
// TODO: change the bitmap
|
||||
m_multi_machine->Show(false);
|
||||
m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"),
|
||||
std::string("tab_multi_active"), false);
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine,
|
||||
_L("Multi-device"), "tab_multi_active");
|
||||
}
|
||||
if (!m_calibration) {
|
||||
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
m_calibration->SetBackgroundColour(*wxWHITE);
|
||||
}
|
||||
m_calibration->Show(false);
|
||||
// Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled,
|
||||
// the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position.
|
||||
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"),
|
||||
std::string("tab_calibration_active"), false);
|
||||
// Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than
|
||||
// append, so its position doesn't depend on the relayout() below running afterwards.
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
|
||||
_L("Calibration"), "tab_calibration_active");
|
||||
|
||||
#ifdef _MSW_DARK_MODE
|
||||
wxGetApp().UpdateDarkUIWin(this);
|
||||
@@ -1519,10 +1520,17 @@ void MainFrame::show_device(bool should_use_native) {
|
||||
});
|
||||
}
|
||||
m_printer_view->Show(false);
|
||||
m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"),
|
||||
std::string("tab_monitor_active"));
|
||||
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view,
|
||||
_L("Device"), "tab_monitor_active");
|
||||
}
|
||||
fit_tab_labels(); // ORCA on printer change
|
||||
m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
|
||||
}
|
||||
|
||||
bool MainFrame::is_prepare_or_preview_tab() const
|
||||
{
|
||||
const wxString tab = m_tabpanel->GetSelectedPageName();
|
||||
return tab == TAB_ID_PREPARE || tab == TAB_ID_PREVIEW;
|
||||
}
|
||||
|
||||
void MainFrame::fit_tab_labels()
|
||||
@@ -1554,7 +1562,7 @@ void MainFrame::fit_tab_labels()
|
||||
bool MainFrame::preview_only_hint()
|
||||
{
|
||||
if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) {
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelection() %tp3DEditor;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelectedPageName() %wxString(TAB_ID_PREPARE);
|
||||
|
||||
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"));
|
||||
confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) {
|
||||
@@ -1872,22 +1880,22 @@ bool MainFrame::can_clone() const {
|
||||
|
||||
bool MainFrame::can_select() const
|
||||
{
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty();
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty();
|
||||
}
|
||||
|
||||
bool MainFrame::can_deselect() const
|
||||
{
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty();
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty();
|
||||
}
|
||||
|
||||
bool MainFrame::can_delete() const
|
||||
{
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty();
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty();
|
||||
}
|
||||
|
||||
bool MainFrame::can_delete_all() const
|
||||
{
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty();
|
||||
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty();
|
||||
}
|
||||
|
||||
bool MainFrame::can_reslice() const
|
||||
@@ -1996,7 +2004,7 @@ wxBoxSizer* MainFrame::create_side_tools()
|
||||
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
|
||||
else
|
||||
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
|
||||
this->m_tabpanel->SetSelection(tpPreview);
|
||||
this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2322,6 +2330,11 @@ bool MainFrame::get_enable_slice_status()
|
||||
}
|
||||
}
|
||||
|
||||
// A mixed filament whose components were deleted, or whose components disagree in type,
|
||||
// cannot be resolved at slicing time. Block the slice until the user fixes it.
|
||||
if (enable && m_plater->sidebar().has_broken_mixed_filament())
|
||||
enable = false;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable;
|
||||
return enable;
|
||||
}
|
||||
@@ -3143,7 +3156,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective"));
|
||||
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
|
||||
},
|
||||
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
|
||||
this, [this]() { return is_prepare_or_preview_tab(); },
|
||||
[this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this);
|
||||
|
||||
viewMenu->AppendSeparator();
|
||||
@@ -3152,7 +3165,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().toggle_show_gcode_window();
|
||||
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
|
||||
},
|
||||
this, [this]() { return m_tabpanel->GetSelection() == tpPreview; },
|
||||
this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; },
|
||||
[this]() { return wxGetApp().show_gcode_window(); }, this);
|
||||
|
||||
append_menu_check_item(
|
||||
@@ -3161,7 +3174,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().toggle_show_3d_navigator();
|
||||
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
|
||||
},
|
||||
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
|
||||
this, [this]() { return is_prepare_or_preview_tab(); },
|
||||
[this]() { return wxGetApp().show_3d_navigator(); }, this);
|
||||
|
||||
append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"),
|
||||
@@ -3169,15 +3182,14 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().toggle_show_plate_gridlines();
|
||||
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
|
||||
}, this,
|
||||
[this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
|
||||
[this]() { return is_prepare_or_preview_tab(); },
|
||||
[this]() { return wxGetApp().show_plate_gridlines(); }, this);
|
||||
|
||||
append_menu_item(
|
||||
viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"),
|
||||
[this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this,
|
||||
[this]() {
|
||||
return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) &&
|
||||
m_plater->is_sidebar_enabled();
|
||||
return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled();
|
||||
},
|
||||
this);
|
||||
|
||||
@@ -3199,7 +3211,7 @@ void MainFrame::init_menubar_as_editor()
|
||||
wxGetApp().toggle_show_outline();
|
||||
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
|
||||
},
|
||||
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; },
|
||||
this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; },
|
||||
[this]() { return wxGetApp().show_outline(); }, this);
|
||||
|
||||
/*viewMenu->AppendSeparator();
|
||||
@@ -4000,13 +4012,16 @@ void MainFrame::select_tab(wxPanel* panel)
|
||||
wxGetApp().params_dialog()->Popup();
|
||||
return;
|
||||
}
|
||||
// Not panel->GetName(): Prepare and Preview share the single m_plater window, so the
|
||||
// window has no one correct name. The slot -> id lookup is the only correct resolution.
|
||||
int page_idx = m_tabpanel->FindPage(panel);
|
||||
if (page_idx == tp3DEditor && m_tabpanel->GetSelection() == tpPreview)
|
||||
wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast<size_t>(page_idx));
|
||||
if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)
|
||||
return;
|
||||
//BBS GUI refactor: remove unused layout new/dlg
|
||||
/*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg)
|
||||
page_idx++;*/
|
||||
select_tab(size_t(page_idx));
|
||||
select_tab(page_name);
|
||||
}
|
||||
|
||||
//BBS
|
||||
@@ -4014,7 +4029,7 @@ void MainFrame::jump_to_monitor(std::string dev_id)
|
||||
{
|
||||
if(!m_monitor)
|
||||
return;
|
||||
m_tabpanel->SetSelection(tpMonitor);
|
||||
m_tabpanel->SelectPageByName(TAB_ID_MONITOR);
|
||||
if (!dev_id.empty()) {
|
||||
((MonitorPanel*)m_monitor)->select_machine(dev_id);
|
||||
}
|
||||
@@ -4024,26 +4039,26 @@ void MainFrame::jump_to_multipage()
|
||||
{
|
||||
if(!m_multi_machine)
|
||||
return;
|
||||
m_tabpanel->SetSelection(tpMultiDevice);
|
||||
m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE);
|
||||
((MultiMachinePage*)m_multi_machine)->jump_to_send_page();
|
||||
}
|
||||
|
||||
|
||||
//BBS GUI refactor: remove unused layout new/dlg
|
||||
void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
|
||||
void MainFrame::select_tab(const wxString& id/* = wxString()*/)
|
||||
{
|
||||
//bool tabpanel_was_hidden = false;
|
||||
|
||||
// Controls on page are created on active page of active tab now.
|
||||
// We should select/activate tab before its showing to avoid an UI-flickering
|
||||
auto select = [this, tab](bool was_hidden) {
|
||||
// when tab == -1, it means we should show the last selected tab
|
||||
auto select = [this, id](bool was_hidden) {
|
||||
// when id is empty, it means we should show the last selected tab
|
||||
//BBS GUI refactor: remove unused layout new/dlg
|
||||
//size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab;
|
||||
size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : tab;
|
||||
wxString new_selection = id.empty() ? m_last_selected_tab : id;
|
||||
|
||||
if (m_tabpanel->GetSelection() != (int)new_selection)
|
||||
m_tabpanel->SetSelection(new_selection);
|
||||
if (m_tabpanel->GetSelectedPageName() != new_selection)
|
||||
m_tabpanel->SelectPageByName(new_selection);
|
||||
#ifdef _MSW_DARK_MODE
|
||||
/*if (wxGetApp().tabs_as_menu()) {
|
||||
if (Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection)))
|
||||
@@ -4052,10 +4067,12 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
|
||||
m_plater->get_current_canvas3D()->render();
|
||||
}*/
|
||||
#endif
|
||||
if (tab == MainFrame::tp3DEditor && m_layout == ESettingsLayout::Old)
|
||||
// Intentionally `id`, not `new_selection`: the fallback-to-last-tab path must not
|
||||
// trigger this render even when the last selected tab was Prepare.
|
||||
if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old)
|
||||
m_plater->canvas3D()->render();
|
||||
else if (was_hidden) {
|
||||
Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection));
|
||||
Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPageByName(new_selection));
|
||||
if (cur_tab)
|
||||
cur_tab->OnActivate();
|
||||
}
|
||||
@@ -4064,10 +4081,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
|
||||
select(false);
|
||||
}
|
||||
|
||||
void MainFrame::request_select_tab(TabPosition pos)
|
||||
void MainFrame::request_select_tab(const wxString& id)
|
||||
{
|
||||
wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB);
|
||||
evt->SetInt(pos);
|
||||
evt->SetString(id);
|
||||
wxQueueEvent(this, evt);
|
||||
}
|
||||
|
||||
@@ -4333,21 +4350,33 @@ void MainFrame::load_printer_url(wxString url, wxString apikey)
|
||||
void MainFrame::load_printer_url()
|
||||
{
|
||||
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
|
||||
if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents"))
|
||||
if (preset_bundle.use_bbl_device_tab() && !wxGetApp().app_config->get_bool("use_printer_agents"))
|
||||
return;
|
||||
|
||||
auto cfg = preset_bundle.printers.get_edited_preset().config;
|
||||
if (cfg.opt_string("print_host").empty()) {
|
||||
if (auto *device_manager = wxGetApp().getDeviceManager()) {
|
||||
auto *machine = device_manager->get_selected_machine();
|
||||
if (!machine) {
|
||||
auto machines = device_manager->get_my_machine_list();
|
||||
if (machines.size() == 1)
|
||||
machine = machines.begin()->second;
|
||||
}
|
||||
if (machine && !machine->get_dev_ip().empty())
|
||||
cfg.opt_string("print_host") = machine->get_dev_ip();
|
||||
}
|
||||
}
|
||||
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
|
||||
wxString apikey;
|
||||
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
|
||||
if (cfg.has("printhost_apikey") && (host_type == htPrusaLink || host_type == htPrusaConnect))
|
||||
if (cfg.has("printhost_apikey") && host_type != htSimplyPrint)
|
||||
apikey = cfg.opt_string("printhost_apikey");
|
||||
if (!url.empty()) {
|
||||
load_printer_url(url, apikey);
|
||||
}
|
||||
}
|
||||
|
||||
bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; }
|
||||
bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName() == TAB_ID_MONITOR; }
|
||||
|
||||
|
||||
void MainFrame::refresh_plugin_tips()
|
||||
|
||||
@@ -35,6 +35,21 @@
|
||||
#include "PrinterWebView.hpp"
|
||||
#include "calib_dlg.hpp"
|
||||
#include "MultiMachinePage.hpp"
|
||||
#include "slic3r/plugin/host/PluginPages.hpp"
|
||||
|
||||
// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
|
||||
// names rather than positional indices so optional pages cannot shift them.
|
||||
#define TAB_ID_HOME "home"
|
||||
#define TAB_ID_PREPARE "prepare"
|
||||
#define TAB_ID_PREVIEW "preview"
|
||||
#define TAB_ID_MONITOR "monitor"
|
||||
// Printer-agents mode shows the legacy web page alongside the native Device tab, so it needs an
|
||||
// id of its own: sharing TAB_ID_MONITOR makes every name lookup resolve to whichever of the two
|
||||
// comes first, which silently defeats PluginPages' selection round-trip across a tab relayout.
|
||||
#define TAB_ID_MONITOR_WEB "monitor_web"
|
||||
#define TAB_ID_MULTI_DEVICE "multi_device"
|
||||
#define TAB_ID_PROJECT "project"
|
||||
#define TAB_ID_CALIBRATION "calibration"
|
||||
|
||||
#define ENABEL_PRINT_ALL 0
|
||||
|
||||
@@ -115,7 +130,7 @@ class MainFrame : public DPIFrame
|
||||
wxMenuItem* m_menu_item_reslice_now { nullptr };
|
||||
wxSizer* m_main_sizer{ nullptr };
|
||||
|
||||
size_t m_last_selected_tab;
|
||||
wxString m_last_selected_tab;
|
||||
|
||||
std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const;
|
||||
std::string get_dir_name(const wxString &full_name) const;
|
||||
@@ -214,19 +229,6 @@ public:
|
||||
#ifdef __APPLE__
|
||||
bool get_mac_full_screen() { return m_mac_fullscreen; }
|
||||
#endif
|
||||
//BBS GUI refactor
|
||||
enum TabPosition
|
||||
{
|
||||
tpHome = 0,
|
||||
tp3DEditor = 1,
|
||||
tpPreview = 2,
|
||||
tpMonitor = 3,
|
||||
tpMultiDevice = 4,
|
||||
tpProject = 5,
|
||||
tpCalibration = 6,
|
||||
tpAuxiliary = 7,
|
||||
toDebugTool = 8,
|
||||
};
|
||||
|
||||
//BBS: add slice&&print status update logic
|
||||
enum SlicePrintEventType
|
||||
@@ -326,8 +328,8 @@ public:
|
||||
// When tab == -1, will be selected last selected tab
|
||||
//BBS: GUI refactor
|
||||
void select_tab(wxPanel* panel);
|
||||
void select_tab(size_t tab = size_t(-1));
|
||||
void request_select_tab(TabPosition pos);
|
||||
void select_tab(const wxString& id = wxString());
|
||||
void request_select_tab(const wxString& id);
|
||||
int get_calibration_curr_tab();
|
||||
void select_view(const std::string& direction);
|
||||
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
|
||||
@@ -360,6 +362,9 @@ public:
|
||||
//SoftFever
|
||||
void show_device(bool should_use_native);
|
||||
void fit_tab_labels(); // ORCA
|
||||
// True while either of the two tabs backed by m_plater is selected.
|
||||
bool is_prepare_or_preview_tab() const;
|
||||
PluginPages& plugin_pages() { return m_plugin_pages; }
|
||||
|
||||
PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr };
|
||||
FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr };
|
||||
@@ -385,7 +390,8 @@ public:
|
||||
CalibrationPanel* m_calibration{ nullptr };
|
||||
WebViewPanel* m_webview { nullptr };
|
||||
PrinterWebView* m_printer_view{nullptr};
|
||||
wxLogWindow* m_log_window { nullptr };
|
||||
PluginPages m_plugin_pages;
|
||||
wxLogWindow* m_log_window { nullptr };
|
||||
// BBS
|
||||
//wxBookCtrlBase* m_tabpanel { nullptr };
|
||||
Notebook* m_tabpanel{ nullptr };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,183 @@
|
||||
#ifndef slic3r_MixedFilamentDialog_hpp_
|
||||
#define slic3r_MixedFilamentDialog_hpp_
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/tglbtn.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
|
||||
class Button;
|
||||
class CheckBox;
|
||||
class ComboBox;
|
||||
class wxMouseEvent;
|
||||
class wxScrolledWindow;
|
||||
class wxTextCtrl;
|
||||
class wxWrapSizer;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class GradientCurveEditor;
|
||||
class RatioLabelPanel;
|
||||
|
||||
struct MixedFilamentResult {
|
||||
std::vector<unsigned int> components; // 1-based physical filament indices
|
||||
std::vector<int> ratios; // percentages, sum = 100
|
||||
bool gradient_enabled = false;
|
||||
int gradient_direction = 0; // 0 = A→B, 1 = B→A (only for 2-color)
|
||||
bool per_part_gradient = false; // valid only when gradient_enabled == true
|
||||
// Optional Photoshop-style custom curve overriding the linear A→B gradient.
|
||||
// Empty -> use linear (gradient_direction). Non-empty -> cubic Hermite over [0,1]^2
|
||||
// with optional per-anchor tangent overrides (see GradientAnchor).
|
||||
std::vector<GradientAnchor> gradient_curve;
|
||||
};
|
||||
|
||||
class MixedFilamentDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
MixedFilamentDialog(wxWindow* parent,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_names,
|
||||
const std::vector<std::string>& physical_types = {});
|
||||
|
||||
MixedFilamentDialog(wxWindow* parent,
|
||||
const MixedFilamentResult& existing,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_names,
|
||||
const std::vector<std::string>& physical_types = {});
|
||||
|
||||
~MixedFilamentDialog();
|
||||
|
||||
MixedFilamentResult get_result() const { return m_result; }
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
private:
|
||||
void build_ui();
|
||||
wxBoxSizer* create_preview_panel();
|
||||
wxBoxSizer* create_material_selection();
|
||||
wxBoxSizer* create_ratio_slider();
|
||||
wxBoxSizer* create_triangle_picker();
|
||||
wxBoxSizer* create_gradient_section();
|
||||
wxBoxSizer* create_recommendation_grid();
|
||||
wxBoxSizer* create_button_panel();
|
||||
|
||||
void on_filament_changed();
|
||||
void on_ratio_changed(int new_ratio_a);
|
||||
void on_gradient_toggled();
|
||||
void on_gradient_direction_changed();
|
||||
void on_gradient_curve_changed();
|
||||
void on_per_part_gradient_toggled();
|
||||
void on_add_material();
|
||||
void on_remove_material();
|
||||
void on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b);
|
||||
void on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c);
|
||||
void apply_manual_ratio(size_t idx, int value);
|
||||
void apply_dragged_triangle_ratio(int r0, int r1, int r2);
|
||||
void reset_manual_ratio_state();
|
||||
void refresh_ratio_labels();
|
||||
void sync_triangle_weights_from_ratios();
|
||||
void start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect);
|
||||
void commit_ratio_editor(bool apply);
|
||||
void commit_ratio_editor_from_background(wxMouseEvent& e);
|
||||
void update_preview();
|
||||
void update_ok_button_state();
|
||||
void update_gradient_direction_items();
|
||||
void update_component_count_ui();
|
||||
// Picks dialog (width, height) based on current state so the gradient curve
|
||||
// editor and the recommendation list stay visible at the same time.
|
||||
wxSize compute_dialog_size() const;
|
||||
void rebuild_all_combos();
|
||||
void rebuild_recommendation_items();
|
||||
void refresh_curve_editor_colors();
|
||||
void paint_warning_panel(wxPaintEvent& evt);
|
||||
|
||||
wxBitmap make_swatch_bitmap(size_t idx);
|
||||
|
||||
// Reserves the same width on every material row label so the combo boxes line up.
|
||||
static void apply_uniform_label_width(wxStaticText* lbl);
|
||||
// Appends one "Filament N" label + combo row to m_material_rows_sizer. N follows the
|
||||
// number of rows already there, so callers must not renumber anything themselves.
|
||||
void append_material_row();
|
||||
|
||||
// Helpers for component/ratio access
|
||||
size_t num_components() const { return m_result.components.size(); }
|
||||
unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; }
|
||||
int ratio(size_t i) const { return (i < m_result.ratios.size()) ? m_result.ratios[i] : 0; }
|
||||
wxColour comp_colour(size_t i) const;
|
||||
|
||||
MixedFilamentResult m_result;
|
||||
bool m_edit_mode{false};
|
||||
std::vector<std::string> m_physical_colors;
|
||||
std::vector<std::string> m_physical_names;
|
||||
std::vector<std::string> m_physical_types;
|
||||
wxString m_type_mismatch_msg;
|
||||
|
||||
// Combo item index -> 1-based physical filament index (per combo)
|
||||
std::vector<std::vector<unsigned int>> m_combo_to_physical;
|
||||
|
||||
// UI controls
|
||||
wxPanel* m_preview_canvas{nullptr};
|
||||
wxPanel* m_summary_panel{nullptr};
|
||||
std::vector<ComboBox*> m_combo_filaments;
|
||||
wxBoxSizer* m_material_rows_sizer{nullptr};
|
||||
wxPanel* m_ratio_bar{nullptr};
|
||||
wxPanel* m_triangle_panel{nullptr};
|
||||
RatioLabelPanel* m_label_ratio_a{nullptr};
|
||||
RatioLabelPanel* m_label_ratio_b{nullptr};
|
||||
wxPanel* m_ratio_editor_panel{nullptr};
|
||||
wxTextCtrl* m_ratio_editor{nullptr};
|
||||
CheckBox* m_chk_gradient{nullptr};
|
||||
wxStaticText* m_label_gradient{nullptr};
|
||||
ComboBox* m_combo_gradient_dir{nullptr};
|
||||
wxBoxSizer* m_gradient_sizer{nullptr};
|
||||
GradientCurveEditor* m_curve_editor{nullptr};
|
||||
wxBoxSizer* m_curve_sizer{nullptr};
|
||||
CheckBox* m_chk_per_part_gradient{nullptr};
|
||||
wxStaticText* m_label_per_part_gradient{nullptr};
|
||||
wxBoxSizer* m_per_part_gradient_sizer{nullptr};
|
||||
Button* m_btn_add_material{nullptr};
|
||||
Button* m_btn_remove_material{nullptr};
|
||||
Button* m_btn_ok{nullptr};
|
||||
Button* m_btn_cancel{nullptr};
|
||||
wxBoxSizer* m_warning_sizer{nullptr};
|
||||
wxPanel* m_warning_panel{nullptr};
|
||||
|
||||
wxBoxSizer* m_ratio_sizer{nullptr};
|
||||
wxBoxSizer* m_triangle_sizer{nullptr};
|
||||
wxBoxSizer* m_right_sizer{nullptr};
|
||||
|
||||
wxScrolledWindow* m_recommendation_scroll{nullptr};
|
||||
wxWrapSizer* m_recommendation_grid{nullptr};
|
||||
|
||||
// Drag state. The ratio bar and the triangle picker capture the mouse
|
||||
// independently, so they must not share a flag: a mouse-up on one would
|
||||
// otherwise clear the other's flag and skip its ReleaseMouse().
|
||||
bool m_ratio_dragging{false};
|
||||
bool m_tri_dragging{false};
|
||||
std::vector<size_t> m_ratio_manual_order;
|
||||
size_t m_ratio_editor_idx{0};
|
||||
bool m_ratio_editor_committing{false};
|
||||
wxWindow* m_ratio_editor_anchor{nullptr};
|
||||
// Triangle picker drag point (barycentric weights)
|
||||
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334};
|
||||
|
||||
// Cached triangle color bitmap (invalidated when colors or size change)
|
||||
wxBitmap m_tri_cache_bmp;
|
||||
wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2;
|
||||
wxSize m_tri_cache_size;
|
||||
std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_MixedFilamentDialog_hpp_
|
||||
@@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel()
|
||||
|
||||
//m_status_add_machine_panel = new AddMachinePanel(m_tabpanel);
|
||||
m_status_info_panel = new StatusPanel(m_tabpanel);
|
||||
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), "", true);
|
||||
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
|
||||
|
||||
m_media_file_panel = new MediaFilePanel(m_tabpanel);
|
||||
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), "", false);
|
||||
//m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false);
|
||||
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false);
|
||||
//m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false);
|
||||
|
||||
m_upgrade_panel = new UpgradePanel(m_tabpanel);
|
||||
m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false);
|
||||
m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false);
|
||||
|
||||
m_hms_panel = new HMSPanel(m_tabpanel);
|
||||
m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), "", false);
|
||||
m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false);
|
||||
|
||||
std::string network_ver = Slic3r::NetworkAgent::get_version();
|
||||
if (!network_ver.empty()) {
|
||||
@@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag()
|
||||
bool MonitorPanel::Show(bool show)
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize());
|
||||
// Notebook::InsertPage() hides every page it appends, so this also runs while MainFrame is
|
||||
// still constructing, before GUI_App::mainframe is assigned. Same guard as Plater::Show().
|
||||
if (wxGetApp().mainframe)
|
||||
wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize());
|
||||
#endif
|
||||
|
||||
NetworkAgent* m_agent = wxGetApp().getAgent();
|
||||
|
||||
@@ -86,9 +86,9 @@ void MultiMachinePage::init_tabpanel()
|
||||
m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel);
|
||||
m_machine_manager = new MultiMachineManagerPage(m_tabpanel);
|
||||
|
||||
m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true);
|
||||
m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false);
|
||||
m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), "", false);
|
||||
m_tabpanel->AddPage(m_machine_manager, _L("Device"), true);
|
||||
m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), false);
|
||||
m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), false);
|
||||
}
|
||||
|
||||
void MultiMachinePage::init_timer()
|
||||
|
||||
@@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale()
|
||||
|
||||
void ButtonsListCtrl::SetSelection(int sel)
|
||||
{
|
||||
if (m_selection == sel)
|
||||
if (m_selection == sel && sel >= 0 && sel < static_cast<int>(m_pageButtons.size()))
|
||||
return;
|
||||
// BBS: change button color
|
||||
wxColour selected_btn_bg("#009688"); // Gradient #009688
|
||||
if (m_selection >= 0) {
|
||||
if (m_selection >= 0 && m_selection < static_cast<int>(m_pageButtons.size())) {
|
||||
StateColor bg_color = StateColor(
|
||||
std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered},
|
||||
std::pair{wxColour(59, 68, 70), (int) StateColor::Normal});
|
||||
@@ -132,9 +132,15 @@ void ButtonsListCtrl::SetSelection(int sel)
|
||||
StateColor text_color = StateColor(
|
||||
std::pair{wxColour(254,254, 254), (int) StateColor::Normal}
|
||||
);
|
||||
m_pageButtons[m_selection]->SetSelected(false);
|
||||
m_pageButtons[m_selection]->SetTextColor(text_color);
|
||||
}
|
||||
|
||||
if (sel < 0 || sel >= static_cast<int>(m_pageButtons.size())) {
|
||||
m_selection = -1;
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
m_selection = sel;
|
||||
|
||||
StateColor bg_color = StateColor(
|
||||
@@ -145,17 +151,19 @@ void ButtonsListCtrl::SetSelection(int sel)
|
||||
StateColor text_color = StateColor(
|
||||
std::pair{wxColour(254, 254, 254), (int) StateColor::Normal}
|
||||
);
|
||||
m_pageButtons[m_selection]->SetSelected(true);
|
||||
m_pageButtons[m_selection]->SetTextColor(text_color);
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const std::string &inactive_bmp_name)
|
||||
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
|
||||
{
|
||||
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
|
||||
btn->SetCornerRadius(0);
|
||||
|
||||
if (bmp_name.empty() && bmp.IsOk())
|
||||
btn->SetIcon(bmp);
|
||||
|
||||
int em = em_unit(this);
|
||||
//BBS set size for button
|
||||
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
|
||||
@@ -168,8 +176,6 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
|
||||
StateColor text_color = StateColor(
|
||||
std::pair{wxColour(254,254, 254), (int) StateColor::Normal});
|
||||
btn->SetTextColor(text_color);
|
||||
btn->SetInactiveIcon(inactive_bmp_name);
|
||||
btn->SetSelected(false);
|
||||
btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) {
|
||||
if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) {
|
||||
auto sel = it - m_pageButtons.begin();
|
||||
@@ -192,6 +198,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
|
||||
|
||||
void ButtonsListCtrl::RemovePage(size_t n)
|
||||
{
|
||||
if (n >= m_pageButtons.size())
|
||||
return;
|
||||
|
||||
if (m_selection == static_cast<int>(n))
|
||||
m_selection = -1;
|
||||
else if (m_selection > static_cast<int>(n))
|
||||
--m_selection;
|
||||
|
||||
Button* btn = m_pageButtons[n];
|
||||
m_pageButtons.erase(m_pageButtons.begin() + n);
|
||||
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
|
||||
@@ -240,6 +254,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
|
||||
return btn->GetLabel();
|
||||
}
|
||||
|
||||
// ORCA
|
||||
void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
|
||||
{
|
||||
if (m_overflow_button == button)
|
||||
return;
|
||||
|
||||
if (m_overflow_button != nullptr)
|
||||
m_sizer->Detach(m_overflow_button);
|
||||
|
||||
m_overflow_button = button;
|
||||
|
||||
if (m_overflow_button != nullptr)
|
||||
// Right after the tab buttons (index 0), ahead of any stretch spacer / side_tools.
|
||||
m_sizer->Insert(1, m_overflow_button, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM, m_btn_margin);
|
||||
|
||||
m_sizer->Layout();
|
||||
}
|
||||
|
||||
//#endif // _WIN32
|
||||
|
||||
void Notebook::Init()
|
||||
@@ -253,6 +285,8 @@ void Notebook::Init()
|
||||
|
||||
m_showTimeout = m_hideTimeout = 0;
|
||||
|
||||
m_pageNames.clear();
|
||||
|
||||
/* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with
|
||||
* 32-bit X11 visuals (the overlay does not work). Is this a wxWindows
|
||||
* bug? Is this a Gstreamer bug? No idea, but it is our problem ...
|
||||
|
||||
+106
-33
@@ -3,7 +3,11 @@
|
||||
|
||||
//#ifdef _WIN32
|
||||
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/bookctrl.h>
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
class ScalableButton;
|
||||
@@ -23,13 +27,16 @@ public:
|
||||
void SetSelection(int sel);
|
||||
void UpdateMode();
|
||||
void Rescale();
|
||||
bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const std::string &inactive_bmp_name = "");
|
||||
bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const wxBitmap &bmp = wxNullBitmap);
|
||||
void RemovePage(size_t n);
|
||||
bool SetPageImage(size_t n, const std::string& bmp_name) const;
|
||||
void SetPageText(size_t n, const wxString& strText);
|
||||
void SetCompact(size_t n, bool compact); // ORCA
|
||||
wxString GetPageText(size_t n) const;
|
||||
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
|
||||
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
|
||||
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
|
||||
void SetOverflowButton(wxWindow* button);
|
||||
|
||||
private:
|
||||
wxFlexGridSizer* m_buttons_sizer;
|
||||
@@ -40,9 +47,10 @@ private:
|
||||
int m_btn_margin;
|
||||
int m_line_margin;
|
||||
std::vector<wxString> m_pageLabels; // ORCA
|
||||
wxWindow* m_overflow_button{nullptr}; // ORCA
|
||||
};
|
||||
|
||||
class Notebook: public wxBookCtrlBase
|
||||
class Notebook : public wxBookCtrlBase
|
||||
{
|
||||
public:
|
||||
Notebook(wxWindow * parent,
|
||||
@@ -103,7 +111,7 @@ public:
|
||||
// by this control) and show it immediately.
|
||||
bool ShowNewPage(wxWindow * page)
|
||||
{
|
||||
return AddPage(page, wxString(), "", "");
|
||||
return AddPage(page, wxString(), false, NO_IMAGE);
|
||||
}
|
||||
|
||||
|
||||
@@ -135,51 +143,56 @@ public:
|
||||
|
||||
// Implement base class pure virtual methods.
|
||||
|
||||
// adds a new page to the control
|
||||
bool AddPage(wxWindow* page,
|
||||
// Page management. Every insertion funnels through the InsertPage() below; `id` is the
|
||||
// stable page name FindPageByName() resolves. Built-in tabs name a resource bitmap,
|
||||
// plugin pages hand over a ready wxBitmap; wx's own imageId overloads carry neither.
|
||||
bool AddPage(const wxString& id,
|
||||
wxWindow* page,
|
||||
const wxString& text,
|
||||
const std::string& bmp_name,
|
||||
const std::string& inactive_bmp_name,
|
||||
const std::string& bmp_name = "",
|
||||
bool bSelect = false)
|
||||
{
|
||||
DoInvalidateBestSize();
|
||||
return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect);
|
||||
return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect);
|
||||
}
|
||||
|
||||
// Page management
|
||||
virtual bool InsertPage(size_t n,
|
||||
wxWindow * page,
|
||||
const wxString & text,
|
||||
bool bSelect = false,
|
||||
int imageId = NO_IMAGE) override
|
||||
bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override
|
||||
{
|
||||
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId))
|
||||
DoInvalidateBestSize();
|
||||
return InsertPage(GetPageCount(), page, text, bSelect, imageId);
|
||||
}
|
||||
|
||||
bool InsertPage(size_t n,
|
||||
const wxString& id,
|
||||
wxWindow * page,
|
||||
const wxString & text,
|
||||
const std::string& bmp_name = "",
|
||||
bool bSelect = false,
|
||||
const wxBitmap& bmp = wxNullBitmap)
|
||||
{
|
||||
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
|
||||
return false;
|
||||
|
||||
GetBtnsListCtrl()->InsertPage(n, text, bSelect);
|
||||
m_pageNames.insert(m_pageNames.begin() + n, id);
|
||||
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, bmp);
|
||||
|
||||
// wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the new
|
||||
// page to the current page's rect — it never touches visibility, and a freshly
|
||||
// constructed page defaults to shown. Without this it renders on top of whatever
|
||||
// page is currently selected until the next SetSelection() call hides it.
|
||||
if (!DoSetSelectionAfterInsertion(n, bSelect))
|
||||
page->Hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InsertPage(size_t n,
|
||||
wxWindow * page,
|
||||
const wxString & text,
|
||||
const std::string& bmp_name = "",
|
||||
const std::string& inactive_bmp_name = "",
|
||||
bool bSelect = false)
|
||||
virtual bool InsertPage(size_t n,
|
||||
wxWindow * page,
|
||||
const wxString & text,
|
||||
bool bSelect = false,
|
||||
int WXUNUSED(imageId) = NO_IMAGE) override
|
||||
{
|
||||
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
|
||||
return false;
|
||||
|
||||
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name);
|
||||
|
||||
if (bSelect)
|
||||
SetSelection(n);
|
||||
|
||||
return true;
|
||||
return InsertPage(n, wxString(), page, text, "", bSelect);
|
||||
}
|
||||
|
||||
virtual int SetSelection(size_t n) override
|
||||
@@ -211,8 +224,8 @@ public:
|
||||
return DoSetSelection(n);
|
||||
}
|
||||
|
||||
// Neither labels nor images are supported but we still store the labels
|
||||
// just in case the user code attaches some importance to them.
|
||||
// Labels are stored by the custom button list; wx's image-list API is unused — tab icons
|
||||
// are set directly on the buttons, either from a resource name or a ready wxBitmap.
|
||||
virtual bool SetPageText(size_t n, const wxString & strText) override
|
||||
{
|
||||
wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page"));
|
||||
@@ -251,7 +264,64 @@ public:
|
||||
page->SetFocus();
|
||||
}
|
||||
|
||||
// The base clears its page list directly instead of calling DoRemovePage() per page,
|
||||
// which would leave m_pageNames behind. No caller today; kept in sync regardless.
|
||||
virtual bool DeleteAllPages() override
|
||||
{
|
||||
m_pageNames.clear();
|
||||
return wxBookCtrlBase::DeleteAllPages();
|
||||
}
|
||||
|
||||
ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast<ButtonsListCtrl*>(m_bookctrl); }
|
||||
void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); }
|
||||
|
||||
// Insertion index just past the first of `ids` that is present, or the end of the bar
|
||||
// if none is — lets call sites state tab order as "after X" instead of re-deriving it.
|
||||
size_t PositionAfter(std::initializer_list<const char*> ids) const
|
||||
{
|
||||
for (const char* id : ids)
|
||||
if (const int idx = FindPageByName(id); idx != wxNOT_FOUND)
|
||||
return static_cast<size_t>(idx) + 1;
|
||||
return GetPageCount();
|
||||
}
|
||||
|
||||
int FindPageByName(const wxString& id) const
|
||||
{
|
||||
if (id.empty())
|
||||
return wxNOT_FOUND;
|
||||
for (size_t i = 0; i < m_pageNames.size(); ++i)
|
||||
if (m_pageNames[i] == id)
|
||||
return static_cast<int>(i);
|
||||
return wxNOT_FOUND;
|
||||
}
|
||||
|
||||
wxWindow* GetPageByName(const wxString& id) const
|
||||
{
|
||||
const int idx = FindPageByName(id);
|
||||
return idx == wxNOT_FOUND ? nullptr : GetPage(static_cast<size_t>(idx));
|
||||
}
|
||||
|
||||
bool SelectPageByName(const wxString& id)
|
||||
{
|
||||
const int idx = FindPageByName(id);
|
||||
if (idx == wxNOT_FOUND)
|
||||
return false;
|
||||
SetSelection(static_cast<size_t>(idx));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inverse of FindPageByName: index -> id. Empty string for an out-of-range
|
||||
// index or a page that was never given an id (e.g. settings Tab pages).
|
||||
wxString GetPageName(size_t n) const
|
||||
{
|
||||
return n < m_pageNames.size() ? m_pageNames[n] : wxString();
|
||||
}
|
||||
|
||||
wxString GetSelectedPageName() const
|
||||
{
|
||||
const int sel = GetSelection();
|
||||
return sel < 0 ? wxString() : GetPageName(static_cast<size_t>(sel));
|
||||
}
|
||||
|
||||
void UpdateMode()
|
||||
{
|
||||
@@ -369,6 +439,7 @@ protected:
|
||||
wxWindow* const win = wxBookCtrlBase::DoRemovePage(page);
|
||||
if (win)
|
||||
{
|
||||
m_pageNames.erase(m_pageNames.begin() + page);
|
||||
GetBtnsListCtrl()->RemovePage(page);
|
||||
DoSetSelectionAfterRemoval(page);
|
||||
}
|
||||
@@ -394,6 +465,8 @@ protected:
|
||||
private:
|
||||
void Init();
|
||||
|
||||
std::vector<wxString> m_pageNames; // index-parallel to wxBookCtrlBase::m_pages
|
||||
|
||||
wxShowEffect m_showEffect,
|
||||
m_hideEffect;
|
||||
|
||||
|
||||
@@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
|
||||
wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L"");
|
||||
}
|
||||
else {
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
|
||||
wxGetApp().sidebar().jump_to_option(opt, opt_type, L"");
|
||||
}
|
||||
else {
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -2015,7 +2015,7 @@ void NotificationManager::push_slicing_error_notification(const std::string &tex
|
||||
if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
|
||||
}
|
||||
if (!ovs.empty()) {
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
wxGetApp().obj_list()->select_items(ovs);
|
||||
}
|
||||
return false;
|
||||
@@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t
|
||||
auto& objects = wxGetApp().model().objects;
|
||||
auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; });
|
||||
if (iter != objects.end()) {
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
wxGetApp().obj_list()->select_items({ {*iter, nullptr} });
|
||||
}
|
||||
return false;
|
||||
@@ -2693,7 +2693,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s
|
||||
if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
|
||||
}
|
||||
if (!ovs.empty()) {
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
wxGetApp().obj_list()->select_items(ovs);
|
||||
wxGetApp().obj_list()->update_selections_on_canvas();
|
||||
}
|
||||
@@ -2777,7 +2777,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s
|
||||
}
|
||||
}
|
||||
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
|
||||
if (!sel_items.empty()) {
|
||||
obj_list->select_items(sel_items);
|
||||
|
||||
@@ -162,6 +162,8 @@ enum class NotificationType
|
||||
//BBL: plugin install hint
|
||||
BBLPluginInstallHint,
|
||||
BBLFlushingVolumeZero,
|
||||
// A mixed-color filament references a deleted component, or its components disagree in type.
|
||||
BBLMixedFilamentBroken,
|
||||
BBLPluginUpdateAvailable,
|
||||
BBLPreviewOnlyMode,
|
||||
BBLPrinterConfigUpdateAvailable,
|
||||
@@ -172,6 +174,8 @@ enum class NotificationType
|
||||
BBLBedFilamentIncompatible,
|
||||
BBLMixUsePLAAndPETG,
|
||||
BBLNozzleFilamentIncompatible,
|
||||
// A mixed-color filament is printed on a single-nozzle printer (frequent changes and purging).
|
||||
BBLSingleExtruderMixedFilamentRisk,
|
||||
OrcaSharedProfilesAvailable,
|
||||
OrcaCloudAPIError,
|
||||
OrcaSyncConflict,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include <future>
|
||||
#include <glad/gl.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -1675,6 +1676,25 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
|
||||
std::sort(plate_extruders.begin(), plate_extruders.end());
|
||||
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
|
||||
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
|
||||
|
||||
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
|
||||
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
|
||||
// physical filaments it resolves to instead.
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
std::vector<unsigned int> ext_0based;
|
||||
for (int e : plate_extruders)
|
||||
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
|
||||
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
|
||||
plate_extruders.clear();
|
||||
for (unsigned int e : expanded)
|
||||
plate_extruders.push_back((int)(e + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return plate_extruders;
|
||||
}
|
||||
|
||||
@@ -1836,6 +1856,24 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
|
||||
std::sort(plate_extruders.begin(), plate_extruders.end());
|
||||
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
|
||||
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
|
||||
|
||||
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
|
||||
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
|
||||
// physical filaments it resolves to instead.
|
||||
{
|
||||
auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
std::vector<unsigned int> ext_0based;
|
||||
for (int e : plate_extruders)
|
||||
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
|
||||
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
|
||||
plate_extruders.clear();
|
||||
for (unsigned int e : expanded)
|
||||
plate_extruders.push_back((int)(e + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return plate_extruders;
|
||||
}
|
||||
|
||||
@@ -1889,6 +1927,25 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc
|
||||
std::sort(plate_extruders.begin(), plate_extruders.end());
|
||||
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
|
||||
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
|
||||
|
||||
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
|
||||
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
|
||||
// physical filaments it resolves to instead.
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
std::vector<unsigned int> ext_0based;
|
||||
for (int e : plate_extruders)
|
||||
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
|
||||
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
|
||||
plate_extruders.clear();
|
||||
for (unsigned int e : expanded)
|
||||
plate_extruders.push_back((int)(e + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return plate_extruders;
|
||||
}
|
||||
|
||||
@@ -1990,6 +2047,50 @@ bool PartPlate::check_tpu_printable_status(const DynamicPrintConfig & config, co
|
||||
return true;
|
||||
}
|
||||
|
||||
// A mixed-color filament alternates between its components constantly. On a single-nozzle
|
||||
// printer every one of those switches is a full filament change plus a purge, so warn before
|
||||
// slicing. Multi-nozzle printers keep the components loaded at once and are not affected.
|
||||
bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const
|
||||
{
|
||||
warning_text.clear();
|
||||
|
||||
auto *nozzle_diameter_opt = config.option<ConfigOptionFloatsNullable>("nozzle_diameter");
|
||||
if (!nozzle_diameter_opt || nozzle_diameter_opt->values.size() > 1)
|
||||
return false;
|
||||
|
||||
auto *is_mixed_opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
if (!is_mixed_opt || !has_any_mixed_filament(is_mixed_opt->values))
|
||||
return false;
|
||||
|
||||
auto is_mixed_slot = [&](int extruder_1based) {
|
||||
size_t idx = (size_t)(extruder_1based - 1);
|
||||
return idx < is_mixed_opt->values.size() && is_mixed_opt->values[idx];
|
||||
};
|
||||
|
||||
const std::string mixed_warn_msg = _u8L("Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, "
|
||||
"which may significantly increase waste and the risk of nozzle / waste-chute clogging.");
|
||||
|
||||
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) {
|
||||
if (!contain_instance_totally(obj_idx, 0))
|
||||
continue;
|
||||
ModelObject *mo = m_model->objects[obj_idx];
|
||||
int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1;
|
||||
if (is_mixed_slot(obj_ext)) {
|
||||
warning_text = mixed_warn_msg;
|
||||
return true;
|
||||
}
|
||||
for (ModelVolume *mv : mo->volumes) {
|
||||
int vol_ext = mv->config.has("extruder") ? mv->config.extruder() : obj_ext;
|
||||
if (is_mixed_slot(vol_ext)) {
|
||||
warning_text = mixed_warn_msg;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config)
|
||||
{
|
||||
bool has_pla = false;
|
||||
@@ -4445,8 +4546,6 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
|
||||
//this may be happened after machine changed
|
||||
void PartPlateList::reset_size(int width, int depth, int height, bool reload_objects, bool update_shapes)
|
||||
{
|
||||
Vec3d origin1, origin2;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":before size: plate_width %1%, plate_depth %2%, plate_height %3%") % m_plate_width % m_plate_depth % m_plate_height;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":after size: plate_width %1%, plate_depth %2%, plate_height %3%") % width % depth % height;
|
||||
if ((m_plate_width != width) || (m_plate_depth != depth) || (m_plate_height != height))
|
||||
@@ -6386,6 +6485,31 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
|
||||
}
|
||||
//parse filament info
|
||||
plate_data_item->parse_filament_info(m_plate_list[i]->get_slice_result());
|
||||
|
||||
// Record mixed (virtual) filaments actually used on this plate.
|
||||
// Source is ToolOrdering::used_mixed_filaments (slots that appeared in
|
||||
// layer tools before resolve), persisted on GCodeProcessorResult / Print —
|
||||
// not print->extruders() which only reflects assignment.
|
||||
{
|
||||
std::vector<unsigned int> used_mixed;
|
||||
if (auto *slice_result = m_plate_list[i]->get_slice_result())
|
||||
used_mixed = slice_result->used_mixed_filaments;
|
||||
if (used_mixed.empty() && print)
|
||||
used_mixed = print->get_slice_used_mixed_filaments();
|
||||
if (!used_mixed.empty() && print) {
|
||||
const auto &fila_types = print->config().filament_type.values;
|
||||
const auto &fila_colors = print->config().filament_colour.values;
|
||||
const auto &fila_comps = print->config().filament_mixed_components.values;
|
||||
for (unsigned int fid : used_mixed) {
|
||||
PlateMixedFilamentInfo mixed_info;
|
||||
mixed_info.id = (int) fid + 1;
|
||||
if (fid < fila_types.size()) mixed_info.type = fila_types[fid];
|
||||
if (fid < fila_colors.size()) mixed_info.color = fila_colors[fid];
|
||||
if (fid < fila_comps.size()) mixed_info.components = fila_comps[fid];
|
||||
plate_data_item->mixed_filaments_info.push_back(mixed_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "slice result = " << m_plate_list[i]->get_slice_result()
|
||||
<< ", result valid = " << m_plate_list[i]->is_slice_result_valid();
|
||||
@@ -6452,6 +6576,13 @@ int PartPlateList::load_from_3mf_structure(PlateDataPtrs& plate_data_list, int f
|
||||
m_plate_list[index]->slice_filaments_info = plate_data_list[i]->slice_filaments_info;
|
||||
gcode_result->warnings = plate_data_list[i]->warnings;
|
||||
gcode_result->filament_maps = plate_data_list[i]->filament_maps;
|
||||
gcode_result->used_mixed_filaments.clear();
|
||||
for (const auto &mixed_info : plate_data_list[i]->mixed_filaments_info) {
|
||||
if (mixed_info.id > 0)
|
||||
gcode_result->used_mixed_filaments.push_back(static_cast<unsigned int>(mixed_info.id - 1));
|
||||
}
|
||||
if (Print *print = dynamic_cast<Print*>(fff_print))
|
||||
print->set_slice_used_mixed_filaments(gcode_result->used_mixed_filaments);
|
||||
|
||||
// Reconstruct the device-side nozzle grouping from the loaded 3mf so
|
||||
// the monitor/preview can map filaments to physical nozzles.
|
||||
|
||||
@@ -354,6 +354,9 @@ public:
|
||||
bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message);
|
||||
bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector<int> &tpu_filaments);
|
||||
bool check_mixture_of_pla_and_petg(const DynamicPrintConfig & config);
|
||||
// Warns when a mixed-color filament is used on a single-nozzle printer, where every
|
||||
// component switch costs a full filament change and purge.
|
||||
bool check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const;
|
||||
bool check_mixture_filament_compatible(const DynamicPrintConfig& config, std::string &error_msg);
|
||||
bool check_compatible_of_nozzle_and_filament(const DynamicPrintConfig & config, const std::vector<std::string>& filament_presets, std::string& error_msg);
|
||||
|
||||
|
||||
@@ -472,6 +472,31 @@ PlateSettingsDialog::PlateSettingsDialog(wxWindow* parent, const wxString& title
|
||||
m_sizer_main->AddSpacer(FromDIP(5));
|
||||
m_sizer_main->Add(m_other_layers_seq_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30));
|
||||
|
||||
// A mixed-color slot resolves to a different physical filament per layer, so a user-defined
|
||||
// filament order cannot be honoured; grey out the choice and explain that in the dialog.
|
||||
{
|
||||
auto &proj_cfg = wxGetApp().preset_bundle->project_config;
|
||||
auto *is_mixed_opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
if (is_mixed_opt && Slic3r::has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
m_first_layer_print_seq_choice->Enable(false);
|
||||
m_other_layers_seq_panel->enable_seq_choice(false);
|
||||
|
||||
auto *warn_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto *warn_icon = new wxStaticBitmap(this, wxID_ANY, create_scaled_bitmap("warning", this, 16),
|
||||
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
|
||||
auto *warn_text = new wxStaticText(this, wxID_ANY,
|
||||
_L("The filament list contains mixed filaments. Custom filament sequence will not take effect."));
|
||||
warn_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00")));
|
||||
warn_text->SetFont(Label::Body_12);
|
||||
warn_text->Wrap(FromDIP(300));
|
||||
|
||||
warn_sizer->Add(warn_icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
warn_sizer->Add(warn_text, 1, wxALIGN_CENTER_VERTICAL, 0);
|
||||
m_sizer_main->AddSpacer(FromDIP(5));
|
||||
m_sizer_main->Add(warn_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(30));
|
||||
}
|
||||
}
|
||||
|
||||
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
|
||||
|
||||
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](auto& e) {
|
||||
|
||||
@@ -62,6 +62,9 @@ public:
|
||||
int get_layers_print_seq_choice() { return m_other_layer_print_seq_choice->GetSelection(); };
|
||||
|
||||
std::vector<LayerSeqInfo> get_layers_print_seq_infos() { return m_layer_seq_infos; }
|
||||
// Lets callers grey out the sequence choice (e.g. when a mixed filament makes a
|
||||
// user-defined filament order impossible).
|
||||
void enable_seq_choice(bool enable) { m_other_layer_print_seq_choice->Enable(enable); }
|
||||
|
||||
protected:
|
||||
void append_layer(const LayerSeqInfo* layer_info = nullptr);
|
||||
|
||||
+2161
-107
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
#include <vector>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <wx/colour.h>
|
||||
#include <wx/panel.h>
|
||||
// BBS
|
||||
#include <wx/notebook.h>
|
||||
@@ -86,6 +87,10 @@ using t_optgroups = std::vector <std::shared_ptr<ConfigOptionsGroup>>;
|
||||
class Plater;
|
||||
enum class ActionButtonType : int;
|
||||
|
||||
// Sentinel filament id meaning "use the slot the sidebar context menu was opened on"
|
||||
// (Sidebar::priv::m_menu_filament_id) rather than an explicit index.
|
||||
inline constexpr int kSidebarContextMenuFilamentId = -2;
|
||||
|
||||
#define EVT_PUBLISHING_START 1
|
||||
#define EVT_PUBLISHING_STOP 2
|
||||
|
||||
@@ -188,7 +193,7 @@ public:
|
||||
void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default
|
||||
void change_filament(size_t from_id, size_t to_id); // 0 base
|
||||
void edit_filament();
|
||||
void add_custom_filament(wxColour new_col);
|
||||
void add_custom_filament(wxColour new_col, const std::string& preset_name = std::string(), bool skip_preset_validation = false);
|
||||
bool is_new_project_in_gcode3mf();
|
||||
// BBS
|
||||
void on_bed_type_change(BedType bed_type);
|
||||
@@ -262,6 +267,20 @@ public:
|
||||
std::vector<PlaterPresetComboBox*>& combos_filament();
|
||||
void clear_combos_filament_badge();
|
||||
void udpate_combos_filament_badge();
|
||||
|
||||
// Mixed-color filament sidebar section
|
||||
void add_mixed_filament();
|
||||
void edit_mixed_filament(size_t idx);
|
||||
void delete_mixed_filament_at(size_t idx);
|
||||
void decompose_filament_color(int filament_idx);
|
||||
void recalc_filament_scroll_sizes();
|
||||
void update_mixed_filament_list();
|
||||
bool has_broken_mixed_filament() const;
|
||||
bool has_broken_mixed_filament(const PartPlate* plate) const;
|
||||
void collect_physical_filament_info(std::vector<std::string>& color_strs,
|
||||
std::vector<std::string>& names,
|
||||
std::vector<std::string>& types,
|
||||
std::vector<size_t>* config_indices = nullptr);
|
||||
Search::OptionsSearcher& get_searcher();
|
||||
std::string& get_search_line();
|
||||
void update_printer_thumbnail();
|
||||
@@ -290,7 +309,7 @@ public:
|
||||
Plater(const Plater &) = delete;
|
||||
Plater &operator=(Plater &&) = delete;
|
||||
Plater &operator=(const Plater &) = delete;
|
||||
~Plater() = default;
|
||||
~Plater();
|
||||
|
||||
bool Show(bool show = true);
|
||||
|
||||
@@ -313,6 +332,11 @@ public:
|
||||
const SLAPrint& sla_print() const;
|
||||
SLAPrint& sla_print();
|
||||
|
||||
// Helper: returns config indices where filament_is_mixed == true
|
||||
std::vector<size_t> mixed_filament_config_indices() const;
|
||||
// Helper: returns config indices where filament_is_mixed == false
|
||||
std::vector<size_t> physical_filament_config_indices() const;
|
||||
|
||||
int new_project(bool skip_confirm = false, bool silent = false, const wxString& project_name = wxString());
|
||||
// BBS: save & backup
|
||||
void load_project(wxString const & filename = "", wxString const & originfile = "-");
|
||||
@@ -568,7 +592,7 @@ public:
|
||||
|
||||
void on_filament_change(size_t filament_idx);
|
||||
void on_filament_count_change(size_t extruders_count);
|
||||
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1);
|
||||
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1, const std::vector<unsigned char>& is_mixed_before_delete = {});
|
||||
std::vector<Slic3r::ColorRGBA> get_extruders_colors();
|
||||
// BBS
|
||||
void on_bed_type_change(BedType bed_type);
|
||||
@@ -583,6 +607,12 @@ public:
|
||||
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const;
|
||||
std::vector<std::string> get_filament_colors_render_info() const;
|
||||
std::vector<std::string> get_filament_color_render_type() const;
|
||||
|
||||
// Per slot, the colours a gradient mixed filament actually prints, sampled bottom (index 0)
|
||||
// to top, so the sidebar, the paint gizmo and the extruder icons draw the same fade the
|
||||
// editor previews rather than a straight blend of two endpoints. A slot that is not a
|
||||
// gradient mixed filament gets an empty ramp. Cached; recomputed when the config changes.
|
||||
const std::vector<std::vector<wxColour>>& get_filament_gradient_ramps() const;
|
||||
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
|
||||
|
||||
void set_global_filament_map_mode(FilamentMapMode mode);
|
||||
@@ -1020,4 +1050,4 @@ wxArrayString get_all_camera_view_type();
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
|
||||
// plugin page looks native while any CSS the plugin ships still wins. Built on the
|
||||
// --orca-* variables the host injects (see WebViewHostDialog); document-start injected
|
||||
// AFTER the host contract so the variables are defined (shares the base injector's
|
||||
// WebView2 timing guard).
|
||||
std::string plugin_defaults_user_script()
|
||||
{
|
||||
std::string css;
|
||||
css += "<style id=\"orca-plugin-defaults\">";
|
||||
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
|
||||
"font-family:var(--orca-font);font-size:13px;}";
|
||||
css += "body{margin:0;}";
|
||||
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
|
||||
css += "a{color:var(--orca-accent);}";
|
||||
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
|
||||
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
|
||||
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
|
||||
css += "button:hover{filter:brightness(1.1);}";
|
||||
css += "button:disabled{opacity:.5;cursor:default;}";
|
||||
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
|
||||
"background:var(--orca-bg);border:1px solid var(--orca-border);"
|
||||
"border-radius:4px;padding:4px 8px;}";
|
||||
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
|
||||
css += "table{border-collapse:collapse;}";
|
||||
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
|
||||
css += "th{color:var(--orca-muted);font-weight:600;}";
|
||||
css += "::-webkit-scrollbar{width:12px;height:12px;}";
|
||||
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
|
||||
css += "::-webkit-scrollbar-track{background:transparent;}";
|
||||
css += "</style>";
|
||||
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
|
||||
}
|
||||
|
||||
// Injected into the top-level page at document start (before the plugin's own
|
||||
// scripts). Defines window.orca as the only host surface the page may use. It
|
||||
// references window.wx lazily (at call time) so it never races the backend's
|
||||
@@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||
void PluginWebDialog::add_user_scripts()
|
||||
{
|
||||
if (wxWebView* wv = browser()) {
|
||||
wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script()));
|
||||
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
||||
wv->AddUserScript(ORCA_BRIDGE_JS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public:
|
||||
Bind(wxEVT_LEFT_DOWN, &WikiLabel::OnLeftDown, this);
|
||||
}
|
||||
|
||||
void SetLabel(const wxString& label)
|
||||
void SetLabel(const wxString& label) override
|
||||
{
|
||||
m_label = label;
|
||||
m_last_wrap_width = -1; // force re-wrap
|
||||
@@ -1748,11 +1748,26 @@ void PreferencesDialog::create_items()
|
||||
g_sizer->Add(item_pop_up_filament_map_dialog);
|
||||
#endif
|
||||
|
||||
//// GENERAL > Plugins
|
||||
g_sizer->Add(create_item_title(_L("Plugins")), 1, wxEXPAND);
|
||||
|
||||
auto item_plugin_pages_visible_count = create_item_spinctrl(
|
||||
_L("Visible plugin pages"),
|
||||
"",
|
||||
_L("pages"),
|
||||
_L("Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab."),
|
||||
SETTING_PLUGIN_PAGES_VISIBLE_COUNT,
|
||||
PLUGIN_PAGES_VISIBLE_COUNT_MIN,
|
||||
PLUGIN_PAGES_VISIBLE_COUNT_MAX,
|
||||
[](int value) { wxGetApp().mainframe->plugin_pages().set_visible_page_count(value); }
|
||||
);
|
||||
g_sizer->Add(item_plugin_pages_visible_count);
|
||||
|
||||
g_sizer->AddSpacer(FromDIP(10));
|
||||
sizer_page->Add(g_sizer, 0, wxEXPAND);
|
||||
|
||||
//////////////////////////
|
||||
//// CONTROL TAB
|
||||
//// CONTROL TAB
|
||||
/////////////////////////////////////
|
||||
m_pref_tabs->AppendItem(_L("Control"));
|
||||
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
|
||||
|
||||
@@ -1042,7 +1042,7 @@ bool PlaterPresetComboBox::switch_to_tab()
|
||||
|
||||
//BBS Select NoteBook Tab params
|
||||
if (tab->GetParent() == wxGetApp().params_panel())
|
||||
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
|
||||
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
|
||||
else {
|
||||
wxGetApp().params_dialog()->Popup();
|
||||
tab->OnActivate();
|
||||
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr);
|
||||
~PresetComboBox();
|
||||
|
||||
enum LabelItemType {
|
||||
enum LabelItemType : std::size_t {
|
||||
LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01,
|
||||
LABEL_ITEM_PRINTER_MODELS,
|
||||
LABEL_ITEM_DISABLED,
|
||||
|
||||
@@ -163,7 +163,7 @@ public:
|
||||
BedType bedType() const { return m_BedType; }
|
||||
|
||||
virtual void init() override;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const
|
||||
virtual std::map<std::string, std::string> extendedInfo() const override
|
||||
{
|
||||
return {{"bedType", std::to_string(static_cast<int>(m_BedType))},
|
||||
{"timeLapse", std::to_string(m_timeLapse)},
|
||||
@@ -200,7 +200,7 @@ public:
|
||||
PrintHost* printhost);
|
||||
|
||||
virtual void init() override;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const;
|
||||
virtual std::map<std::string, std::string> extendedInfo() const override;
|
||||
|
||||
private:
|
||||
static constexpr const char* CONFIG_KEY_ENABLESELFTEST = "crealityprint_enable_self_test";
|
||||
|
||||
+51
-10
@@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
|
||||
Fit();
|
||||
}
|
||||
|
||||
ProjectPanel::~ProjectPanel() {}
|
||||
ProjectPanel::~ProjectPanel()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void ProjectPanel::shutdown()
|
||||
{
|
||||
m_reload_cancel_token->store(true, std::memory_order_release);
|
||||
if (m_reload_task && m_reload_task->joinable())
|
||||
m_reload_task->join();
|
||||
m_reload_task.reset();
|
||||
}
|
||||
|
||||
// Helper to convert newlines to <br>
|
||||
static std::string convert_newlines_to_br(const std::string& text) {
|
||||
@@ -101,7 +112,17 @@ void ProjectPanel::onWebNavigating(wxWebViewEvent& evt)
|
||||
|
||||
void ProjectPanel::on_reload(wxCommandEvent& evt)
|
||||
{
|
||||
boost::thread reload = boost::thread([this] {
|
||||
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
|
||||
m_reload_cancel_token->load(std::memory_order_acquire))
|
||||
return;
|
||||
|
||||
if (m_reload_task && m_reload_task->joinable())
|
||||
m_reload_task->join();
|
||||
|
||||
const auto cancel_token = m_reload_cancel_token;
|
||||
m_reload_task = std::make_unique<boost::thread>([this, cancel_token] {
|
||||
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
||||
return;
|
||||
std::string update_type;
|
||||
std::string license;
|
||||
std::string model_name;
|
||||
@@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
|
||||
|
||||
std::map<std::string, std::vector<json>> files;
|
||||
|
||||
if (wxGetApp().plater() == nullptr)
|
||||
return;
|
||||
|
||||
Model model = wxGetApp().plater()->model();
|
||||
|
||||
auto model_info = model.model_info;
|
||||
@@ -156,7 +180,14 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
|
||||
std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str());
|
||||
if (!file_path.empty()) {
|
||||
files = Reload(file_path);
|
||||
wxGetApp().CallAfter([this, file_path, files] { m_auxiliary->Reload(file_path, files); });
|
||||
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
||||
return;
|
||||
|
||||
wxGetApp().CallAfter([this, cancel_token, file_path, files] {
|
||||
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
||||
return;
|
||||
m_auxiliary->Reload(file_path, files);
|
||||
});
|
||||
} else {
|
||||
clear_model_info();
|
||||
return;
|
||||
@@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
|
||||
|
||||
json m_Res = json::object();
|
||||
m_Res["command"] = "show_3mf_info";
|
||||
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++);
|
||||
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
|
||||
m_Res["model"] = j;
|
||||
|
||||
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
|
||||
|
||||
if (m_web_init_completed) {
|
||||
wxGetApp().CallAfter([this, strJS] {
|
||||
if (m_web_init_completed.load(std::memory_order_acquire) &&
|
||||
!cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
|
||||
wxGetApp().CallAfter([this, cancel_token, strJS] {
|
||||
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
||||
return;
|
||||
RunScript(strJS.ToStdString());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
||||
}
|
||||
}
|
||||
else if (strCmd == "request_3mf_info") {
|
||||
m_web_init_completed = true;
|
||||
m_web_init_completed.store(true, std::memory_order_release);
|
||||
}
|
||||
else if (strCmd == "edit_project_info") {
|
||||
show_info_editor(true);
|
||||
@@ -307,13 +341,20 @@ void ProjectPanel::update_model_data()
|
||||
|
||||
void ProjectPanel::clear_model_info()
|
||||
{
|
||||
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
|
||||
m_reload_cancel_token->load(std::memory_order_acquire))
|
||||
return;
|
||||
|
||||
json m_Res = json::object();
|
||||
m_Res["command"] = "clear_3mf_info";
|
||||
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++);
|
||||
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
|
||||
|
||||
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
|
||||
|
||||
wxGetApp().CallAfter([this, strJS] {
|
||||
const auto cancel_token = m_reload_cancel_token;
|
||||
wxGetApp().CallAfter([this, cancel_token, strJS] {
|
||||
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
||||
return;
|
||||
RunScript(strJS.ToStdString());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "slic3r/Utils/json_diff.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <boost/thread.hpp>
|
||||
#include "Event.hpp"
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
@@ -60,14 +62,17 @@ struct project_file{
|
||||
class ProjectPanel : public wxPanel
|
||||
{
|
||||
private:
|
||||
bool m_web_init_completed = {false};
|
||||
std::atomic<bool> m_web_init_completed{false};
|
||||
bool m_reload_already = {false};
|
||||
|
||||
std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)};
|
||||
std::unique_ptr<boost::thread> m_reload_task;
|
||||
|
||||
wxWebView* m_browser = {nullptr};
|
||||
AuxiliaryPanel* m_auxiliary{nullptr};
|
||||
wxString m_project_home_url;
|
||||
wxString m_root_dir;
|
||||
static inline int m_sequence_id = 8000;
|
||||
static inline std::atomic<int> m_sequence_id{8000};
|
||||
|
||||
void show_info_editor(bool show);
|
||||
|
||||
@@ -75,6 +80,7 @@ private:
|
||||
public:
|
||||
ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL);
|
||||
~ProjectPanel();
|
||||
void shutdown();
|
||||
|
||||
|
||||
void onWebNavigating(wxWebViewEvent& evt);
|
||||
|
||||
@@ -1991,7 +1991,7 @@ void InputIpAddressDialog::workerThreadFunc(std::string str_ip, std::string str_
|
||||
if (w.expired()) return;
|
||||
|
||||
if (m_obj) {
|
||||
m_obj->set_user_access_code(str_access_code);
|
||||
m_obj->set_access_code(str_access_code);
|
||||
wxGetApp().getDeviceManager()->set_selected_machine(m_obj->get_dev_id());
|
||||
}
|
||||
|
||||
@@ -2055,6 +2055,11 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
|
||||
{
|
||||
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
|
||||
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
|
||||
|
||||
if (str_access_code.empty()) {
|
||||
str_access_code = "88888888";
|
||||
}
|
||||
|
||||
auto str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both);
|
||||
auto str_sn = m_input_sn->GetTextCtrl()->GetValue().Strip(wxString::both);
|
||||
bool invalid_access_code = true;
|
||||
@@ -2062,7 +2067,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
|
||||
for (char c : str_access_code) {
|
||||
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
|
||||
invalid_access_code = false;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_Preview.hpp"
|
||||
@@ -1088,8 +1089,8 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res
|
||||
}
|
||||
}
|
||||
relayout_nozzle_cards();
|
||||
auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection();
|
||||
if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) {
|
||||
wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName();
|
||||
if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) {
|
||||
updata_thumbnail_data_after_connected_printer();
|
||||
}
|
||||
}
|
||||
@@ -2846,7 +2847,7 @@ void SelectMachineDialog::on_ok_btn(wxCommandEvent &event)
|
||||
});
|
||||
|
||||
// STUDIO-9580
|
||||
/* use warning color if there are warning and normal messages* /
|
||||
/* use warning color if there are warning and normal messages*/
|
||||
/* use indexes if there are several messages*/
|
||||
/* add header and ending if there are several messages or has none block warnings*/
|
||||
if (confirm_text.size() > 1 || !is_printing_block)
|
||||
@@ -3912,7 +3913,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager,
|
||||
};
|
||||
|
||||
// collect from user machine list
|
||||
const auto& user_machine_list = dev_manager->get_my_machine_list();// user machine list
|
||||
const auto& user_machine_list = dev_manager->get_my_machine_list(dev_manager->get_current_printer_agent_id());// user machine list
|
||||
for (const auto& elem : user_machine_list)
|
||||
{
|
||||
MachineObject* mobj = elem.second;
|
||||
@@ -5693,10 +5694,15 @@ void SelectMachineDialog::clone_thumbnail_data() {
|
||||
m_preview_colors_in_thumbnail.resize(m_materialList.size());
|
||||
}
|
||||
while (iter != m_materialList.end()) {
|
||||
int id = iter->first;
|
||||
Material * item = iter->second;
|
||||
MaterialItem *m = item->item;
|
||||
m_preview_colors_in_thumbnail[id] = m->m_material_coloul;
|
||||
// Orca: key the preview colours by filament slot, as m_cur_colors_in_thumbnail and
|
||||
// SyncAmsInfoDialog already do, so recompute_mixed_slot_colors() below can look a mixed
|
||||
// slot's component colours up by id (BBS keys this array by list position).
|
||||
if (item->id >= m_preview_colors_in_thumbnail.size()) {
|
||||
m_preview_colors_in_thumbnail.resize(item->id + 1);
|
||||
}
|
||||
m_preview_colors_in_thumbnail[item->id] = m->m_material_coloul;
|
||||
if (item->id < m_cur_colors_in_thumbnail.size()) {
|
||||
m_cur_colors_in_thumbnail[item->id] = m->m_ams_coloul;
|
||||
}
|
||||
@@ -5706,6 +5712,20 @@ void SelectMachineDialog::clone_thumbnail_data() {
|
||||
}
|
||||
iter++;
|
||||
}
|
||||
|
||||
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
size_t total = 0;
|
||||
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
|
||||
total = opt->values.size();
|
||||
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
|
||||
if (m_cur_colors_in_thumbnail.size() < target)
|
||||
m_cur_colors_in_thumbnail.resize(target);
|
||||
if (m_preview_colors_in_thumbnail.size() < target)
|
||||
m_preview_colors_in_thumbnail.resize(target);
|
||||
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
//copy data
|
||||
auto &data = m_cur_input_thumbnail_data;
|
||||
m_preview_thumbnail_data.reset();
|
||||
@@ -5880,6 +5900,10 @@ void SelectMachineDialog::change_default_normal(int old_filament_id, wxColour te
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Recompute mixed slot colors after physical slot color change
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
ThumbnailData& data = m_cur_input_thumbnail_data;
|
||||
ThumbnailData& no_light_data = m_cur_no_light_thumbnail_data;
|
||||
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {
|
||||
|
||||
@@ -522,7 +522,7 @@ public:
|
||||
bool is_timeout();
|
||||
int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path);
|
||||
void set_print_type(PrintFromType type) {m_print_type = type;};
|
||||
bool Show(bool show);
|
||||
bool Show(bool show) override;
|
||||
void show_init();
|
||||
bool do_ams_mapping(MachineObject *obj_,bool use_ams);
|
||||
bool get_ams_mapping_result(std::string& mapping_array_str, std::string& mapping_array_str2, std::string& ams_mapping_info) const;
|
||||
|
||||
@@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices()
|
||||
DeviceManager* dev = wxGetApp().getDeviceManager();
|
||||
if (!dev) return;
|
||||
m_free_machine_list = dev->get_local_machinelist();
|
||||
const std::string current_agent_id = dev->get_current_printer_agent_id();
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start";
|
||||
this->Freeze();
|
||||
@@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices()
|
||||
/* do not show printer bind state is empty */
|
||||
if (!mobj->is_avaliable()) continue;
|
||||
|
||||
/* do not show devices discovered/bound by a different printer agent */
|
||||
if (mobj->printer_agent_id != current_agent_id)
|
||||
continue;
|
||||
|
||||
if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer())
|
||||
continue;
|
||||
|
||||
@@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices()
|
||||
}
|
||||
|
||||
m_bind_machine_list.clear();
|
||||
m_bind_machine_list = dev->get_my_machine_list();
|
||||
m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
|
||||
|
||||
//sort list
|
||||
std::vector<std::pair<std::string, MachineObject*>> user_machine_list;
|
||||
@@ -704,7 +709,6 @@ void SelectMachinePopup::update_user_devices()
|
||||
}
|
||||
|
||||
mobj->set_access_code("");
|
||||
mobj->erase_user_access_code();
|
||||
}
|
||||
|
||||
if (GUI::wxGetApp().plater())
|
||||
|
||||
@@ -180,7 +180,7 @@ public:
|
||||
SendToPrinterDialog(Plater *plater = nullptr);
|
||||
~SendToPrinterDialog();
|
||||
|
||||
bool Show(bool show);
|
||||
bool Show(bool show) override;
|
||||
bool is_timeout();
|
||||
void on_rename_click(wxCommandEvent& event);
|
||||
void on_rename_enter();
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "DeviceCore/DevManager.h"
|
||||
#include "DeviceCore/DevMapping.h"
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
@@ -1218,8 +1219,8 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul
|
||||
iter++;
|
||||
}
|
||||
}
|
||||
auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection();
|
||||
if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) {
|
||||
wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName();
|
||||
if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) {
|
||||
updata_thumbnail_data_after_connected_printer();
|
||||
}
|
||||
}
|
||||
@@ -2575,6 +2576,10 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list()
|
||||
m_materialList.clear();
|
||||
m_filaments.clear();
|
||||
|
||||
// Mixed-color slots are virtual: they never occupy a tray, so they must not appear as
|
||||
// AMS sync targets.
|
||||
auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
|
||||
bool use_double_extruder = get_is_double_extruder();
|
||||
if (use_double_extruder) {
|
||||
const auto &project_config = preset_bundle->project_config;
|
||||
@@ -2592,6 +2597,8 @@ void SyncAmsInfoDialog::reset_and_sync_ams_list()
|
||||
auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]);
|
||||
if (extruder >= materials.size() || extruder < 0 || extruder >= display_materials.size())
|
||||
continue;
|
||||
if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder])
|
||||
continue;
|
||||
|
||||
if (contronal_index % SYNC_FLEX_GRID_COL == 0) {
|
||||
wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
@@ -2793,6 +2800,10 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list()
|
||||
m_fix_materialList.clear();
|
||||
m_fix_filaments.clear();
|
||||
|
||||
// Mixed-color slots are virtual: they never occupy a tray, so they must not appear as
|
||||
// AMS sync targets.
|
||||
auto* is_mixed_opt = preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
|
||||
bool use_double_extruder = get_is_double_extruder();
|
||||
if (use_double_extruder) {
|
||||
const auto &project_config = preset_bundle->project_config;
|
||||
@@ -2810,6 +2821,8 @@ void SyncAmsInfoDialog::generate_override_fix_ams_list()
|
||||
auto colour_rgb = wxColour((int) rgb[0], (int) rgb[1], (int) rgb[2], (int) rgb[3]);
|
||||
if (extruder >= extruders.size() || extruder < 0 || extruder >= m_ams_combo_info.ams_filament_colors.size())
|
||||
continue;
|
||||
if (is_mixed_opt && extruder < (int) is_mixed_opt->values.size() && is_mixed_opt->values[extruder])
|
||||
continue;
|
||||
|
||||
if (contronal_index % SYNC_FLEX_GRID_COL == 0) {
|
||||
wxBoxSizer *ams_tip_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
@@ -2931,6 +2944,20 @@ void SyncAmsInfoDialog::clone_thumbnail_data()
|
||||
iter++;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
size_t total = 0;
|
||||
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
|
||||
total = opt->values.size();
|
||||
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
|
||||
if (m_cur_colors_in_thumbnail.size() < target)
|
||||
m_cur_colors_in_thumbnail.resize(target);
|
||||
if (m_preview_colors_in_thumbnail.size() < target)
|
||||
m_preview_colors_in_thumbnail.resize(target);
|
||||
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
// copy data
|
||||
auto &data = m_cur_input_thumbnail_data;
|
||||
m_preview_thumbnail_data.reset();
|
||||
@@ -3119,6 +3146,10 @@ void SyncAmsInfoDialog::change_default_normal(int old_filament_id, wxColour temp
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Recompute mixed slot colors after physical slot color change
|
||||
const auto& cfg = wxGetApp().preset_bundle->project_config;
|
||||
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
|
||||
|
||||
ThumbnailData &data = m_cur_input_thumbnail_data;
|
||||
ThumbnailData &no_light_data = m_cur_no_light_thumbnail_data;
|
||||
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {
|
||||
|
||||
@@ -371,7 +371,7 @@ public:
|
||||
};
|
||||
FinishSyncAmsDialog(InputInfo &input_info);
|
||||
~FinishSyncAmsDialog() override;
|
||||
void deal_ok();
|
||||
void deal_ok() override;
|
||||
void update_info(InputInfo& info);
|
||||
bool Layout() override;
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#ifdef _WIN32
|
||||
// The standard Windows includes.
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <psapi.h>
|
||||
#endif /* _WIN32 */
|
||||
|
||||
+43
-15
@@ -4,6 +4,7 @@
|
||||
#include "PresetHints.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
@@ -2173,18 +2174,25 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
|
||||
//Orca: sync filament num if it's a multi tool printer
|
||||
if (opt_key == "extruders_count" && !m_config->opt_bool("single_extruder_multi_material")){
|
||||
auto num_extruder = boost::any_cast<size_t>(value);
|
||||
int old_filament_size = wxGetApp().preset_bundle->filament_presets.size();
|
||||
std::vector<std::string> new_colors;
|
||||
for (int i = old_filament_size; i < num_extruder; ++i) {
|
||||
wxColour new_col = Plater::get_next_color_for_filament();
|
||||
std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
new_colors.push_back(new_color);
|
||||
const size_t num_extruder = boost::any_cast<size_t>(value);
|
||||
auto *bundle = wxGetApp().preset_bundle;
|
||||
Sidebar &sidebar = wxGetApp().plater()->sidebar();
|
||||
// A tool changer feeds filament N from nozzle N, so the extruder count sizes the physical
|
||||
// run only; mixed slots are virtual and keep the tail. Go one slot at a time through the
|
||||
// sidebar's own +/- calls: they insert ahead of the mixed tail and renumber filament ids,
|
||||
// painted facets, custom g-code and mixed components, which a bulk resize clamps away.
|
||||
// Both also refresh the print tab and export the selections, so nothing to do afterwards.
|
||||
size_t physical = bundle->num_physical_filaments();
|
||||
while (physical != num_extruder) {
|
||||
if (physical < num_extruder)
|
||||
sidebar.add_custom_filament(Plater::get_next_color_for_filament());
|
||||
else
|
||||
sidebar.delete_filament(physical - 1); // physical > num_extruder >= 1
|
||||
const size_t updated = bundle->num_physical_filaments();
|
||||
if (updated == physical)
|
||||
break; // the call declined, e.g. the total slot limit - do not spin
|
||||
physical = updated;
|
||||
}
|
||||
wxGetApp().preset_bundle->set_num_filaments(num_extruder, new_colors);
|
||||
wxGetApp().plater()->on_filament_count_change(num_extruder);
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINT)->update();
|
||||
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
|
||||
}
|
||||
|
||||
//Orca: disable purge_in_prime_tower if single_extruder_multi_material is disabled
|
||||
@@ -2629,6 +2637,7 @@ void TabPrint::build()
|
||||
auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height");
|
||||
optgroup->append_single_option_line("layer_height","quality_settings_layer_height");
|
||||
optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height");
|
||||
optgroup->append_single_option_line("enable_mixed_color_sublayer");
|
||||
|
||||
optgroup = page->new_optgroup(L("Line width"), L"param_line_width");
|
||||
optgroup->append_single_option_line("line_width","quality_settings_line_width");
|
||||
@@ -2790,7 +2799,7 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
|
||||
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
|
||||
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
|
||||
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor");
|
||||
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
|
||||
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
|
||||
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
|
||||
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
|
||||
@@ -3497,6 +3506,21 @@ void TabPrintModel::activate_selected_page(std::function<void()> throw_if_cancel
|
||||
f->set_value(boost::any(), false);
|
||||
}
|
||||
}
|
||||
if (m_type == Preset::TYPE_PLATE)
|
||||
static_cast<TabPrintPlate *>(this)->update_mixed_filament_seq_state();
|
||||
}
|
||||
|
||||
// A mixed-color slot resolves to a different physical filament per layer, so a
|
||||
// user-defined filament print order cannot be honoured while one exists.
|
||||
void TabPrintPlate::update_mixed_filament_seq_state()
|
||||
{
|
||||
if (!m_active_page) return;
|
||||
auto &proj_cfg = m_preset_bundle->project_config;
|
||||
auto *opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
bool has_mixed = opt && has_any_mixed_filament(opt->values);
|
||||
|
||||
toggle_option("first_layer_sequence_choice", !has_mixed);
|
||||
toggle_option("other_layers_sequence_choice", !has_mixed);
|
||||
}
|
||||
|
||||
void TabPrintModel::on_value_change(const std::string& opt_id, const boost::any& value)
|
||||
@@ -6458,7 +6482,7 @@ void Tab::load_current_preset()
|
||||
std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" :
|
||||
tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog";
|
||||
tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab
|
||||
dynamic_cast<Notebook*>(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name);
|
||||
dynamic_cast<Notebook*>(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), wxString(), tab, tab->title(), bmp_name);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
@@ -8537,8 +8561,12 @@ void Page::activate(ConfigOptionMode mode, std::function<void()> throw_if_cancel
|
||||
|
||||
#ifdef __WXMSW__
|
||||
// BBS: fix field control position
|
||||
wxTheApp->CallAfter([this]() {
|
||||
for (auto group : m_optgroups) {
|
||||
wxTheApp->CallAfter([wp = std::weak_ptr<Page>(shared_from_this())]() {
|
||||
auto page = wp.lock();
|
||||
if (!page)
|
||||
return;
|
||||
|
||||
for (auto group : page->m_optgroups) {
|
||||
if (group->custom_ctrl)
|
||||
group->custom_ctrl->fixup_items_positions();
|
||||
}
|
||||
|
||||
@@ -515,13 +515,13 @@ public:
|
||||
bool has_key(std::string const &key);
|
||||
|
||||
protected:
|
||||
virtual void activate_selected_page(std::function<void()> throw_if_canceled);
|
||||
virtual void activate_selected_page(std::function<void()> throw_if_canceled) override;
|
||||
|
||||
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
|
||||
|
||||
virtual void notify_changed(ObjectBase * object) = 0;
|
||||
|
||||
virtual void reload_config();
|
||||
virtual void reload_config() override;
|
||||
|
||||
virtual void update_custom_dirty(std::vector<std::string> &dirty_options, std::vector<std::string> &nonsys_options) override;
|
||||
|
||||
@@ -545,6 +545,8 @@ public:
|
||||
void build() override;
|
||||
void reset_model_config() override;
|
||||
int show_spiral_mode_settings_dialog(bool is_object_config) { return m_config_manipulation.show_spiral_mode_settings_dialog(is_object_config); }
|
||||
// Disables the user-defined filament print order while a mixed-color filament exists.
|
||||
void update_mixed_filament_seq_state();
|
||||
|
||||
protected:
|
||||
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
|
||||
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
|
||||
void SetBitmap(ScalableBitmap &bitmap);
|
||||
|
||||
bool Enable(bool enable = true);
|
||||
bool Enable(bool enable = true) override;
|
||||
|
||||
void Rescale();
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ public:
|
||||
TabButton* pageButton;
|
||||
|
||||
private:
|
||||
wxWindow* m_parent;
|
||||
wxFlexGridSizer* m_buttons_sizer;
|
||||
wxBoxSizer* m_sizer;
|
||||
ScalableBitmap m_arrow_img;
|
||||
@@ -108,7 +107,7 @@ public:
|
||||
// by this control) and show it immediately.
|
||||
bool ShowNewPage(wxWindow * page)
|
||||
{
|
||||
return AddPage(page, wxString(), ""/*true *//* select it */);
|
||||
return AddPage(page, wxString());
|
||||
}
|
||||
|
||||
// Set effect to use for showing/hiding pages.
|
||||
@@ -139,14 +138,13 @@ public:
|
||||
|
||||
// Implement base class pure virtual methods.
|
||||
|
||||
// adds a new page to the control
|
||||
bool AddPage(wxWindow* page,
|
||||
const wxString& text,
|
||||
const std::string& bmp_name,
|
||||
bool bSelect = false)
|
||||
bool bSelect = false,
|
||||
int imageId = NO_IMAGE) override
|
||||
{
|
||||
DoInvalidateBestSize();
|
||||
return InsertNewPage(GetPageCount(), page, text, bmp_name, bSelect);
|
||||
return InsertPage(GetPageCount(), page, text, bSelect, imageId);
|
||||
}
|
||||
|
||||
//// Page management
|
||||
@@ -167,24 +165,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InsertNewPage(size_t n,
|
||||
wxWindow * page,
|
||||
const wxString & text,
|
||||
const std::string& bmp_name = "",
|
||||
bool bSelect = false)
|
||||
{
|
||||
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
|
||||
return false;
|
||||
|
||||
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name);
|
||||
|
||||
if (bSelect)
|
||||
SetSelection(n);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemovePage(size_t n)
|
||||
bool RemovePage(size_t n) override
|
||||
{
|
||||
if (!wxBookCtrlBase::RemovePage(n))
|
||||
return false;
|
||||
@@ -418,8 +399,6 @@ private:
|
||||
unsigned m_showTimeout,
|
||||
m_hideTimeout;
|
||||
|
||||
TabButtonsListCtrl *m_ctrl{nullptr};
|
||||
|
||||
};
|
||||
//#endif // _WIN32
|
||||
#endif // slic3r_Tabbook_hpp_
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,407 @@
|
||||
#pragma once
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
#include "libslic3r/TexturePainting.hpp"
|
||||
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include "Widgets/PopupWindow.hpp"
|
||||
#include <wx/panel.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include "Widgets/SpinInput.hpp"
|
||||
#include <wx/checkbox.h>
|
||||
#include <wx/button.h>
|
||||
#include "Widgets/Button.hpp"
|
||||
#include <wx/glcanvas.h>
|
||||
#include <wx/event.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
class AccentSlider;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent);
|
||||
|
||||
enum class TextureImportState {
|
||||
Idle,
|
||||
Computing,
|
||||
Ready,
|
||||
Error
|
||||
};
|
||||
|
||||
enum class TextureAutoMixMode {
|
||||
CMYW,
|
||||
RYBW
|
||||
};
|
||||
|
||||
enum class TextureFilamentKind {
|
||||
ExistingPhysical,
|
||||
ExistingMixed,
|
||||
NewPhysical,
|
||||
NewMixed
|
||||
};
|
||||
|
||||
struct TextureFilamentEntry {
|
||||
TextureFilamentKind kind{TextureFilamentKind::ExistingPhysical};
|
||||
int dialog_index{-1};
|
||||
size_t project_config_index{size_t(-1)};
|
||||
std::string color_hex;
|
||||
std::string name;
|
||||
std::string type;
|
||||
std::string preset_name;
|
||||
std::vector<unsigned int> mixed_components;
|
||||
std::vector<int> mixed_ratios;
|
||||
};
|
||||
|
||||
struct TextureNewMixedFilament {
|
||||
int dialog_index{-1};
|
||||
std::string color_hex;
|
||||
std::vector<int> component_dialog_indices;
|
||||
std::vector<int> ratios;
|
||||
};
|
||||
|
||||
struct FilamentMappingRow {
|
||||
int cluster_id = -1;
|
||||
std::array<std::size_t, 3> source_color = {0, 0, 0};
|
||||
std::string source_hex;
|
||||
int target_filament_idx = 0;
|
||||
wxPanel* source_panel = nullptr;
|
||||
wxPanel* target_panel = nullptr;
|
||||
};
|
||||
|
||||
class FilamentSelectPopup;
|
||||
class AutoMixSelectPopup;
|
||||
// Lightweight 3D preview panel using wxGLCanvas.
|
||||
// Renders: original textured, multi-color, or filament-mapped.
|
||||
class TexturePreviewCanvas : public wxGLCanvas
|
||||
{
|
||||
public:
|
||||
enum class RenderMode { Original, MultiColor, FilamentMap };
|
||||
|
||||
TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs);
|
||||
~TexturePreviewCanvas();
|
||||
|
||||
void set_mesh_data(
|
||||
const std::vector<std::array<float, 3>>& vertices,
|
||||
const std::vector<std::array<int, 3>>& indices);
|
||||
|
||||
void set_texture_data(
|
||||
const std::vector<std::array<float, 2>>& uvs,
|
||||
const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels);
|
||||
|
||||
void set_texture_render_data(
|
||||
const std::vector<std::vector<unsigned char>>& tex_pixels_rgb,
|
||||
const std::vector<int>& tex_widths,
|
||||
const std::vector<int>& tex_heights,
|
||||
const std::vector<std::array<std::array<float,2>, 3>>& face_uvs,
|
||||
const std::vector<int>& face_tex_ids);
|
||||
|
||||
void set_painted_mesh_data(
|
||||
const std::vector<std::array<float, 3>>& vertices,
|
||||
const std::vector<std::array<int, 3>>& indices);
|
||||
void set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
|
||||
void set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
|
||||
void set_filament_color_map(const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map);
|
||||
|
||||
void set_render_mode(RenderMode mode);
|
||||
RenderMode get_render_mode() const { return m_mode; }
|
||||
void set_computing_overlay(bool show);
|
||||
void reset_view();
|
||||
|
||||
private:
|
||||
void on_paint(wxPaintEvent& evt);
|
||||
void on_size(wxSizeEvent& evt);
|
||||
void on_mouse(wxMouseEvent& evt);
|
||||
void ensure_gl_ready();
|
||||
void render();
|
||||
void render_mesh();
|
||||
void render_textured_original();
|
||||
void render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size);
|
||||
void upload_reset_icon_textures();
|
||||
unsigned int upload_reset_icon_texture(const std::string& icon_name);
|
||||
wxRect reset_overlay_rect() const;
|
||||
bool handle_reset_overlay_mouse(wxMouseEvent& evt);
|
||||
void upload_textures();
|
||||
void compute_smooth_normals();
|
||||
void update_bounding_box();
|
||||
|
||||
wxGLContext* m_context = nullptr;
|
||||
bool m_gl_initialized = false;
|
||||
RenderMode m_mode = RenderMode::Original;
|
||||
|
||||
float m_zoom = 1.0f;
|
||||
float m_rot_x = -30.0f;
|
||||
float m_rot_y = 30.0f;
|
||||
float m_pan_x = 0.0f;
|
||||
float m_pan_y = 0.0f;
|
||||
wxPoint m_last_mouse_pos;
|
||||
enum class DragMode { None, Rotate, Pan };
|
||||
DragMode m_drag_mode = DragMode::None;
|
||||
|
||||
std::vector<std::array<float, 3>> m_vertices;
|
||||
std::vector<std::array<int, 3>> m_indices;
|
||||
std::vector<std::array<float, 2>> m_uvs;
|
||||
std::vector<std::array<float, 3>> m_painted_vertices;
|
||||
std::vector<std::array<int, 3>> m_painted_indices;
|
||||
std::vector<std::array<float, 3>> m_face_colors_rgb;
|
||||
std::vector<std::array<float, 3>> m_original_face_colors_rgb;
|
||||
std::vector<std::array<float, 3>> m_filament_colors_rgb;
|
||||
std::map<std::array<std::size_t, 3>, std::array<float, 3>> m_color_map;
|
||||
|
||||
unsigned int m_tex_id = 0;
|
||||
int m_tex_w = 0;
|
||||
int m_tex_h = 0;
|
||||
int m_tex_channels = 3;
|
||||
bool m_tex_dirty = false;
|
||||
std::vector<unsigned char> m_tex_data;
|
||||
|
||||
std::vector<unsigned int> m_gl_tex_ids;
|
||||
std::vector<std::vector<unsigned char>> m_tex_pixels_rgb;
|
||||
std::vector<int> m_tex_widths;
|
||||
std::vector<int> m_tex_heights;
|
||||
std::vector<std::array<std::array<float,2>, 3>> m_face_uvs;
|
||||
std::vector<int> m_face_tex_ids;
|
||||
bool m_multi_tex_dirty = false;
|
||||
|
||||
std::vector<std::array<float, 3>> m_vertex_normals;
|
||||
|
||||
std::array<float, 3> m_center = {0, 0, 0};
|
||||
float m_radius = 1.0f;
|
||||
|
||||
unsigned int m_reset_icon_tex = 0;
|
||||
unsigned int m_reset_icon_hover_tex = 0;
|
||||
unsigned int m_reset_icon_dark_tex = 0;
|
||||
unsigned int m_reset_icon_dark_hover_tex = 0;
|
||||
bool m_reset_overlay_hovered = false;
|
||||
bool m_reset_overlay_pressed = false;
|
||||
};
|
||||
|
||||
|
||||
class TextureImportDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
TextureImportDialog(wxWindow* parent,
|
||||
const Slic3r::TexturedMesh& textured_mesh,
|
||||
const std::vector<TextureFilamentEntry>& filament_entries,
|
||||
std::function<bool()> initial_cancel_callback = {},
|
||||
std::function<bool(int)> initial_progress_callback = {});
|
||||
~TextureImportDialog();
|
||||
|
||||
int ShowModal() override;
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
Slic3r::PaintedMesh get_painted_mesh() const;
|
||||
std::vector<Slic3r::FilamentMatch> get_matches() const;
|
||||
bool was_skipped() const { return m_skipped; }
|
||||
bool fallback_to_geometry_only() const { return m_fallback_to_geometry_only; }
|
||||
// Colors of virtual filaments that need to be created after dialog confirmation.
|
||||
// Index i corresponds to filament index (m_existing_filament_count + i).
|
||||
const std::vector<std::array<float, 4>>& get_new_filament_colors() const { return m_new_filament_colors; }
|
||||
const std::vector<std::string>& get_new_filament_preset_names() const { return m_new_filament_preset_names; }
|
||||
const std::vector<TextureNewMixedFilament>& get_new_mixed_filaments() const { return m_new_mixed_filaments; }
|
||||
const std::vector<TextureFilamentEntry>& get_filament_entries() const { return m_filament_entries; }
|
||||
size_t get_existing_filament_count() const { return m_existing_filament_count; }
|
||||
|
||||
private:
|
||||
void build_ui();
|
||||
void build_preview_panel(wxWindow* parent, wxSizer* sizer);
|
||||
void build_params_panel(wxWindow* parent, wxSizer* sizer);
|
||||
void build_mapping_panel(wxWindow* parent, wxSizer* sizer);
|
||||
void build_bottom_buttons(wxSizer* sizer);
|
||||
|
||||
void set_state(TextureImportState new_state);
|
||||
void update_ui_for_state();
|
||||
|
||||
void start_computation(bool auto_color = false, bool initial = false);
|
||||
void cancel_computation();
|
||||
void on_computation_complete(wxCommandEvent& evt);
|
||||
void on_computation_progress(wxCommandEvent& evt);
|
||||
void on_computation_error(wxCommandEvent& evt);
|
||||
void on_mesh_repair_decision_required(wxCommandEvent& evt);
|
||||
|
||||
void rebuild_mapping_rows();
|
||||
void do_auto_match();
|
||||
// Reorder m_current_matches into a canonical, predictable order (ascending
|
||||
// filament_index, with unmapped entries pushed to the end). Used right
|
||||
// after the initial computation so the first view the user sees has a
|
||||
// stable, intuitive layout.
|
||||
void sort_current_matches_by_filament_index();
|
||||
// Reorder m_current_matches so they appear in the same order as
|
||||
// `previous_matches` (keyed by cluster_index). Entries whose cluster_index
|
||||
// was not present before are appended at the end, preserving their current
|
||||
// relative order. Used when the user toggles auto-merge so the rows do not
|
||||
// visually jump around. Assumes each cluster_index appears at most once in
|
||||
// both vectors (this invariant is currently guaranteed by do_auto_match,
|
||||
// which produces one match per cluster).
|
||||
void restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches);
|
||||
std::vector<Slic3r::FilamentMatch> build_matches_from_rows() const;
|
||||
void update_filament_color_map();
|
||||
void show_filament_popup(size_t row_index);
|
||||
void dismiss_filament_popup();
|
||||
void dismiss_filament_popup_on_wheel(wxMouseEvent& evt);
|
||||
void show_auto_mix_popup();
|
||||
void dismiss_auto_mix_popup();
|
||||
void set_auto_mix_mode(TextureAutoMixMode mode);
|
||||
void apply_auto_standard_mix(TextureAutoMixMode mode);
|
||||
void reset_auto_mix();
|
||||
void update_auto_mix_reset_visibility();
|
||||
bool add_decomposed_mixed_filament(size_t row_index);
|
||||
int add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex,
|
||||
const std::string& preset_name = std::string());
|
||||
int add_virtual_mixed_filament(const std::string& color_hex,
|
||||
const std::vector<int>& component_dialog_indices,
|
||||
const std::vector<int>& ratios);
|
||||
size_t max_filament_count() const;
|
||||
bool can_add_virtual_filament() const;
|
||||
// Recomputes m_drop_warning_label visibility from m_filaments_dropped and
|
||||
// m_state. Safe to call whether or not the label has been created yet.
|
||||
// Visibility reflects ONLY the result of the most recent do_auto_match():
|
||||
// if the latest match did not drop any cluster, the label is hidden even
|
||||
// if a previous match had dropped (no historical accumulation).
|
||||
void update_drop_warning_visibility();
|
||||
void compact_used_virtual_filaments();
|
||||
int find_closest_filament_index(const std::array<std::size_t, 3>& color) const;
|
||||
// Returns a vector indexed by dialog_index whose value is the 1-based
|
||||
// display number that mirrors the final sidebar ordering produced by
|
||||
// apply_textured_mesh_import_result (Plater.cpp): ExistingPhysical,
|
||||
// NewPhysical, ExistingMixed, NewMixed. Used so the dialog shows the
|
||||
// same IDs the sidebar will show after OK, instead of the raw
|
||||
// dialog_index + 1 (which interleaves physicals and mixeds).
|
||||
// MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896).
|
||||
std::vector<int> compute_display_numbers() const;
|
||||
|
||||
void on_color_preset_clicked(wxCommandEvent& evt);
|
||||
void on_color_slider_changed(wxCommandEvent& evt);
|
||||
void on_color_spin_changed(wxCommandEvent& evt);
|
||||
void on_color_spin_text_changed(wxCommandEvent& evt);
|
||||
void on_smooth_slider_changed(wxCommandEvent& evt);
|
||||
void on_smooth_spin_changed(wxCommandEvent& evt);
|
||||
void on_smooth_spin_text_changed(wxCommandEvent& evt);
|
||||
void on_apply_clicked(wxCommandEvent& evt);
|
||||
void on_auto_merge_toggled(wxCommandEvent& evt);
|
||||
void highlight_view_button(int view_index);
|
||||
void on_skip_clicked(wxCommandEvent& evt);
|
||||
void on_ok_clicked(wxCommandEvent& evt);
|
||||
|
||||
void set_color_count_value(int value, bool update_spin);
|
||||
void set_smooth_value(int value, bool update_spin);
|
||||
void preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param,
|
||||
int min_value, int max_value, const wxString& text,
|
||||
std::function<void()> on_value_changed = {});
|
||||
void update_color_count_preset_buttons();
|
||||
|
||||
bool has_valid_result() const;
|
||||
bool is_params_dirty() const;
|
||||
void update_confirm_button_state();
|
||||
void style_confirm_button(bool dirty);
|
||||
|
||||
Slic3r::TexturedMesh m_textured_mesh;
|
||||
std::vector<std::string> m_filament_color_strs; // existing + virtual
|
||||
std::vector<std::string> m_filament_names; // existing + virtual
|
||||
std::vector<std::array<float, 4>> m_filament_colors_rgba; // existing + virtual
|
||||
std::vector<TextureFilamentEntry> m_filament_entries; // aligned with m_filament_colors_rgba
|
||||
size_t m_existing_filament_count = 0;
|
||||
std::vector<std::array<float, 4>> m_new_filament_colors; // only virtual (to be created)
|
||||
std::vector<std::string> m_new_filament_preset_names; // only virtual, aligned with m_new_filament_colors
|
||||
std::vector<TextureNewMixedFilament> m_new_mixed_filaments;
|
||||
std::string m_default_virtual_filament_preset_name;
|
||||
|
||||
TextureImportState m_state = TextureImportState::Idle;
|
||||
bool m_skipped = false;
|
||||
bool m_fallback_to_geometry_only = false;
|
||||
// True iff *the most recent* do_auto_match() ran into the global filament
|
||||
// limit and had to drop one or more clusters. Reset to false on every
|
||||
// do_auto_match() entry so it never accumulates across runs: a run that
|
||||
// does not drop anything must observe false here, regardless of whether
|
||||
// previous runs dropped. Drives the inline orange warning above the
|
||||
// bottom buttons; never affects the mapping itself.
|
||||
bool m_filaments_dropped = false;
|
||||
bool m_auto_merge_enabled = true;
|
||||
TextureAutoMixMode m_auto_mix_mode = TextureAutoMixMode::CMYW;
|
||||
int m_auto_mix_font_point_size = 10;
|
||||
|
||||
Slic3r::PaintedMesh m_painted;
|
||||
std::vector<Slic3r::FilamentMatch> m_current_matches;
|
||||
|
||||
std::unique_ptr<std::thread> m_worker;
|
||||
std::atomic<bool> m_cancel_flag{false};
|
||||
std::mutex m_result_mutex;
|
||||
Slic3r::PaintedMesh m_pending_result;
|
||||
std::function<bool()> m_initial_cancel_callback;
|
||||
std::function<bool(int)> m_initial_progress_callback;
|
||||
bool m_current_computation_initial = false;
|
||||
bool m_initial_computation_pending = false;
|
||||
bool m_initial_computation_cancelled = false;
|
||||
bool m_initial_computation_failed = false;
|
||||
bool m_initial_tooltips_set = false;
|
||||
bool m_current_computation_auto_color = false;
|
||||
Slic3r::TexturePaintingSettings::MeshRepairDecision m_mesh_repair_decision =
|
||||
Slic3r::TexturePaintingSettings::MeshRepairDecision::Ask;
|
||||
|
||||
Button* m_btn_color_4 = nullptr;
|
||||
Button* m_btn_color_8 = nullptr;
|
||||
Button* m_btn_color_16 = nullptr;
|
||||
Button* m_btn_color_auto = nullptr;
|
||||
AccentSlider* m_color_slider = nullptr;
|
||||
SpinInput* m_color_spin = nullptr;
|
||||
AccentSlider* m_smooth_slider = nullptr;
|
||||
SpinInput* m_smooth_spin = nullptr;
|
||||
Button* m_btn_apply = nullptr;
|
||||
|
||||
wxCheckBox* m_auto_merge_cb = nullptr;
|
||||
Button* m_btn_auto_mix = nullptr;
|
||||
Button* m_btn_mix_reset = nullptr;
|
||||
bool m_auto_mix_applied = false;
|
||||
AutoMixSelectPopup* m_auto_mix_popup = nullptr;
|
||||
wxScrolledWindow* m_mapping_scroll = nullptr;
|
||||
wxBoxSizer* m_mapping_sizer = nullptr;
|
||||
std::vector<FilamentMappingRow> m_mapping_rows;
|
||||
FilamentSelectPopup* m_filament_popup = nullptr;
|
||||
int m_filament_popup_row = -1;
|
||||
int m_skip_next_filament_popup_row = -1;
|
||||
|
||||
TexturePreviewCanvas* m_preview_canvas = nullptr;
|
||||
wxPanel* m_tab_panel = nullptr;
|
||||
Button* m_btn_view_original = nullptr;
|
||||
Button* m_btn_view_multicolor = nullptr;
|
||||
|
||||
ProgressDialog* m_progress_dlg = nullptr;
|
||||
|
||||
Button* m_btn_skip = nullptr;
|
||||
Button* m_btn_ok = nullptr;
|
||||
wxStaticText* m_drop_warning_label = nullptr;
|
||||
|
||||
int m_param_color_count = 4;
|
||||
int m_param_smooth = 5;
|
||||
|
||||
int m_applied_color_count = -1;
|
||||
int m_applied_smooth = -1;
|
||||
wxStaticText* m_hint_label = nullptr;
|
||||
|
||||
static const int ID_COLOR_4 = wxID_HIGHEST + 200;
|
||||
static const int ID_COLOR_8 = wxID_HIGHEST + 201;
|
||||
static const int ID_COLOR_16 = wxID_HIGHEST + 202;
|
||||
static const int ID_COLOR_AUTO = wxID_HIGHEST + 203;
|
||||
static const int ID_BTN_APPLY = wxID_HIGHEST + 204;
|
||||
static const int ID_BTN_SKIP = wxID_HIGHEST + 205;
|
||||
static const int ID_VIEW_ORIGINAL = wxID_HIGHEST + 206;
|
||||
static const int ID_VIEW_MULTICOLOR = wxID_HIGHEST + 207;
|
||||
|
||||
wxDECLARE_EVENT_TABLE();
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -343,7 +343,7 @@ public:
|
||||
UnsavedChangesDialog(const wxString &caption, const wxString &header, DynamicConfig *config, int from, int to, bool left_to_right, NozzleVolumeType nozzle);
|
||||
~UnsavedChangesDialog() override = default;
|
||||
|
||||
int ShowModal();
|
||||
int ShowModal() override;
|
||||
|
||||
void build(Preset::Type type, PresetCollection *dependent_presets, const std::string &new_selected_preset, const wxString &header = "");
|
||||
void update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "WebGuideDialog.hpp"
|
||||
#include "ConfigWizard.hpp"
|
||||
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/iostreams/detail/select.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -9,7 +11,9 @@
|
||||
#include "I18N.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PresetCacheFormat.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
@@ -41,8 +45,6 @@ using namespace nlohmann;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
json m_ProfileJson;
|
||||
|
||||
static wxString update_custom_filaments()
|
||||
{
|
||||
json m_Res = json::object();
|
||||
@@ -190,12 +192,10 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
|
||||
|
||||
GuideFrame::~GuideFrame()
|
||||
{
|
||||
m_destroy = true;
|
||||
if (m_load_task && m_load_task->joinable()) {
|
||||
*m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join
|
||||
if (m_load_task && m_load_task->joinable())
|
||||
m_load_task->join();
|
||||
delete m_load_task;
|
||||
m_load_task = nullptr;
|
||||
}
|
||||
m_load_task.reset();
|
||||
if (m_browser) {
|
||||
delete m_browser;
|
||||
m_browser = nullptr;
|
||||
@@ -301,15 +301,71 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
|
||||
/**
|
||||
* Callback invoked when a navigation request was accepted
|
||||
*/
|
||||
// The empty shape every profile-loading path starts from or falls back to.
|
||||
void GuideFrame::reset_profile_json()
|
||||
{
|
||||
m_ProfileJson["model"] = json::array();
|
||||
m_ProfileJson["machine"] = json::object();
|
||||
m_ProfileJson["filament"] = json::object();
|
||||
m_ProfileJson["process"] = json::array();
|
||||
}
|
||||
|
||||
void GuideFrame::init_guide_paths()
|
||||
{
|
||||
m_ProfileJson = json::parse("{}");
|
||||
reset_profile_json();
|
||||
|
||||
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
|
||||
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
|
||||
orca_bundle_rsrc = true;
|
||||
|
||||
if (boost::filesystem::exists(vendor_dir)) {
|
||||
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
|
||||
if (!boost::filesystem::is_directory(entry) &&
|
||||
boost::iequals(entry.path().extension().string(), ".json") &&
|
||||
!boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
|
||||
orca_bundle_rsrc = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
|
||||
m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json)
|
||||
? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string()
|
||||
: (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
|
||||
}
|
||||
|
||||
void GuideFrame::on_profile_loaded()
|
||||
{
|
||||
// Must be called on the main thread.
|
||||
SaveProfileData();
|
||||
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
|
||||
json res;
|
||||
res["command"] = "userguide_profile_load_finish";
|
||||
res["sequence_id"] = "10001";
|
||||
RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true)));
|
||||
}
|
||||
|
||||
void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
|
||||
{
|
||||
//wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
|
||||
if (!bFirstComplete) {
|
||||
m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this));
|
||||
// boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this));
|
||||
//LoadProfileThread.detach();
|
||||
|
||||
bFirstComplete = true;
|
||||
try {
|
||||
init_guide_paths();
|
||||
if (BuildProfileDataFromPresetBundle()) {
|
||||
if (!*m_cancel_token)
|
||||
on_profile_loaded();
|
||||
} else {
|
||||
// Presets not yet in memory — delegate to background thread.
|
||||
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what();
|
||||
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
|
||||
}
|
||||
}
|
||||
|
||||
m_browser->Show();
|
||||
@@ -762,11 +818,9 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
|
||||
bool check_unsaved_preset_changes = false;
|
||||
std::vector<std::string> install_bundles;
|
||||
std::vector<std::string> remove_bundles;
|
||||
const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
|
||||
for (const auto &it : enabled_vendors) {
|
||||
if (it.second.size() > 0) {
|
||||
auto vendor_file = vendor_dir/(it.first + ".json");
|
||||
if (!fs::exists(vendor_file)) {
|
||||
if (!is_vendor_installed(it.first)) {
|
||||
install_bundles.emplace_back(it.first);
|
||||
}
|
||||
}
|
||||
@@ -777,8 +831,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
|
||||
if (it.second.size() > 0) {
|
||||
if (enabled_vendors.find(it.first) != enabled_vendors.end())
|
||||
continue;
|
||||
auto vendor_file = vendor_dir/(it.first + ".json");
|
||||
if (fs::exists(vendor_file)) {
|
||||
if (is_vendor_installed(it.first)) {
|
||||
remove_bundles.emplace_back(it.first);
|
||||
}
|
||||
}
|
||||
@@ -1127,99 +1180,324 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
|
||||
return status;
|
||||
}
|
||||
|
||||
int GuideFrame::LoadProfileData()
|
||||
bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors)
|
||||
{
|
||||
try {
|
||||
m_ProfileJson = json::parse("{}");
|
||||
m_ProfileJson["model"] = json::array();
|
||||
m_ProfileJson["machine"] = json::object();
|
||||
m_ProfileJson["filament"] = json::object();
|
||||
m_ProfileJson["process"] = json::array();
|
||||
// Models from vendor profiles
|
||||
for (const auto& [vendor_id, vp] : bundle.vendors) {
|
||||
for (const auto& model : vp.models) {
|
||||
std::string nozzle_str;
|
||||
for (const auto& v : model.variants) {
|
||||
if (!nozzle_str.empty()) nozzle_str += ";";
|
||||
nozzle_str += v.name;
|
||||
}
|
||||
const std::string materials_str = boost::algorithm::join(model.default_materials, ";");
|
||||
boost::filesystem::path cover_path =
|
||||
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png"))
|
||||
.make_preferred();
|
||||
if (!boost::filesystem::exists(cover_path))
|
||||
cover_path =
|
||||
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png"))
|
||||
.make_preferred();
|
||||
|
||||
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
|
||||
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
|
||||
|
||||
// Orca: add custom as default
|
||||
// Orca: add json logic for vendor bundle
|
||||
orca_bundle_rsrc = true;
|
||||
|
||||
// search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false
|
||||
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
|
||||
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
|
||||
orca_bundle_rsrc = false;
|
||||
break;
|
||||
json entry;
|
||||
entry["model"] = model.id;
|
||||
entry["name"] = model.name;
|
||||
entry["vendor"] = vp.id;
|
||||
entry["nozzle_diameter"] = nozzle_str;
|
||||
entry["materials"] = materials_str;
|
||||
entry["cover"] = cover_path.string();
|
||||
entry["nozzle_selected"] = "";
|
||||
entry["sub_path"] = "";
|
||||
m_ProfileJson["model"].push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// load the default filament library first
|
||||
std::set<std::string> loaded_vendors;
|
||||
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
|
||||
if (boost::filesystem::exists(vendor_dir / filament_library_name)) {
|
||||
m_OrcaFilaLibPath = (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
|
||||
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
|
||||
} else {
|
||||
m_OrcaFilaLibPath = (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
|
||||
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
|
||||
}
|
||||
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
// Machine map: preset name -> {model, nozzle variant}
|
||||
for (const Preset& p : bundle.printers()) {
|
||||
if (!p.is_system || !p.vendor) continue;
|
||||
const auto* printer_model = p.config.option<ConfigOptionString>("printer_model");
|
||||
const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant");
|
||||
if (!printer_model || printer_model->value.empty() || !printer_variant) continue;
|
||||
|
||||
//load custom bundle from user data path
|
||||
boost::filesystem::directory_iterator endIter;
|
||||
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
|
||||
if (!boost::filesystem::is_directory(*iter)) {
|
||||
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
|
||||
strVendor = strVendor.AfterLast('\\');
|
||||
strVendor = strVendor.AfterLast('/');
|
||||
|
||||
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
|
||||
if(strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
|
||||
continue;
|
||||
|
||||
LoadProfileFamily(w2s(strVendor), iter->path().string());
|
||||
loaded_vendors.insert(w2s(strVendor));
|
||||
}
|
||||
if (m_destroy)
|
||||
return 0;
|
||||
json mach;
|
||||
mach["model"] = printer_model->value;
|
||||
mach["nozzle"] = printer_variant->value;
|
||||
m_ProfileJson["machine"][p.name] = mach;
|
||||
}
|
||||
|
||||
boost::filesystem::directory_iterator others_endIter;
|
||||
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
|
||||
if (!boost::filesystem::is_directory(*iter)) {
|
||||
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
|
||||
strVendor = strVendor.AfterLast('\\');
|
||||
strVendor = strVendor.AfterLast('/');
|
||||
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
|
||||
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
|
||||
continue;
|
||||
// Filament map from system filament presets (vendor/type already resolved in config)
|
||||
const json& machines = m_ProfileJson["machine"];
|
||||
for (const Preset& p : bundle.filaments()) {
|
||||
if (!p.is_system || !p.vendor) continue;
|
||||
const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor");
|
||||
const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type");
|
||||
const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers");
|
||||
|
||||
LoadProfileFamily(w2s(strVendor), iter->path().string());
|
||||
loaded_vendors.insert(w2s(strVendor));
|
||||
std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : "";
|
||||
std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : "";
|
||||
|
||||
std::string model_list;
|
||||
if (compat_printers) {
|
||||
for (const std::string& pname : compat_printers->values) {
|
||||
auto it = machines.find(pname);
|
||||
if (it != machines.end()) {
|
||||
const std::string m = (*it)["model"];
|
||||
const std::string n = (*it)["nozzle"];
|
||||
model_list += "[" + m + "++" + n + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_destroy)
|
||||
return 0;
|
||||
|
||||
json ff;
|
||||
ff["name"] = p.name;
|
||||
ff["sub_path"] = p.file;
|
||||
ff["vendor"] = vendor;
|
||||
ff["type"] = type;
|
||||
ff["models"] = model_list;
|
||||
ff["selected"] = 0;
|
||||
m_ProfileJson["filament"][p.name] = ff;
|
||||
}
|
||||
|
||||
wxGetApp().CallAfter([this] {
|
||||
if (!m_destroy) {
|
||||
//sync to appconfig first to populate current selections
|
||||
SaveProfileData();
|
||||
// Process list from visible system print presets
|
||||
for (const Preset& p : bundle.prints()) {
|
||||
if (!p.is_system || !p.vendor || !p.is_visible) continue;
|
||||
json entry;
|
||||
entry["name"] = p.name;
|
||||
entry["sub_path"] = p.file;
|
||||
m_ProfileJson["process"].push_back(entry);
|
||||
}
|
||||
|
||||
//sync to web after selections are populated
|
||||
std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
|
||||
if (require_all_resource_vendors) {
|
||||
// If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a
|
||||
// packaged build ships instead) not covered by the current bundle, the
|
||||
// bundle is incomplete (e.g. dev env where data_dir/system only has
|
||||
// OrcaFilamentLibrary+Custom). Fall back so the slow path reads both dirs.
|
||||
try {
|
||||
for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) {
|
||||
if (bundle.vendors.find(name) == bundle.vendors.end()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name
|
||||
<< "' in resources but not in preset_bundle — falling back to JSON loading";
|
||||
reset_profile_json();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll;
|
||||
json m_Res = json::object();
|
||||
m_Res["command"] = "userguide_profile_load_finish";
|
||||
m_Res["sequence_id"] = "10001";
|
||||
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true));
|
||||
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data ("
|
||||
<< m_ProfileJson["model"].size() << " models, "
|
||||
<< m_ProfileJson["machine"].size() << " machines, "
|
||||
<< m_ProfileJson["filament"].size() << " filaments)";
|
||||
return !m_ProfileJson["machine"].empty();
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what()
|
||||
<< " — falling back to JSON loading";
|
||||
reset_profile_json();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RunScript(strJS);
|
||||
bool GuideFrame::BuildProfileDataFromPresetBundle()
|
||||
{
|
||||
PresetBundle* pb = wxGetApp().preset_bundle;
|
||||
if (!pb || pb->vendors.empty())
|
||||
return false;
|
||||
return BuildProfileJson(*pb, /*require_all_resource_vendors=*/true);
|
||||
}
|
||||
|
||||
bool GuideFrame::BuildProfileDataFromVendors()
|
||||
{
|
||||
try {
|
||||
// Same vendor set and precedence as the JSON scan in LoadProfileData: a
|
||||
// vendor in the user's system dir shadows the bundled one of that name.
|
||||
// vendor_names_in names a vendor by its profile or, where a build ships
|
||||
// preset caches instead, by its cache alone.
|
||||
std::map<std::string, boost::filesystem::path> vendor_sources;
|
||||
for (const boost::filesystem::path& dir : { vendor_dir, rsrc_vendor_dir }) {
|
||||
boost::system::error_code ec;
|
||||
if (boost::filesystem::exists(dir, ec))
|
||||
for (const std::string& name : vendor_names_in(dir))
|
||||
vendor_sources.emplace(name, dir); // first dir wins
|
||||
}
|
||||
|
||||
// The load order: the filament library first, because the others'
|
||||
// filaments inherit from it, then every versioned vendor — each loaded
|
||||
// from the directory it was found in, so a vendor that is not installed
|
||||
// is served from the shipped profiles. Each is stamped by name and
|
||||
// version alone: a profile change requires a version bump, so those two
|
||||
// determine content wherever the vendor's copy sits.
|
||||
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
|
||||
std::vector<VendorSource> ordered;
|
||||
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
|
||||
// The version a load from `dir` would serve: the profile's where one
|
||||
// exists (a cache is only served while it covers the profile beside
|
||||
// it), the cache's own stamp where the cache is the whole vendor.
|
||||
// A profile without a version (blacklist.json) carries no presets
|
||||
// and is passed over.
|
||||
const boost::filesystem::path profile = dir / (name + ".json");
|
||||
if (boost::filesystem::exists(profile)) {
|
||||
const Semver v = get_version_from_json(profile.string());
|
||||
if (v.valid())
|
||||
ordered.push_back({name, dir, v.to_string()});
|
||||
} else {
|
||||
ordered.push_back({name, dir,
|
||||
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
|
||||
}
|
||||
};
|
||||
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
||||
add_vendor(filament_library, it->second);
|
||||
for (const auto& [name, dir] : vendor_sources)
|
||||
if (name != filament_library)
|
||||
add_vendor(name, dir);
|
||||
if (ordered.empty())
|
||||
return false;
|
||||
json stamps = json::array();
|
||||
for (const VendorSource& v : ordered)
|
||||
stamps.push_back({v.name, v.version});
|
||||
|
||||
// What this function derives is a pure function of that stamped set, so
|
||||
// the derived JSON is cached whole: a fresh cache makes an open one
|
||||
// file read, with no bundle built and no preset installed. Stale or
|
||||
// absent, the bundle is rebuilt below and the result written back.
|
||||
const boost::filesystem::path cache_file =
|
||||
boost::filesystem::path(Slic3r::data_dir()) / "cache" / "wizard_profile_data.json";
|
||||
try {
|
||||
// Slurped whole and parsed from the buffer — nlohmann's fastest
|
||||
// input path; a stream adapter costs real time on a multi-MB file.
|
||||
boost::nowide::ifstream ifs(cache_file.string(), std::ios::binary);
|
||||
if (ifs.is_open()) {
|
||||
const std::string text{std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>()};
|
||||
json cached = json::parse(text);
|
||||
if (cached.value("format", 0) == 1 && cached["vendors"] == stamps &&
|
||||
! cached["profile"]["machine"].empty()) {
|
||||
for (const char* key : { "model", "machine", "filament", "process" })
|
||||
m_ProfileJson[key] = std::move(cached["profile"][key]);
|
||||
BOOST_LOG_TRIVIAL(info) << "GuideFrame: profile data served from " << cache_file;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(info) << "GuideFrame: rejecting cached profile data: " << e.what();
|
||||
}
|
||||
|
||||
// Each vendor comes from its preset cache where one covers it, which is
|
||||
// what makes this worth doing instead of the scan below; loading into a
|
||||
// bundle per vendor keeps the install order the startup path has.
|
||||
PresetBundle bundle;
|
||||
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
|
||||
const boost::filesystem::path& dir, const PresetBundle* base) {
|
||||
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
|
||||
};
|
||||
for (const VendorSource& v : ordered) {
|
||||
if (*m_cancel_token)
|
||||
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
|
||||
if (v.name == filament_library) {
|
||||
load_vendor(bundle, v.name, v.dir, nullptr);
|
||||
} else {
|
||||
PresetBundle tmp;
|
||||
load_vendor(tmp, v.name, v.dir, &bundle);
|
||||
bundle.merge_presets(std::move(tmp));
|
||||
}
|
||||
}
|
||||
if (bundle.vendors.empty())
|
||||
return false;
|
||||
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
||||
return false;
|
||||
|
||||
// Written through a temp file and moved into place, as the preset caches
|
||||
// are: half a cache must never be readable, and the PID suffix keeps two
|
||||
// instances from interleaving on one temp file.
|
||||
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
|
||||
try {
|
||||
json out;
|
||||
out["format"] = 1;
|
||||
out["vendors"] = std::move(stamps);
|
||||
json& profile = out["profile"];
|
||||
for (const char* key : { "model", "machine", "filament", "process" })
|
||||
profile[key] = m_ProfileJson[key];
|
||||
boost::filesystem::create_directories(cache_file.parent_path());
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
||||
ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore);
|
||||
ofs.close();
|
||||
if (! ofs.good())
|
||||
throw std::runtime_error("write failed");
|
||||
}
|
||||
if (const std::error_code ec = rename_file(tmp_path, cache_file.string()))
|
||||
throw std::runtime_error(ec.message());
|
||||
} catch (const std::exception& e) {
|
||||
boost::system::error_code rm;
|
||||
boost::filesystem::remove(tmp_path, rm);
|
||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what();
|
||||
reset_profile_json();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int GuideFrame::LoadProfileData()
|
||||
{
|
||||
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
|
||||
// Loading order (fastest to slowest):
|
||||
// 1. Load every vendor, from its preset cache wherever one covers it
|
||||
// 2. Read all vendor JSONs by hand
|
||||
try {
|
||||
if (!BuildProfileDataFromVendors()) {
|
||||
// Last resort — read all vendor JSONs
|
||||
std::set<std::string> loaded_vendors;
|
||||
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
|
||||
if (boost::filesystem::exists(vendor_dir / filament_library_name))
|
||||
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
|
||||
else
|
||||
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
|
||||
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
|
||||
boost::filesystem::directory_iterator endIter;
|
||||
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
|
||||
if (!boost::filesystem::is_directory(*iter)) {
|
||||
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
|
||||
strVendor = strVendor.AfterLast('\\');
|
||||
strVendor = strVendor.AfterLast('/');
|
||||
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
|
||||
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
|
||||
continue;
|
||||
LoadProfileFamily(w2s(strVendor), iter->path().string());
|
||||
loaded_vendors.insert(w2s(strVendor));
|
||||
}
|
||||
if (*m_cancel_token) return 0;
|
||||
}
|
||||
|
||||
boost::filesystem::directory_iterator others_endIter;
|
||||
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
|
||||
if (!boost::filesystem::is_directory(*iter)) {
|
||||
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
|
||||
strVendor = strVendor.AfterLast('\\');
|
||||
strVendor = strVendor.AfterLast('/');
|
||||
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
|
||||
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
|
||||
continue;
|
||||
LoadProfileFamily(w2s(strVendor), iter->path().string());
|
||||
loaded_vendors.insert(w2s(strVendor));
|
||||
}
|
||||
if (*m_cancel_token) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the cancel token by value (shared_ptr) so the lambda doesn't
|
||||
// touch `this` if GuideFrame is destroyed before the event fires.
|
||||
auto tok = m_cancel_token;
|
||||
wxGetApp().CallAfter([this, tok] {
|
||||
if (!*tok)
|
||||
on_profile_loaded();
|
||||
});
|
||||
} catch (std::exception& e) {
|
||||
// wxLogMessage("GUIDE: load_profile_error %s ", e.what());
|
||||
// wxMessageBox(e.what(), "", MB_OK);
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what();
|
||||
}
|
||||
|
||||
filament_info_cache.clear();
|
||||
|
||||
@@ -30,10 +30,14 @@
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/Utils/PresetUpdater.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <boost/thread.hpp>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class GuideFrame : public DPIDialog
|
||||
@@ -78,6 +82,12 @@ public:
|
||||
int LoadProfileData();
|
||||
int SaveProfileData();
|
||||
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
|
||||
void init_guide_paths();
|
||||
void on_profile_loaded();
|
||||
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
|
||||
bool BuildProfileDataFromPresetBundle();
|
||||
bool BuildProfileDataFromVendors();
|
||||
void reset_profile_json();
|
||||
int SaveProfile();
|
||||
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
|
||||
|
||||
@@ -112,8 +122,11 @@ private:
|
||||
|
||||
//First Load
|
||||
bool bFirstComplete{false};
|
||||
bool m_destroy{false};
|
||||
boost::thread* m_load_task{ nullptr };
|
||||
// Set once in the destructor. Read through `this` by the loading thread
|
||||
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
|
||||
// lambdas so they don't touch `this` after the object is freed.
|
||||
std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)};
|
||||
std::unique_ptr<boost::thread> m_load_task;
|
||||
|
||||
// User Config
|
||||
bool PrivacyUse;
|
||||
@@ -123,6 +136,7 @@ private:
|
||||
bool InstallNetplugin;
|
||||
bool network_plugin_ready {false};
|
||||
|
||||
json m_ProfileJson;
|
||||
json m_OrcaFilaList;
|
||||
std::string m_OrcaFilaLibPath;
|
||||
|
||||
|
||||
@@ -2083,9 +2083,6 @@ void AMSRoad::OnPassRoad(std::vector<AMSPassRoadMode> prord_list)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
|
||||
/*************************************************
|
||||
Description:AMSRoadUpPart
|
||||
**************************************************/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user