Blend mixed slots in the machine-send and AMS-sync thumbnails

This commit is contained in:
SoftFever
2026-08-23 13:37:14 +08:00
parent d27766aff0
commit 9985688b5b
6 changed files with 256 additions and 2 deletions

View File

@@ -4,7 +4,10 @@
#include <cmath>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r { namespace GUI {
@@ -265,4 +268,65 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
}
}
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg)
{
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
const auto* ratio_opt = cfg.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
if (!is_mixed_opt || !comp_opt) return;
const size_t n = is_mixed_opt->values.size();
if (colors.size() < n) colors.resize(n);
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
const auto kFallback = wxColour(128, 128, 128, 255);
for (size_t i = 0; i < n; ++i) {
if (!is_mixed_opt->values[i]) continue;
if (i >= comp_opt->values.size()) { colors[i] = kFallback; continue; }
auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[i]);
if (comp_ids.empty()) { colors[i] = kFallback; continue; }
bool is_gradient = grad_opt && i < grad_opt->values.size() && grad_opt->values[i];
std::vector<unsigned int> use_ids = comp_ids;
std::vector<int> weights;
if (is_gradient && comp_ids.size() >= 2) {
use_ids = { comp_ids.front(), comp_ids.back() };
weights = { 5000, 5000 };
} else {
auto ratios_d = Slic3r::parse_mixed_ratios(
(ratio_opt && i < ratio_opt->values.size()) ? ratio_opt->values[i] : std::string{},
comp_ids.size());
weights.reserve(comp_ids.size());
for (double r : ratios_d)
weights.push_back(static_cast<int>(std::lround(r * 10000.0)));
}
std::vector<std::string> hex_colors;
hex_colors.reserve(use_ids.size());
bool any_invalid = false;
for (unsigned int id : use_ids) {
if (id == 0 || id > colors.size()) { any_invalid = true; break; }
wxColour c = colors[id - 1];
if (c.IsOk() && (c.Red() > 0 || c.Green() > 0 || c.Blue() > 0)) {
hex_colors.push_back(wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString());
} else if (colour_opt && (id - 1) < colour_opt->values.size()) {
hex_colors.push_back(colour_opt->values[id - 1]);
} else {
any_invalid = true; break;
}
}
if (any_invalid) { colors[i] = kFallback; continue; }
std::string hex = Slic3r::blend_color_multi(hex_colors, weights);
wxColour blended(hex);
if (!blended.IsOk()) blended = kFallback;
colors[i] = wxColour(blended.Red(), blended.Green(), blended.Blue(), 255);
}
}
}} // namespace Slic3r::GUI

View File

@@ -7,6 +7,10 @@
#include <wx/gdicmn.h>
#include <vector>
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
// force-included pch (the GUI test suite includes it directly).
namespace Slic3r { class DynamicPrintConfig; }
namespace Slic3r { namespace GUI {
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
@@ -28,6 +32,12 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors,
const wxSize& size,
bool force_gradient = false);
// Recompute blended representative colors for mixed (virtual) filament slots.
// Reads mixed-filament config keys from cfg and writes back into colors[i]
// for every slot where filament_is_mixed[i] is true.
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg);
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_

View File

