init work to integrate OrcaSlicer-FullSpectrum fork Integrations up to commit b3c41fda41.

- libslic3r: vendor FilamentMixer; MixedFilamentManager (auto-gen, resolve,
    serialize; manual-pattern / gradient / pointillism); 19 new PrintConfig
    keys; PresetBundle owns the canonical manager with 3MF + AppConfig
    roundtrip and AMS-safe strip+restore; Print owns the slicing-time copy
    with PrintApply auto-regen on color change; TriangleSelector::
    shift_states_above + filament-id remap; inset_idx propagation through
    ExtrusionPath/Loop/MultiPath copy/assign.
  - Slicing: virtual filament IDs in painted regions (same-physical channels
    collapse when mixed_filament_region_collapse is on); ByObject
    collect_filament_data expands mixed slots; pair-cadence + whole-object +
    3+component Local-Z plan generators; LocalZOrderOptimizer utility.
  - GCode + ToolOrdering: LayerTools resolves virtual IDs through wall /
    infill / sparse / solid queries; SameLayerPointillisme in process_layer
    (uniform-segment + grouped per-perimeter-index splitters); WipeTower2
    Local-Z reservation + sub-layer G-code emission; per-layer infill
    filament override.
  - PartPlate: get_extruders* expand virtual slots into physical components;
    CLI path rebuilds a local manager from full_config.
  - GUI: five widget files extracted from FS Plater.cpp (~5000 LOC) —
    MixedMixPreview, MixedGradientSelector + WeightsDialog,
    MixedFilamentColorMapPanel, MixedFilamentColorMatchDialog (ΔE₀₀ recipe
    search), MixedFilamentConfigPanel; Sidebar Mixed Filaments panel
    (drag-reorder, enable/delete, Add Gradient/Pattern/Color); Tab exposure
    of mixed-filament / dithering / per-layer infill-override settings +
    ConfigManipulation visibility and slot-validation rules;
    BBLMixedFilamentBroken / BBLSingleExtruderMixedFilamentRisk
    notifications + slice gate; WipeTowerDialog edits physical P×P
    sub-matrix; bounds-safe extruder_id guards in 3DScene / GLCanvas3D /
    GLGizmoMmuSegmentation; change_filament merge guard and
    on_filaments_delete is_mixed_before_delete propagation.
  - Tests: 4 Catch2 tests for 3MF roundtrip (auto/custom persistence,
    PresetBundle string path, total_filaments stability); full-pipeline
    slice E2E deferred — TODO in file.

Co-authored-by: Rad <radugheorghiu96@gmail.com>
Co-authored-by: Justin Hayes <justinh@rahb.ca>
Co-authored-by: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-authored-by: xSil3nt <ahmedshazin21@gmail.com>
Co-authored-by: ratdoux <62392831+ratdoux@users.noreply.github.com>

Update TriangleSelector.cpp

