Add controls for auto-generated mixed filament gradients

Add an app preference for mixed filament gradient auto-generation and
sync the manager state from startup and preferences changes.

Prompt before generating large gradient sets, preserve custom rows
when auto-generation is disabled, and refresh remapping/UI handling
accordingly. Add mixed filament coverage and a dumpinfo helper for
inspecting Windows minidumps.
This commit is contained in:
Rad
2026-03-21 21:05:31 +01:00
parent 970630275e
commit 778a8e45af
10 changed files with 334 additions and 8 deletions

128
dumpinfo.cs Normal file
View File

@@ -0,0 +1,128 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
class DumpInfo
{
const int MiniDumpExceptionStream = 6;
const int MiniDumpModuleListStream = 4;
[StructLayout(LayoutKind.Sequential)]
struct MINIDUMP_LOCATION_DESCRIPTOR { public uint DataSize; public uint Rva; }
[StructLayout(LayoutKind.Sequential)]
struct MINIDUMP_DIRECTORY { public uint StreamType; public MINIDUMP_LOCATION_DESCRIPTOR Location; }
[StructLayout(LayoutKind.Sequential)]
struct MINIDUMP_EXCEPTION
{
public uint ExceptionCode;
public uint ExceptionFlags;
public ulong ExceptionRecord;
public ulong ExceptionAddress;
public uint NumberParameters;
public uint __unusedAlignment;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 15)] public ulong[] ExceptionInformation;
}
[StructLayout(LayoutKind.Sequential)]
struct MINIDUMP_EXCEPTION_STREAM
{
public uint ThreadId;
public uint __alignment;
public MINIDUMP_EXCEPTION ExceptionRecord;
public MINIDUMP_LOCATION_DESCRIPTOR ThreadContext;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct MINIDUMP_STRING_HEADER { public uint Length; }
[StructLayout(LayoutKind.Sequential)]
struct VS_FIXEDFILEINFO
{
public uint dwSignature; public uint dwStrucVersion; public uint dwFileVersionMS; public uint dwFileVersionLS;
public uint dwProductVersionMS; public uint dwProductVersionLS; public uint dwFileFlagsMask; public uint dwFileFlags;
public uint dwFileOS; public uint dwFileType; public uint dwFileSubtype; public uint dwFileDateMS; public uint dwFileDateLS;
}
[StructLayout(LayoutKind.Sequential)]
struct MINIDUMP_MODULE
{
public ulong BaseOfImage;
public uint SizeOfImage;
public uint CheckSum;
public uint TimeDateStamp;
public uint ModuleNameRva;
public VS_FIXEDFILEINFO VersionInfo;
public MINIDUMP_LOCATION_DESCRIPTOR CvRecord;
public MINIDUMP_LOCATION_DESCRIPTOR MiscRecord;
public ulong Reserved0;
public ulong Reserved1;
}
[DllImport("dbghelp.dll", SetLastError = true)]
static extern bool MiniDumpReadDumpStream(IntPtr BaseOfDump, int StreamNumber, out IntPtr Dir, out IntPtr StreamPointer, out uint StreamSize);
static T PtrTo<T>(IntPtr p) where T : struct => Marshal.PtrToStructure<T>(p);
static string ReadMinidumpString(IntPtr basePtr, uint rva)
{
if (rva == 0) return "";
var header = PtrTo<MINIDUMP_STRING_HEADER>(IntPtr.Add(basePtr, (int)rva));
var strPtr = IntPtr.Add(basePtr, (int)rva + 4);
return Marshal.PtrToStringUni(strPtr, (int)header.Length / 2) ?? "";
}
static void Main(string[] args)
{
if (args.Length == 0) { Console.WriteLine("usage: DumpInfo <dump>"); return; }
var path = args[0];
using var fs = File.OpenRead(path);
using var mm = System.IO.MemoryMappedFiles.MemoryMappedFile.CreateFromFile(fs, null, 0, System.IO.MemoryMappedFiles.MemoryMappedFileAccess.Read, null, System.IO.HandleInheritability.None, false);
using var view = mm.CreateViewAccessor(0, 0, System.IO.MemoryMappedFiles.MemoryMappedFileAccess.Read);
SafeMemoryMappedViewHandle handle = view.SafeMemoryMappedViewHandle;
handle.AcquirePointer(ref bytePtr);
try {
IntPtr basePtr = (IntPtr)bytePtr;
if (MiniDumpReadDumpStream(basePtr, MiniDumpExceptionStream, out _, out var exPtr, out _)) {
var ex = PtrTo<MINIDUMP_EXCEPTION_STREAM>(exPtr);
Console.WriteLine($"ThreadId: {ex.ThreadId}");
Console.WriteLine($"ExceptionCode: 0x{ex.ExceptionRecord.ExceptionCode:X8}");
Console.WriteLine($"ExceptionAddress: 0x{ex.ExceptionRecord.ExceptionAddress:X16}");
ulong addr = ex.ExceptionRecord.ExceptionAddress;
if (MiniDumpReadDumpStream(basePtr, MiniDumpModuleListStream, out _, out var modPtr, out _)) {
uint count = (uint)Marshal.ReadInt32(modPtr);
IntPtr cur = IntPtr.Add(modPtr, 4);
string bestName = "";
ulong bestBase = 0; uint bestSize = 0;
for (uint i = 0; i < count; ++i) {
var mod = PtrTo<MINIDUMP_MODULE>(cur);
if (addr >= mod.BaseOfImage && addr < mod.BaseOfImage + mod.SizeOfImage) {
bestName = ReadMinidumpString(basePtr, mod.ModuleNameRva);
bestBase = mod.BaseOfImage;
bestSize = mod.SizeOfImage;
break;
}
cur = IntPtr.Add(cur, Marshal.SizeOf<MINIDUMP_MODULE>());
}
if (bestName.Length > 0) {
Console.WriteLine($"Module: {bestName}");
Console.WriteLine($"ModuleBase: 0x{bestBase:X16}");
Console.WriteLine($"Offset: 0x{(addr - bestBase):X}");
Console.WriteLine($"ModuleSize: 0x{bestSize:X}");
} else {
Console.WriteLine("Module: <not found>");
}
}
} else {
Console.WriteLine("MiniDumpReadDumpStream for exception stream failed");
Console.WriteLine(Marshal.GetLastWin32Error());
}
}
finally {
handle.ReleasePointer();
}
}
static unsafe byte* bytePtr;
}

