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()); +}