Match mixed filament swatches to the editor's gradient preview

The sidebar Mixed Filament list, the extruder icons, the color painting
gizmo and the canvas filament bar now show the same bottom-to-top fade the
Edit Mixed Filament preview shows, custom gradient curves included, instead
of a horizontal fade between the two component colours. Ordinary and vendor
multi-colour filaments are drawn exactly as before.
This commit is contained in:
SoftFever
2026-08-23 21:55:02 +08:00
parent ff35dacf4c
commit 4a32a9e066
13 changed files with 429 additions and 133 deletions

View File

@@ -31,6 +31,114 @@ void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from,
}
}
static std::string to_hex(const wxColour& c)
{
return wxString::Format("#%02X%02X%02X", c.Red(), c.Green(), c.Blue()).ToStdString();
}
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights)
{
const size_t n = std::min(cols.size(), weights.size());
std::vector<std::string> hex_colors;
std::vector<int> int_weights;
hex_colors.reserve(n);
int_weights.reserve(n);
for (size_t i = 0; i < n; ++i) {
hex_colors.push_back(to_hex(cols[i]));
// Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi;
// only relative magnitude matters.
int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000.0)));
}
wxColour blended(Slic3r::blend_color_multi(hex_colors, int_weights));
return blended.IsOk() ? blended : wxColour(128, 128, 128);
}
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
const wxColour& second,
const Slic3r::GradientCurve& curve,
int steps)
{
std::vector<wxColour> ramp;
if (steps <= 0 || curve.points.size() < 2) return ramp;
ramp.reserve(steps);
for (int i = 0; i < steps; ++i) {
const double t = (steps > 1) ? (i + 0.5) / steps : 0.5;
const double r1 = Slic3r::sample_gradient_curve(curve, t);
ramp.push_back(blend_n_colors({first, second}, {r1, 1.0 - r1}));
}
return ramp;
}
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
// endpoints, otherwise the 0.10 -> 0.90 default.
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
{
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
Slic3r::GradientCurve custom = Slic3r::parse_gradient_curve(curve_opt->values[slot]);
if (custom.points.size() >= 2) return custom;
}
double start = kGradientMinRatio, end = kGradientMaxRatio;
const auto* range_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_range");
if (range_opt && slot < range_opt->values.size() && !range_opt->values[slot].empty()) {
CNumericLocalesSetter c_locale_setter;
float v0 = 0, v1 = 0;
if (std::sscanf(range_opt->values[slot].c_str(), "%f,%f", &v0, &v1) == 2 &&
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
start = v0;
end = v1;
}
}
Slic3r::GradientCurve curve;
curve.points = {{0.0, start, NAN, NAN}, {1.0, end, NAN, NAN}};
return curve;
}
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps)
{
const auto* is_mixed_opt = cfg.option<ConfigOptionBools>("filament_is_mixed");
const auto* grad_opt = cfg.option<ConfigOptionBools>("filament_mixed_gradient");
const auto* comp_opt = cfg.option<ConfigOptionStrings>("filament_mixed_components");
const auto* colour_opt = cfg.option<ConfigOptionStrings>("filament_colour");
if (!is_mixed_opt || !grad_opt || !comp_opt || !colour_opt) return {};
if (slot >= is_mixed_opt->values.size() || !is_mixed_opt->values[slot]) return {};
if (slot >= grad_opt->values.size() || !grad_opt->values[slot]) return {};
if (slot >= comp_opt->values.size()) return {};
// Only two-component slots fade; anything else stays on the plain blended swatch.
const auto comp_ids = Slic3r::parse_mixed_components(comp_opt->values[slot]);
if (comp_ids.size() != 2) return {};
auto component_colour = [&](unsigned int id) {
wxColour c = (id >= 1 && id <= colour_opt->values.size()) ? wxColour(colour_opt->values[id - 1]) : wxColour();
return c.IsOk() ? c : wxColour("#D9D9D9");
};
// Both gradient_range and the curve express the *first* component's ratio over Z, so
// the components stay in config order and the curve alone decides which end is which.
return sample_gradient_ramp(component_colour(comp_ids[0]), component_colour(comp_ids[1]),
mixed_gradient_curve(cfg, slot), steps);
}
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp)
{
if (rect.width <= 0 || rect.height <= 0 || ramp.empty()) return;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int y = 0; y < rect.height; ++y) {
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
// Mapping over height - 1 keeps both ends of the ramp on screen; a swatch is often
// shorter than the ramp is long, so truncating either end would be visible.
const double t = (rect.height > 1) ? (double) (rect.height - 1 - y) / (rect.height - 1) : 0.5;
dc.SetBrush(wxBrush(ramp[static_cast<size_t>(t * (ramp.size() - 1) + 0.5)]));
dc.DrawRectangle(rect.x, rect.y + y, rect.width, 1);
}
}
// Helper struct to hold bitmap and DC
struct BitmapDC {
wxBitmap bitmap;
@@ -50,6 +158,19 @@ static BitmapDC init_bitmap_dc(const wxSize& size) {
return BitmapDC(size);
}
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size)
{
if (ramp.empty()) return wxNullBitmap;
BitmapDC bdc = init_bitmap_dc(size);
if (!bdc.dc.IsOk()) return wxNullBitmap;
fill_gradient_ramp_rect(bdc.dc, wxRect(0, 0, size.GetWidth(), size.GetHeight()), ramp);
bdc.dc.SelectObject(wxNullBitmap);
return bdc.bitmap;
}
// Check if a color is transparent (alpha == 0)
static bool is_transparent_color(const wxColour& color) {
return color.Alpha() == 0;
@@ -313,7 +434,7 @@ void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
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());
hex_colors.push_back(to_hex(c));
} else if (colour_opt && (id - 1) < colour_opt->values.size()) {
hex_colors.push_back(colour_opt->values[id - 1]);
} else {

View File

@@ -9,7 +9,7 @@
// 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 { class DynamicPrintConfig; struct GradientCurve; }
namespace Slic3r { namespace GUI {
@@ -32,6 +32,33 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors,
const wxSize& size,
bool force_gradient = false);
// Blend colours at the given relative weights through blend_color_multi, so a measured
// real-world mix is used where one exists instead of a plain channel lerp.
wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights);
// Sample a gradient mixed filament the way the slicer builds it: t runs 0..1 over the
// model's height, the curve gives the first component's ratio at t, and the two
// components are blended at that ratio. Entry 0 is the bottom of the model, the last
// entry its top. Blending goes through blend_n_colors, so measured mixes and the
// reserved [kGradientMinRatio, kGradientMaxRatio] band are both respected — a plain
// two-endpoint fade is neither.
std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
const wxColour& second,
const Slic3r::GradientCurve& curve,
int steps);
// Same ramp for a project config slot, resolving components, colours and curve (or the
// linear gradient_range fallback) from cfg. Empty unless the slot is a two-component
// gradient mixed filament, which is what gates every caller to mixed slots only.
// steps is the ramp's resolution; pass the destination's height in pixels.
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
// Fill rect with a ramp, ramp.front() along the bottom edge.
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp.
wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wxSize& size);
// Recompute blended representative colors for mixed (virtual) filament slots.
// Reads mixed-filament config keys from cfg and writes back into colors[i]
// for every slot where filament_is_mixed[i] is true.