@@ -4,6 +4,7 @@
#include "libslic3r/Utils.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/Color.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Preview.hpp"
@@ -5693,10 +5694,15 @@ void SelectMachineDialog::clone_thumbnail_data() {
m_preview_colors_in_thumbnail.resize(m_materialList.size());
}
while (iter != m_materialList.end()) {
int id = iter->first;
Material * item = iter->second;
MaterialItem *m = item->item;
m_preview_colors_in_thumbnail[id] = m->m_material_coloul;
// Orca: key the preview colours by filament slot, as m_cur_colors_in_thumbnail and
// SyncAmsInfoDialog already do, so recompute_mixed_slot_colors() below can look a mixed
// slot's component colours up by id (BBS keys this array by list position).
if (item->id >= m_preview_colors_in_thumbnail.size()) {
m_preview_colors_in_thumbnail.resize(item->id + 1);
}
m_preview_colors_in_thumbnail[item->id] = m->m_material_coloul;
if (item->id < m_cur_colors_in_thumbnail.size()) {
m_cur_colors_in_thumbnail[item->id] = m->m_ams_coloul;
}
@@ -5706,6 +5712,20 @@ void SelectMachineDialog::clone_thumbnail_data() {
}
iter++;
}
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
const auto& cfg = wxGetApp().preset_bundle->project_config;
size_t total = 0;
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
total = opt->values.size();
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
if (m_cur_colors_in_thumbnail.size() < target)
m_cur_colors_in_thumbnail.resize(target);
if (m_preview_colors_in_thumbnail.size() < target)
m_preview_colors_in_thumbnail.resize(target);
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
//copy data
auto &data = m_cur_input_thumbnail_data;
m_preview_thumbnail_data.reset();
@@ -5880,6 +5900,10 @@ void SelectMachineDialog::change_default_normal(int old_filament_id, wxColour te
return;
}
}
// Recompute mixed slot colors after physical slot color change
const auto& cfg = wxGetApp().preset_bundle->project_config;
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
ThumbnailData& data = m_cur_input_thumbnail_data;
ThumbnailData& no_light_data = m_cur_no_light_thumbnail_data;
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {

View File

@@ -30,6 +30,7 @@
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevMapping.h"
#include "DeviceCore/DevStorage.h"
#include "FilamentBitmapUtils.hpp"
using namespace Slic3r;
using namespace Slic3r::GUI;
@@ -2943,6 +2944,20 @@ void SyncAmsInfoDialog::clone_thumbnail_data()
iter++;
}
}
// Expand color arrays to cover mixed (virtual) slots and compute their blended colors
const auto& cfg = wxGetApp().preset_bundle->project_config;
size_t total = 0;
if (auto* opt = cfg.option<ConfigOptionBools>("filament_is_mixed"))
total = opt->values.size();
size_t target = std::max(total, m_cur_colors_in_thumbnail.size());
if (m_cur_colors_in_thumbnail.size() < target)
m_cur_colors_in_thumbnail.resize(target);
if (m_preview_colors_in_thumbnail.size() < target)
m_preview_colors_in_thumbnail.resize(target);
recompute_mixed_slot_colors(m_preview_colors_in_thumbnail, cfg);
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
// copy data
auto &data = m_cur_input_thumbnail_data;
m_preview_thumbnail_data.reset();
@@ -3131,6 +3146,10 @@ void SyncAmsInfoDialog::change_default_normal(int old_filament_id, wxColour temp
return;
}
}
// Recompute mixed slot colors after physical slot color change
const auto& cfg = wxGetApp().preset_bundle->project_config;
recompute_mixed_slot_colors(m_cur_colors_in_thumbnail, cfg);
ThumbnailData &data = m_cur_input_thumbnail_data;
ThumbnailData &no_light_data = m_cur_no_light_thumbnail_data;
if (data.width > 0 && data.height > 0 && data.width == no_light_data.width && data.height == no_light_data.height) {

View File

@@ -2,6 +2,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests_main.cpp
test_dev_mapping.cpp
test_filament_bitmap_utils.cpp
test_network_versions.cpp
test_action_source.cpp
test_plugin_host_api.cpp

View File

@@ -0,0 +1,136 @@
// recompute_mixed_slot_colors lives in libslic3r_gui; this is the only suite that links it.
// Same Windows include prologue as test_dev_mapping.cpp (wx pulls in <windows.h>; keep
// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers).
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include <wx/colour.h>
#include <wx/string.h>
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "slic3r/GUI/FilamentBitmapUtils.hpp"
using namespace Slic3r;
using Slic3r::GUI::recompute_mixed_slot_colors;
namespace {
// Two physical slots (1 = red, 2 = blue) and mixed slot 3 built from them.
DynamicPrintConfig mixed_config(const std::string& components = "1,2", const std::string& ratios = "0.5,0.5")
{
DynamicPrintConfig cfg;
cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true}));
cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", components}));
cfg.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings({"", "", ratios}));
cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false}));
cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#0000FF", "#000000"}));
return cfg;
}
wxColour expected_blend(const std::vector<std::string>& hex, const std::vector<int>& weights)
{
return wxColour(wxString(blend_color_multi(hex, weights)));
}
// Compare channels one at a time so a failure names the channel.
void require_same_rgb(const wxColour& actual, const wxColour& expected)
{
REQUIRE(int(actual.Red()) == int(expected.Red()));
REQUIRE(int(actual.Green()) == int(expected.Green()));
REQUIRE(int(actual.Blue()) == int(expected.Blue()));
}
} // namespace
TEST_CASE("recompute_mixed_slot_colors blends a mixed slot from its components' colours", "[FilamentBitmapUtils]")
{
std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)};
recompute_mixed_slot_colors(colors, mixed_config());
REQUIRE(colors.size() == 3);
require_same_rgb(colors[2], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000}));
REQUIRE(int(colors[2].Alpha()) == 255);
// Physical slots are left alone.
require_same_rgb(colors[0], wxColour(255, 0, 0));
require_same_rgb(colors[1], wxColour(0, 0, 255));
}
TEST_CASE("recompute_mixed_slot_colors leaves the colours alone without mixed slots", "[FilamentBitmapUtils]")
{
std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)};
SECTION("no mixed keys at all") {
recompute_mixed_slot_colors(colors, DynamicPrintConfig{});
}
SECTION("mixed flags present but all false") {
DynamicPrintConfig cfg;
cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false}));
cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", ""}));
recompute_mixed_slot_colors(colors, cfg);
}
REQUIRE(colors.size() == 2);
require_same_rgb(colors[0], wxColour(255, 0, 0));
require_same_rgb(colors[1], wxColour(0, 0, 255));
}
TEST_CASE("recompute_mixed_slot_colors falls back to grey for a broken component reference", "[FilamentBitmapUtils]")
{
const wxColour grey(128, 128, 128, 255);
std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)};
SECTION("dangling component id") {
recompute_mixed_slot_colors(colors, mixed_config("1,9"));
}
SECTION("empty component list") {
recompute_mixed_slot_colors(colors, mixed_config(""));
}
REQUIRE(colors.size() == 3);
require_same_rgb(colors[2], grey);
}
TEST_CASE("recompute_mixed_slot_colors uses the project colour when a slot colour is unset", "[FilamentBitmapUtils]")
{
// Slot 2 carries no colour in the vector; filament_colour[1] = "#0000FF" is used instead.
std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour()};
recompute_mixed_slot_colors(colors, mixed_config());
require_same_rgb(colors[2], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000}));
}
TEST_CASE("recompute_mixed_slot_colors blends a gradient slot from its end points only", "[FilamentBitmapUtils]")
{
DynamicPrintConfig cfg;
cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, false, true}));
cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "", "1,2,3"}));
cfg.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings({"", "", "", "0.2,0.3,0.5"}));
cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false, true}));
cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00", "#0000FF", "#000000"}));
std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 255, 0), wxColour(0, 0, 255)};
recompute_mixed_slot_colors(colors, cfg);
REQUIRE(colors.size() == 4);
require_same_rgb(colors[3], expected_blend({"#FF0000", "#0000FF"}, {5000, 5000}));
}
TEST_CASE("recompute_mixed_slot_colors honours the configured ratios and is idempotent", "[FilamentBitmapUtils]")
{
std::vector<wxColour> colors{wxColour(255, 0, 0), wxColour(0, 0, 255)};
const DynamicPrintConfig cfg = mixed_config("1,2", "0.7,0.3");
recompute_mixed_slot_colors(colors, cfg);
const wxColour first = colors[2];
// The configured 70/30 ratio must reach the blend (it is not the equal-share default).
require_same_rgb(first, expected_blend({"#FF0000", "#0000FF"}, {7000, 3000}));
REQUIRE(first != expected_blend({"#FF0000", "#0000FF"}, {5000, 5000}));
recompute_mixed_slot_colors(colors, cfg);
require_same_rgb(colors[2], first);
}