View File

@@ -321,6 +321,10 @@ void AppConfig::set_defaults()
set_bool("auto_calculate_when_filament_change", true); set_bool("auto_calculate_when_filament_change", true);
} }
if (get("auto_generate_gradients").empty()) {
set_bool("auto_generate_gradients", true);
}
if (get("show_home_page").empty()) { if (get("show_home_page").empty()) {
set_bool("show_home_page", true); set_bool("show_home_page", true);
} }

View File

@@ -2,6 +2,7 @@
#include "filament_mixer.h" #include "filament_mixer.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cctype> #include <cctype>
#include <cmath> #include <cmath>
@@ -15,6 +16,12 @@
namespace Slic3r { namespace Slic3r {
namespace {
std::atomic_bool s_mixed_filament_auto_generate_enabled { true };
} // namespace
static uint64_t canonical_pair_key(unsigned int a, unsigned int b) static uint64_t canonical_pair_key(unsigned int a, unsigned int b)
{ {
const unsigned int lo = std::min(a, b); const unsigned int lo = std::min(a, b);
@@ -810,6 +817,16 @@ uint64_t MixedFilamentManager::normalize_stable_id(uint64_t stable_id)
return stable_id; return stable_id;
} }
void MixedFilamentManager::set_auto_generate_enabled(bool enabled)
{
s_mixed_filament_auto_generate_enabled.store(enabled, std::memory_order_relaxed);
}
bool MixedFilamentManager::auto_generate_enabled()
{
return s_mixed_filament_auto_generate_enabled.load(std::memory_order_relaxed);
}
void MixedFilamentManager::auto_generate(const std::vector<std::string> &filament_colours) void MixedFilamentManager::auto_generate(const std::vector<std::string> &filament_colours)
{ {
// Keep a copy of the old list so we can preserve user-modified ratios and // Keep a copy of the old list so we can preserve user-modified ratios and
@@ -818,8 +835,6 @@ void MixedFilamentManager::auto_generate(const std::vector<std::string> &filamen
m_mixed.clear(); m_mixed.clear();
const size_t n = filament_colours.size(); const size_t n = filament_colours.size();
if (n < 2)
return;
std::vector<MixedFilament> custom_rows; std::vector<MixedFilament> custom_rows;
custom_rows.reserve(old.size()); custom_rows.reserve(old.size());
@@ -837,6 +852,13 @@ void MixedFilamentManager::auto_generate(const std::vector<std::string> &filamen
custom_rows.push_back(std::move(custom)); custom_rows.push_back(std::move(custom));
} }
if (n < 2 || !auto_generate_enabled()) {
for (MixedFilament &mf : custom_rows)
m_mixed.push_back(std::move(mf));
refresh_display_colors(filament_colours);
return;
}
// Generate all C(N,2) pairwise combinations. // Generate all C(N,2) pairwise combinations.
for (size_t i = 0; i < n; ++i) { for (size_t i = 0; i < n; ++i) {
for (size_t j = i + 1; j < n; ++j) { for (size_t j = i + 1; j < n; ++j) {

View File

@@ -116,6 +116,9 @@ class MixedFilamentManager
public: public:
MixedFilamentManager() = default; MixedFilamentManager() = default;
static void set_auto_generate_enabled(bool enabled);
static bool auto_generate_enabled();
// ---- Auto-generation ------------------------------------------------ // ---- Auto-generation ------------------------------------------------
// Rebuild the mixed-filament list from the current set of physical // Rebuild the mixed-filament list from the current set of physical

View File

@@ -3469,7 +3469,7 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
// Build old->new filament ID remap for painted facet data normalization. // Build old->new filament ID remap for painted facet data normalization.
// This is needed for both deletion and addition of physical filaments so // This is needed for both deletion and addition of physical filaments so
// painted mixed states keep pointing at the same virtual mixed entries. // painted mixed states keep pointing at the same virtual mixed entries.
if (old_num_filaments != num_filaments || deleting_filament) if (old_num_filaments != num_filaments || deleting_filament || old_mixed != this->mixed_filaments.mixed_filaments())
build_filament_id_remap(old_mixed, old_num_filaments, num_filaments, deleting_filament, build_filament_id_remap(old_mixed, old_num_filaments, num_filaments, deleting_filament,
deleting_filament ? unsigned(to_delete_filament_id + 1) : 0u); deleting_filament ? unsigned(to_delete_filament_id + 1) : 0u);
} }

View File

@@ -16,6 +16,7 @@
#include "slic3r/GUI/WCPDownloadManager.hpp" #include "slic3r/GUI/WCPDownloadManager.hpp"
#include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/Config/Version.hpp" #include "slic3r/Config/Version.hpp"
#include "libslic3r/MixedFilament.hpp"
// Localization headers: include libslic3r version first so everything in this file // Localization headers: include libslic3r version first so everything in this file
// uses the slic3r/GUI version (the macros will take precedence over the functions). // uses the slic3r/GUI version (the macros will take precedence over the functions).
@@ -2130,6 +2131,7 @@ void GUI_App::init_app_config()
} }
#endif // _WIN32 #endif // _WIN32
} }
MixedFilamentManager::set_auto_generate_enabled(app_config->get_bool("auto_generate_gradients"));
set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level"))); set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level")));
} }