View File

@@ -9718,9 +9718,9 @@ void GLCanvas3D::_render_paint_toolbar() const
bool disabled = !wxGetApp().plater()->can_fillcolor();
ColorRGBA rgba;
// Gradient mixed filaments fade between two colours over Z, so their swatch is drawn as a
// two-tone fade rather than the single blended colour in `colors`.
auto gradient_info = wxGetApp().plater()->get_filament_gradient_info();
// Gradient mixed filaments fade over Z, so their swatch is drawn as that fade rather than
// the single blended colour in `colors`. Every other slot's ramp is empty.
const auto& gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
for (int i = 0; i < extruder_num; i++) {
if (i > 0)
@@ -9735,16 +9735,8 @@ void GLCanvas3D::_render_paint_toolbar() const
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max))
wxPostEvent(m_canvas, IntEvent(EVT_GLTOOLBAR_FILLCOLOR, i + 1));
}
if (i < (int) gradient_info.size() && gradient_info[i].is_gradient) {
auto to_imu32 = [](const std::array<float, 4> &c) -> ImU32 {
return IM_COL32(uint8_t(c[0]*255.f), uint8_t(c[1]*255.f), uint8_t(c[2]*255.f), uint8_t(c[3]*255.f));
};
ImVec2 r_min = ImGui::GetItemRectMin();
ImVec2 r_max = ImGui::GetItemRectMax();
ImU32 col_from = to_imu32(gradient_info[i].color_from);
ImU32 col_to = to_imu32(gradient_info[i].color_to);
ImGui::GetWindowDrawList()->AddRectFilledMultiColor(r_min, r_max, col_from, col_to, col_to, col_from);
}
if (i < (int) gradient_ramps.size() && !gradient_ramps[i].empty())
ImGuiWrapper::draw_gradient_ramp(draw_list, ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), gradient_ramps[i]);
if (ImGui::IsItemHovered() && i < 9) {
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });

View File

