From 4a32a9e0664b4d2a9fac452018ed51ceef96ac67 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 23 Aug 2026 21:55:02 +0800 Subject: [PATCH] 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. --- src/slic3r/GUI/FilamentBitmapUtils.cpp | 123 +++++++++++++++++- src/slic3r/GUI/FilamentBitmapUtils.hpp | 29 ++++- src/slic3r/GUI/GLCanvas3D.cpp | 18 +-- .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 35 ++--- .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 20 ++- src/slic3r/GUI/ImGuiWrapper.cpp | 19 +++ src/slic3r/GUI/ImGuiWrapper.hpp | 16 +++ src/slic3r/GUI/MixedFilamentDialog.cpp | 33 +---- src/slic3r/GUI/Plater.cpp | 74 ++++++----- src/slic3r/GUI/Plater.hpp | 14 +- src/slic3r/GUI/wxExtensions.cpp | 56 +++++--- src/slic3r/GUI/wxExtensions.hpp | 5 +- .../test_filament_bitmap_utils.cpp | 120 +++++++++++++++++ 13 files changed, 429 insertions(+), 133 deletions(-) diff --git a/src/slic3r/GUI/FilamentBitmapUtils.cpp b/src/slic3r/GUI/FilamentBitmapUtils.cpp index b3f58fa4f1..45368e0537 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.cpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.cpp @@ -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& cols, const std::vector& weights) +{ + const size_t n = std::min(cols.size(), weights.size()); + std::vector hex_colors; + std::vector 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(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 sample_gradient_ramp(const wxColour& first, + const wxColour& second, + const Slic3r::GradientCurve& curve, + int steps) +{ + std::vector 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("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("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 mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps) +{ + const auto* is_mixed_opt = cfg.option("filament_is_mixed"); + const auto* grad_opt = cfg.option("filament_mixed_gradient"); + const auto* comp_opt = cfg.option("filament_mixed_components"); + const auto* colour_opt = cfg.option("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& 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(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& 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& 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 { diff --git a/src/slic3r/GUI/FilamentBitmapUtils.hpp b/src/slic3r/GUI/FilamentBitmapUtils.hpp index 2e428e8d32..9cb8d64249 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.hpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.hpp @@ -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& 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& cols, const std::vector& 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 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 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& ramp); + +// Swatch bitmap for a gradient mixed filament, drawn bottom to top from the ramp. +wxBitmap create_gradient_ramp_bitmap(const std::vector& 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. diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 0aeb1b504e..94b0923885 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -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 &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 }); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 45839b66ea..3f05f9c22c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -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* 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& 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 } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 9f080fb300..7d83468ea5 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -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(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 color_from = {0.5f, 0.5f, 0.5f, 1.0f}; - std::array 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 m_extruder_remap; // index → target extruder index - std::vector 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> m_gradient_ramps; // ORCA: Cache used filaments to filter UI std::set 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* 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 diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index d46e8ed31b..0c237a36f2 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -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 &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); diff --git a/src/slic3r/GUI/ImGuiWrapper.hpp b/src/slic3r/GUI/ImGuiWrapper.hpp index b586094ab3..db94b3edcb 100644 --- a/src/slic3r/GUI/ImGuiWrapper.hpp +++ b/src/slic3r/GUI/ImGuiWrapper.hpp @@ -3,10 +3,12 @@ #include #include +#include #include #include +#include #include #include "libslic3r/Point.hpp" @@ -299,6 +301,20 @@ public: int num_segments = 0, float thickness = 4.f); + /// + /// 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. + /// + /// Define where to draw it + /// Upper left corner of the rect + /// Lower right corner of the rect + /// Colours printed, bottom of the model first + static void draw_gradient_ramp(ImDrawList * draw_list, + const ImVec2 & top_left, + const ImVec2 & bottom_right, + const std::vector &ramp); + /// /// Check that font ranges contain all chars in string /// (rendered Unicodes are stored in GlyphRanges) diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index b72846452f..42ed49bb70 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -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& cols, const std::vector& weights) -{ - std::vector hex_colors; - std::vector 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(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(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(b * band_h); - int bh = static_cast((b + 1) * band_h) - static_cast(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. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7ff7b193df..c730dd705e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -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 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 Plater::get_filament_color_render_type() const return ctype; } -std::vector Plater::get_filament_gradient_info() const +const std::vector>& 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 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> 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("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 diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 147f61bed6..5308deec61 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -5,6 +5,7 @@ #include #include +#include #include // BBS #include @@ -607,14 +608,11 @@ public: std::vector get_filament_colors_render_info() const; std::vector 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 color_from = {0.5f, 0.5f, 0.5f, 1.0f}; - std::array color_to = {0.5f, 0.5f, 0.5f, 1.0f}; - }; - std::vector 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>& get_filament_gradient_ramps() const; std::vector get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const; void set_global_filament_map_mode(FilamentMapMode mode); diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 2ca8f3cfbd..88e2df31c0 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -555,14 +555,20 @@ std::vector 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* 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 colors, bool is_gradient, std::string label, int icon_width, int icon_height){ +wxBitmap *get_extruder_color_icon(std::vector colors, bool is_gradient, std::string label, int icon_width, int icon_height, + const std::vector *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 colors, bool is_gradi #endif if (bitmap == nullptr) { - std::vector 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 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 diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 502614eb92..2754b3e5ba 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -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 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 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 colors, bool is_gradient, std::string label, int icon_width, int icon_height, + const std::vector *ramp = nullptr); std::vector> read_color_pack(std::vector color_pack); wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_data); diff --git a/tests/slic3rutils/test_filament_bitmap_utils.cpp b/tests/slic3rutils/test_filament_bitmap_utils.cpp index 9054a82018..35c32570f4 100644 --- a/tests/slic3rutils/test_filament_bitmap_utils.cpp +++ b/tests/slic3rutils/test_filament_bitmap_utils.cpp @@ -13,6 +13,8 @@ #include +#include + #include #include @@ -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()); +}