View File

@@ -1352,6 +1352,7 @@ Sidebar::Sidebar(Plater *parent)
return; return;
int filament_count = p->combos_filament.size() + 1; int filament_count = p->combos_filament.size() + 1;
wxGetApp().plater()->confirm_auto_generated_gradients(filament_count);
wxColour new_col = Plater::get_next_color_for_filament(); wxColour new_col = Plater::get_next_color_for_filament();
std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color);
@@ -6878,7 +6879,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
if (p->m_btn_add_color) if (p->m_btn_add_color)
p->m_btn_add_color->Enable(num_physical >= 2); p->m_btn_add_color->Enable(num_physical >= 2);
if (mixed.empty()) { if (num_physical < 2) {
p->m_panel_mixed_filaments_title->Hide(); p->m_panel_mixed_filaments_title->Hide();
p->m_panel_mixed_filaments_content->Hide(); p->m_panel_mixed_filaments_content->Hide();
Layout(); Layout();
@@ -6900,6 +6901,27 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
auto *rows_sizer = new wxBoxSizer(wxVERTICAL); auto *rows_sizer = new wxBoxSizer(wxVERTICAL);
rows_scroller->SetSizer(rows_sizer); rows_scroller->SetSizer(rows_sizer);
if (mixed.empty()) {
auto *empty_label = new wxStaticText(rows_scroller, wxID_ANY,
_L("No mixed filaments yet. Use Add Gradient, Add Pattern, or Add Color to create one."));
empty_label->SetForegroundColour(mixed_summary_fg);
empty_label->SetFont(::Label::Body_13);
empty_label->Wrap(FromDIP(360));
rows_sizer->Add(empty_label, 0, wxALL | wxEXPAND, FromDIP(12));
rows_scroller->Layout();
rows_scroller->FitInside();
const int empty_content_h = empty_label->GetBestSize().GetHeight() + FromDIP(28);
const int empty_rows_h = std::max(FromDIP(86), empty_content_h);
rows_scroller->SetMinSize(wxSize(-1, empty_rows_h));
rows_scroller->SetMaxSize(wxSize(-1, empty_rows_h));
if (content_sizer)
content_sizer->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(SidebarProps::ContentMargin()));
p->m_panel_mixed_filaments_content->Layout();
Layout();
refresh_model_canvas_colors();
return;
}
auto adjust_rows_scroller_height = [this, rows_scroller]() { auto adjust_rows_scroller_height = [this, rows_scroller]() {
if (!rows_scroller) if (!rows_scroller)
return; return;
@@ -7581,6 +7603,7 @@ void Sidebar::add_custom_filament(wxColour new_col) {
if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return;
int filament_count = p->combos_filament.size() + 1; int filament_count = p->combos_filament.size() + 1;
wxGetApp().plater()->confirm_auto_generated_gradients(filament_count);
std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color);
wxGetApp().plater()->on_filaments_change(filament_count); wxGetApp().plater()->on_filaments_change(filament_count);
@@ -8409,9 +8432,13 @@ struct Plater::priv
static const std::regex pattern_prusa; static const std::regex pattern_prusa;
bool m_is_dark = false; bool m_is_dark = false;
size_t m_last_auto_gradient_prompt_physical_count = 0;
bool m_last_auto_gradient_prompt_accepted = false;
priv(Plater *q, MainFrame *main_frame); priv(Plater *q, MainFrame *main_frame);
~priv(); ~priv();
bool confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical);
void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients);
bool need_update() const { return m_need_update; } bool need_update() const { return m_need_update; }
@@ -9933,9 +9960,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
"mixed_filament_surface_indentation" "mixed_filament_surface_indentation"
}; };
preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true); preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true);
if (current_num_filaments != desired_physical_filaments) if (current_num_filaments != desired_physical_filaments) {
q->confirm_auto_generated_gradients(desired_physical_filaments);
preset_bundle->set_num_filaments(unsigned(desired_physical_filaments)); preset_bundle->set_num_filaments(unsigned(desired_physical_filaments));
else } else
preset_bundle->update_multi_material_filament_presets(); preset_bundle->update_multi_material_filament_presets();
BOOST_LOG_TRIVIAL(info) << "3MF geometry import applied imported project config" BOOST_LOG_TRIVIAL(info) << "3MF geometry import applied imported project config"
<< " current_num_filaments=" << current_num_filaments << " current_num_filaments=" << current_num_filaments
@@ -9948,6 +9976,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
new_colors.assign(imported_filament_colors.begin() + current_num_filaments, new_colors.assign(imported_filament_colors.begin() + current_num_filaments,
imported_filament_colors.begin() + desired_physical_filaments); imported_filament_colors.begin() + desired_physical_filaments);
} }
q->confirm_auto_generated_gradients(desired_physical_filaments);
preset_bundle->set_num_filaments(unsigned(desired_physical_filaments), new_colors); preset_bundle->set_num_filaments(unsigned(desired_physical_filaments), new_colors);
wxGetApp().plater()->on_filaments_change(desired_physical_filaments); wxGetApp().plater()->on_filaments_change(desired_physical_filaments);
} }
@@ -9956,6 +9985,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
int filament_size = sidebar->combos_filament().size(); int filament_size = sidebar->combos_filament().size();
while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) {
int filament_count = filament_size + 1; int filament_count = filament_size + 1;
wxGetApp().plater()->confirm_auto_generated_gradients(filament_count);
wxColour new_col = Plater::get_next_color_for_filament(); wxColour new_col = Plater::get_next_color_for_filament();
std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color);
@@ -13746,6 +13776,76 @@ void Plater::priv::on_repair_model(wxCommandEvent &event)
wxGetApp().obj_list()->fix_through_netfabb(); wxGetApp().obj_list()->fix_through_netfabb();
} }
bool Plater::priv::confirm_auto_generated_gradients(wxWindow *parent, size_t num_physical)
{
auto *app_config = wxGetApp().app_config;
if (app_config == nullptr)
return MixedFilamentManager::auto_generate_enabled();
const bool pref_enabled = app_config->get_bool("auto_generate_gradients");
if (!pref_enabled) {
m_last_auto_gradient_prompt_physical_count = 0;
m_last_auto_gradient_prompt_accepted = false;
MixedFilamentManager::set_auto_generate_enabled(false);
return false;
}
if (num_physical <= 4) {
m_last_auto_gradient_prompt_physical_count = 0;
m_last_auto_gradient_prompt_accepted = false;
MixedFilamentManager::set_auto_generate_enabled(true);
return true;
}
if (parent == nullptr || !parent->IsShownOnScreen()) {
m_last_auto_gradient_prompt_physical_count = 0;
m_last_auto_gradient_prompt_accepted = false;
MixedFilamentManager::set_auto_generate_enabled(true);
return true;
}
if (m_last_auto_gradient_prompt_physical_count == num_physical) {
MixedFilamentManager::set_auto_generate_enabled(m_last_auto_gradient_prompt_accepted);
return m_last_auto_gradient_prompt_accepted;
}
const size_t auto_gradient_count = num_physical * (num_physical - 1) / 2;
const wxString message = wxString::Format(
_L("Using %d physical filaments will create %d auto-generated gradients.\nDo you want to create them now?"),
int(num_physical),
int(auto_gradient_count));
const int result = MessageDialog(parent,
message,
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Auto gradients"),
wxYES_NO | wxYES_DEFAULT | wxCENTRE | wxICON_QUESTION)
.ShowModal();
const bool accepted = result == wxID_YES;
m_last_auto_gradient_prompt_physical_count = num_physical;
m_last_auto_gradient_prompt_accepted = accepted;
MixedFilamentManager::set_auto_generate_enabled(accepted);
return accepted;
}
void Plater::priv::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients)
{
m_last_auto_gradient_prompt_physical_count = num_physical;
m_last_auto_gradient_prompt_accepted = create_auto_gradients;
MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients);
}
bool Plater::confirm_auto_generated_gradients(size_t num_physical)
{
return p != nullptr ? p->confirm_auto_generated_gradients(this, num_physical) : MixedFilamentManager::auto_generate_enabled();
}
void Plater::set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients)
{
if (p != nullptr)
p->set_auto_generated_gradient_decision(num_physical, create_auto_gradients);
else
MixedFilamentManager::set_auto_generate_enabled(create_auto_gradients);
}
void Plater::priv::on_filament_color_changed(wxCommandEvent &event) void Plater::priv::on_filament_color_changed(wxCommandEvent &event)
{ {
//q->update_all_plate_thumbnails(true); //q->update_all_plate_thumbnails(true);
@@ -13760,7 +13860,9 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event)
sidebar->auto_calc_flushing_volumes(modify_id); sidebar->auto_calc_flushing_volumes(modify_id);
} }
// Regenerate mixed filaments and update the sidebar panel. // Regenerate mixed filaments and refresh the mixed panel only. Color
// changes do not alter filament IDs, so the full on_filaments_change()
// path is unnecessary and can re-enter UI rebuilds mid-update.
wxGetApp().preset_bundle->update_multi_material_filament_presets(); wxGetApp().preset_bundle->update_multi_material_filament_presets();
sidebar->update_mixed_filament_panel(); sidebar->update_mixed_filament_panel();
} }
@@ -19647,6 +19749,8 @@ void Plater::on_filaments_change(size_t num_filaments)
update_filament_colors_in_full_config(); update_filament_colors_in_full_config();
const size_t old_num_filaments = sidebar().combos_filament().size(); const size_t old_num_filaments = sidebar().combos_filament().size();
const bool auto_generate_before = MixedFilamentManager::auto_generate_enabled();
const bool allow_auto_gradients = p->confirm_auto_generated_gradients(this, num_filaments);
auto summarize_uint_vector = [](const std::vector<unsigned int> &values, size_t max_items = 24) { auto summarize_uint_vector = [](const std::vector<unsigned int> &values, size_t max_items = 24) {
std::string out = "["; std::string out = "[";
const size_t n = std::min(values.size(), max_items); const size_t n = std::min(values.size(), max_items);
@@ -19681,6 +19785,8 @@ void Plater::on_filaments_change(size_t num_filaments)
return out; return out;
}; };
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle != nullptr && auto_generate_before && !allow_auto_gradients)
preset_bundle->update_multi_material_filament_presets(size_t(-1), old_num_filaments);
// Consume remap before sidebar refresh, which may trigger config sync // Consume remap before sidebar refresh, which may trigger config sync
// paths that regenerate mixed filaments and clear this remap buffer. // paths that regenerate mixed filaments and clear this remap buffer.
std::vector<unsigned int> id_remap; std::vector<unsigned int> id_remap;