Co-Authored-By: Rad <radugheorghiu96@gmail.com>
Co-Authored-By: Justin Hayes <justinh@rahb.ca>
Co-Authored-By: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-Authored-By: xSil3nt <ahmedshazin21@gmail.com>
Co-Authored-By: ratdoux <62392831+ratdoux@users.noreply.github.com>
This commit is contained in:
SoftFever
2026-06-01 09:24:53 -03:00
committed by Ian Bassi
co-authored by Rad Justin Hayes Calogero Guagenti xSil3nt ratdoux
parent 7a0c149701
commit 0e0e34c8b4
68 changed files with 18988 additions and 267 deletions
+8 -4
View File
@@ -614,6 +614,9 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
if (shader) {
if (idx == 0) {
int extruder_id = model_volume->extruder_id();
// Clamp to valid range; fall back to extruder 1 on overflow
if (extruder_id <= 0 || extruder_id > (int)extruder_colors.size())
extruder_id = 1;
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]);
if (ban_light) {
@@ -623,7 +626,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
// shader->set_uniform("uniform_color", new_color);
}
else {
if (idx <= extruder_colors.size()) {
if (idx <= (int)extruder_colors.size()) {
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]);
if (ban_light) {
@@ -854,7 +857,7 @@ int GLVolumeCollection::load_wipe_tower_preview(
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
TriangleMesh wipe_tower_shell = make_cube(width, depth, height);
for (int extruder_id : plate_extruders) {
if (extruder_id <= extruder_colors.size())
if (extruder_id >= 1 && extruder_id <= (int)extruder_colors.size())
colors.push_back(extruder_colors[extruder_id - 1]);
else
colors.push_back(extruder_colors[0]);
@@ -895,8 +898,9 @@ int GLVolumeCollection::load_real_wipe_tower_preview(
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
std::vector<Slic3r::ColorRGBA> colors;
if (!plate_extruders.empty()) {
if (plate_extruders.front() <= extruder_colors.size())
colors.push_back(extruder_colors[plate_extruders.front() - 1]);
const int front_id = plate_extruders.front();
if (front_id >= 1 && front_id <= (int)extruder_colors.size())
colors.push_back(extruder_colors[front_id - 1]);
else
colors.push_back(extruder_colors[0]);
}
+81 -13
View File
@@ -479,20 +479,24 @@ 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]);
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (opt != nullptr) {
if (opt->getInt() > filament_cnt) {
// BBS: Rule 1 — reject out-of-range AND mixed filament IDs in support/wall/infill slots.
// Mixed filaments (virtual IDs > num_phys) cannot be used in these roles; reset to 0.
{
static const char* filament_slot_keys[] = {
"support_filament", "support_interface_filament",
"wall_filament", "sparse_infill_filament", "solid_infill_filament"
};
size_t total = wxGetApp().preset_bundle->total_filament_count();
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
for (auto key : filament_slot_keys) {
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (!opt) continue;
int val = opt->getInt();
bool out_of_range = val > (int)total;
bool is_mixed = (val > (int)num_phys && val <= (int)total);
if (out_of_range || is_mixed) {
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);
}
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
new_conf.set_key_value(key, new ConfigOptionInt(0));
apply(config, &new_conf);
}
}
@@ -541,6 +545,40 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
// Rule 2 — Local-Z dithering is incompatible with mixed-filament region collapse.
// When dithering_local_z_mode is on, force mixed_filament_region_collapse off.
if (config->has("dithering_local_z_mode") && config->has("mixed_filament_region_collapse") &&
config->opt_bool("dithering_local_z_mode") &&
config->opt_bool("mixed_filament_region_collapse")) {
DynamicPrintConfig new_conf = *config;
new_conf.set_key_value("mixed_filament_region_collapse", new ConfigOptionBool(false));
apply(config, &new_conf);
}
// Rule 3 — One-time warning when Local-Z dithering is enabled alongside variable layer height.
{
static bool s_local_z_varlay_warned = false;
bool dithering_on = config->has("dithering_local_z_mode") &&
config->opt_bool("dithering_local_z_mode");
if (dithering_on && !s_local_z_varlay_warned) {
bool has_var = false;
for (const auto* obj : wxGetApp().plater()->model().objects)
if (obj->layer_height_profile.get().size() > 4) { has_var = true; break; }
if (has_var) {
MessageDialog dlg(m_msg_dlg_parent,
_L("Using variable layer height together with Local-Z dithering "
"may result in poor color mixing quality."),
"", wxICON_WARNING | wxOK);
is_msg_dlg_already_exist = true;
dlg.ShowModal();
is_msg_dlg_already_exist = false;
s_local_z_varlay_warned = true;
}
}
if (!dithering_on)
s_local_z_varlay_warned = false;
}
}
void ConfigManipulation::apply_null_fff_config(DynamicPrintConfig *config, std::vector<std::string> const &keys, std::map<ObjectBase *, ModelConfig *> const &configs)
@@ -978,6 +1016,36 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle);
toggle_line("enable_wrapping_detection", DevPrinterConfigUtil::support_wrapping_detection(printer_type));
// Mixed-filament / dithering visibility rules.
const bool local_z_dithering_on =
config->has("dithering_local_z_mode") && config->option("dithering_local_z_mode") != nullptr &&
config->opt_bool("dithering_local_z_mode");
toggle_line("dithering_local_z_whole_objects", local_z_dithering_on);
toggle_line("dithering_local_z_direct_multicolor", local_z_dithering_on);
// local_z_wipe_tower_purge_lines: only when prime tower + local-Z + non-BBL
toggle_line("local_z_wipe_tower_purge_lines",
config->has("enable_prime_tower") && config->opt_bool("enable_prime_tower") &&
local_z_dithering_on && !is_BBL_Printer);
// mixed_filament_surface_indentation only when bias is enabled
const bool component_bias_enabled =
config->has("mixed_filament_component_bias_enabled") &&
config->option("mixed_filament_component_bias_enabled") != nullptr &&
config->opt_bool("mixed_filament_component_bias_enabled");
toggle_line("mixed_filament_surface_indentation", component_bias_enabled);
// infill override sub-options gated by enable_infill_filament_override
const bool show_infill_filament_override_v =
!is_global_config && have_infill && !bSEMM;
const bool show_infill_filament_details_v =
show_infill_filament_override_v &&
config->has("enable_infill_filament_override") &&
config->option("enable_infill_filament_override") != nullptr &&
config->opt_bool("enable_infill_filament_override");
toggle_line("infill_filament_use_base_first_layers", show_infill_filament_details_v);
toggle_line("infill_filament_use_base_last_layers", show_infill_filament_details_v);
}
void ConfigManipulation::update_print_sla_config(DynamicPrintConfig* config, const bool is_global_config/* = false*/)
+19 -1
View File
@@ -2561,7 +2561,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
if (!model_volume.is_model_part())
continue;
unsigned int filaments_count = (unsigned int)dynamic_cast<const ConfigOptionStrings*>(m_config->option("filament_colour"))->values.size();
unsigned int filament_colour_size = (unsigned int)dynamic_cast<const ConfigOptionStrings*>(m_config->option("filament_colour"))->values.size();
unsigned int filaments_count = std::max(filament_colour_size, (unsigned int)wxGetApp().preset_bundle->total_filament_count());
model_volume.update_extruder_count(filaments_count);
}
}
@@ -2831,6 +2832,23 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
volume->set_sla_shift_z(shift_zs[volume->object_idx()]);
}
// BBS: single-extruder mixed filament risk notification
if (printer_technology == ptFFF && wxGetApp().preset_bundle) {
const size_t total_filaments = wxGetApp().preset_bundle->total_filament_count();
const size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
const bool any_mixed = total_filaments > num_phys;
auto* printer_extruder_id_opt = wxGetApp().preset_bundle->printers.get_edited_preset()
.config.option<ConfigOptionInts>("printer_extruder_id");
const int printer_extruders_count = printer_extruder_id_opt ? (int)printer_extruder_id_opt->values.size() : 1;
auto& nm = *wxGetApp().plater()->get_notification_manager();
if (printer_extruders_count == 1 && any_mixed)
nm.push_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk,
NotificationManager::NotificationLevel::WarningNotificationLevel,
_u8L("Mixed filaments are unreliable on a single-extruder printer."));
else
nm.close_notification_of_type(NotificationType::BBLSingleExtruderMixedFilamentRisk);
}
// BBS
if (printer_technology == ptFFF && m_config->has("filament_colour") && (m_canvas_type != ECanvasType::CanvasAssembleView)) {
// Should the wipe tower be visualized ?
+6
View File
@@ -75,6 +75,7 @@
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "GUI.hpp"
#include "GUI_Utils.hpp"
@@ -2950,6 +2951,11 @@ bool GUI_App::on_init_inner()
// BBS if load user preset failed
//if (loaded_preset_result != 0) {
try {
// Apply the user's auto_generate_gradients preference before load_presets
// triggers PresetBundle::sync_mixed_filaments_from_config, which calls
// MixedFilamentManager::auto_generate. The static atomic defaults to true,
// so without this the preference is ignored on the initial preset load.
MixedFilamentManager::set_auto_generate_enabled(app_config->get_bool("auto_generate_gradients"));
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
// If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force
// installation of a compatible system preset, thus nullifying the system preset substitutions.
+2 -1
View File
@@ -2280,8 +2280,9 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
item_name << " (" + _L("current") + ")";
}
wxBitmap bm = (i == 0 || size_t(i - 1) >= icons.size()) ? wxNullBitmap : *icons[i - 1];
append_menu_item(extruder_selection_menu, wxID_ANY, item_name, "",
[i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, i == 0 ? wxNullBitmap : *icons[i - 1], menu,
[i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, bm, menu,
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
}
menu->Append(wxID_ANY, name, extruder_selection_menu, _L("Change Filament"));
+20 -5
View File
@@ -1,4 +1,5 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_Factories.hpp"
@@ -75,9 +76,20 @@ static DynamicPrintConfig& printer_config()
return wxGetApp().preset_bundle->printers.get_edited_preset().config;
}
static size_t total_filaments_count(size_t physical_count)
{
if (wxGetApp().preset_bundle == nullptr)
return physical_count;
return wxGetApp().preset_bundle->mixed_filaments.total_filaments(physical_count);
}
static int filaments_count()
{
return wxGetApp().filaments_cnt();
if (wxGetApp().preset_bundle == nullptr)
return 0;
return static_cast<int>(total_filaments_count(size_t(std::max(wxGetApp().filaments_cnt(), 0))));
}
static void take_snapshot(const std::string& snapshot_name)
@@ -1000,16 +1012,18 @@ void ObjectList::update_objects_list_filament_column(size_t filaments_count)
if (printer_technology() == ptSLA)
filaments_count = 1;
const size_t total_filaments = total_filaments_count(filaments_count);
m_prevent_update_filament_in_config = true;
// BBS: update extruder values even when filaments_count is 1, because it may be reduced from value greater than 1
// Orca: update extruder values even when total_filaments is 1, because it may be reduced from value greater than 1
if (m_objects)
update_filament_values_for_items(filaments_count);
update_filament_values_for_items(total_filaments);
update_filament_colors();
// set show/hide for this column
set_filament_column_hidden(filaments_count == 1);
set_filament_column_hidden(total_filaments == 1);
//a workaround for a wrong last column width updating under OSX
auto em = em_unit(this);
GetColumn(colEditing)->SetWidth(m_columns_width[colEditing]*em);
@@ -1020,6 +1034,7 @@ void ObjectList::update_objects_list_filament_column(size_t filaments_count)
void ObjectList::update_objects_list_filament_column_when_delete_filament(size_t filament_id, size_t filaments_count, int replace_filament_id)
{
m_prevent_update_filament_in_config = true;
size_t total_filaments = total_filaments_count(filaments_count);
// BBS: update extruder values even when filaments_count is 1, because it may be reduced from value greater than 1
if (m_objects)
@@ -1028,7 +1043,7 @@ void ObjectList::update_objects_list_filament_column_when_delete_filament(size_t
update_filament_colors();
// set show/hide for this column
set_filament_column_hidden(filaments_count == 1);
set_filament_column_hidden(total_filaments == 1);
// a workaround for a wrong last column width updating under OSX
GetColumn(colEditing)->SetWidth(25);
+30 -31
View File
@@ -2809,50 +2809,49 @@ int ObjectTablePanel::init_bitmap()
int ObjectTablePanel::init_filaments_and_colors()
{
//DynamicPrintConfig& global_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
const DynamicPrintConfig* global_config = m_plater->config();
const std::vector<std::string> filament_presets = wxGetApp().preset_bundle->filament_presets;
m_filaments_count = filament_presets.size();
const std::vector<std::string> filament_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config();
m_filaments_count = filament_colors.size();
if (m_filaments_count <= 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") %m_filaments_count;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") % m_filaments_count;
set_default_filaments_and_colors();
return -1;
}
const ConfigOptionStrings* filament_opt = dynamic_cast<const ConfigOptionStrings*>(global_config->option("filament_colour"));
if (filament_opt == nullptr) {
set_default_filaments_and_colors();
return -1;
}
m_filaments_colors.resize(m_filaments_count);
m_filaments_name.resize(m_filaments_count);
unsigned int color_count = filament_opt->values.size();
if (color_count != m_filaments_count) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", invalid color count:%1%, extruder count: %2%") %color_count %m_filaments_count;
}
unsigned int i = 0;
const size_t physical_count = filament_presets.size();
ColorRGB rgb;
while (i < m_filaments_count) {
const std::string& txt_color = global_config->opt_string("filament_colour", i);
if (i < color_count) {
if (decode_color(txt_color, rgb))
{
m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar());
}
else
{
m_filaments_colors[i] = *wxGREEN;
}
}
else {
for (int i = 0; i < (int)m_filaments_count; ++i) {
if (size_t(i) < filament_colors.size() && decode_color(filament_colors[size_t(i)], rgb))
m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar());
else
m_filaments_colors[i] = *wxGREEN;
if (size_t(i) < physical_count) {
m_filaments_name[i] = wxString(std::to_string(i + 1) + ": " + filament_presets[size_t(i)]);
continue;
}
//parse the filaments
m_filaments_name[i] = wxString(std::to_string(i+1) + ": " + filament_presets[i]);
// Mixed-slot row: walk the manager and find the (physical_count + offset)-th enabled, non-deleted entry.
size_t mixed_offset = 0;
for (const MixedFilament &mf : wxGetApp().preset_bundle->mixed_filaments.mixed_filaments()) {
if (!mf.enabled || mf.deleted)
continue;
if (size_t(i) != physical_count + mixed_offset) {
++mixed_offset;
continue;
}
i++;
m_filaments_name[i] = wxString::Format("%d: Mixed Filament %d (F%u + F%u)",
i + 1, i + 1,
unsigned(mf.component_a), unsigned(mf.component_b));
break;
}
if (m_filaments_name[i].empty())
m_filaments_name[i] = wxString::Format("%d: Filament %d", i + 1, i + 1);
}
return 0;
@@ -179,13 +179,14 @@ void GLGizmoMmuSegmentation::data_changed(bool is_serializing)
ModelObject* model_object = m_c->selection_info()->model_object();
int prev_extruders_count = int(m_extruders_colors.size());
if (prev_extruders_count != wxGetApp().filaments_cnt()) {
if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT))
int cur_filaments_count = int(wxGetApp().preset_bundle->total_filament_count());
if (prev_extruders_count != cur_filaments_count) {
if (cur_filaments_count > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT))
show_notification_extruders_limit_exceeded();
this->init_extruders_data();
// Reinitialize triangle selectors because of change of extruder count need also change the size of GLIndexedVertexArray
if (prev_extruders_count != wxGetApp().filaments_cnt())
if (prev_extruders_count != int(m_extruders_colors.size()))
this->init_model_triangle_selectors();
} else if (wxGetApp().plater()->get_extruders_colors() != m_extruders_colors) {
this->init_extruders_data();
@@ -731,6 +732,8 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
continue;
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
extruder_idx = std::min(extruder_idx, (int)m_extruders_colors.size() - 1);
if (extruder_idx < 0) 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 +756,7 @@ 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);
extruder_color_idx = std::min(extruder_color_idx, (int)m_extruders_colors.size() - 1);
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());
@@ -767,7 +771,8 @@ void GLGizmoMmuSegmentation::update_from_model_object(bool first_update)
// Extruder colors need to be reloaded before calling init_model_triangle_selectors to render painted triangles
// using colors from loaded 3MF and not from printer profile in Slicer.
if (int prev_extruders_count = int(m_extruders_colors.size());
prev_extruders_count != wxGetApp().filaments_cnt() || wxGetApp().plater()->get_extruders_colors() != m_extruders_colors)
prev_extruders_count != int(wxGetApp().preset_bundle->total_filament_count()) ||
wxGetApp().plater()->get_extruders_colors() != m_extruders_colors)
this->init_extruders_data();
this->init_model_triangle_selectors();
+3
View File
@@ -2221,6 +2221,9 @@ bool MainFrame::get_enable_slice_status()
}
}
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;
}
@@ -0,0 +1,810 @@
// MixedFilamentColorMapPanel.cpp
// Extracted verbatim from FullSpectrum Plater.cpp:3087-3781 (Task 17).
#include "MixedFilamentColorMapPanel.hpp"
#include "libslic3r/filament_mixer.h"
#include <wx/dcbuffer.h>
#include <wx/event.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <numeric>
#include <vector>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helpers — free functions used only by this widget.
// Copied verbatim from FullSpectrum Plater.cpp:2443-2640.
// ---------------------------------------------------------------------------
namespace {
wxColour blend_multi_filament_mixer(const std::vector<wxColour> &colors, const std::vector<double> &weights)
{
if (colors.empty() || weights.empty())
return wxColour("#26A69A");
unsigned char out_r = 0;
unsigned char out_g = 0;
unsigned char out_b = 0;
double accumulated_weight = 0.0;
bool has_color = false;
for (size_t i = 0; i < colors.size() && i < weights.size(); ++i) {
const double weight = std::max(0.0, weights[i]);
if (weight <= 0.0)
continue;
const wxColour safe = colors[i].IsOk() ? colors[i] : wxColour("#26A69A");
const unsigned char r = static_cast<unsigned char>(safe.Red());
const unsigned char g = static_cast<unsigned char>(safe.Green());
const unsigned char b = static_cast<unsigned char>(safe.Blue());
if (!has_color) {
out_r = r;
out_g = g;
out_b = b;
accumulated_weight = weight;
has_color = true;
continue;
}
const double new_total = accumulated_weight + weight;
if (new_total <= 0.0)
continue;
const float t = float(weight / new_total);
::Slic3r::filament_mixer_lerp(out_r, out_g, out_b, r, g, b, t, &out_r, &out_g, &out_b);
accumulated_weight = new_total;
}
if (!has_color)
return wxColour("#26A69A");
return wxColour(out_r, out_g, out_b);
}
std::vector<int> normalize_color_match_weights(const std::vector<int> &weights, size_t count)
{
std::vector<int> out = weights;
if (out.size() != count)
out.assign(count, count > 0 ? int(100 / int(count)) : 0);
int sum = 0;
for (int &value : out) {
value = std::max(0, value);
sum += value;
}
if (sum <= 0 && count > 0) {
out.assign(count, 0);
out[0] = 100;
return out;
}
std::vector<double> remainders(count, 0.0);
int assigned = 0;
for (size_t idx = 0; idx < count; ++idx) {
const double exact = 100.0 * double(out[idx]) / double(sum);
out[idx] = int(std::floor(exact));
remainders[idx] = exact - double(out[idx]);
assigned += out[idx];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_remainder = -1.0;
for (size_t idx = 0; idx < remainders.size(); ++idx) {
if (remainders[idx] > best_remainder) {
best_remainder = remainders[idx];
best_idx = idx;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
bool color_match_raw_weights_within_range(const std::vector<double> &weights, int min_component_percent)
{
if (min_component_percent <= 0)
return true;
const double min_allowed = double(std::clamp(min_component_percent, 0, 50));
int active_components = 0;
for (const double weight : weights) {
if (weight <= 1e-4)
continue;
++active_components;
if (weight * 100.0 + 1e-6 < min_allowed)
return false;
}
return active_components >= 2;
}
} // anonymous namespace
// ===========================================================================
// MixedFilamentColorMapPanel — implementation
// Verbatim from FullSpectrum Plater.cpp:3087-3781.
// ===========================================================================
MixedFilamentColorMapPanel::MixedFilamentColorMapPanel(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights,
const wxSize &min_size)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, min_size, wxBORDER_SIMPLE)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(min_size);
m_render_timer.SetOwner(this);
m_colors.reserve(filament_ids.size());
for (const unsigned int filament_id : filament_ids) {
if (filament_id >= 1 && filament_id <= palette.size())
m_colors.emplace_back(palette[filament_id - 1]);
else
m_colors.emplace_back(wxColour("#26A69A"));
}
if (m_colors.empty())
m_colors.emplace_back(wxColour("#26A69A"));
set_normalized_weights(initial_weights, false);
Bind(wxEVT_PAINT, &MixedFilamentColorMapPanel::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &MixedFilamentColorMapPanel::on_left_down, this);
Bind(wxEVT_LEFT_UP, &MixedFilamentColorMapPanel::on_left_up, this);
Bind(wxEVT_MOTION, &MixedFilamentColorMapPanel::on_mouse_move, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedFilamentColorMapPanel::on_capture_lost, this);
Bind(wxEVT_SIZE, &MixedFilamentColorMapPanel::on_size, this);
Bind(wxEVT_TIMER, &MixedFilamentColorMapPanel::on_render_timer, this, m_render_timer.GetId());
}
MixedFilamentColorMapPanel::~MixedFilamentColorMapPanel()
{
if (HasCapture())
ReleaseMouse();
if (m_render_timer.IsRunning())
m_render_timer.Stop();
}
std::vector<int> MixedFilamentColorMapPanel::normalized_weights() const
{
return m_weights;
}
wxColour MixedFilamentColorMapPanel::selected_color() const
{
std::vector<double> weights;
weights.reserve(m_weights.size());
for (const int weight : m_weights)
weights.emplace_back(double(std::max(0, weight)));
return blend_multi_filament_mixer(m_colors, weights);
}
void MixedFilamentColorMapPanel::set_normalized_weights(const std::vector<int> &weights, bool notify)
{
m_weights = normalize_color_match_weights(weights, m_colors.size());
initialize_cursor_from_weights();
Refresh();
if (notify)
emit_changed();
}
void MixedFilamentColorMapPanel::set_min_component_percent(int min_component_percent)
{
const int clamped = std::clamp(min_component_percent, 0, 50);
if (m_min_component_percent == clamped)
return;
m_min_component_percent = clamped;
invalidate_cached_bitmap();
Refresh();
}
// ---------------------------------------------------------------------------
// Private: geometry helpers
// ---------------------------------------------------------------------------
MixedFilamentColorMapPanel::GeometryMode MixedFilamentColorMapPanel::geometry_mode() const
{
if (m_colors.size() <= 1)
return GeometryMode::Point;
if (m_colors.size() == 2)
return GeometryMode::Line;
if (m_colors.size() == 3)
return GeometryMode::Triangle;
if (m_colors.size() == 4)
return GeometryMode::TriangleWithCenter;
return GeometryMode::Radial;
}
wxRect MixedFilamentColorMapPanel::canvas_rect() const
{
const wxSize size = GetClientSize();
return wxRect(0, 0, std::max(1, size.GetWidth()), std::max(1, size.GetHeight()));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::make_vec(double x, double y)
{
return Vec2 { x, y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::add_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return Vec2 { lhs.x + rhs.x, lhs.y + rhs.y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::sub_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return Vec2 { lhs.x - rhs.x, lhs.y - rhs.y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::scale_vec(const Vec2 &value, double factor)
{
return Vec2 { value.x * factor, value.y * factor };
}
double MixedFilamentColorMapPanel::dot_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y;
}
double MixedFilamentColorMapPanel::length_sq(const Vec2 &value)
{
return dot_vec(value, value);
}
double MixedFilamentColorMapPanel::dist_sq(const Vec2 &lhs, const Vec2 &rhs)
{
return length_sq(sub_vec(lhs, rhs));
}
std::array<MixedFilamentColorMapPanel::Vec2, 3> MixedFilamentColorMapPanel::simplex_vertices() const
{
return { make_vec(0.50, 0.05), make_vec(0.08, 0.94), make_vec(0.92, 0.94) };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::simplex_center() const
{
const auto vertices = simplex_vertices();
return make_vec((vertices[0].x + vertices[1].x + vertices[2].x) / 3.0,
(vertices[0].y + vertices[1].y + vertices[2].y) / 3.0);
}
std::vector<MixedFilamentColorMapPanel::AnchorPoint> MixedFilamentColorMapPanel::radial_anchor_points() const
{
std::vector<AnchorPoint> anchors;
const size_t count = m_colors.size();
anchors.reserve(count);
if (count == 0)
return anchors;
if (count == 1) {
anchors.emplace_back(AnchorPoint { 0.5, 0.5 });
return anchors;
}
if (count == 2) {
anchors.emplace_back(AnchorPoint { 0.0, 0.5 });
anchors.emplace_back(AnchorPoint { 1.0, 0.5 });
return anchors;
}
if (count == 3) {
anchors.emplace_back(AnchorPoint { 0.0, 0.5 });
anchors.emplace_back(AnchorPoint { 1.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 1.0 });
return anchors;
}
if (count == 4) {
anchors.emplace_back(AnchorPoint { 0.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 1.0 });
anchors.emplace_back(AnchorPoint { 0.0, 1.0 });
return anchors;
}
constexpr double k_pi = 3.14159265358979323846;
const double center_x = 0.5;
const double center_y = 0.5;
const double radius = 0.45;
for (size_t idx = 0; idx < count; ++idx) {
const double angle = (2.0 * k_pi * double(idx)) / double(count);
anchors.emplace_back(AnchorPoint { center_x + radius * std::cos(angle), center_y + radius * std::sin(angle) });
}
return anchors;
}
std::vector<MixedFilamentColorMapPanel::AnchorPoint> MixedFilamentColorMapPanel::anchor_points() const
{
std::vector<AnchorPoint> anchors;
switch (geometry_mode()) {
case GeometryMode::Point:
anchors.emplace_back(AnchorPoint { 0.5, 0.5 });
break;
case GeometryMode::Line:
anchors.emplace_back(AnchorPoint { 0.06, 0.5 });
anchors.emplace_back(AnchorPoint { 0.94, 0.5 });
break;
case GeometryMode::Triangle: {
const auto vertices = simplex_vertices();
for (const Vec2 &vertex : vertices)
anchors.emplace_back(AnchorPoint { vertex.x, vertex.y });
break;
}
case GeometryMode::TriangleWithCenter: {
const auto vertices = simplex_vertices();
for (const Vec2 &vertex : vertices)
anchors.emplace_back(AnchorPoint { vertex.x, vertex.y });
const Vec2 center = simplex_center();
anchors.emplace_back(AnchorPoint { center.x, center.y });
break;
}
case GeometryMode::Radial:
anchors = radial_anchor_points();
break;
}
return anchors;
}
std::array<double, 3> MixedFilamentColorMapPanel::triangle_barycentric(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
const Vec2 &a = triangle[0];
const Vec2 &b = triangle[1];
const Vec2 &c = triangle[2];
const double denom = ((b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y));
if (std::abs(denom) <= 1e-9)
return { 1.0, 0.0, 0.0 };
const double w0 = ((b.y - c.y) * (point.x - c.x) + (c.x - b.x) * (point.y - c.y)) / denom;
const double w1 = ((c.y - a.y) * (point.x - c.x) + (a.x - c.x) * (point.y - c.y)) / denom;
const double w2 = 1.0 - w0 - w1;
return { w0, w1, w2 };
}
bool MixedFilamentColorMapPanel::point_in_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
const auto barycentric = triangle_barycentric(point, triangle);
constexpr double eps = 1e-6;
return barycentric[0] >= -eps && barycentric[1] >= -eps && barycentric[2] >= -eps;
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::closest_point_on_segment(const Vec2 &point, const Vec2 &start, const Vec2 &end)
{
const Vec2 edge = sub_vec(end, start);
const double edge_len_sq = length_sq(edge);
if (edge_len_sq <= 1e-9)
return start;
const double t = std::clamp(dot_vec(sub_vec(point, start), edge) / edge_len_sq, 0.0, 1.0);
return add_vec(start, scale_vec(edge, t));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::closest_point_on_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
if (point_in_triangle(point, triangle))
return point;
Vec2 best = triangle[0];
double best_dist = std::numeric_limits<double>::max();
for (int edge_idx = 0; edge_idx < 3; ++edge_idx) {
const Vec2 candidate = closest_point_on_segment(point, triangle[edge_idx], triangle[(edge_idx + 1) % 3]);
const double candidate_dist = dist_sq(point, candidate);
if (candidate_dist < best_dist) {
best_dist = candidate_dist;
best = candidate;
}
}
return best;
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::normalized_point_from_mouse(const wxMouseEvent &evt) const
{
const wxRect rect = canvas_rect();
const int width = std::max(1, rect.GetWidth() - 1);
const int height = std::max(1, rect.GetHeight() - 1);
return make_vec(
std::clamp(double(evt.GetX() - rect.GetLeft()) / double(width), 0.0, 1.0),
std::clamp(double(evt.GetY() - rect.GetTop()) / double(height), 0.0, 1.0));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::clamp_point_to_geometry(const Vec2 &point) const
{
switch (geometry_mode()) {
case GeometryMode::Point:
return make_vec(0.5, 0.5);
case GeometryMode::Line:
return make_vec(std::clamp(point.x, 0.0, 1.0), 0.5);
case GeometryMode::Triangle:
case GeometryMode::TriangleWithCenter:
return closest_point_on_triangle(point, simplex_vertices());
case GeometryMode::Radial:
return make_vec(std::clamp(point.x, 0.0, 1.0), std::clamp(point.y, 0.0, 1.0));
}
return point;
}
std::vector<double> MixedFilamentColorMapPanel::simplex_weights_from_pos(const Vec2 &point) const
{
const auto triangle = simplex_vertices();
const Vec2 clamped = closest_point_on_triangle(point, triangle);
const auto barycentric = triangle_barycentric(clamped, triangle);
if (geometry_mode() == GeometryMode::Triangle)
return { std::max(0.0, barycentric[0]), std::max(0.0, barycentric[1]), std::max(0.0, barycentric[2]) };
const double shared = std::max(0.0, std::min({ barycentric[0], barycentric[1], barycentric[2] }));
return {
std::max(0.0, barycentric[0] - shared),
std::max(0.0, barycentric[1] - shared),
std::max(0.0, barycentric[2] - shared),
std::max(0.0, shared * 3.0)
};
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::triangle_point_from_weights() const
{
const auto vertices = simplex_vertices();
double total = 0.0;
for (size_t idx = 0; idx < 3 && idx < m_weights.size(); ++idx)
total += std::max(0, m_weights[idx]);
if (total <= 0.0)
return simplex_center();
Vec2 out = make_vec(0.0, 0.0);
for (size_t idx = 0; idx < 3 && idx < m_weights.size(); ++idx) {
const double weight = double(std::max(0, m_weights[idx])) / total;
out = add_vec(out, scale_vec(vertices[idx], weight));
}
return out;
}
void MixedFilamentColorMapPanel::initialize_cursor_from_grid_search()
{
double best_x = 0.5;
double best_y = 0.5;
double best_error = std::numeric_limits<double>::max();
constexpr int grid = 96;
for (int y_idx = 0; y_idx <= grid; ++y_idx) {
for (int x_idx = 0; x_idx <= grid; ++x_idx) {
const Vec2 point = clamp_point_to_geometry(make_vec(double(x_idx) / double(grid), double(y_idx) / double(grid)));
const std::vector<int> probe = normalized_weights_from_pos(point.x, point.y);
if (probe.size() != m_weights.size())
continue;
double error = 0.0;
for (size_t idx = 0; idx < probe.size(); ++idx) {
const double delta = double(probe[idx] - m_weights[idx]);
error += delta * delta;
}
if (error < best_error) {
best_error = error;
best_x = point.x;
best_y = point.y;
}
}
}
m_cursor_x = best_x;
m_cursor_y = best_y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
}
std::vector<double> MixedFilamentColorMapPanel::raw_weights_from_pos(double normalized_x, double normalized_y) const
{
switch (geometry_mode()) {
case GeometryMode::Point:
return { 1.0 };
case GeometryMode::Line: {
const double t = std::clamp(normalized_x, 0.0, 1.0);
return { 1.0 - t, t };
}
case GeometryMode::Triangle:
case GeometryMode::TriangleWithCenter:
return simplex_weights_from_pos(make_vec(normalized_x, normalized_y));
case GeometryMode::Radial:
break;
}
const std::vector<AnchorPoint> anchors = radial_anchor_points();
std::vector<double> out(anchors.size(), 0.0);
if (anchors.empty())
return out;
constexpr double eps = 1e-8;
size_t exact_idx = size_t(-1);
for (size_t idx = 0; idx < anchors.size(); ++idx) {
const double dx = normalized_x - anchors[idx].x;
const double dy = normalized_y - anchors[idx].y;
const double d2 = dx * dx + dy * dy;
if (d2 <= eps) {
exact_idx = idx;
break;
}
out[idx] = 1.0 / std::max(1e-6, d2);
}
if (exact_idx != size_t(-1)) {
std::fill(out.begin(), out.end(), 0.0);
out[exact_idx] = 1.0;
return out;
}
double sum = 0.0;
for (const double value : out)
sum += value;
if (sum <= 0.0) {
out.assign(out.size(), 0.0);
out[0] = 1.0;
return out;
}
for (double &value : out)
value /= sum;
return out;
}
std::vector<int> MixedFilamentColorMapPanel::normalized_weights_from_pos(double normalized_x, double normalized_y) const
{
std::vector<int> raw_weights;
const std::vector<double> raw = raw_weights_from_pos(normalized_x, normalized_y);
raw_weights.reserve(raw.size());
for (const double value : raw)
raw_weights.emplace_back(std::max(0, int(std::lround(value * 100.0))));
return normalize_color_match_weights(raw_weights, raw.size());
}
void MixedFilamentColorMapPanel::initialize_cursor_from_weights()
{
if (m_weights.empty()) {
m_cursor_x = 0.5;
m_cursor_y = 0.5;
return;
}
switch (geometry_mode()) {
case GeometryMode::Point:
m_cursor_x = 0.5;
m_cursor_y = 0.5;
break;
case GeometryMode::Line: {
const int total = std::accumulate(m_weights.begin(), m_weights.end(), 0);
const double t = total > 0 && m_weights.size() >= 2 ? double(std::max(0, m_weights[1])) / double(total) : 0.5;
m_cursor_x = std::clamp(t, 0.0, 1.0);
m_cursor_y = 0.5;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
break;
}
case GeometryMode::Triangle: {
const Vec2 point = triangle_point_from_weights();
m_cursor_x = point.x;
m_cursor_y = point.y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
break;
}
case GeometryMode::TriangleWithCenter:
case GeometryMode::Radial:
initialize_cursor_from_grid_search();
break;
}
}
// ---------------------------------------------------------------------------
// Private: interaction + rendering
// ---------------------------------------------------------------------------
void MixedFilamentColorMapPanel::emit_changed()
{
wxCommandEvent evt(wxEVT_SLIDER, GetId());
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
void MixedFilamentColorMapPanel::update_from_mouse(const wxMouseEvent &evt, bool notify)
{
const Vec2 point = clamp_point_to_geometry(normalized_point_from_mouse(evt));
m_cursor_x = point.x;
m_cursor_y = point.y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
Refresh();
if (notify)
emit_changed();
}
wxColour MixedFilamentColorMapPanel::canvas_background_color() const
{
return GetBackgroundColour().IsOk() ? GetBackgroundColour() : wxColour(245, 245, 245);
}
bool MixedFilamentColorMapPanel::cached_bitmap_matches(const wxSize &size, const wxColour &background) const
{
return m_cached_bitmap.IsOk() && m_cached_bitmap_size == size && m_cached_background == background;
}
void MixedFilamentColorMapPanel::schedule_cached_bitmap_render()
{
if (!m_render_timer.IsRunning())
m_render_timer.StartOnce(80);
}
void MixedFilamentColorMapPanel::invalidate_cached_bitmap()
{
m_cached_bitmap = wxBitmap();
m_cached_bitmap_size = wxSize();
m_cached_background = wxColour();
}
void MixedFilamentColorMapPanel::render_cached_bitmap(const wxSize &size, const wxColour &background)
{
const int width = size.GetWidth();
const int height = size.GetHeight();
if (width <= 0 || height <= 0)
return;
wxImage image(width, height);
unsigned char *data = image.GetData();
if (data != nullptr) {
for (int y = 0; y < height; ++y) {
const double normalized_y = (height > 1) ? double(y) / double(height - 1) : 0.5;
for (int x = 0; x < width; ++x) {
const double normalized_x = (width > 1) ? double(x) / double(width - 1) : 0.5;
const int data_idx = (y * width + x) * 3;
bool paint_pixel = true;
if (geometry_mode() == GeometryMode::Triangle || geometry_mode() == GeometryMode::TriangleWithCenter)
paint_pixel = point_in_triangle(make_vec(normalized_x, normalized_y), simplex_vertices());
const std::vector<double> raw_weights = raw_weights_from_pos(normalized_x, normalized_y);
wxColour color = paint_pixel ? blend_multi_filament_mixer(m_colors, raw_weights) : background;
if (paint_pixel && m_min_component_percent > 0 &&
!color_match_raw_weights_within_range(raw_weights, m_min_component_percent)) {
const bool stripe = (((x + y) / 8) % 2) == 0;
const double factor = stripe ? 0.12 : 0.38;
color = wxColour(
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Red()) * factor)), 0, 255)),
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Green()) * factor)), 0, 255)),
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Blue()) * factor)), 0, 255)));
}
data[data_idx + 0] = color.Red();
data[data_idx + 1] = color.Green();
data[data_idx + 2] = color.Blue();
}
}
}
m_cached_bitmap = wxBitmap(image);
m_cached_bitmap_size = size;
m_cached_background = background;
}
void MixedFilamentColorMapPanel::draw_cached_bitmap(wxAutoBufferedPaintDC &dc, const wxRect &rect)
{
if (!m_cached_bitmap.IsOk())
return;
if (m_cached_bitmap_size == rect.GetSize()) {
dc.DrawBitmap(m_cached_bitmap, rect.GetLeft(), rect.GetTop(), false);
return;
}
wxMemoryDC memdc;
memdc.SelectObject(m_cached_bitmap);
dc.StretchBlit(rect.GetLeft(), rect.GetTop(), rect.GetWidth(), rect.GetHeight(),
&memdc, 0, 0, m_cached_bitmap_size.GetWidth(), m_cached_bitmap_size.GetHeight());
memdc.SelectObject(wxNullBitmap);
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void MixedFilamentColorMapPanel::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const wxRect rect = canvas_rect();
const int width = rect.GetWidth();
const int height = rect.GetHeight();
if (width <= 0 || height <= 0)
return;
const wxColour background = canvas_background_color();
if (!cached_bitmap_matches(rect.GetSize(), background)) {
if (!m_cached_bitmap.IsOk())
render_cached_bitmap(rect.GetSize(), background);
else
schedule_cached_bitmap_render();
}
draw_cached_bitmap(dc, rect);
if (geometry_mode() == GeometryMode::Triangle || geometry_mode() == GeometryMode::TriangleWithCenter) {
const auto triangle = simplex_vertices();
wxPoint points[3] = {
wxPoint(rect.GetLeft() + int(std::lround(triangle[0].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[0].y * double(std::max(1, height - 1))))),
wxPoint(rect.GetLeft() + int(std::lround(triangle[1].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[1].y * double(std::max(1, height - 1))))),
wxPoint(rect.GetLeft() + int(std::lround(triangle[2].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[2].y * double(std::max(1, height - 1)))))
};
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawPolygon(3, points);
if (geometry_mode() == GeometryMode::TriangleWithCenter) {
const Vec2 center = simplex_center();
const wxPoint center_pt(rect.GetLeft() + int(std::lround(center.x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(center.y * double(std::max(1, height - 1)))));
dc.SetPen(wxPen(wxColour(180, 180, 180), 1, wxPENSTYLE_DOT));
for (const wxPoint &vertex : points)
dc.DrawLine(center_pt, vertex);
}
} else {
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
}
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
const auto anchors = anchor_points();
for (size_t idx = 0; idx < anchors.size() && idx < m_colors.size(); ++idx) {
const int anchor_x = rect.GetLeft() + int(std::lround(anchors[idx].x * double(std::max(1, width - 1))));
const int anchor_y = rect.GetTop() + int(std::lround(anchors[idx].y * double(std::max(1, height - 1))));
dc.SetPen(wxPen(wxColour(30, 30, 30), 1));
dc.SetBrush(wxBrush(m_colors[idx]));
dc.DrawCircle(wxPoint(anchor_x, anchor_y), FromDIP(4));
}
const int cursor_x = rect.GetLeft() + int(std::lround(m_cursor_x * double(std::max(1, width - 1))));
const int cursor_y = rect.GetTop() + int(std::lround(m_cursor_y * double(std::max(1, height - 1))));
dc.SetPen(wxPen(wxColour(255, 255, 255), 3));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(wxPoint(cursor_x, cursor_y), FromDIP(7));
dc.SetPen(wxPen(wxColour(30, 30, 30), 1));
dc.DrawCircle(wxPoint(cursor_x, cursor_y), FromDIP(7));
}
void MixedFilamentColorMapPanel::on_left_down(wxMouseEvent &evt)
{
if (!HasCapture())
CaptureMouse();
m_dragging = true;
update_from_mouse(evt, true);
}
void MixedFilamentColorMapPanel::on_left_up(wxMouseEvent &evt)
{
if (m_dragging)
update_from_mouse(evt, true);
m_dragging = false;
if (HasCapture())
ReleaseMouse();
}
void MixedFilamentColorMapPanel::on_mouse_move(wxMouseEvent &evt)
{
if (m_dragging && evt.LeftIsDown())
update_from_mouse(evt, true);
}
void MixedFilamentColorMapPanel::on_capture_lost(wxMouseCaptureLostEvent &)
{
m_dragging = false;
}
void MixedFilamentColorMapPanel::on_size(wxSizeEvent &evt)
{
if (m_cached_bitmap.IsOk())
schedule_cached_bitmap_render();
Refresh(false);
evt.Skip();
}
void MixedFilamentColorMapPanel::on_render_timer(wxTimerEvent &)
{
const wxRect rect = canvas_rect();
render_cached_bitmap(rect.GetSize(), canvas_background_color());
Refresh(false);
}
} } // namespace Slic3r::GUI
@@ -0,0 +1,138 @@
#pragma once
#include <wx/panel.h>
#include <wx/timer.h>
#include <wx/bitmap.h>
#include <wx/colour.h>
#include <vector>
#include <array>
#include <functional>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// MixedFilamentColorMapPanel
//
// Interactive colour-map widget that lets the user pick a multi-filament
// blend by dragging a cursor across a geometry-specific gradient map.
//
// Extracted verbatim from FullSpectrum Plater.cpp:3087-3781 (Task 17).
// ---------------------------------------------------------------------------
class MixedFilamentColorMapPanel : public wxPanel
{
public:
MixedFilamentColorMapPanel(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights,
const wxSize &min_size);
~MixedFilamentColorMapPanel() override;
// Returns the current normalised per-filament weights (sum == 100).
std::vector<int> normalized_weights() const;
// Returns the blended wxColour that corresponds to the current cursor position.
wxColour selected_color() const;
// Programmatically update weights; notify==true fires wxEVT_SLIDER.
void set_normalized_weights(const std::vector<int> &weights, bool notify);
// Minimum per-component percentage below which the region is dimmed/striped.
void set_min_component_percent(int min_component_percent);
private:
// -----------------------------------------------------------------------
// Private nested types (verbatim from FullSpectrum Plater.cpp:3160-3180)
// -----------------------------------------------------------------------
enum class GeometryMode {
Point,
Line,
Triangle,
TriangleWithCenter,
Radial
};
struct AnchorPoint {
double x { 0.5 };
double y { 0.5 };
};
struct Vec2 {
double x { 0.0 };
double y { 0.0 };
};
// -----------------------------------------------------------------------
// Geometry helpers (all inlined in .cpp)
// -----------------------------------------------------------------------
GeometryMode geometry_mode() const;
wxRect canvas_rect() const;
static Vec2 make_vec(double x, double y);
static Vec2 add_vec(const Vec2 &lhs, const Vec2 &rhs);
static Vec2 sub_vec(const Vec2 &lhs, const Vec2 &rhs);
static Vec2 scale_vec(const Vec2 &value, double factor);
static double dot_vec(const Vec2 &lhs, const Vec2 &rhs);
static double length_sq(const Vec2 &value);
static double dist_sq(const Vec2 &lhs, const Vec2 &rhs);
std::array<Vec2, 3> simplex_vertices() const;
Vec2 simplex_center() const;
std::vector<AnchorPoint> radial_anchor_points() const;
std::vector<AnchorPoint> anchor_points() const;
static std::array<double, 3> triangle_barycentric(const Vec2 &point, const std::array<Vec2, 3> &triangle);
static bool point_in_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle);
static Vec2 closest_point_on_segment(const Vec2 &point, const Vec2 &start, const Vec2 &end);
static Vec2 closest_point_on_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle);
Vec2 normalized_point_from_mouse(const wxMouseEvent &evt) const;
Vec2 clamp_point_to_geometry(const Vec2 &point) const;
std::vector<double> simplex_weights_from_pos(const Vec2 &point) const;
Vec2 triangle_point_from_weights() const;
void initialize_cursor_from_grid_search();
std::vector<double> raw_weights_from_pos(double normalized_x, double normalized_y) const;
std::vector<int> normalized_weights_from_pos(double normalized_x, double normalized_y) const;
void initialize_cursor_from_weights();
// -----------------------------------------------------------------------
// Rendering helpers
// -----------------------------------------------------------------------
void emit_changed();
void update_from_mouse(const wxMouseEvent &evt, bool notify);
wxColour canvas_background_color() const;
bool cached_bitmap_matches(const wxSize &size, const wxColour &background) const;
void schedule_cached_bitmap_render();
void invalidate_cached_bitmap();
void render_cached_bitmap(const wxSize &size, const wxColour &background);
void draw_cached_bitmap(wxAutoBufferedPaintDC &dc, const wxRect &rect);
// -----------------------------------------------------------------------
// wx event handlers
// -----------------------------------------------------------------------
void on_paint(wxPaintEvent &evt);
void on_left_down(wxMouseEvent &evt);
void on_left_up(wxMouseEvent &evt);
void on_mouse_move(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_size(wxSizeEvent &evt);
void on_render_timer(wxTimerEvent &evt);
// -----------------------------------------------------------------------
// Member variables (verbatim from FullSpectrum Plater.cpp:3761-3779)
// -----------------------------------------------------------------------
std::vector<wxColour> m_colors;
std::vector<int> m_weights;
wxBitmap m_cached_bitmap;
wxSize m_cached_bitmap_size;
wxColour m_cached_background;
wxTimer m_render_timer;
int m_min_component_percent { 0 };
double m_cursor_x { 0.5 };
double m_cursor_y { 0.5 };
bool m_dragging { false };
};
} } // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
#pragma once
#include <wx/dialog.h>
#include <wx/clrpicker.h>
#include <wx/colour.h>
#include <wx/gauge.h>
#include <wx/scrolwin.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/timer.h>
#include <wx/wrapsizer.h>
#include <limits>
#include <string>
#include <vector>
#include "GUI_Utils.hpp"
#include "libslic3r/MixedFilament.hpp"
namespace Slic3r { namespace GUI {
class MixedFilamentColorMapPanel; // Task 17
// ---------------------------------------------------------------------------
// MixedColorMatchRecipeResult
//
// Verbatim from FullSpectrum Plater.cpp:230-242.
// Holds the result of a brute-force C(N,2)/C(N,3)/C(N,4) ΔE₀₀ search.
// ---------------------------------------------------------------------------
struct MixedColorMatchRecipeResult
{
bool cancelled = false;
bool valid = false;
unsigned int component_a = 1;
unsigned int component_b = 2;
int mix_b_percent = 50;
std::string manual_pattern;
std::string gradient_component_ids;
std::string gradient_component_weights;
wxColour preview_color = wxColour("#26A69A");
double delta_e = std::numeric_limits<double>::infinity();
};
// Free helper (was declared at Plater.cpp:244-246): launch dialog, return recipe.
MixedColorMatchRecipeResult prompt_best_color_match_recipe(
wxWindow *parent,
const std::vector<std::string> &physical_colors,
const wxColour &initial_color);
// Free helper (was declared at Plater.cpp:251):
// build a MixedFilamentDisplayContext from a flat color vector.
MixedFilamentDisplayContext build_mixed_filament_display_context(
const std::vector<std::string> &physical_colors);
// Free helper (was declared in Plater.cpp anon namespace at 252):
// map a recipe to a swatch colour using the display context.
wxColour compute_color_match_recipe_display_color(
const MixedColorMatchRecipeResult &recipe,
const MixedFilamentDisplayContext &context);
// Free helper (was declared at Plater.cpp:247): ΔE₀₀ between two wxColours.
double color_delta_e00(const wxColour &lhs, const wxColour &rhs);
// ---------------------------------------------------------------------------
// MixedFilamentColorMatchDialog
//
// Extracted verbatim from FullSpectrum Plater.cpp:3782-4288.
// The user types/picks an arbitrary target colour and a brute-force search
// finds the C(N,2)/C(N,3)/C(N,4) recipe that minimises ΔE₀₀ to that target.
// ---------------------------------------------------------------------------
class MixedFilamentColorMatchDialog : public DPIDialog
{
public:
MixedFilamentColorMatchDialog(wxWindow *parent,
const std::vector<std::string> &physical_colors,
const wxColour &initial_color);
~MixedFilamentColorMatchDialog() override;
// Kick off the initial background recipe search (called after ShowModal starts).
void begin_initial_recipe_load();
MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; }
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
// UI helpers
void update_range_label();
void rebuild_presets_ui();
void set_recipe_loading(bool loading, const wxString &message);
void sync_inputs_to_requested();
bool apply_requested_target(const wxColour &requested_target);
bool apply_hex_input(bool show_invalid_error);
void request_recipe_match(const wxColour &requested_target, bool debounce, const wxString &loading_message);
void refresh_selected_recipe();
void launch_recipe_match(size_t request_token, const wxColour &requested_target);
void update_dialog_state();
// Declared in the task spec's public API
void sync_recipe_preview(MixedColorMatchRecipeResult &recipe, const wxColour *requested_target = nullptr);
void handle_recipe_result(size_t request_token, const wxColour &requested_target, MixedColorMatchRecipeResult recipe);
void apply_preset(MixedColorMatchRecipeResult preset);
// Data
std::vector<std::string> m_physical_colors;
MixedFilamentDisplayContext m_display_context;
std::vector<wxColour> m_palette;
std::vector<MixedColorMatchRecipeResult> m_presets;
MixedFilamentColorMapPanel *m_color_map = nullptr;
// Widgets
wxTextCtrl *m_hex_input = nullptr;
wxColourPickerCtrl *m_classic_picker = nullptr;
wxSlider *m_range_slider = nullptr;
wxStaticText *m_range_value = nullptr;
wxStaticText *m_presets_label = nullptr;
wxScrolledWindow *m_presets_host = nullptr;
wxWrapSizer *m_presets_sizer = nullptr;
wxPanel *m_loading_panel = nullptr;
wxStaticText *m_loading_label = nullptr;
wxGauge *m_loading_gauge = nullptr;
wxPanel *m_selected_preview = nullptr;
wxStaticText *m_selected_label = nullptr;
wxPanel *m_recipe_preview = nullptr;
wxStaticText *m_recipe_label = nullptr;
wxStaticText *m_delta_label = nullptr;
wxStaticText *m_error_label = nullptr;
// State
wxColour m_requested_target { wxColour("#26A69A") };
wxColour m_selected_target { wxColour("#26A69A") };
MixedColorMatchRecipeResult m_selected_recipe;
wxTimer m_recipe_timer;
wxTimer m_loading_timer;
wxString m_loading_message;
size_t m_recipe_request_token { 0 };
int m_min_component_percent { 15 };
bool m_has_recipe_result { false };
bool m_recipe_loading { false };
bool m_recipe_refresh_pending { false };
bool m_syncing_inputs { false };
};
} } // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <wx/panel.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/spinctrl.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <vector>
#include <string>
#include <functional>
#include <memory>
#include "libslic3r/MixedFilament.hpp"
namespace Slic3r { namespace GUI {
class MixedMixPreview; // Task 15
class MixedGradientSelector; // Task 16
// ---------------------------------------------------------------------------
// MixedFilamentConfigPanel
//
// Inline per-row editor for a single MixedFilament entry. Composes
// MixedMixPreview, MixedGradientSelector and MixedGradientWeightsDialog.
//
// Extracted from FullSpectrum Plater.cpp:4588-6857.
// ---------------------------------------------------------------------------
class MixedFilamentConfigPanel : public wxPanel
{
public:
using OnChangeFn = std::function<void(const MixedFilament &)>;
MixedFilamentConfigPanel(wxWindow *parent,
size_t mixed_id,
const MixedFilament &mf,
size_t num_physical,
const std::vector<std::string> &physical_colors,
const std::vector<double> &nozzle_diameters,
const std::vector<wxColour> &palette,
const MixedFilamentPreviewSettings &preview_settings,
bool bias_mode_enabled,
OnChangeFn on_change = {});
// Get the updated mixed filament data.
MixedFilament get_mixed_filament() const { return m_mf; }
bool has_changes() const { return m_has_changes; }
static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings);
private:
void build_ui();
void update_preview();
void update_local_z_breakdown();
void update_component_picker_visuals();
// Static helpers — verbatim from FullSpectrum Plater.cpp:4943-6042.
static std::vector<unsigned int> decode_gradient_ids(const std::string &s);
static std::string encode_gradient_ids(const std::vector<unsigned int> &ids);
static std::vector<unsigned int> decode_manual_pattern_ids(const std::string &pattern,
unsigned int a,
unsigned int b,
size_t num_physical,
size_t wall_loops = 0);
static std::vector<int> decode_gradient_weights(const std::string &s, size_t n);
static std::vector<int> normalize_gradient_weights(const std::vector<int> &w, size_t n);
static std::string encode_gradient_weights(const std::vector<int> &w);
static std::vector<unsigned int> build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false);
static std::vector<unsigned int> build_weighted_multi_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights,
size_t max_cycle_limit = 0);
static std::string summarize_sequence(const std::vector<unsigned int> &seq);
static std::string summarize_local_z_breakdown(const MixedFilament &mf,
const std::vector<int> &weights,
const MixedFilamentPreviewSettings &preview_settings);
static std::string blend_from_sequence(const std::vector<std::string> &colors,
const std::vector<unsigned int> &seq,
const std::string &fallback);
static std::vector<double> build_local_z_preview_pass_heights(double nominal_layer_height,
double lower_bound,
double upper_bound,
double preferred_a_height,
double preferred_b_height,
int mix_b_percent,
int max_sublayers_limit);
size_t m_mixed_id;
MixedFilament m_mf;
size_t m_num_physical;
std::vector<std::string> m_physical_colors;
std::vector<double> m_nozzle_diameters;
std::vector<wxColour> m_palette;
MixedFilamentPreviewSettings m_preview_settings;
bool m_bias_mode_enabled = false;
bool m_has_changes = false;
wxChoice *m_choice_a = nullptr;
wxChoice *m_choice_b = nullptr;
wxChoice *m_choice_c = nullptr;
wxChoice *m_choice_d = nullptr;
wxPanel *m_picker_a_container = nullptr;
wxPanel *m_picker_b_container = nullptr;
wxPanel *m_picker_c_container = nullptr;
wxPanel *m_picker_d_container = nullptr;
wxPanel *m_picker_a_swatch = nullptr;
wxPanel *m_picker_b_swatch = nullptr;
wxPanel *m_picker_c_swatch = nullptr;
wxPanel *m_picker_d_swatch = nullptr;
wxStaticText *m_picker_a_label = nullptr;
wxStaticText *m_picker_b_label = nullptr;
wxStaticText *m_picker_c_label = nullptr;
wxStaticText *m_picker_d_label = nullptr;
wxPanel *m_surface_offset_target_container = nullptr;
wxPanel *m_surface_offset_target_swatch = nullptr;
wxStaticText *m_surface_offset_target_label = nullptr;
MixedGradientSelector *m_blend_selector = nullptr;
wxStaticText *m_blend_label = nullptr;
wxTextCtrl *m_pattern_ctrl = nullptr;
wxCheckBox *m_local_z_limit_checkbox = nullptr;
wxSpinCtrl *m_local_z_limit_spin = nullptr;
wxSpinCtrlDouble *m_surface_offset_spin = nullptr;
std::vector<wxButton *> m_pattern_quick_buttons;
MixedMixPreview *m_mix_preview = nullptr;
wxStaticText *m_breakdown_label = nullptr;
wxPanel *m_swatch = nullptr;
std::shared_ptr<std::vector<int>> m_selected_weight_state;
OnChangeFn m_on_change;
};
} } // namespace Slic3r::GUI
+272
View File
@@ -0,0 +1,272 @@
#include "MixedGradientSelector.hpp"
#include "GUI_App.hpp" // wxGetApp() / dark_mode()
#include "I18N.hpp" // _L()
#include "Widgets/Label.hpp" // Label::Body_10
#include "libslic3r/filament_mixer.h" // filament_mixer_lerp
#include <wx/dcbuffer.h>
#include <algorithm>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helper: copied verbatim from FullSpectrum Plater.cpp:2424
// ---------------------------------------------------------------------------
namespace {
wxColour blend_pair_filament_mixer(const wxColour &left, const wxColour &right, float t)
{
const wxColour safe_left = left.IsOk() ? left : wxColour("#26A69A");
const wxColour safe_right = right.IsOk() ? right : wxColour("#26A69A");
unsigned char out_r = static_cast<unsigned char>(safe_left.Red());
unsigned char out_g = static_cast<unsigned char>(safe_left.Green());
unsigned char out_b = static_cast<unsigned char>(safe_left.Blue());
::Slic3r::filament_mixer_lerp(static_cast<unsigned char>(safe_left.Red()),
static_cast<unsigned char>(safe_left.Green()),
static_cast<unsigned char>(safe_left.Blue()),
static_cast<unsigned char>(safe_right.Red()),
static_cast<unsigned char>(safe_right.Green()),
static_cast<unsigned char>(safe_right.Blue()),
std::clamp(t, 0.f, 1.f),
&out_r, &out_g, &out_b);
return wxColour(out_r, out_g, out_b);
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Constructor / destructor
// ---------------------------------------------------------------------------
MixedGradientSelector::MixedGradientSelector(wxWindow *parent,
const wxColour &left,
const wxColour &right,
int value_percent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
, m_left(left)
, m_right(right)
, m_value(std::clamp(value_percent, 0, 100))
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(wxSize(FromDIP(96), FromDIP(12)));
Bind(wxEVT_PAINT, &MixedGradientSelector::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &MixedGradientSelector::on_left_down, this);
Bind(wxEVT_LEFT_UP, &MixedGradientSelector::on_left_up, this);
Bind(wxEVT_MOTION, &MixedGradientSelector::on_mouse_move, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedGradientSelector::on_capture_lost, this);
}
MixedGradientSelector::~MixedGradientSelector()
{
if (HasCapture())
ReleaseMouse();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void MixedGradientSelector::set_colors(const wxColour &left, const wxColour &right)
{
m_left = left;
m_right = right;
m_multi_mode = false;
m_multi_colors.clear();
m_multi_weights.clear();
Refresh();
}
void MixedGradientSelector::set_multi_preview(const std::vector<wxColour> &corner_colors,
const std::vector<int> &weights)
{
m_multi_mode = corner_colors.size() >= 3;
m_multi_colors = corner_colors;
m_multi_weights = weights;
Refresh();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
wxRect MixedGradientSelector::gradient_rect() const
{
const int margin_x = FromDIP(2);
const int margin_y = FromDIP(1);
const wxSize sz = GetClientSize();
return wxRect(margin_x, margin_y,
std::max(1, sz.GetWidth() - margin_x * 2),
std::max(1, sz.GetHeight() - margin_y * 2));
}
int MixedGradientSelector::value_from_x(int x) const
{
const wxRect rect = gradient_rect();
const int min_x = rect.GetLeft();
const int max_x = rect.GetLeft() + rect.GetWidth();
const int clamp_x = std::clamp(x, min_x, max_x);
return ((clamp_x - min_x) * 100 + rect.GetWidth() / 2) / rect.GetWidth();
}
void MixedGradientSelector::update_from_x(int x, bool notify)
{
m_value = value_from_x(x);
Refresh();
if (notify) {
wxCommandEvent evt(wxEVT_SLIDER, GetId());
evt.SetInt(m_value);
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void MixedGradientSelector::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const bool is_dark = wxGetApp().dark_mode();
const wxRect rect = gradient_rect();
if (m_multi_mode && m_multi_colors.size() >= 3) {
const wxPoint tl(rect.GetLeft(), rect.GetTop());
const wxPoint tr(rect.GetRight(), rect.GetTop());
const wxPoint br(rect.GetRight(), rect.GetBottom());
const wxPoint bl(rect.GetLeft(), rect.GetBottom());
const wxPoint cc(rect.GetLeft() + rect.GetWidth() / 2,
rect.GetTop() + rect.GetHeight() / 2);
auto draw_tri = [&dc](const wxColour &color,
const wxPoint &a,
const wxPoint &b,
const wxPoint &c) {
wxPoint pts[3] = { a, b, c };
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(color));
dc.DrawPolygon(3, pts);
};
if (m_multi_colors.size() >= 4) {
draw_tri(m_multi_colors[0], tl, tr, cc);
draw_tri(m_multi_colors[1], tr, br, cc);
draw_tri(m_multi_colors[2], br, bl, cc);
draw_tri(m_multi_colors[3], bl, tl, cc);
} else {
// 3-colour layout: first colour occupies one full side, two others on the opposite corners.
draw_tri(m_multi_colors[0], tl, bl, cc);
draw_tri(m_multi_colors[1], tl, tr, cc);
draw_tri(m_multi_colors[2], bl, br, cc);
}
if (m_multi_weights.size() == m_multi_colors.size()) {
dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20));
dc.SetFont(Label::Body_10);
const int pad = FromDIP(2);
if (m_multi_colors.size() >= 4) {
dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]),
rect.GetLeft() + pad, rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]),
rect.GetRight() - FromDIP(28), rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]),
rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14));
dc.DrawText(wxString::Format("%d%%", m_multi_weights[3]),
rect.GetLeft() + pad, rect.GetBottom() - FromDIP(14));
} else {
dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]),
rect.GetLeft() + pad,
rect.GetTop() + rect.GetHeight() / 2 - FromDIP(6));
dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]),
rect.GetRight() - FromDIP(28), rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]),
rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14));
}
}
} else {
const int w = rect.GetWidth();
const int h = rect.GetHeight();
wxImage img(w, h);
unsigned char *data = img.GetData();
if (data != nullptr) {
for (int x = 0; x < w; ++x) {
const float t = (w > 1) ? float(x) / float(w - 1) : 0.5f;
const wxColour col = blend_pair_filament_mixer(m_left, m_right, t);
const unsigned char r = static_cast<unsigned char>(col.Red());
const unsigned char g = static_cast<unsigned char>(col.Green());
const unsigned char b = static_cast<unsigned char>(col.Blue());
for (int y = 0; y < h; ++y) {
const int idx = (y * w + x) * 3;
data[idx + 0] = r;
data[idx + 1] = g;
data[idx + 2] = b;
}
}
dc.DrawBitmap(wxBitmap(img), rect.GetLeft(), rect.GetTop(), false);
} else {
dc.GradientFillLinear(rect, m_left, m_right, wxEAST);
}
}
dc.SetPen(wxPen(is_dark ? wxColour(100, 100, 106) : wxColour(170, 170, 170), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
if (m_multi_mode) {
dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(30, 30, 30));
dc.SetFont(Label::Body_10);
const wxString hint = _L("Click to edit");
wxSize text_sz = dc.GetTextExtent(hint);
dc.DrawText(hint, rect.GetRight() - text_sz.GetWidth() - FromDIP(4), rect.GetTop() + FromDIP(2));
return;
}
int marker_x = rect.GetLeft() + (rect.GetWidth() * m_value + 50) / 100;
marker_x = std::clamp(marker_x, rect.GetLeft(), rect.GetRight());
dc.SetPen(wxPen(wxColour(255, 255, 255), 3));
dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom());
dc.SetPen(wxPen(wxColour(33, 33, 33), 1));
dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom());
}
void MixedGradientSelector::on_left_down(wxMouseEvent &evt)
{
if (m_multi_mode)
return;
if (!HasCapture())
CaptureMouse();
m_dragging = true;
update_from_x(evt.GetX(), false);
}
void MixedGradientSelector::on_left_up(wxMouseEvent &evt)
{
if (m_multi_mode) {
wxCommandEvent click_evt(wxEVT_BUTTON, GetId());
click_evt.SetEventObject(this);
ProcessWindowEvent(click_evt);
return;
}
if (m_dragging)
update_from_x(evt.GetX(), true);
m_dragging = false;
if (HasCapture())
ReleaseMouse();
}
void MixedGradientSelector::on_mouse_move(wxMouseEvent &evt)
{
if (m_dragging && evt.LeftIsDown())
update_from_x(evt.GetX(), false);
}
void MixedGradientSelector::on_capture_lost(wxMouseCaptureLostEvent &)
{
m_dragging = false;
}
} } // namespace Slic3r::GUI
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <wx/panel.h>
#include <wx/colour.h>
#include <vector>
#include <functional>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// MixedGradientSelector
//
// A small horizontal panel that renders a two-colour gradient (or a
// multi-colour preview in "multi mode") and lets the user drag a marker
// to pick a blend percentage. In multi mode the panel renders coloured
// triangles showing corner weights and emits wxEVT_BUTTON on click so the
// owner can open MixedGradientWeightsDialog.
//
// Extracted from FullSpectrum Plater.cpp:4290-4505.
// ---------------------------------------------------------------------------
class MixedGradientSelector : public wxPanel
{
public:
MixedGradientSelector(wxWindow *parent,
const wxColour &left,
const wxColour &right,
int value_percent);
~MixedGradientSelector() override;
// Current blend value 0-100.
int value() const { return m_value; }
bool is_multi_mode() const { return m_multi_mode; }
// Switch to two-colour gradient mode.
void set_colors(const wxColour &left, const wxColour &right);
// Switch to multi-colour preview mode (>= 3 corner colours required).
void set_multi_preview(const std::vector<wxColour> &corner_colors,
const std::vector<int> &weights);
private:
wxRect gradient_rect() const;
int value_from_x(int x) const;
void update_from_x(int x, bool notify);
void on_paint(wxPaintEvent &evt);
void on_left_down(wxMouseEvent &evt);
void on_left_up(wxMouseEvent &evt);
void on_mouse_move(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
wxColour m_left;
wxColour m_right;
bool m_multi_mode { false };
std::vector<wxColour> m_multi_colors;
std::vector<int> m_multi_weights;
int m_value { 50 };
bool m_dragging { false };
};
} } // namespace Slic3r::GUI
@@ -0,0 +1,150 @@
#include "MixedGradientWeightsDialog.hpp"
#include "MixedFilamentColorMapPanel.hpp"
#include "I18N.hpp" // _L()
#include "Widgets/Label.hpp" // Label::Body_12
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/panel.h>
#include <algorithm>
#include <cmath>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helper: copied verbatim from FullSpectrum Plater.cpp:2558
// ---------------------------------------------------------------------------
namespace {
std::vector<int> normalize_color_match_weights(const std::vector<int> &weights, size_t count)
{
std::vector<int> out = weights;
if (out.size() != count)
out.assign(count, count > 0 ? int(100 / int(count)) : 0);
int sum = 0;
for (int &value : out) {
value = std::max(0, value);
sum += value;
}
if (sum <= 0 && count > 0) {
out.assign(count, 0);
out[0] = 100;
return out;
}
std::vector<double> remainders(count, 0.0);
int assigned = 0;
for (size_t idx = 0; idx < count; ++idx) {
const double exact = 100.0 * double(out[idx]) / double(sum);
out[idx] = int(std::floor(exact));
remainders[idx] = exact - double(out[idx]);
assigned += out[idx];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_remainder = -1.0;
for (size_t idx = 0; idx < remainders.size(); ++idx) {
if (remainders[idx] > best_remainder) {
best_remainder = remainders[idx];
best_idx = idx;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Constructor — verbatim from FullSpectrum Plater.cpp:4506-4583
// ---------------------------------------------------------------------------
MixedGradientWeightsDialog::MixedGradientWeightsDialog(
wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights)
: wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"),
wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
m_colors.reserve(filament_ids.size());
m_weights = normalize_color_match_weights(initial_weights, filament_ids.size());
for (const unsigned int filament_id : filament_ids) {
if (filament_id >= 1 && filament_id <= palette.size())
m_colors.emplace_back(palette[filament_id - 1]);
else
m_colors.emplace_back(wxColour("#26A69A"));
}
if (m_colors.empty())
m_colors.emplace_back(wxColour("#26A69A"));
auto *root = new wxBoxSizer(wxVERTICAL);
auto *hint = new wxStaticText(this, wxID_ANY,
_L("Pick a point in the gradient map to control multi-filament mix."));
root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10));
m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights,
wxSize(FromDIP(240), FromDIP(240)));
root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10));
for (size_t i = 0; i < filament_ids.size(); ++i) {
auto *row = new wxBoxSizer(wxHORIZONTAL);
wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition,
wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE);
chip->SetBackgroundColour(m_colors[i]);
row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
row->Add(new wxStaticText(this, wxID_ANY,
wxString::Format("F%d", int(filament_ids[i]))),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
auto *label = new wxStaticText(this, wxID_ANY,
wxString::Format("%d%%", m_weights[i]));
label->SetFont(Label::Body_12);
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8));
m_weight_labels.emplace_back(label);
}
root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8));
SetSizerAndFit(root);
SetMinSize(wxSize(FromDIP(380),
std::max(GetSize().GetHeight(), FromDIP(460))));
update_weight_labels();
if (m_color_map) {
m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) {
m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights;
update_weight_labels();
});
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
std::vector<int> MixedGradientWeightsDialog::normalized_weights() const
{
return m_color_map ? m_color_map->normalized_weights() : m_weights;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
void MixedGradientWeightsDialog::update_weight_labels()
{
for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) {
if (m_weight_labels[i])
m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i]));
}
Layout();
}
} } // namespace Slic3r::GUI
@@ -0,0 +1,45 @@
#pragma once
#include <wx/dialog.h>
#include <wx/colour.h>
#include <wx/stattext.h>
#include <vector>
namespace Slic3r { namespace GUI {
// Forward-declare Task-17 panel: defined in MixedFilamentColorMapPanel.hpp.
class MixedFilamentColorMapPanel;
// ---------------------------------------------------------------------------
// MixedGradientWeightsDialog
//
// A modal dialog that shows a MixedFilamentColorMapPanel and per-filament
// weight labels so the user can pick multi-filament blend weights for a
// gradient mix.
//
// Extracted from FullSpectrum Plater.cpp:4506-4583.
//
// NOTE (Task 17 dependency): the constructor body that instantiates
// MixedFilamentColorMapPanel is guarded with #if 0 until Task 17 lands.
// See MixedGradientWeightsDialog.cpp for details.
// ---------------------------------------------------------------------------
class MixedGradientWeightsDialog : public wxDialog
{
public:
MixedGradientWeightsDialog(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights);
// Returns the normalised per-filament weight vector chosen by the user.
std::vector<int> normalized_weights() const;
private:
void update_weight_labels();
MixedFilamentColorMapPanel *m_color_map { nullptr };
std::vector<wxColour> m_colors;
std::vector<int> m_weights;
std::vector<wxStaticText *> m_weight_labels;
};
} } // namespace Slic3r::GUI
+181
View File
@@ -0,0 +1,181 @@
#include "MixedMixPreview.hpp"
#include "GUI_App.hpp" // wxGetApp() / dark_mode()
#include <wx/dcbuffer.h> // wxAutoBufferedPaintDC
#include <algorithm>
#include <cmath>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
MixedMixPreview::MixedMixPreview(wxWindow *parent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(wxSize(FromDIP(120), FromDIP(20)));
Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void MixedMixPreview::set_data(const std::vector<wxColour> &palette,
const std::vector<unsigned int> &sequence,
bool same_layer_mode,
const std::vector<double> &surface_offsets_mm,
const wxColour &fallback,
const wxString &left_overlay,
const wxString &right_overlay)
{
m_palette = palette;
m_sequence = sequence;
m_same_layer = same_layer_mode;
m_surface_offsets_mm = surface_offsets_mm;
m_fallback = fallback;
m_left_overlay = left_overlay;
m_right_overlay = right_overlay;
Refresh();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
wxRect MixedMixPreview::preview_rect() const
{
const int margin_x = FromDIP(1);
const int margin_y = FromDIP(1);
const wxSize sz = GetClientSize();
return wxRect(margin_x, margin_y,
std::max(1, sz.GetWidth() - margin_x * 2),
std::max(1, sz.GetHeight() - margin_y * 2));
}
wxColour MixedMixPreview::color_for_extruder(unsigned int extruder_id) const
{
if (extruder_id >= 1 && extruder_id <= m_palette.size())
return m_palette[extruder_id - 1];
return m_fallback;
}
double MixedMixPreview::max_active_surface_offset_mm() const
{
double max_offset = 0.0;
for (double offset_mm : m_surface_offsets_mm)
max_offset = std::max(max_offset, std::abs(offset_mm));
return std::max(0.001, max_offset);
}
int MixedMixPreview::slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const
{
if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2)
return 0;
const double offset_mm = m_surface_offsets_mm[extruder_id];
if (std::abs(offset_mm) <= EPSILON)
return 0;
const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0);
const int inset = int(std::round(normalized * slot_extent * 0.45))
* (offset_mm < 0.0 ? -1 : 1);
return std::clamp(inset,
-std::max(0, slot_extent / 2),
std::max(0, slot_extent / 2));
}
// ---------------------------------------------------------------------------
// Paint handler
// ---------------------------------------------------------------------------
void MixedMixPreview::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const wxRect rect = preview_rect();
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_fallback));
dc.DrawRectangle(rect);
if (!m_sequence.empty()) {
if (m_same_layer) {
// Same-layer preview: full-height stripe lines.
const int stripes = 24;
const int stripe_w = std::max(1, rect.GetWidth() / stripes);
const size_t seq_len = m_sequence.size();
for (int s = 0; s < stripes; ++s) {
const size_t idx = size_t(s % int(seq_len));
const unsigned int extruder_id = m_sequence[idx];
dc.SetBrush(wxBrush(color_for_extruder(extruder_id)));
const int x = rect.GetLeft() + s * stripe_w;
const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w;
const int inset = slot_inset_for_extruder(extruder_id, w);
wxRect draw_rect(x + inset / 2, rect.GetTop(),
std::max(1, w - inset), rect.GetHeight());
draw_rect.Intersect(rect);
if (draw_rect.GetWidth() > 0)
dc.DrawRectangle(draw_rect);
}
} else {
const int bars = 24;
const int bar_w = std::max(1, rect.GetWidth() / bars);
for (int i = 0; i < bars; ++i) {
size_t idx = 0;
if (m_sequence.size() > size_t(bars))
idx = (size_t(i) * m_sequence.size()) / size_t(bars);
else
idx = size_t(i) % m_sequence.size();
const unsigned int extruder_id = m_sequence[idx];
dc.SetBrush(wxBrush(color_for_extruder(extruder_id)));
const int x = rect.GetLeft() + i * bar_w;
const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w;
const int inset = slot_inset_for_extruder(extruder_id, w);
wxRect draw_rect(x + inset / 2, rect.GetTop(),
std::max(1, w - inset), rect.GetHeight());
draw_rect.Intersect(rect);
if (draw_rect.GetWidth() > 0)
dc.DrawRectangle(draw_rect);
}
}
}
auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) {
if (text.empty())
return;
dc.SetTextForeground(wxColour(255, 255, 255));
const int outline_radius = std::max(2, FromDIP(2));
for (int ox = -outline_radius; ox <= outline_radius; ++ox) {
for (int oy = -outline_radius; oy <= outline_radius; ++oy) {
if (ox == 0 && oy == 0)
continue;
dc.DrawText(text, x + ox, y + oy);
}
}
dc.SetTextForeground(wxColour(22, 22, 22));
dc.DrawText(text, x, y);
};
wxCoord left_w = 0, left_h = 0;
wxCoord right_w = 0, right_h = 0;
dc.GetTextExtent(m_left_overlay, &left_w, &left_h);
dc.GetTextExtent(m_right_overlay, &right_w, &right_h);
const int text_y = rect.GetTop()
+ std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2);
const int pad = FromDIP(6);
if (!m_left_overlay.empty())
draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y);
if (!m_right_overlay.empty())
draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y);
const bool is_dark = wxGetApp().dark_mode();
dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
}
} } // namespace Slic3r::GUI
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <wx/panel.h>
#include <wx/colour.h>
#include <vector>
#include <string>
namespace Slic3r { namespace GUI {
// Preview strip that shows the layer-by-layer or same-layer colour sequence
// produced by a mixed filament definition. All logic is self-contained; the
// owning panel calls set_data() whenever the underlying MixedFilament changes.
class MixedMixPreview : public wxPanel
{
public:
explicit MixedMixPreview(wxWindow *parent);
void set_data(const std::vector<wxColour> &palette,
const std::vector<unsigned int> &sequence,
bool same_layer_mode,
const std::vector<double> &surface_offsets_mm,
const wxColour &fallback,
const wxString &left_overlay,
const wxString &right_overlay);
private:
wxRect preview_rect() const;
wxColour color_for_extruder(unsigned int extruder_id) const;
double max_active_surface_offset_mm() const;
int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const;
void on_paint(wxPaintEvent &evt);
std::vector<wxColour> m_palette;
std::vector<unsigned int> m_sequence;
std::vector<double> m_surface_offsets_mm;
bool m_same_layer { false };
wxColour m_fallback { wxColour(38, 166, 154) };
wxString m_left_overlay;
wxString m_right_overlay;
};
} } // namespace Slic3r::GUI
+2
View File
@@ -166,6 +166,8 @@ enum class NotificationType
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
BBLMixedFilamentBroken,
BBLSingleExtruderMixedFilamentRisk,
NotificationTypeCount
};
+111 -3
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <string>
#include <regex>
#include <sstream>
#include <future>
#include <glad/gl.h>
#include <boost/algorithm/string.hpp>
@@ -23,6 +24,7 @@
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -1619,12 +1621,37 @@ 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));
return plate_extruders;
// Expand any mixed-filament virtual slots to their physical component extruders
{
const auto& mgr = wxGetApp().preset_bundle->mixed_filaments;
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
std::vector<int> expanded;
for (int e : plate_extruders) {
if (e <= 0) continue;
auto u = static_cast<unsigned int>(e);
if (mgr.is_mixed(u, num_phys)) {
if (auto* mf = mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(static_cast<int>(mf->component_a));
expanded.push_back(static_cast<int>(mf->component_b));
}
} else {
expanded.push_back(e);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
return expanded;
}
}
std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const
{
std::vector<int> plate_extruders;
BOOST_LOG_TRIVIAL(debug) << "PartPlate::get_extruders_under_cli begin"
<< " plate=" << m_plate_index
<< " obj_to_instance_count=" << obj_to_instance_set.size()
<< " consider_custom_gcode=" << conside_custom_gcode;
// if 3mf file
int glb_support_intf_extr = full_config.opt_int("support_interface_filament");
@@ -1644,7 +1671,27 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
if ((obj_id >= 0) && (obj_id < m_model->objects.size()))
{
ModelObject* object = m_model->objects[obj_id];
if (object == nullptr) {
BOOST_LOG_TRIVIAL(error) << "PartPlate::get_extruders_under_cli encountered null model object"
<< " plate=" << m_plate_index
<< " obj_id=" << obj_id;
continue;
}
if (instance_id < 0 || instance_id >= object->instances.size()) {
BOOST_LOG_TRIVIAL(error) << "PartPlate::get_extruders_under_cli encountered invalid instance index"
<< " plate=" << m_plate_index
<< " obj_id=" << obj_id
<< " instance_id=" << instance_id
<< " instance_count=" << object->instances.size();
continue;
}
ModelInstance* instance = object->instances[instance_id];
BOOST_LOG_TRIVIAL(debug) << "PartPlate::get_extruders_under_cli object"
<< " plate=" << m_plate_index
<< " obj_id=" << obj_id
<< " instance_id=" << instance_id
<< " volume_count=" << object->volumes.size()
<< " printable=" << instance->printable;
if (!instance->printable)
continue;
@@ -1741,7 +1788,45 @@ 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));
return plate_extruders;
// Expand any mixed-filament virtual slots to their physical component extruders.
// CLI context: rebuild the manager inline from full_config (no wxGetApp).
{
MixedFilamentManager local_mgr;
std::vector<std::string> filament_colours;
if (const auto* col_opt = dynamic_cast<const ConfigOptionStrings*>(full_config.option("filament_colour")))
filament_colours = col_opt->values;
local_mgr.auto_generate(filament_colours);
if (const auto* defs_opt = dynamic_cast<const ConfigOptionString*>(full_config.option("mixed_filament_definitions")))
if (!defs_opt->value.empty())
local_mgr.load_custom_entries(defs_opt->value, filament_colours);
size_t num_phys = filament_colours.size();
std::vector<int> expanded;
for (int e : plate_extruders) {
if (e <= 0) continue;
auto u = static_cast<unsigned int>(e);
if (local_mgr.is_mixed(u, num_phys)) {
if (auto* mf = local_mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(static_cast<int>(mf->component_a));
expanded.push_back(static_cast<int>(mf->component_b));
}
} else {
expanded.push_back(e);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
std::ostringstream extruders_list;
for (size_t i = 0; i < expanded.size(); ++i) {
if (i != 0)
extruders_list << ",";
extruders_list << expanded[i];
}
BOOST_LOG_TRIVIAL(debug) << "PartPlate::get_extruders_under_cli result"
<< " plate=" << m_plate_index
<< " extruders=[" << extruders_list.str() << "]";
return expanded;
}
}
bool PartPlate::check_objects_empty_and_gcode3mf(std::vector<int> &result) const
@@ -1794,7 +1879,28 @@ 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));
return plate_extruders;
// Expand any mixed-filament virtual slots to their physical component extruders
{
const auto& mgr = wxGetApp().preset_bundle->mixed_filaments;
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
std::vector<int> expanded;
for (int e : plate_extruders) {
if (e <= 0) continue;
auto u = static_cast<unsigned int>(e);
if (mgr.is_mixed(u, num_phys)) {
if (auto* mf = mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(static_cast<int>(mf->component_a));
expanded.push_back(static_cast<int>(mf->component_b));
}
} else {
expanded.push_back(e);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
return expanded;
}
}
/* -1 is invalid, return physical extruder idx*/
@@ -1821,6 +1927,8 @@ int PartPlate::get_physical_extruder_by_filament_id(const DynamicConfig& g_confi
}
int zero_base_logical_idx = filament_map[idx - 1] - 1;
if (zero_base_logical_idx < 0 || zero_base_logical_idx >= (int)the_map->values.size())
return -1;
return the_map->values[zero_base_logical_idx];
}
+12
View File
@@ -216,6 +216,18 @@ OtherLayersSeqPanel::OtherLayersSeqPanel(wxWindow* parent)
Layout();
top_sizer->Fit(this);
// Disable custom sequence when mixed (virtual) filaments are in use.
{
size_t total = wxGetApp().preset_bundle->total_filament_count();
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
if (total > num_phys) {
m_other_layer_print_seq_choice->Disable();
auto* warn = new wxStaticText(this, wxID_ANY,
_L("Custom layer sequence is unavailable when mixed filaments are used."));
warn->SetForegroundColour(wxColour(255, 100, 0));
top_sizer->Add(warn, 0, wxALIGN_LEFT | wxTOP, FromDIP(4));
}
}
m_other_layer_print_seq_choice->Bind(wxEVT_COMBOBOX, [this, buttons_sizer](auto& e) {
if (e.GetSelection() == 0) {
+1109 -6
View File
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -182,6 +182,14 @@ public:
void on_filament_count_change(size_t num_filaments);
void on_filaments_delete(size_t filament_id);
// Mixed Filaments panel
void update_mixed_filament_panel(bool sync_manager = true);
std::vector<unsigned int> get_ui_ordered_filament_ids() const;
// Returns true when any mixed filament references a component ID that is
// out of the physical filament range (e.g. after the user reduces the
// physical filament count).
bool has_broken_mixed_filament() const;
void add_filament();
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
@@ -556,7 +564,18 @@ 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 = {});
// FullSpectrum: gate auto gradient generation when many physical filaments would create a large grid.
// Returns true when callers may proceed with auto-generated gradients. As a side effect, this
// call also sets MixedFilamentManager's static auto-generate flag to match the returned decision,
// so callers do not need to set it themselves. Pops a yes/no dialog at most once per
// physical-filament count (cached per Plater instance).
bool confirm_auto_generated_gradients(size_t num_physical);
// Force a decision into the prompt cache without showing a dialog. Pass num_physical = 0 to
// invalidate the cache (so the next genuine count-growth event re-prompts), or the current
// count to record the user's decision. Used by the Preferences toggle.
void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients);
std::vector<Slic3r::ColorRGBA> get_extruders_colors();
// BBS
void on_bed_type_change(BedType bed_type);
@@ -568,7 +587,7 @@ public:
void force_print_bed_update();
// On activating the parent window.
void on_activate();
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const;
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr, bool include_mixed = true) const;
std::vector<std::string> get_filament_colors_render_info() const;
std::vector<std::string> get_filament_color_render_type() const;
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
+17
View File
@@ -7,6 +7,7 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/MixedFilament.hpp"
#include <wx/language.h>
#include "OG_CustomCtrl.hpp"
#include "wx/graphics.h"
@@ -1040,6 +1041,19 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
}
}
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();
// FullSpectrum: record the toggle as the user's authoritative decision for
// the current count, suppressing any future dialog at this same count.
// Adding more filaments later misses the cache and re-prompts as expected.
wxGetApp().plater()->set_auto_generated_gradient_decision(num_physical, checkbox->GetValue());
wxGetApp().preset_bundle->update_multi_material_filament_presets();
wxGetApp().plater()->on_filament_count_change(num_physical);
}
}
if (param == "enable_high_low_temp_mixed_printing") {
if (checkbox->GetValue()) {
const wxString warning_title = _L("Bed Temperature Difference Warning");
@@ -1479,6 +1493,9 @@ void PreferencesDialog::create_items()
auto item_auto_flush = create_item_combobox(_L("Auto flush after changing..."), _L("Auto calculate flushing volumes when selected values changed"), "auto_calculate_flush", FlushOptionLabels, FlushOptionValues);
g_sizer->Add(item_auto_flush);
auto item_auto_generate_gradients = create_item_checkbox(_L("Mixed filaments: Auto-generate gradients."), _L("If enabled, OrcaSlicer automatically creates gradient mixed filaments from physical filament pairs."), "auto_generate_gradients");
g_sizer->Add(item_auto_generate_gradients);
auto item_auto_arrange = create_item_checkbox(_L("Auto arrange plate after cloning"), "", "auto_arrange");
g_sizer->Add(item_auto_arrange);
+24
View File
@@ -2318,6 +2318,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("mixed_filament_gradient_mode");
optgroup = page->new_optgroup(L("Line width"), L"param_line_width");
optgroup->append_single_option_line("line_width","quality_settings_line_width");
@@ -2469,6 +2470,13 @@ 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");
if (m_type >= Preset::TYPE_COUNT) {
// Per-object / per-model only: infill filament override
optgroup->append_single_option_line("enable_infill_filament_override");
optgroup->append_single_option_line("infill_filament_use_base_first_layers");
optgroup->append_single_option_line("infill_filament_use_base_last_layers");
optgroup->append_single_option_line("sparse_infill_filament", "multimaterial_settings_filament_for_features#infill");
}
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");
@@ -2663,6 +2671,7 @@ void TabPrint::build()
optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall");
optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers");
optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("local_z_wipe_tower_purge_lines", "multimaterial_settings_prime_tower");
optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features");
optgroup->append_single_option_line("wall_filament", "multimaterial_settings_filament_for_features#walls");
@@ -2726,6 +2735,21 @@ void TabPrint::build()
optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse");
optgroup->append_single_option_line("enable_wrapping_detection");
// Mixed Filaments / Dithering settings
optgroup = page->new_optgroup(L("Dithering"));
optgroup->append_single_option_line("mixed_filament_height_lower_bound");
optgroup->append_single_option_line("mixed_filament_height_upper_bound");
optgroup->append_single_option_line("mixed_filament_advanced_dithering");
optgroup->append_single_option_line("mixed_filament_component_bias_enabled");
optgroup->append_single_option_line("mixed_filament_surface_indentation");
optgroup->append_single_option_line("mixed_filament_region_collapse");
optgroup->append_single_option_line("dithering_z_step_size");
optgroup->append_single_option_line("dithering_step_painted_zones_only");
// Local-Z subgroup (gated by dithering_local_z_mode)
optgroup->append_single_option_line("dithering_local_z_mode");
optgroup->append_single_option_line("dithering_local_z_whole_objects");
optgroup->append_single_option_line("dithering_local_z_direct_multicolor");
optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-mode");
+108 -19
View File
@@ -199,24 +199,71 @@ std::string RammingPanel::get_parameters()
static const float g_min_flush_multiplier = 0.f;
static const float g_max_flush_multiplier = 3.f;
// Extract the num_phys×num_phys top-left block from a flat total×total matrix.
// When there are no mixed filaments (total == num_phys) the function is a no-op
// and returns the input unchanged.
static std::vector<double> extract_physical_sub_matrix(
const std::vector<double>& full, size_t total, size_t num_phys)
{
if (num_phys >= total || total == 0)
return full;
std::vector<double> phys(num_phys * num_phys, 0.0);
for (size_t row = 0; row < num_phys; ++row)
for (size_t col = 0; col < num_phys; ++col)
phys[row * num_phys + col] = full[row * total + col];
return phys;
}
// Write the edited num_phys×num_phys sub-matrix back into a copy of
// original_full (total×total), preserving the mixed-slot rows and columns.
static std::vector<double> expand_physical_to_full_matrix(
const std::vector<double>& phys, const std::vector<double>& original_full,
size_t total, size_t num_phys)
{
if (num_phys >= total || total == 0)
return phys;
std::vector<double> result(original_full); // preserve mixed rows/cols
for (size_t row = 0; row < num_phys; ++row)
for (size_t col = 0; col < num_phys; ++col)
result[row * total + col] = phys[row * num_phys + col];
return result;
}
bool is_flush_config_modified()
{
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;
// Physical filament count (excludes mixed virtual slots).
const size_t num_phys = static_cast<size_t>(wxGetApp().filaments_cnt());
const size_t nozzle_num = config_multiplier.size();
// Total filament count stored per nozzle block.
const size_t total = (nozzle_num > 0 && !config_matrix.empty())
? static_cast<size_t>(std::round(std::sqrt(config_matrix.size() / nozzle_num)))
: num_phys;
bool has_modify = false;
for (int i = 0; i < config_multiplier.size(); i++) {
for (int i = 0; i < (int)nozzle_num; i++) {
if (config_multiplier[i] != 1) {
has_modify = true;
break;
}
// Extract the per-nozzle block from the flat full matrix.
std::vector<double> nozzle_full(config_matrix.begin() + i * (int)(total * total),
config_matrix.begin() + (i + 1) * (int)(total * total));
// Only compare the physical sub-matrix; mixed-slot rows/cols are computed.
const std::vector<double> phys_stored = extract_physical_sub_matrix(nozzle_full, total, num_phys);
std::vector<std::vector<double>> default_matrix = WipingDialog::CalcFlushingVolumes(i);
int len = default_matrix.size();
for (int m = 0; m < len; m++) {
for (int n = 0; n < len; n++) {
int idx = i * len * len + m * len + n;
if (config_matrix[idx] != default_matrix[m][n] * config_multiplier[i]) {
// CalcFlushingVolumes also spans total×total; take physical sub-matrix.
int def_total = (int)default_matrix.size();
for (int m = 0; m < (int)num_phys; m++) {
for (int n = 0; n < (int)num_phys; n++) {
double def_val = (m < def_total && n < (int)default_matrix[m].size())
? default_matrix[m][n] * config_multiplier[i]
: 0.0;
if (phys_stored[m * num_phys + n] != def_val) {
has_modify = true;
break;
}
@@ -265,24 +312,46 @@ wxString WipingDialog::BuildTableObjStr()
auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
auto nozzle_flush_dataset = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->values;
std::vector<std::vector<double>> flush_matrixs;
// Physical filament count — the editor only shows the P×P physical block.
const size_t num_phys = static_cast<size_t>(wxGetApp().filaments_cnt());
const size_t total = (num_phys > 0 && !filament_colors.empty())
? filament_colors.size()
: num_phys;
// Per-nozzle full matrices (total×total), stored for expand-on-save.
std::vector<std::vector<double>> full_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
flush_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num));
full_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num));
}
flush_multiplier.resize(nozzle_num, 1);
std::vector<std::vector<float>> default_matrixs;
// Physical sub-matrices sent to the web editor (num_phys×num_phys).
std::vector<std::vector<double>> flush_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
default_matrixs.emplace_back(MatrixFlatten(CalcFlushingVolumes(idx)));
flush_matrixs.emplace_back(extract_physical_sub_matrix(full_matrixs[idx], total, num_phys));
}
m_raw_matrixs = flush_matrixs;
// Default matrices for the auto-calc button — physical sub-matrix only.
std::vector<std::vector<float>> default_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
std::vector<float> def_flat = MatrixFlatten(CalcFlushingVolumes(idx));
std::vector<double> def_d(def_flat.begin(), def_flat.end());
std::vector<double> def_phys = extract_physical_sub_matrix(def_d, total, num_phys);
default_matrixs.emplace_back(def_phys.begin(), def_phys.end());
}
// Store full matrices so storeData can expand back when saving.
m_raw_matrixs = full_matrixs;
m_flush_multipliers = flush_multiplier;
// Only send physical filament colours to the editor.
std::vector<std::string> phys_colors(filament_colors.begin(),
filament_colors.begin() + std::min(num_phys, filament_colors.size()));
json obj;
obj["flush_multiplier"] = flush_multiplier;
obj["extruder_num"] = nozzle_num;
obj["filament_colors"] = filament_colors;
obj["filament_colors"] = phys_colors;
obj["flush_volume_matrixs"] = json::array();
obj["min_flush_volumes"] = json::array();
obj["max_flush_volumes"] = json::array();
@@ -299,8 +368,12 @@ wxString WipingDialog::BuildTableObjStr()
}
for (int idx = 0; idx < nozzle_num; ++idx) {
// min_flush_volumes is indexed by physical slot; slice off at num_phys.
const std::vector<int> &min_flush_volumes = get_min_flush_volumes(full_config, idx);
int min_flush_from_nozzle_volume = *min_element(min_flush_volumes.begin(), min_flush_volumes.end());
int min_flush_from_nozzle_volume = min_flush_volumes.empty()
? 0
: *min_element(min_flush_volumes.begin(),
min_flush_volumes.begin() + std::min(num_phys, min_flush_volumes.size()));
GenericFlushPredictor pd(nozzle_flush_dataset[idx]);
int min_flush_from_flush_data = pd.get_min_flush_volume();
obj["min_flush_volumes"].push_back(std::min(min_flush_from_flush_data,min_flush_from_nozzle_volume));
@@ -468,26 +541,42 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
}
else if (j["msg"].get<std::string>() == "storeData") {
int extruder_num = j["number_of_extruders"].get<int>();
std::vector<std::vector<double>> store_matrixs;
// The web editor works on the physical sub-matrix (P×P).
std::vector<std::vector<double>> phys_matrixs;
for (auto iter = j["raw_matrix"].begin(); iter != j["raw_matrix"].end(); ++iter) {
store_matrixs.emplace_back((*iter).get<std::vector<double>>());
phys_matrixs.emplace_back((*iter).get<std::vector<double>>());
}
std::vector<double>store_multipliers = j["flush_multiplier"].get<std::vector<double>>();
{// limit all matrix value before write to gcode, the limitation is depends on the multipliers
size_t cols_temp_matrix = 0;
if (!store_matrixs.empty()) { cols_temp_matrix = store_matrixs[0].size(); }
if (store_multipliers.size() == store_matrixs.size() && cols_temp_matrix>0) // nuzzles==nuzzles
if (!phys_matrixs.empty()) { cols_temp_matrix = phys_matrixs[0].size(); }
if (store_multipliers.size() == phys_matrixs.size() && cols_temp_matrix>0) // nuzzles==nuzzles
{
for (size_t idx = 0; idx < store_multipliers.size(); ++idx) {
double m_max_flush_volume_t = (double)m_max_flush_volume, m_store_multipliers=store_multipliers[idx];
std::transform(store_matrixs[idx].begin(), store_matrixs[idx].end(),
store_matrixs[idx].begin(),
std::transform(phys_matrixs[idx].begin(), phys_matrixs[idx].end(),
phys_matrixs[idx].begin(),
[m_max_flush_volume_t, m_store_multipliers](double inputx) {
return std::clamp(inputx, 0.0, m_max_flush_volume_t / m_store_multipliers);
});
}
}
}
// Expand physical sub-matrices back to full total×total,
// preserving the mixed-slot rows/cols from the snapshot taken
// in BuildTableObjStr.
const size_t num_phys = static_cast<size_t>(wxGetApp().filaments_cnt());
std::vector<std::vector<double>> store_matrixs;
for (size_t idx = 0; idx < phys_matrixs.size(); ++idx) {
if (idx < m_raw_matrixs.size() && !m_raw_matrixs[idx].empty()) {
const size_t total = static_cast<size_t>(
std::round(std::sqrt(static_cast<double>(m_raw_matrixs[idx].size()))));
store_matrixs.emplace_back(
expand_physical_to_full_matrix(phys_matrixs[idx], m_raw_matrixs[idx], total, num_phys));
} else {
store_matrixs.emplace_back(phys_matrixs[idx]);
}
}
this->StoreFlushData(extruder_num, store_matrixs, store_multipliers);
m_submit_flag = true;
this->Close();