@@ -78,13 +78,8 @@ void GLGizmoMmuSegmentation::init_extruders_data()
m_extruders_colors = wxGetApp().plater()->get_extruders_colors();
m_selected_extruder_idx = 0;
auto plater_grad = wxGetApp().plater()->get_filament_gradient_info();
m_gradient_info.resize(m_extruders_colors.size());
for (size_t i = 0; i < m_gradient_info.size() && i < plater_grad.size(); ++i) {
m_gradient_info[i].is_gradient = plater_grad[i].is_gradient;
m_gradient_info[i].color_from = plater_grad[i].color_from;
m_gradient_info[i].color_to = plater_grad[i].color_to;
}
m_gradient_ramps = wxGetApp().plater()->get_filament_gradient_ramps();
m_gradient_ramps.resize(m_extruders_colors.size());
// keep remap table consistent with current extruder count
m_extruder_remap.resize(m_extruders_colors.size());
@@ -325,25 +320,19 @@ bool GLGizmoMmuSegmentation::draw_color_button(int idx, const char* id_str, cons
ImVec4 color_vec = ImGuiWrapper::to_ImVec4(color);
ImU32 br_color = ImGui::ColorConvertFloat4ToU32(active ? ImGuiWrapper::COL_ORCA : m_is_dark_mode ? ImVec4(.35f, .35f, .35f, 1) : ImVec4(.85f, .85f, .85f, 1));
// Every caller labels the button with the 1 based slot number, so idx - 1 picks out the slot's fade.
const GradientInfo* gradient = gradient_of(idx - 1);
// ImGui interpolates the fade linearly, so the centered slot number lands on the midpoint of the two
// endpoints - take its contrast from there, not from the slot's blended color.
ColorRGBA tone = gradient ? ColorRGBA(0.5f * (gradient->color_from[0] + gradient->color_to[0]),
0.5f * (gradient->color_from[1] + gradient->color_to[1]),
0.5f * (gradient->color_from[2] + gradient->color_to[2]), 1.f)
: color;
bool dark_tone = (0.299f * tone.r() + 0.587f * tone.g() + 0.114f * tone.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
const std::vector<wxColour>* gradient = gradient_of(idx - 1);
// The centered slot number sits at the swatch's mid height, so take its contrast from the colour
// printed there rather than from the slot's blended color.
bool dark_tone = gradient ? (*gradient)[gradient->size() / 2].GetLuminance() < 0.51 :
(0.299f * color.r() + 0.587f * color.g() + 0.114f * color.b()) < 0.51f; // matching values used by wxWidgets with clr.GetLuminance() < 0.51
// Paint a gradient mixed filament's fade before the button and keep the button transparent, so the
// slot number and the frame below stay on top of it. AddRectFilledMultiColor cannot round its
// corners, so the fade is drawn at the frame's inset and the frame masks it into the same shape a
// plain color slot gets.
// slot number and the frame below stay on top of it. The bands cannot round their corners, so the
// fade is drawn at the frame's inset and the frame masks it into the same shape a plain color slot
// gets.
if (gradient) {
auto to_imu32 = [](const std::array<float, 4>& c) { return ImGui::ColorConvertFloat4ToU32({c[0], c[1], c[2], c[3]}); };
draw_list->AddRectFilledMultiColor({pos.x + frame_inset * scale, pos.y + frame_inset * scale},
{pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale},
to_imu32(gradient->color_from), to_imu32(gradient->color_to),
to_imu32(gradient->color_to), to_imu32(gradient->color_from));
ImGuiWrapper::draw_gradient_ramp(draw_list, {pos.x + frame_inset * scale, pos.y + frame_inset * scale},
{pos.x + size.x - frame_inset * scale, pos.y + size.y - frame_inset * scale}, *gradient);
color_vec.w = 0.f; // let the fade show through
}

View File

@@ -78,14 +78,6 @@ public:
// filaments occupy ordinary slots, so they draw from the same budget as physical ones.
static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
// Endpoint colours for gradient mixed filaments, mirrored from Plater so the extruder
// swatches below can be drawn as a two-tone fade instead of a single blended colour.
struct GradientInfo {
bool is_gradient = false;
std::array<float, 4> color_from = {0.5f, 0.5f, 0.5f, 1.0f};
std::array<float, 4> color_to = {0.5f, 0.5f, 0.5f, 1.0f};
};
const float get_cursor_radius_min() const override { return CursorRadiusMin; }
// BBS
@@ -123,7 +115,10 @@ protected:
// Filament remap feature
std::vector<size_t> m_extruder_remap; // index → target extruder index
std::vector<GradientInfo> m_gradient_info; // per-slot gradient endpoints, empty entries for plain filaments
// Colours each gradient mixed filament actually prints, bottom of the model first, mirrored
// from Plater so the extruder swatches draw the same fade the editor previews. Plain
// filament slots keep an empty ramp.
std::vector<std::vector<wxColour>> m_gradient_ramps;
// ORCA: Cache used filaments to filter UI
std::set<size_t> m_used_filaments; // Set of used filament indices (cached)
@@ -146,10 +141,11 @@ private:
// ORCA
bool draw_color_button(int idx, const char* id_str, const ColorRGBA& color, ColorRGBA& map_color, bool active, float scale);
// Gradient endpoints of a filament slot, or nullptr when the slot is a plain single color filament.
const GradientInfo* gradient_of(int idx) const
// Gradient ramp of a filament slot, or nullptr when the slot is a plain single color
// filament, so callers can index into what they get back freely.
const std::vector<wxColour>* gradient_of(int idx) const
{
return idx >= 0 && idx < (int) m_gradient_info.size() && m_gradient_info[idx].is_gradient ? &m_gradient_info[idx] : nullptr;
return idx >= 0 && idx < (int) m_gradient_ramps.size() && !m_gradient_ramps[idx].empty() ? &m_gradient_ramps[idx] : nullptr;
}
// BBS

View File

@@ -2404,6 +2404,25 @@ void ImGuiWrapper::draw(
}
}
void ImGuiWrapper::draw_gradient_ramp(ImDrawList *draw_list, const ImVec2 &top_left, const ImVec2 &bottom_right, const std::vector<wxColour> &ramp)
{
if (draw_list == nullptr || ramp.empty() || bottom_right.x <= top_left.x || bottom_right.y <= top_left.y)
return;
const int rows = std::max(1, (int) std::lround(bottom_right.y - top_left.y));
const float row_h = (bottom_right.y - top_left.y) / rows;
const size_t last = ramp.size() - 1;
for (int r = 0; r < rows; ++r) {
// Row 0 is the top of the rect and so takes the ramp's last entry, the model's top.
const double t = (rows > 1) ? (double) (rows - 1 - r) / (rows - 1) : 0.5;
const wxColour &c = ramp[(size_t) (t * last + 0.5)];
// The bottom row snaps to the rect's edge so rounding never leaves a sliver uncovered.
const float y0 = top_left.y + r * row_h;
const float y1 = (r + 1 == rows) ? bottom_right.y : top_left.y + (r + 1) * row_h;
draw_list->AddRectFilled({top_left.x, y0}, {bottom_right.x, y1}, IM_COL32(c.Red(), c.Green(), c.Blue(), c.Alpha()));
}
}
void ImGuiWrapper::draw_cross_hair(const ImVec2 &position, float radius, ImU32 color, int num_segments, float thickness) {
auto draw_list = ImGui::GetOverlayDrawList();
draw_list->AddCircle(position, radius, color, num_segments, thickness);

View File

@@ -3,10 +3,12 @@
#include <string>
#include <map>
#include <vector>
#include <cstdlib>
#include <imgui/imgui.h>
#include <wx/colour.h>
#include <wx/string.h>
#include "libslic3r/Point.hpp"
@@ -299,6 +301,20 @@ public:
int num_segments = 0,
float thickness = 4.f);
/// <summary>
/// Fill a rect with a filament gradient ramp, one band per pixel row, ramp.front() along
/// the bottom edge. Bands rather than one interpolated rect, because the ramp follows the
/// slot's gradient curve and ImGui's corner interpolation could only draw a straight fade.
/// </summary>
/// <param name="draw_list">Define where to draw it</param>
/// <param name="top_left">Upper left corner of the rect</param>
/// <param name="bottom_right">Lower right corner of the rect</param>
/// <param name="ramp">Colours printed, bottom of the model first</param>
static void draw_gradient_ramp(ImDrawList * draw_list,
const ImVec2 & top_left,
const ImVec2 & bottom_right,
const std::vector<wxColour> &ramp);
/// <summary>
/// Check that font ranges contain all chars in string
/// (rendered Unicodes are stored in GlyphRanges)

View File

@@ -21,6 +21,7 @@
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GradientCurveEditor.hpp"
#include "FilamentBitmapUtils.hpp"
#include "wxExtensions.hpp"
#include "Tab.hpp"
#include "libslic3r/Preset.hpp"
@@ -115,19 +116,6 @@ static wxColour blend_colors(const wxColour& a, const wxColour& b, double ratio_
return wxColour(r, g, bl);
}
static wxColour blend_n_colors(const std::vector<wxColour>& cols, const std::vector<double>& weights)
{
std::vector<std::string> hex_colors;
std::vector<int> int_weights;
for (size_t i = 0; i < cols.size() && i < weights.size(); ++i) {
hex_colors.push_back(cols[i].GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
// Scale double weights (e.g. 0.5) to int (5000) for blend_color_multi;
// only relative magnitude matters.
int_weights.push_back(static_cast<int>(std::lround(weights[i] * 10000)));
}
std::string hex = Slic3r::blend_color_multi(hex_colors, int_weights);
return wxColour(hex);
}
// ---- Constructors ----
@@ -709,21 +697,10 @@ wxBoxSizer* MixedFilamentDialog::create_preview_panel()
curve.points = {{0.0, yStart, NAN, NAN}, {1.0, yEnd, NAN, NAN}};
}
wxColour colA = comp_colour(0);
wxColour colB = comp_colour(1);
const int bands = std::max(80, swatch_sz);
double band_h = static_cast<double>(swatch_sz) / bands;
dc.SetPen(*wxTRANSPARENT_PEN);
for (int b = 0; b < bands; ++b) {
double t = 1.0 - (b + 0.5) / bands;
double r1 = Slic3r::sample_gradient_curve(curve, t);
double r2 = 1.0 - r1;
wxColour band_col = blend_n_colors({colA, colB}, {r1, r2});
dc.SetBrush(wxBrush(band_col));
int by = y0 + static_cast<int>(b * band_h);
int bh = static_cast<int>((b + 1) * band_h) - static_cast<int>(b * band_h) + 1;
dc.DrawRectangle(x0, by, swatch_sz, bh);
}
// Same sampler the sidebar, extruder icons and paint gizmo swatches use, so this
// preview and every swatch drawn for the filament agree on what it looks like.
auto ramp = sample_gradient_ramp(comp_colour(0), comp_colour(1), curve, std::max(80, swatch_sz));
fill_gradient_ramp_rect(dc, wxRect(x0, y0, swatch_sz, swatch_sz), ramp);
// Mask corners: overdraw a thick background-colored rounded rect frame
// so the inner edge forms the desired rounded corners.

View File

@@ -4101,30 +4101,27 @@ void Sidebar::update_mixed_filament_list()
wxColour mix_col(mix_color_str);
unsigned int mix_num = (unsigned int)(cfg_idx + 1);
if (is_gradient && comp_ids.size() == 2) {
unsigned int from_id = (gradient_direction == 0) ? comp_ids[0] : comp_ids[1];
unsigned int to_id = (gradient_direction == 0) ? comp_ids[1] : comp_ids[0];
wxColour col_from = (from_id >= 1 && from_id <= physical_colors.size())
? wxColour(physical_colors[from_id - 1]) : wxColour("#D9D9D9");
wxColour col_to = (to_id >= 1 && to_id <= physical_colors.size())
? wxColour(physical_colors[to_id - 1]) : wxColour("#D9D9D9");
int swatch_sz = FromDIP(20);
// The swatch fades bottom to top over the model's height, sampled the same way
// the slicer builds the sublayers, so it matches the editor's Effect Preview. It
// comes back empty for every slot that is not a two component gradient mix.
const int swatch_sz = FromDIP(20);
const std::vector<wxColour> gradient_ramp = mixed_gradient_ramp(project_config, cfg_idx, swatch_sz);
if (!gradient_ramp.empty()) {
auto* grad_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY,
wxDefaultPosition, wxSize(swatch_sz, swatch_sz));
grad_panel->SetMinSize(wxSize(swatch_sz, swatch_sz));
grad_panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
grad_panel->Bind(wxEVT_PAINT, [grad_panel, col_from, col_to, mix_num, mc_text](wxPaintEvent&) {
grad_panel->Bind(wxEVT_PAINT, [grad_panel, gradient_ramp, mix_num, mc_text](wxPaintEvent&) {
wxBufferedPaintDC dc(grad_panel);
wxSize sz = grad_panel->GetClientSize();
fill_gradient_rect_east(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()), col_from, col_to);
fill_gradient_ramp_rect(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()), gradient_ramp);
wxString txt = wxString::Format("%u", mix_num);
dc.SetFont(::Label::Body_14);
wxSize txt_sz = dc.GetTextExtent(txt);
wxColour mid(
(col_from.Red() + col_to.Red()) / 2,
(col_from.Green() + col_to.Green()) / 2,
(col_from.Blue() + col_to.Blue()) / 2);
dc.SetTextForeground(mid.GetLuminance() > 0.5 ? mc_text : *wxWHITE);
// The number sits at the swatch's middle, so take its contrast from the
// colour printed at mid height rather than from either endpoint.
dc.SetTextForeground(gradient_ramp[gradient_ramp.size() / 2].GetLuminance() > 0.5 ? mc_text : *wxWHITE);
dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2,
(sz.GetHeight() - txt_sz.GetHeight()) / 2);
});
@@ -19989,24 +19986,41 @@ std::vector<std::string> Plater::get_filament_color_render_type() const
return ctype;
}
std::vector<Plater::FilamentGradientInfo> Plater::get_filament_gradient_info() const
const std::vector<std::vector<wxColour>>& Plater::get_filament_gradient_ramps() const
{
const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config;
size_t n = get_extruder_colors_from_plater_config().size();
std::vector<FilamentGradientInfo> info(n);
// Sampling a ramp walks the measured-blend recipe table once per step, and the paint toolbar
// asks for the ramps on every rendered frame, so they are cached against the config values
// they are built from and resampled only when one of those actually changes.
//
// The cache cannot live on the Plater: the extruder icons ask for the ramps from inside
// MenuFactory::init(), which runs while this Plater is still being constructed, so `this` is
// not usable yet. Everything the ramps are built from is global anyway, and there is one
// Plater per process, which is the same reasoning behind the icons' own static BitmapCache.
static std::string s_ramps_key;
static std::vector<std::vector<wxColour>> s_ramps;
auto slots = parse_mixed_gradient_slots(*config, n);
unsigned char rgba[4] = {};
for (size_t i = 0; i < n; ++i) {
if (!slots[i].is_gradient) continue;
info[i].is_gradient = true;
Slic3r::GUI::BitmapCache::parse_color4(slots[i].color_from, rgba);
info[i].color_from = {rgba[0] / 255.f, rgba[1] / 255.f, rgba[2] / 255.f, rgba[3] / 255.f};
Slic3r::GUI::BitmapCache::parse_color4(slots[i].color_to, rgba);
info[i].color_to = {rgba[0] / 255.f, rgba[1] / 255.f, rgba[2] / 255.f, rgba[3] / 255.f};
}
static const char* ramp_keys[] = {"filament_is_mixed", "filament_mixed_gradient",
"filament_mixed_components", "filament_colour",
"filament_mixed_gradient_range", "filament_mixed_gradient_curve"};
return info;
const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config;
std::string key;
for (const char* opt_key : ramp_keys)
if (const ConfigOption* opt = config.option(opt_key))
key += opt->serialize() + '\n';
if (key == s_ramps_key)
return s_ramps;
// 64 bands outresolve every swatch drawn from this, all of which resample it down to their
// own height, so one cached resolution serves the icons and both ImGui filament bars.
const auto* colour_opt = config.option<ConfigOptionStrings>("filament_colour");
const size_t n = colour_opt ? colour_opt->values.size() : 0;
s_ramps.assign(n, {});
for (size_t i = 0; i < n; ++i)
s_ramps[i] = mixed_gradient_ramp(config, i, 64);
s_ramps_key = std::move(key);
return s_ramps;
}
/* Get vector of colors used for rendering of a Preview scene in "Color print" mode

View File

@@ -5,6 +5,7 @@
#include <vector>
#include <boost/filesystem/path.hpp>
#include <wx/colour.h>
#include <wx/panel.h>
// BBS
#include <wx/notebook.h>
@@ -607,14 +608,11 @@ public:
std::vector<std::string> get_filament_colors_render_info() const;
std::vector<std::string> get_filament_color_render_type() const;
// Endpoint colours for gradient mixed filaments, so the 3D scene and the paint gizmo can
// draw a two-tone swatch. is_gradient is false for every ordinary filament slot.
struct FilamentGradientInfo {
bool is_gradient = false;
std::array<float, 4> color_from = {0.5f, 0.5f, 0.5f, 1.0f};
std::array<float, 4> color_to = {0.5f, 0.5f, 0.5f, 1.0f};
};
std::vector<FilamentGradientInfo> get_filament_gradient_info() const;
// Per slot, the colours a gradient mixed filament actually prints, sampled bottom (index 0)
// to top, so the sidebar, the paint gizmo and the extruder icons draw the same fade the
// editor previews rather than a straight blend of two endpoints. A slot that is not a
// gradient mixed filament gets an empty ramp. Cached; recomputed when the config changes.
const std::vector<std::vector<wxColour>>& get_filament_gradient_ramps() const;
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
void set_global_filament_map_mode(FilamentMapMode mode);

View File

@@ -555,14 +555,20 @@ std::vector<wxBitmap*> get_extruder_color_icons(bool thin_icon/* = false*/)
const int icon_width = lround((thin_icon ? 2 : 4.4) * em);
const int icon_height = lround(2 * em);
// A gradient mixed filament fades over the model's height, so it gets the same
// curve-sampled ramp the editor previews instead of a fade between two endpoints.
const auto& gradient_ramps = Slic3r::GUI::wxGetApp().plater()->get_filament_gradient_ramps();
int index = 0;
for (const auto &colors : readable_color_info) {
auto label = std::to_string(++index);
bool is_gradient = ctype[index-1] == "0";
if (colors.size() == 1) {
const size_t slot = index - 1;
bool is_gradient = ctype[slot] == "0";
const std::vector<wxColour>* ramp = (slot < gradient_ramps.size() && !gradient_ramps[slot].empty()) ? &gradient_ramps[slot] : nullptr;
if (ramp == nullptr && colors.size() == 1) {
bmps.push_back(get_extruder_color_icon(colors[0], label, icon_width, icon_height));
} else {
bmps.push_back(get_extruder_color_icon(colors, is_gradient, label, icon_width, icon_height));
bmps.push_back(get_extruder_color_icon(colors, is_gradient, label, icon_width, icon_height, ramp));
}
}
} else {
@@ -630,14 +636,27 @@ wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_da
return data;
}
wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height){
wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
const std::vector<wxColour> *ramp){
static Slic3r::GUI::BitmapCache bmp_cache;
// build cache key, include all color info
// build cache key, include all color info. A ramp already encodes its slot's components,
// colours and curve, so keying on it rebuilds the icon whenever any of them change.
std::string bitmap_key = "";
for (const auto& color : colors) {
bitmap_key += color + "_";
if (ramp != nullptr) {
static const char hex_digits[] = "0123456789ABCDEF";
bitmap_key = "grad_";
for (const wxColour &c : *ramp)
for (unsigned char v : {c.Red(), c.Green(), c.Blue()}) {
bitmap_key += hex_digits[v >> 4];
bitmap_key += hex_digits[v & 0x0F];
}
bitmap_key += "_";
} else {
for (const auto& color : colors) {
bitmap_key += color + "_";
}
}
bitmap_key += "h" + std::to_string(icon_height) + "-w" + std::to_string(icon_width) + "-i" + label;
@@ -647,16 +666,21 @@ wxBitmap *get_extruder_color_icon(std::vector<std::string> colors, bool is_gradi
#endif
if (bitmap == nullptr) {
std::vector<wxColour> wx_colors;
for (const auto& color_str : colors) {
wx_colors.push_back(wxColour(color_str));
}
if (wx_colors.empty()) {
wx_colors.push_back(wxColour("#636363")); // default color if no colors provided
}
wxBitmap base_bitmap;
if (ramp != nullptr) {
base_bitmap = Slic3r::GUI::create_gradient_ramp_bitmap(*ramp, wxSize(icon_width, icon_height));
} else {
std::vector<wxColour> wx_colors;
for (const auto& color_str : colors) {
wx_colors.push_back(wxColour(color_str));
}
if (wx_colors.empty()) {
wx_colors.push_back(wxColour("#636363")); // default color if no colors provided
}
// create filament bitmap in multi color
wxBitmap base_bitmap = Slic3r::GUI::create_filament_bitmap(wx_colors, wxSize(icon_width, icon_height), is_gradient);
// create filament bitmap in multi color
base_bitmap = Slic3r::GUI::create_filament_bitmap(wx_colors, wxSize(icon_width, icon_height), is_gradient);
}
if (!base_bitmap.IsOk()) {
// if create failed, return nullptr

View File

@@ -75,7 +75,10 @@ wxBitmap create_scaled_bitmap(const std::string& bmp_name, wxWindow *win = nullp
wxBitmap* get_default_extruder_color_icon(bool thin_icon = false);
std::vector<wxBitmap *> get_extruder_color_icons(bool thin_icon = false);
wxBitmap * get_extruder_color_icon(std::string color, std::string label, int icon_width, int icon_height);
wxBitmap * get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height);
// A non-null ramp draws the slot as a gradient mixed filament instead: it holds the colours the
// slot actually prints, bottom entry first, and is drawn bottom to top rather than from colors.
wxBitmap * get_extruder_color_icon(std::vector<std::string> colors, bool is_gradient, std::string label, int icon_width, int icon_height,
const std::vector<wxColour> *ramp = nullptr);
std::vector<std::vector<std::string>> read_color_pack(std::vector<std::string> color_pack);
wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data);

View File

@@ -13,6 +13,8 @@
#include <catch2/catch_all.hpp>
#include <cmath>
#include <wx/colour.h>
#include <wx/string.h>
@@ -134,3 +136,121 @@ TEST_CASE("recompute_mixed_slot_colors honours the configured ratios and is idem
recompute_mixed_slot_colors(colors, cfg);
require_same_rgb(colors[2], first);
}
// --- mixed_gradient_ramp / sample_gradient_ramp -----------------------------------------
//
// The ramp is what every mixed filament swatch is drawn from, so these pin the three things
// a plain two-endpoint fade got wrong: the reserved ratio band, the component order, and the
// custom curve.
namespace {
// Slot 3 (index 2) is a gradient mix of physical slots 1 (red) and 2 (blue).
DynamicPrintConfig gradient_config(const std::string& components = "1,2",
const std::string& range = "0.9,0.1",
const std::string& curve = "")
{
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_gradient", new ConfigOptionBools({false, false, true}));
cfg.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({"", "", range}));
cfg.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({"", "", curve}));
cfg.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#0000FF", "#000000"}));
return cfg;
}
} // namespace
TEST_CASE("mixed_gradient_ramp runs bottom to top and never reaches a pure component", "[FilamentBitmapUtils]")
{
// range "0.9,0.1": component 1 (red) is the majority at the bottom and the minority at the top.
const auto ramp = Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 2, 16);
REQUIRE(ramp.size() == 16);
// Neither end is the pure component colour - the slicer clamps the blend to
// [kGradientMinRatio, kGradientMaxRatio], which is exactly what a two-endpoint fade missed.
REQUIRE(ramp.front() != wxColour(255, 0, 0));
REQUIRE(ramp.back() != wxColour(0, 0, 255));
// Red falls and blue rises monotonically from bottom to top.
for (size_t i = 1; i < ramp.size(); ++i) {
REQUIRE(int(ramp[i].Red()) <= int(ramp[i - 1].Red()));
REQUIRE(int(ramp[i].Blue()) >= int(ramp[i - 1].Blue()));
}
}
TEST_CASE("mixed_gradient_ramp follows the range's direction rather than the component order", "[FilamentBitmapUtils]")
{
const auto rising = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.1,0.9"), 2, 16);
const auto falling = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.9,0.1"), 2, 16);
REQUIRE(rising.size() == 16);
REQUIRE(falling.size() == 16);
// "0.1,0.9" starts blue-heavy at the bottom; "0.9,0.1" starts red-heavy. Reversing the
// range must reverse the ramp, which HSV-sorted endpoint colours could not express.
REQUIRE(int(rising.front().Blue()) > int(rising.front().Red()));
REQUIRE(int(falling.front().Red()) > int(falling.front().Blue()));
require_same_rgb(rising.front(), falling.back());
}
TEST_CASE("mixed_gradient_ramp bends with a custom curve", "[FilamentBitmapUtils]")
{
// Component 1 holds near its maximum for the first half, then drops - a shape a straight
// fade between two endpoints cannot draw.
const auto curved = Slic3r::GUI::mixed_gradient_ramp(
gradient_config("1,2", "0.9,0.1", "0,0.9|0.5,0.85|1,0.1"), 2, 16);
const auto linear = Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2", "0.9,0.1"), 2, 16);
REQUIRE(curved.size() == 16);
// The curve holds component 1 high through the lower half, so every band up to mid height
// is at least as red as the straight fade and mid height is strictly redder.
for (size_t i = 0; i <= curved.size() / 2; ++i)
REQUIRE(int(curved[i].Red()) >= int(linear[i].Red()));
REQUIRE(int(curved[curved.size() / 2].Red()) > int(linear[linear.size() / 2].Red()));
// It still ends blue-dominant, like the straight fade.
REQUIRE(int(curved.back().Blue()) > int(curved.back().Red()));
}
TEST_CASE("mixed_gradient_ramp is empty for anything but a two-component gradient slot", "[FilamentBitmapUtils]")
{
SECTION("slot is not mixed") {
REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 0, 16).empty());
}
SECTION("gradient is off") {
DynamicPrintConfig cfg = gradient_config();
cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, false}));
REQUIRE(Slic3r::GUI::mixed_gradient_ramp(cfg, 2, 16).empty());
}
SECTION("three components") {
REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config("1,2,3"), 2, 16).empty());
}
SECTION("slot out of range") {
REQUIRE(Slic3r::GUI::mixed_gradient_ramp(gradient_config(), 9, 16).empty());
}
SECTION("no mixed keys at all") {
REQUIRE(Slic3r::GUI::mixed_gradient_ramp(DynamicPrintConfig{}, 0, 16).empty());
}
}
TEST_CASE("sample_gradient_ramp blends each step through the shared blender", "[FilamentBitmapUtils]")
{
// A flat curve makes every step the same 30/70 mix, which must come out as the blend the
// dialog's own swatches are drawn with - not a channel lerp between the two components.
GradientCurve curve;
curve.points = {{0.0, 0.3, NAN, NAN}, {1.0, 0.3, NAN, NAN}};
const auto ramp = Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 4);
REQUIRE(ramp.size() == 4);
const wxColour expected = Slic3r::GUI::blend_n_colors({wxColour(255, 0, 0), wxColour(0, 0, 255)}, {0.3, 0.7});
for (const wxColour& c : ramp)
require_same_rgb(c, expected);
}
TEST_CASE("sample_gradient_ramp returns nothing without a usable curve or step count", "[FilamentBitmapUtils]")
{
GradientCurve curve;
REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 8).empty());
curve.points = {{0.0, kGradientMaxRatio, NAN, NAN}, {1.0, kGradientMinRatio, NAN, NAN}};
REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 0).empty());
}