View File

@@ -492,6 +492,8 @@ public:
void on_filaments_change(size_t extruders_count); void on_filaments_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);
bool confirm_auto_generated_gradients(size_t num_physical);
void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients);
// BBS // BBS
void on_bed_type_change(BedType bed_type); void on_bed_type_change(BedType bed_type);
bool update_filament_colors_in_full_config(); bool update_filament_colors_in_full_config();

View File

@@ -6,6 +6,7 @@
#include "MsgDialog.hpp" #include "MsgDialog.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "libslic3r/MixedFilament.hpp"
#include <wx/language.h> #include <wx/language.h>
#include <wx/notebook.h> #include <wx/notebook.h>
#include "Notebook.hpp" #include "Notebook.hpp"
@@ -778,6 +779,16 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false"); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false");
} }
if (param == "auto_generate_gradients") {
MixedFilamentManager::set_auto_generate_enabled(checkbox->GetValue());
if (wxGetApp().preset_bundle != nullptr && wxGetApp().plater() != nullptr) {
const size_t num_physical = wxGetApp().preset_bundle->filament_presets.size();
wxGetApp().plater()->set_auto_generated_gradient_decision(num_physical, checkbox->GetValue());
wxGetApp().preset_bundle->update_multi_material_filament_presets();
wxGetApp().plater()->on_filaments_change(num_physical);
}
}
#ifdef __WXMSW__ #ifdef __WXMSW__
if (param == "associate_3mf") { if (param == "associate_3mf") {
bool pbool = app_config->get("associate_3mf") == "true" ? true : false; bool pbool = app_config->get("associate_3mf") == "true" ? true : false;
@@ -1041,6 +1052,8 @@ PreferencesDialog::PreferencesDialog(wxWindow *parent, wxWindowID id, const wxSt
j["auto_flushing"] = value; j["auto_flushing"] = value;
value = wxGetApp().app_config->get("auto_calculate_when_filament_change"); value = wxGetApp().app_config->get("auto_calculate_when_filament_change");
j["auto_calculate_when_filament_change"] = value; j["auto_calculate_when_filament_change"] = value;
value = wxGetApp().app_config->get("auto_generate_gradients");
j["auto_generate_gradients"] = value;
agent->track_event("preferences_changed", j.dump()); agent->track_event("preferences_changed", j.dump());
} }
} catch(...) {} } catch(...) {}
@@ -1228,6 +1241,8 @@ wxWindow* PreferencesDialog::create_general_page()
auto item_calc_mode = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time the color changed."), page, _L("If enabled, auto-calculate every time the color changed."), 50, "auto_calculate"); auto item_calc_mode = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time the color changed."), page, _L("If enabled, auto-calculate every time the color changed."), 50, "auto_calculate");
auto item_calc_in_long_retract = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time when the filament is changed."), page, _L("If enabled, auto-calculate every time when filament is changed"), 50, "auto_calculate_when_filament_change"); auto item_calc_in_long_retract = create_item_checkbox(_L("Flushing volumes: Auto-calculate every time when the filament is changed."), page, _L("If enabled, auto-calculate every time when filament is changed"), 50, "auto_calculate_when_filament_change");
auto item_auto_generate_gradients = create_item_checkbox(_L("Mixed filaments: Auto-generate gradients."), page, _L("If enabled, Snapmaker Orca automatically creates gradient mixed filaments from physical filament pairs."), 50, "auto_generate_gradients");
auto title_full_spectrum = create_item_title(_devL("FullSpectrum"), page, _devL("FullSpectrum"));
auto item_remember_printer_config = create_item_checkbox(_L("Remember printer configuration"), page, _L("If enabled, Orca will remember and switch filament/process configuration for each printer automatically."), 50, "remember_printer_config"); auto item_remember_printer_config = create_item_checkbox(_L("Remember printer configuration"), page, _L("If enabled, Orca will remember and switch filament/process configuration for each printer automatically."), 50, "remember_printer_config");
auto item_step_mesh_setting = create_item_checkbox(_L("Show the step mesh parameter setting dialog."), page, _L("If enabled,a parameter settings dialog will appear during STEP file import."), 50, "enable_step_mesh_setting"); auto item_step_mesh_setting = create_item_checkbox(_L("Show the step mesh parameter setting dialog."), page, _L("If enabled,a parameter settings dialog will appear during STEP file import."), 50, "enable_step_mesh_setting");
auto item_multi_machine = create_item_checkbox(_L("Multi-device Management (Take effect after restarting Snapmaker Orca)."), page, _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), 50, "enable_multi_machine"); auto item_multi_machine = create_item_checkbox(_L("Multi-device Management (Take effect after restarting Snapmaker Orca)."), page, _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), 50, "enable_multi_machine");
@@ -1328,16 +1343,18 @@ wxWindow* PreferencesDialog::create_general_page()
sizer_page->Add(camera_orbit_mult, 0, wxTOP, FromDIP(3)); sizer_page->Add(camera_orbit_mult, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_show_splash_screen, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_show_splash_screen, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_hints, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_hints, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_calc_in_long_retract, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_multi_machine, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_multi_machine, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_step_mesh_setting, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_step_mesh_setting, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_auto_arrange, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_auto_arrange, 0, wxTOP, FromDIP(3));
sizer_page->Add(title_presets, 0, wxTOP | wxEXPAND, FromDIP(20)); sizer_page->Add(title_presets, 0, wxTOP | wxEXPAND, FromDIP(20));
sizer_page->Add(item_calc_mode, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_calc_mode, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_calc_in_long_retract, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_user_sync, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_user_sync, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_system_sync, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_system_sync, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_remember_printer_config, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_remember_printer_config, 0, wxTOP, FromDIP(3));
sizer_page->Add(item_save_presets, 0, wxTOP, FromDIP(3)); sizer_page->Add(item_save_presets, 0, wxTOP, FromDIP(3));
sizer_page->Add(title_full_spectrum, 0, wxTOP | wxEXPAND, FromDIP(20));
sizer_page->Add(item_auto_generate_gradients, 0, wxTOP, FromDIP(3));
//sizer_page->Add(title_network, 0, wxTOP | wxEXPAND, FromDIP(20)); //sizer_page->Add(title_network, 0, wxTOP | wxEXPAND, FromDIP(20));
//sizer_page->Add(item_check_stable_version_only, 0, wxTOP, FromDIP(3)); //sizer_page->Add(item_check_stable_version_only, 0, wxTOP, FromDIP(3));

View File

@@ -46,6 +46,22 @@ static unsigned int virtual_id_for_stable_id(const std::vector<MixedFilament> &m
return 0; return 0;
} }
struct MixedAutoGenerateGuard
{
explicit MixedAutoGenerateGuard(bool enabled)
: previous(MixedFilamentManager::auto_generate_enabled())
{
MixedFilamentManager::set_auto_generate_enabled(enabled);
}
~MixedAutoGenerateGuard()
{
MixedFilamentManager::set_auto_generate_enabled(previous);
}
bool previous = true;
};
} // namespace } // namespace
TEST_CASE("Mixed filament remap follows stable row ids when same-pair rows reorder", "[MixedFilament]") TEST_CASE("Mixed filament remap follows stable row ids when same-pair rows reorder", "[MixedFilament]")
@@ -146,6 +162,32 @@ TEST_CASE("Mixed filament grouped manual patterns normalize and round-trip", "[M
CHECK(loaded.mixed_filaments().front().mix_b_percent == 13); CHECK(loaded.mixed_filaments().front().mix_b_percent == 13);
} }
TEST_CASE("Mixed filament auto generation can be disabled without dropping custom rows", "[MixedFilament]")
{
const std::vector<std::string> colors = {"#FF0000", "#00FF00", "#0000FF"};
MixedFilamentManager enabled_mgr;
enabled_mgr.auto_generate(colors);
REQUIRE(enabled_mgr.mixed_filaments().size() == 3);
const std::string serialized_auto_rows = enabled_mgr.serialize_custom_entries();
MixedAutoGenerateGuard guard(false);
MixedFilamentManager mgr;
mgr.add_custom_filament(1, 2, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
mgr.auto_generate(colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
CHECK(mgr.mixed_filaments().front().custom);
CHECK(mgr.mixed_filaments().front().component_a == 1);
CHECK(mgr.mixed_filaments().front().component_b == 2);
MixedFilamentManager loaded;
loaded.load_custom_entries(serialized_auto_rows, colors);
CHECK(loaded.mixed_filaments().empty());
}
TEST_CASE("Mixed filament perimeter resolver uses grouped manual patterns by inset", "[MixedFilament]") TEST_CASE("Mixed filament perimeter resolver uses grouped manual patterns by inset", "[MixedFilament]")
{ {
const std::vector<std::string> colors = {"#00FFFF", "#FF00FF"}; const std::vector<std::string> colors = {"#00FFFF", "#FF00FF"};