From 684cd37f8dd94e451d9013f065764b007ef6cd12 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 31 Aug 2026 18:01:15 +0800 Subject: [PATCH] UI Fixes and Polish --- src/slic3r/GUI/FilamentBitmapUtils.cpp | 385 +++++++++++++++++++++++ src/slic3r/GUI/FilamentBitmapUtils.hpp | 78 +++++ src/slic3r/GUI/GradientCurveEditor.cpp | 231 +++----------- src/slic3r/GUI/MixedFilamentDialog.cpp | 94 +----- src/slic3r/GUI/MixedFilamentDialog.hpp | 4 - src/slic3r/GUI/PublishSettingsDialog.cpp | 330 ++++++------------- src/slic3r/GUI/Widgets/TabCtrl.cpp | 2 +- 7 files changed, 622 insertions(+), 502 deletions(-) diff --git a/src/slic3r/GUI/FilamentBitmapUtils.cpp b/src/slic3r/GUI/FilamentBitmapUtils.cpp index 23b14c385b..3af3d1ad01 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.cpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.cpp @@ -1,11 +1,22 @@ #include +#include #include +#include +#include #include #include +#include +#include +#include +#include #include "EncodedFilament.hpp" #include "FilamentBitmapUtils.hpp" #include "GUI_App.hpp" +#include "GuiColor.hpp" +#include "I18N.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/StateColor.hpp" #include "libslic3r/FilamentMixer.hpp" #include "libslic3r/PrintConfig.hpp" @@ -488,4 +499,378 @@ void recompute_mixed_slot_colors(std::vector& colors, } } +namespace { + +// Layout ratios of the gradient plot rect, copied from GradientCurveEditor so the read-only +// preview and the interactive editor stay pixel-identical. Plot rect is square 1:1; the +// right/bottom margins host the axis arrows and labels. +constexpr double kPlotLeftRatio = 0.0316; +constexpr double kPlotRightRatio = 0.6766; +constexpr double kPlotTopRatio = 0.1529; +constexpr double kPlotBottomRatio = 0.8474; +constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders. +constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling) +constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP) +constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP) +constexpr int kPointRadius = 4; // anchor outer radius (DIP) +constexpr float kBgSimilarThreshold = 15.0f; +constexpr int kOutlineExtraDip = 2; +constexpr double kTriangleMarginDip = 20.0; + +// Quadratic blend that never goes out of gamut, matching MixedFilamentDialog::blend_colors. +wxColour lerp_blend(const wxColour& a, const wxColour& b, double ratio_a) +{ + unsigned char r, g, bl; + Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(), + b.Red(), b.Green(), b.Blue(), + static_cast(1.0 - ratio_a), &r, &g, &bl); + return wxColour(r, g, bl); +} + +// DIP conversion for these free functions: unlike the wxWindow member FromDIP, it needs the +// window parameter explicitly; nullptr picks the app's default DPI like the Publish dialog does. +int dip_px(int v) { return wxWindow::FromDIP(v, nullptr); } + +} // namespace + +wxRect mixed_gradient_plot_rect(const wxSize& sz) +{ + const int x = static_cast(std::lround(sz.x * kPlotLeftRatio)); + const int y = static_cast(std::lround(sz.y * kPlotTopRatio)); + const int x2 = static_cast(std::lround(sz.x * kPlotRightRatio)); + const int y2 = static_cast(std::lround(sz.y * kPlotBottomRatio)); + const int side = std::max(1, std::min(x2 - x, y2 - y)); + return wxRect(x, y, side, side); +} + +void draw_mixed_gradient_plot(wxDC& raw_dc, const wxSize& canvas, + const std::vector& curves, + const std::vector& anchors, + const MixedGradientTheme& theme) +{ + // Draw into an internal opaque buffer so wxGCDC text/curves anti-alias against a solid + // background (never a transparent one), then blit the finished image onto the caller's + // buffered paint DC. wxGCDC cannot wrap a generic wxDC&, so the buffer is always a + // wxMemoryDC -- the one type wxGCDC accepts on every platform. + if (canvas.x <= 0 || canvas.y <= 0) + return; + const wxRect rc = mixed_gradient_plot_rect(canvas); + if (rc.width <= 0 || rc.height <= 0) + return; + + wxBitmap buf(canvas); + wxMemoryDC memdc(buf); + memdc.SetBackground(wxBrush(theme.background)); + memdc.Clear(); + wxGCDC dc(memdc); + wxGraphicsContext* gc = dc.GetGraphicsContext(); + + // 10x10 light grid (10 lines including outer borders, 9 equal divisions). + dc.SetPen(wxPen(theme.grid, 1)); + for (int i = 0; i <= kGridDivisions; ++i) { + const int x = rc.x + rc.width * i / kGridDivisions; + const int y = rc.y + rc.height * i / kGridDivisions; + dc.DrawLine(x, rc.y, x, rc.y + rc.height); + dc.DrawLine(rc.x, y, rc.x + rc.width, y); + } + + // Set the label font first so text width measurements drive arrow / label placement. + wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); + dc.SetFont(label_font); + + const wxString axis_y_title = _L("Material Ratio"); + const wxString axis_x_title = _L("Model Height"); + const wxString pct_text = wxT("100%"); + const wxSize x_title_sz = dc.GetTextExtent(axis_x_title); + const wxSize y_title_sz = dc.GetTextExtent(axis_y_title); + + wxFont strong_font = label_font; + strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD); + dc.SetFont(strong_font); + const wxSize pct_text_sz = dc.GetTextExtent(pct_text); + dc.SetFont(label_font); + + // Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the + // canvas top edge; X-axis extends past the plot right toward the canvas right edge. + const int arrow_half = dip_px(kAxisArrowHalf); + const int arrow_len = dip_px(kAxisArrowLen); + dc.SetPen(wxPen(theme.axis, kStrokeAxis)); + dc.SetBrush(wxBrush(theme.axis)); + + const int y_axis_x = rc.x; + const int y_title_pct_gap = dip_px(1); + const int y_title_bottom_pad = dip_px(2); + const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad); + const int y_arrow_tip_y = y_title_y; + const int y_arrow_ty = y_arrow_tip_y + arrow_len; + dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height); + { + wxPoint tri[3] = { + wxPoint(y_axis_x, y_arrow_tip_y), + wxPoint(y_axis_x - arrow_half, y_arrow_ty), + wxPoint(y_axis_x + arrow_half, y_arrow_ty), + }; + dc.DrawPolygon(3, tri); + } + + const int x_axis_y = rc.y + rc.height; + const int x_label_gap = dip_px(4); + const int x_edge_pad = dip_px(6); + const int x_arrow_ideal = rc.x + rc.width + dip_px(10); + const int x_arrow_max = canvas.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; + const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, std::min(x_arrow_ideal, x_arrow_max)); + const int x_arrow_tip_x = x_arrow_tx + arrow_len; + const int x_title_x = x_arrow_tip_x + x_label_gap; + dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y); + { + wxPoint tri[3] = { + wxPoint(x_arrow_tip_x, x_axis_y), + wxPoint(x_arrow_tx, x_axis_y - arrow_half), + wxPoint(x_arrow_tx, x_axis_y + arrow_half), + }; + dc.DrawPolygon(3, tri); + } + + // Labels: "Material Ratio" and the leading "100%" share the same left x; the trailing + // "Model Height" follows the X-axis arrow tip (already clamped to make room). + const int label_left_x = y_axis_x + dip_px(10); + dc.SetTextForeground(theme.label); + dc.DrawText(axis_y_title, label_left_x, y_title_y); + + dc.SetFont(strong_font); + dc.SetTextForeground(theme.label_strong); + dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap); + + dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y); + dc.SetFont(label_font); + dc.SetTextForeground(theme.label); + dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); + + if (!gc) + return; + + // Outline only when the curve colour is perceptually close to the background; otherwise the + // plain filament colour reads fine and the extra stroke would look heavy. + auto needs_outline = [&](const wxColour& c) { + return calc_color_distance(c, theme.background) < kBgSimilarThreshold; + }; + + // Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint + // and would quantize the curve back to whole pixels. The pen is still set on the dc, which + // forwards it here while keeping its own cached state in sync for later dc drawing. + auto draw_polyline = [&](const MixedGradientCurve& curve) { + if (curve.points.size() < 2) + return; + dc.SetPen(wxPen(curve.colour, dip_px(curve.stroke_dip))); + gc->StrokeLines(curve.points.size(), curve.points.data()); + }; + + for (const MixedGradientCurve& curve : curves) { + if (needs_outline(curve.colour)) + draw_polyline({curve.points, theme.outline, curve.stroke_dip + kOutlineExtraDip}); + draw_polyline(curve); + } + + // Control points: hollow circle with axis-colour border, theme-aware fill, drawn with a + // sub-pixel centre so the ring stays centred on the curve. + if (!anchors.empty()) { + const double r = dip_px(kPointRadius); + dc.SetPen(wxPen(theme.axis, 1)); + dc.SetBrush(wxBrush(theme.point_fill)); + for (const wxPoint2DDouble& p : anchors) + gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); + } + + memdc.SelectObject(wxNullBitmap); + raw_dc.DrawBitmap(buf, 0, 0); +} + +void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first, + const wxColour& second, double second_fraction) +{ + if (rect.width <= 0 || rect.height <= 0) + return; + for (int x = 0; x < rect.width; ++x) { + const double t = rect.width > 1 ? double(x) / rect.width : 0.0; + const wxColour c = lerp_blend(first, second, 1.0 - t); + dc.SetPen(wxPen(c)); + dc.DrawLine(rect.x + x, rect.y, rect.x + x, rect.y + rect.height); + } + + // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over + // blended filament colour, so it has to keep its contrast against data rather than chrome. + const int div_x = rect.x + static_cast(second_fraction * rect.width); + dc.SetPen(wxPen(wxColour(80, 80, 80), dip_px(4))); + dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height); + dc.SetPen(wxPen(*wxWHITE, dip_px(2))); + dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height); +} + +void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector& colours, + const std::vector& shares) +{ + const size_t n = std::min(colours.size(), shares.size()); + if (n == 0 || rect.width <= 0 || rect.height <= 0) + return; + std::vector norm = shares; + double total = 0.0; + for (double s : norm) + total += s; + if (total <= 0.0) { + norm.assign(n, 1.0 / n); + total = 1.0; + } + auto share_to_px = [&](double share_sum) { return rect.x + int(std::lround(share_sum / total * double(rect.width))); }; + int x0 = rect.x; + std::vector segs(n); + for (size_t i = 0; i < n; ++i) { + int x1 = rect.x + rect.width; + if (i + 1 < n) + x1 = share_to_px(std::accumulate(norm.begin(), norm.begin() + i + 1, 0.0)); + segs[i] = wxRect(x0, rect.y, std::max(1, x1 - x0), rect.height); + x0 = segs[i].GetRight() + 1; + } + for (size_t i = 0; i < n; ++i) { + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(colours[i])); + dc.DrawRectangle(segs[i]); + } + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1)); + dc.DrawRectangle(rect); +} + +namespace { + +struct TriCacheKey +{ + int w, h; + int c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b; + int bg_r, bg_g, bg_b, ol_r, ol_g, ol_b; + bool operator<(const TriCacheKey& o) const + { + return std::tie(w, h, c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b, bg_r, bg_g, bg_b, ol_r, ol_g, ol_b) < + std::tie(o.w, o.h, o.c0r, o.c0g, o.c0b, o.c1r, o.c1g, o.c1b, o.c2r, o.c2g, o.c2b, o.bg_r, o.bg_g, o.bg_b, o.ol_r, o.ol_g, o.ol_b); + } +}; + +std::map& tri_cache() +{ + static std::map cache; + return cache; +} + +} // namespace + +std::array mixed_triangle_vertices(const wxSize& size, double margin_dip) +{ + const double pw = size.GetWidth(), ph = size.GetHeight(); + const double margin = dip_px(int(margin_dip)); + const double avail = std::min(pw, ph) - 2.0 * margin; + const double side = avail; + const double tri_h = side * std::sqrt(3.0) / 2.0; + const double cx = pw / 2.0; + const double top_y = (ph - tri_h) / 2.0; + return {{{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}}; +} + +void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array& colours, + const std::array& weights, const MixedTriangleTheme& theme) +{ + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + return; + const std::array v = mixed_triangle_vertices(size, kTriangleMarginDip); + + dc.SetBrush(wxBrush(theme.background)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight()); + + const wxColour& c0 = colours[0]; + const wxColour& c1 = colours[1]; + const wxColour& c2 = colours[2]; + const TriCacheKey key{size.GetWidth(), size.GetHeight(), + c0.Red(), c0.Green(), c0.Blue(), + c1.Red(), c1.Green(), c1.Blue(), + c2.Red(), c2.Green(), c2.Blue(), + theme.background.Red(), theme.background.Green(), theme.background.Blue(), + theme.outline.Red(), theme.outline.Green(), theme.outline.Blue()}; + + wxBitmap& bmp = tri_cache()[key]; + if (!bmp.IsOk()) { + bmp = wxBitmap(size.GetWidth(), size.GetHeight(), 24); + wxMemoryDC mdc(bmp); + mdc.SetBrush(wxBrush(theme.background)); + mdc.SetPen(*wxTRANSPARENT_PEN); + mdc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight()); + + const int min_y = int(std::min({v[0].y, v[1].y, v[2].y})); + const int max_y = int(std::max({v[0].y, v[1].y, v[2].y})); + const int min_x = int(std::min({v[0].x, v[1].x, v[2].x})); + const int max_x = int(std::max({v[0].x, v[1].x, v[2].x})); + for (int py = min_y; py <= max_y; ++py) { + for (int px = min_x; px <= max_x; ++px) { + const TriPoint p = {double(px), double(py)}; + if (!tri_contains(p, v[0], v[1], v[2])) + continue; + double w0, w1, w2; + tri_barycentric(p, v[0], v[1], v[2], w0, w1, w2); + unsigned char mr, mg, mb; + if (w0 + w1 > 1e-6) { + float t01 = float(w1 / (w0 + w1)); + Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, &mb); + Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), float(w2), &mr, &mg, &mb); + } else { + mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); + } + mdc.SetPen(wxPen(wxColour(mr, mg, mb))); + mdc.DrawPoint(px, py); + } + } + + mdc.SetPen(wxPen(theme.outline, 1)); + mdc.SetBrush(*wxTRANSPARENT_BRUSH); + const wxPoint pts[3] = {{int(v[0].x), int(v[0].y)}, {int(v[1].x), int(v[1].y)}, {int(v[2].x), int(v[2].y)}}; + mdc.DrawPolygon(3, pts); + mdc.SelectObject(wxNullBitmap); + + // Keep the cache from growing without bound across DPI/size changes. + if (tri_cache().size() > 6) { + auto& cache = tri_cache(); + cache.erase(cache.begin()); + } + } + dc.DrawBitmap(bmp, 0, 0); + + // Published-ratio marker (read-only twin of the editor's drag handle). + const double w0 = weights[0], w1 = weights[1], w2 = weights[2]; + const int hx = int(w0 * v[0].x + w1 * v[1].x + w2 * v[2].x); + const int hy = int(w0 * v[0].y + w1 * v[1].y + w2 * v[2].y); + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(wxPen(theme.ring, dip_px(2))); + dc.DrawCircle(hx, hy, dip_px(5)); +} + +void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array& weights, + const MixedTriangleTheme& theme) +{ + const std::array v = mixed_triangle_vertices(size, kTriangleMarginDip); + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(theme.label); + + // "Ratio" title, sitting above the top vertex. + const wxString title = _L("Ratio"); + dc.DrawText(title, dip_px(2), std::max(0, int(v[0].y - dc.GetTextExtent(title).GetHeight() - dip_px(4)))); + + for (int i = 0; i < 3; ++i) { + const wxString text = wxString::Format("%d%%", int(std::lround(weights[i] * 100.0))); + const wxSize tsz = dc.GetTextExtent(text); + int lx = int(v[i].x - tsz.GetWidth() / 2.0); + int ly = (i == 0) ? int(v[i].y - tsz.GetHeight() - dip_px(4)) : int(v[i].y + dip_px(3)); + ly = std::clamp(ly, 0, size.GetHeight() - tsz.GetHeight()); + lx = std::clamp(lx, 0, size.GetWidth() - tsz.GetWidth()); + dc.DrawText(text, lx, ly); + } +} + }} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/FilamentBitmapUtils.hpp b/src/slic3r/GUI/FilamentBitmapUtils.hpp index fc6eb1f9dd..687d34a825 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.hpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.hpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include // Orca: forward-declare so the header is self-contained outside libslic3r_gui's @@ -79,6 +82,81 @@ wxBitmap create_gradient_ramp_bitmap(const std::vector& ramp, const wx void recompute_mixed_slot_colors(std::vector& colors, const Slic3r::DynamicPrintConfig& cfg); +// --- Gradient plot (shared by GradientCurveEditor and the Publish dialog's read-only +// preview). The plot is a square 1:1 rect laid out with the editor's ratios so both +// render identically; curves are drawn as sub-pixel anti-aliased polylines through +// wxGCDC so they never quantize to whole pixels. + +// One curve of the plot: screen-space sub-pixel points already mapped into the plot +// rect, the stroke colour and the stroke width in DIP. +struct MixedGradientCurve +{ + std::vector points; + wxColour colour; + int stroke_dip; +}; + +// Theme tokens, resolved by the caller through StateColor::darkModeColorFor. +struct MixedGradientTheme +{ + wxColour background; // for near-background outline detection + wxColour grid; // grid line + wxColour axis; // axis + arrow fill + wxColour label; // "Material Ratio" / "Model Height" + wxColour label_strong; // "100%" + wxColour outline; // near-background curve lift + wxColour point_fill; // anchor fill +}; + +// Square 1:1 plot rect inside `canvas`, using the editor's plot ratios. +wxRect mixed_gradient_plot_rect(const wxSize& canvas); + +// Draw the whole plot (grid, axes + arrowheads, axis labels, each curve with an optional +// near-background outline, and anchor circles). `anchors` are empty when the caller has +// none to show. `dc` is the caller's buffered paint DC; a wxGCDC is created inside so the +// geometry gets anti-aliased. +void draw_mixed_gradient_plot(wxDC& dc, const wxSize& canvas, + const std::vector& curves, + const std::vector& anchors, + const MixedGradientTheme& theme); + +// --- Ratio bar (2-component continuous blend + divider, matching MixedFilamentDialog). +// Colours blend first->second across the rect; the divider marks `second_fraction` of the +// rect's width (the second component's share, 0..1). +void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first, + const wxColour& second, double second_fraction); + +// Fallback ratio bar for N>2 non-gradient slots: one solid segment per component, +// widths proportional to shares. Label text (the "NN%" inside wide-enough segments) is +// the caller's concern. +void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector& colours, + const std::vector& shares); + +// --- Triangle picker (3-component), shared by MixedFilamentDialog and the Publish preview. +struct MixedTriangleTheme +{ + wxColour background; + wxColour outline; // triangle border + wxColour ring; // drag-handle ring + wxColour label; // "Ratio" title + per-vertex labels +}; + +// The three vertices of the read-only/miniature triangle inside a `size` square panel, +// with `margin_dip` inset. Order: top, bottom-left, bottom-right. +std::array mixed_triangle_vertices(const wxSize& size, double margin_dip = 20.0); + +// Draw background, the cached barycentric fill, the outline and the drag-handle marker. +// `weights` are the three barycentric shares (sum 1). The "Ratio" title and per-vertex +// percentage labels are drawn by the caller so the interactive editor can keep its own +// live child labels while the read-only preview draws them as text. +void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array& colours, + const std::array& weights, const MixedTriangleTheme& theme); + +// Draw the "Ratio" title plus one "NN%" label per vertex (used by the read-only preview; +// the interactive editor positions its own live labels instead). +void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array& weights, + const MixedTriangleTheme& theme); + }} // namespace Slic3r::GUI #endif // slic3r_GUI_FilamentBitmapUtils_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/GradientCurveEditor.cpp b/src/slic3r/GUI/GradientCurveEditor.cpp index 5b1073d231..6d38ef81a5 100644 --- a/src/slic3r/GUI/GradientCurveEditor.cpp +++ b/src/slic3r/GUI/GradientCurveEditor.cpp @@ -1,4 +1,5 @@ #include "GradientCurveEditor.hpp" +#include "FilamentBitmapUtils.hpp" #include "GUI_App.hpp" #include "GuiColor.hpp" #include "I18N.hpp" @@ -19,23 +20,13 @@ namespace GUI { wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); namespace { -// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing. -// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels. -constexpr double kPlotLeftRatio = 0.0316; -constexpr double kPlotRightRatio = 0.6766; -constexpr double kPlotTopRatio = 0.1529; -constexpr double kPlotBottomRatio = 0.8474; -constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders. - -// Hit / stroke (DIP). +// Hit / stroke (DIP). The plot-rect ratios, grid divisions, axis/arrow geometry and the +// near-background outline threshold now live in FilamentBitmapUtils so the read-only Publish +// preview and this editor stay pixel-identical. constexpr int kHitRadius = 6; constexpr int kCurveHitRadius = 5; -constexpr int kPointRadius = 4; // anchor outer radius (DIP) constexpr int kStrokeUnselected = 2; constexpr int kStrokeSelected = 4; -constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention) -constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP) -constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP) // Light-mode design tokens. Resolved through StateColor::darkModeColorFor() // at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B -> @@ -46,12 +37,6 @@ const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700 const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700 const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900 const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements - -// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve -// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than -// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline. -constexpr float kBgSimilarThreshold = 15.0f; -constexpr int kOutlineExtraDip = 2; } // namespace GradientCurveEditor::GradientCurveEditor(wxWindow* parent, @@ -177,15 +162,8 @@ void GradientCurveEditor::emit_changed() wxRect GradientCurveEditor::plot_rect() const { - const wxSize sz = GetClientSize(); - const int x = static_cast(std::lround(sz.x * kPlotLeftRatio)); - const int y = static_cast(std::lround(sz.y * kPlotTopRatio)); - const int x2 = static_cast(std::lround(sz.x * kPlotRightRatio)); - const int y2 = static_cast(std::lround(sz.y * kPlotBottomRatio)); - // Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at - // the top-left so the "100%" labels on the bottom/right still align with the plot edges. - const int side = std::max(1, std::min(x2 - x, y2 - y)); - return wxRect(x, y, side, side); + // Square 1:1 plot, shared with the Publish dialog's read-only preview. + return mixed_gradient_plot_rect(GetClientSize()); } wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const @@ -330,171 +308,52 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) raw_dc.SetBackground(wxBrush(bg)); raw_dc.Clear(); - // Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered - // DC is the actual back buffer that gets blitted to the window. - wxGCDC dc(raw_dc); - // The curve and its anchors are drawn straight on the graphics context so their - // coordinates stay sub-pixel accurate (see data_to_px_f). - wxGraphicsContext* gc = dc.GetGraphicsContext(); - - const wxRect rc = plot_rect(); - if (rc.width <= 0 || rc.height <= 0) - return; - - // 10x10 light grid (10 lines including outer borders, 9 equal divisions). - dc.SetPen(wxPen(grid_color, 1)); - for (int i = 0; i <= kGridDivisions; ++i) { - const int x = rc.x + rc.width * i / kGridDivisions; - const int y = rc.y + rc.height * i / kGridDivisions; - dc.DrawLine(x, rc.y, x, rc.y + rc.height); - dc.DrawLine(rc.x, y, rc.x + rc.width, y); - } - - // Set the label font first so text width measurements drive arrow / label placement. - wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); - dc.SetFont(label_font); - - const wxString axis_y_title = _L("Material Ratio"); - const wxString axis_x_title = _L("Model Height"); - const wxString pct_text = wxT("100%"); - const wxSize x_title_sz = dc.GetTextExtent(axis_x_title); - const wxSize y_title_sz = dc.GetTextExtent(axis_y_title); - - wxFont strong_font = label_font; - strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD); - dc.SetFont(strong_font); - const wxSize pct_text_sz = dc.GetTextExtent(pct_text); - dc.SetFont(label_font); - - // Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the - // canvas top edge; X-axis extends past the plot right toward the canvas right edge. - const int arrow_half = FromDIP(kAxisArrowHalf); - const int arrow_len = FromDIP(kAxisArrowLen); - const wxSize sz = GetClientSize(); - dc.SetPen(wxPen(axis_color, kStrokeAxis)); - dc.SetBrush(wxBrush(axis_color)); - - // Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom. - const int y_axis_x = rc.x; - const int y_title_pct_gap = FromDIP(1); - const int y_title_bottom_pad = FromDIP(2); - const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad); - const int y_arrow_tip_y = y_title_y; - const int y_arrow_ty = y_arrow_tip_y + arrow_len; - dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height); - { - wxPoint tri[3] = { - wxPoint(y_axis_x, y_arrow_tip_y), - wxPoint(y_axis_x - arrow_half, y_arrow_ty), - wxPoint(y_axis_x + arrow_half, y_arrow_ty), + // Render the plot (grid, axes, labels, curves, anchors) through the shared painter so the + // interactive editor and the Publish dialog's read-only preview stay pixel-identical. The + // curves are handed over as sub-pixel polylines and anti-alias inside the helper. + std::vector curves; + std::vector anchors; + if (m_points.size() >= 2) { + auto color_for_curve = [&](int curve_idx) -> wxColour { + wxColour c = (curve_idx == 0) ? m_color_low : m_color_high; + // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible. + if (c.Alpha() == 0) + c.Set(c.Red(), c.Green(), c.Blue(), 150); + return c; }; - dc.DrawPolygon(3, tri); - } - // X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing - // "Material Ratio" label still fits inside the canvas without overlapping the arrow. - const int x_axis_y = rc.y + rc.height; - const int x_label_gap = FromDIP(4); - const int x_edge_pad = FromDIP(6); - const int x_arrow_ideal = rc.x + rc.width + FromDIP(10); - const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; - const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, - std::min(x_arrow_ideal, x_arrow_max)); - const int x_arrow_tip_x = x_arrow_tx + arrow_len; - const int x_title_x = x_arrow_tip_x + x_label_gap; - dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y); - { - wxPoint tri[3] = { - wxPoint(x_arrow_tip_x, x_axis_y), - wxPoint(x_arrow_tx, x_axis_y - arrow_half), - wxPoint(x_arrow_tx, x_axis_y + arrow_half), + auto build_polyline = [&](int curve_idx) -> std::vector { + const int samples = std::max(128, plot_rect().width * 2); + std::vector poly; + poly.reserve(samples + 1); + for (int s = 0; s <= samples; ++s) { + const double x = double(s) / samples; + const double y0 = sample_curve_y(x); + const double vy = to_visual_y(curve_idx, y0); + poly.push_back(data_to_px_f(x, vy)); + } + return poly; }; - dc.DrawPolygon(3, tri); - } - // Labels. - // "Model Height" and "100%" share the same left x; the gap is larger than the - // axis-arrow half-base so the text never visually touches the Y-axis arrow. - const int label_left_x = y_axis_x + FromDIP(10); - dc.SetTextForeground(label_muted); - dc.DrawText(axis_y_title, label_left_x, y_title_y); - - dc.SetFont(strong_font); - dc.SetTextForeground(label_strong); - dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap); - - // Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the - // X-axis arrow tip (placement was already clamped above to leave room). - dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y); - dc.SetFont(label_font); - dc.SetTextForeground(label_muted); - dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); - - if (m_points.size() < 2 || !gc) - return; - - auto color_for_curve = [&](int curve_idx) -> wxColour { - wxColour c = (curve_idx == 0) ? m_color_low : m_color_high; - // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible. - // Lift alpha so the curve stays visible while still hinting at transparency. - if (c.Alpha() == 0) - c.Set(c.Red(), c.Green(), c.Blue(), 150); - return c; - }; - - auto build_polyline = [&](int curve_idx) -> std::vector { - const int samples = std::max(128, rc.width * 2); - std::vector poly; - poly.reserve(samples + 1); - for (int s = 0; s <= samples; ++s) { - const double x = double(s) / samples; - const double y0 = sample_curve_y(x); - const double vy = to_visual_y(curve_idx, y0); - poly.push_back(data_to_px_f(x, vy)); + // Draw unselected first so the selected curve sits on top. + const int other = 1 - m_selected_curve; + for (const int idx : {other, m_selected_curve}) { + std::vector pts = build_polyline(idx); + if (pts.empty()) + continue; + curves.push_back({std::move(pts), color_for_curve(idx), idx == m_selected_curve ? kStrokeSelected : kStrokeUnselected}); } - return poly; - }; - // Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint - // and would quantize the curve back to whole pixels. The pen is still set on the dc, which - // forwards it here while keeping its own cached state in sync for later dc drawing. - auto draw_polyline = [&](const std::vector& poly, const wxColour& col, int stroke_dip) { - dc.SetPen(wxPen(col, FromDIP(stroke_dip))); - gc->StrokeLines(poly.size(), poly.data()); - }; - - // Outline only when the curve color is perceptually close to the background; otherwise - // the plain filament color reads fine and the extra stroke would look heavy. - auto needs_outline = [&](const wxColour& c) { - return calc_color_distance(c, bg) < kBgSimilarThreshold; - }; - - auto draw_one = [&](int curve_idx, int stroke_dip) { - const auto poly = build_polyline(curve_idx); - const wxColour col = color_for_curve(curve_idx); - if (needs_outline(col)) - draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip); - draw_polyline(poly, col, stroke_dip); - }; - - // Draw unselected first so the selected curve sits on top. - const int other = 1 - m_selected_curve; - draw_one(other, kStrokeUnselected); - draw_one(m_selected_curve, kStrokeSelected); - - // Control points (selected curve only): hollow circle with axis-color border, theme-aware fill. - // Drawn on the graphics context with a sub-pixel center so the ring stays centered on the - // curve instead of drifting up to half a pixel off it; pen and brush go through the dc for - // the same reason as in draw_polyline above. - const double r = FromDIP(kPointRadius); - dc.SetPen(wxPen(axis_color, 1)); - dc.SetBrush(wxBrush(point_fill)); - for (size_t i = 0; i < m_points.size(); ++i) { - const double vy = to_visual_y(m_selected_curve, m_points[i].y); - const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy); - gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); + // Control points (selected curve only). + anchors.reserve(m_points.size()); + for (size_t i = 0; i < m_points.size(); ++i) { + const double vy = to_visual_y(m_selected_curve, m_points[i].y); + anchors.push_back(data_to_px_f(m_points[i].x, vy)); + } } + + const MixedGradientTheme theme{bg, grid_color, axis_color, label_muted, label_strong, outline_color, point_fill}; + draw_mixed_gradient_plot(raw_dc, GetClientSize(), curves, anchors, theme); } void GradientCurveEditor::on_left_down(wxMouseEvent& evt) diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index c1cbcc7450..a3047aad34 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -847,23 +847,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider() m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { wxBufferedPaintDC dc(m_ratio_bar); wxSize sz = m_ratio_bar->GetClientSize(); - - wxColour col_a = comp_colour(0), col_b = comp_colour(1); - - for (int x = 0; x < sz.GetWidth(); ++x) { - double t = (double)x / sz.GetWidth(); - wxColour c = blend_colors(col_a, col_b, 1.0 - t); - dc.SetPen(wxPen(c)); - dc.DrawLine(x, 0, x, sz.GetHeight()); - } - - int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth()); - // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over - // blended filament colour, so it has to keep its contrast against data rather than chrome. - dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4))); - dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); - dc.SetPen(wxPen(*wxWHITE, FromDIP(2))); - dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); + draw_mixed_ratio_blend_bar(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()), + comp_colour(0), comp_colour(1), ratio(1) / 100.0); }); m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { @@ -954,72 +939,15 @@ wxBoxSizer* MixedFilamentDialog::create_triangle_picker() wxSize sz = m_triangle_panel->GetClientSize(); auto [v0, v1, v2] = get_vertices(); - wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); - dc.SetBrush(wxBrush(tri_bg)); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); - - wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2); - - const bool cache_valid = m_tri_cache_bmp.IsOk() && - m_tri_cache_size == sz && - m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2; - - if (!cache_valid) { - int min_y = (int)std::min({v0.y, v1.y, v2.y}); - int max_y = (int)std::max({v0.y, v1.y, v2.y}); - int min_x = (int)std::min({v0.x, v1.x, v2.x}); - int max_x = (int)std::max({v0.x, v1.x, v2.x}); - - m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24); - wxMemoryDC mdc(m_tri_cache_bmp); - mdc.SetBrush(wxBrush(tri_bg)); - mdc.SetPen(*wxTRANSPARENT_PEN); - mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); - - for (int py = min_y; py <= max_y; ++py) { - for (int px = min_x; px <= max_x; ++px) { - TriPoint p = {(double)px, (double)py}; - if (!tri_contains(p, v0, v1, v2)) continue; - double w0, w1, w2; - tri_barycentric(p, v0, v1, v2, w0, w1, w2); - unsigned char mr, mg, mb; - if (w0 + w1 > 1e-6) { - float t01 = static_cast(w1 / (w0 + w1)); - Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), - c1.Red(), c1.Green(), c1.Blue(), - t01, &mr, &mg, &mb); - float t2 = static_cast(w2); - Slic3r::filament_mixer_lerp(mr, mg, mb, - c2.Red(), c2.Green(), c2.Blue(), - t2, &mr, &mg, &mb); - } else { - mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); - } - mdc.SetPen(wxPen(wxColour(mr, mg, mb))); - mdc.DrawPoint(px, py); - } - } - - mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); - mdc.SetBrush(*wxTRANSPARENT_BRUSH); - wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}}; - mdc.DrawPolygon(3, pts); - - mdc.SelectObject(wxNullBitmap); - m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2; - m_tri_cache_size = sz; - } - - dc.DrawBitmap(m_tri_cache_bmp, 0, 0); - - // Drag handle (always redrawn on top of cached bitmap) - double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x; - double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y; - int handle_r = FromDIP(5); - dc.SetBrush(*wxWHITE_BRUSH); - dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2))); - dc.DrawCircle((int)hx, (int)hy, handle_r); + // Draw the background, cached barycentric fill, outline and drag-handle marker through the + // shared picker painter (same geometry the read-only Publish preview uses). + draw_mixed_triangle_picker(dc, sz, + {comp_colour(0), comp_colour(1), comp_colour(2)}, + {m_tri_wx, m_tri_wy, m_tri_wz}, + {StateColor::darkModeColorFor(*wxWHITE), + StateColor::darkModeColorFor(wxColour("#CECECE")), + StateColor::darkModeColorFor(wxColour("#262E30")), + StateColor::darkModeColorFor(COLOR_LABEL_MUTED)}); if (m_result.ratios.size() >= 3) { dc.SetFont(::Label::Body_10); diff --git a/src/slic3r/GUI/MixedFilamentDialog.hpp b/src/slic3r/GUI/MixedFilamentDialog.hpp index ea8ac5ad16..274eac1f92 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.hpp +++ b/src/slic3r/GUI/MixedFilamentDialog.hpp @@ -170,10 +170,6 @@ private: // Triangle picker drag point (barycentric weights) double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334}; - // Cached triangle color bitmap (invalidated when colors or size change) - wxBitmap m_tri_cache_bmp; - wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2; - wxSize m_tri_cache_size; std::array m_triangle_ratio_labels{nullptr, nullptr, nullptr}; }; diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 455a318935..73016f4902 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -31,6 +31,7 @@ #include #include #include +#include namespace Slic3r { namespace GUI { namespace { @@ -423,7 +424,7 @@ PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_ spec.tri_weights = spec.ratios; // the picker's barycentric shares } else { const Slic3r::GradientCurve curve = mixed_gradient_curve(full, slot); - constexpr int kSamples = 64; + constexpr int kSamples = 256; for (int i = 0; i <= kSamples; ++i) { const double t = double(i) / kSamples; spec.gradient_samples.emplace_back(t, sample_gradient_curve(curve, t)); @@ -894,9 +895,9 @@ size_t PublishSettingsDialog::section_group_for(Section kind) section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); - // The mixed tabs carry full swatch compositions: give them extra room to breathe so - // neighbouring compositions do not read as one long row (must precede AppendItem). - section.mixed_tabs->SetItemSpace(FromDIP(5)); + // The mixed tabs carry full swatch compositions: give them a touch more room than the + // filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem). + section.mixed_tabs->SetItemSpace(FromDIP(3)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); section.mixed_tabs->Hide(); } @@ -1180,18 +1181,10 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV auto* viz = new wxPanel(category.page, wxID_ANY); viz->SetBackgroundStyle(wxBG_STYLE_PAINT); - // Per-panel fill-bitmap cache for the ternary branch; rebuilt only when size or colours - // change (shared_ptr keeps the lifetime independent of this method's locals). - struct TriCache - { - wxBitmap bmp; - wxSize sz{0, 0}; - wxColour c0, c1, c2; - }; - auto tri_cache = std::make_shared(); // Theme colours and DIP metrics are resolved inside the paint handler so dark-mode toggles - // and DPI changes are picked up on the next repaint without any explicit listener. - viz->Bind(wxEVT_PAINT, [this, panel = viz, spec, tri_cache](wxPaintEvent&) { + // and DPI changes are picked up on the next repaint without any explicit listener. The + // ternary fill cache lives inside the shared triangle painter (FilamentBitmapUtils). + viz->Bind(wxEVT_PAINT, [this, panel = viz, spec](wxPaintEvent&) { const wxColour bg = StateColor::darkModeColorFor(*wxWHITE); wxBufferedPaintDC pdc(panel); pdc.SetBackground(wxBrush(bg)); @@ -1203,236 +1196,117 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV const size_t n = spec.component_colours.size(); if (spec.tri_weights.size() == 3 && n == 3 && !spec.is_gradient) { - // Ternary mix: a read-only miniature of the MixedFilamentDialog's triangle picker. - // Per-pixel barycentric fill is cached into a bitmap keyed on size + colours; the - // marker and labels are redrawn on top every paint. - const wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); - const wxColour outline = StateColor::darkModeColorFor(wxColour("#CECECE")); - const wxColour ring = StateColor::darkModeColorFor(wxColour("#262E30")); - const wxColour label_c = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 - const double margin_dip = 24.0; - auto& cache = *tri_cache; - - auto vertices_for = [&](const wxSize& sz) -> std::tuple { - const double pw = sz.GetWidth(), ph = sz.GetHeight(); - const int margin = FromDIP(int(margin_dip)); - const double avail = std::min(pw, ph) - 2.0 * margin; - const double side = avail; - const double tri_h = side * std::sqrt(3.0) / 2.0; - const double cx = pw / 2.0; - const double top_y = (ph - tri_h) / 2.0; - return {{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}; - }; - - pdc.SetFont(::Label::Body_12); - const wxColour& c0 = spec.component_colours[0]; - const wxColour& c1 = spec.component_colours[1]; - const wxColour& c2 = spec.component_colours[2]; - - if (!cache.bmp.IsOk() || cache.sz != rc.GetSize() || cache.c0 != c0 || cache.c1 != c1 || cache.c2 != c2) { - auto [v0, v1, v2] = vertices_for(rc.GetSize()); - cache.bmp = wxBitmap(rc.width, rc.height, 32); - wxMemoryDC mdc(cache.bmp); - mdc.SetBrush(wxBrush(tri_bg)); - mdc.SetPen(*wxTRANSPARENT_PEN); - mdc.DrawRectangle(0, 0, rc.width, rc.height); - - const int min_y = int(std::min({v0.y, v1.y, v2.y})); - const int max_y = int(std::max({v0.y, v1.y, v2.y})); - const int min_x = int(std::min({v0.x, v1.x, v2.x})); - const int max_x = int(std::max({v0.x, v1.x, v2.x})); - for (int py = min_y; py <= max_y; ++py) - for (int px = min_x; px <= max_x; ++px) { - const TriPoint p = {double(px), double(py)}; - if (!tri_contains(p, v0, v1, v2)) - continue; - double w0, w1, w2; - tri_barycentric(p, v0, v1, v2, w0, w1, w2); - unsigned char mr, mg, mb; - if (w0 + w1 > 1e-6) { - float t01 = static_cast(w1 / (w0 + w1)); - Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, - &mb); - Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), static_cast(w2), &mr, &mg, &mb); - } else { - mr = c2.Red(); - mg = c2.Green(); - mb = c2.Blue(); - } - mdc.SetPen(wxPen(wxColour(mr, mg, mb))); - mdc.DrawPoint(px, py); - } - - mdc.SetPen(wxPen(outline, 1)); - mdc.SetBrush(*wxTRANSPARENT_BRUSH); - const wxPoint pts[3] = {{int(v0.x), int(v0.y)}, {int(v1.x), int(v1.y)}, {int(v2.x), int(v2.y)}}; - mdc.DrawPolygon(3, pts); - mdc.SelectObject(wxNullBitmap); - - cache.sz = rc.GetSize(); - cache.c0 = c0; - cache.c1 = c1; - cache.c2 = c2; - } - pdc.DrawBitmap(cache.bmp, 0, 0); - - // Published-ratio marker (read-only twin of the editor's drag handle). - { - auto [v0, v1, v2] = vertices_for(rc.GetSize()); - const double w0 = spec.tri_weights[0], w1 = spec.tri_weights[1], w2 = spec.tri_weights[2]; - const int hx = int(w0 * v0.x + w1 * v1.x + w2 * v2.x); - const int hy = int(w0 * v0.y + w1 * v1.y + w2 * v2.y); - pdc.SetBrush(*wxWHITE_BRUSH); - pdc.SetPen(wxPen(ring, FromDIP(2))); - pdc.DrawCircle(hx, hy, FromDIP(5)); - - // Percent label beside each vertex. - for (int i = 0; i < 3; ++i) { - const wxString text = wxString::Format("%d%%", int(std::lround(spec.tri_weights[i] * 100.0))); + // Ternary mix: read-only miniature of the MixedFilamentDialog's triangle picker. + const MixedTriangleTheme tri_theme{StateColor::darkModeColorFor(*wxWHITE), + StateColor::darkModeColorFor(wxColour("#CECECE")), + StateColor::darkModeColorFor(wxColour("#262E30")), + StateColor::darkModeColorFor(wxColour(107, 107, 107))}; + draw_mixed_triangle_picker(pdc, rc.GetSize(), {spec.component_colours[0], spec.component_colours[1], spec.component_colours[2]}, + {spec.tri_weights[0], spec.tri_weights[1], spec.tri_weights[2]}, tri_theme); + draw_mixed_triangle_labels(pdc, rc.GetSize(), {spec.tri_weights[0], spec.tri_weights[1], spec.tri_weights[2]}, tri_theme); + } else if (!spec.is_gradient) { + // Ratio bar. A 2-component slot matches the MixedFilamentDialog's continuous blend + + // divider (with its two end labels); wider non-gradient mixes (rare) fall back to one + // solid segment per component. + if (n == 2) { + const int bar_h = FromDIP(27); + draw_mixed_ratio_blend_bar(pdc, wxRect(rc.x, rc.y, rc.width, bar_h), spec.component_colours[0], + spec.component_colours[1], spec.ratios[1]); + // Read-only twin of the dialog's left/right percentage labels. + pdc.SetFont(::Label::Body_12); + pdc.SetTextForeground(StateColor::darkModeColorFor(wxColour(107, 107, 107))); + const wxString la = wxString::Format("%d%%", int(std::lround(spec.ratios[0] * 100.0))); + const wxString lb = wxString::Format("%d%%", int(std::lround(spec.ratios[1] * 100.0))); + const int lab_y = rc.y + bar_h + FromDIP(2); + pdc.DrawText(la, rc.x, lab_y); + pdc.DrawText(lb, rc.x + rc.width - pdc.GetTextExtent(lb).GetWidth(), lab_y); + } else { + draw_mixed_ratio_segments(pdc, rc, spec.component_colours, spec.ratios); + // Percent label centred in each segment wide enough to hold it. + std::vector shares = spec.ratios; + double total = 0.0; + for (double r : shares) + total += r; + if (total <= 0.0) { + shares.assign(n, 1.0 / n); + total = 1.0; + } + pdc.SetFont(::Label::Body_12); + int x0 = rc.x; + for (size_t i = 0; i < n; ++i) { + const int x1 = (i + 1 < n) + ? rc.x + int(std::lround(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0) / total * double(rc.width))) + : rc.x + rc.width; + const int w = std::max(1, x1 - x0); + const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0))); const wxSize tsz = pdc.GetTextExtent(text); - const TriPoint vtx = (i == 0) ? v0 : (i == 1) ? v1 : v2; - int lx = int(vtx.x - tsz.GetWidth() / 2.0); - int ly = (i == 0) ? int(vtx.y - tsz.GetHeight()) : int(vtx.y + FromDIP(3)); - ly = std::clamp(ly, 0, rc.height - tsz.GetHeight()); - lx = std::clamp(lx, 0, rc.width - tsz.GetWidth()); - pdc.SetTextForeground(label_c); - pdc.DrawText(text, lx, ly); + if (tsz.GetWidth() + FromDIP(4) <= w) { + const wxColour& c = spec.component_colours[i]; + const double lum = 0.299 * c.Red() + 0.587 * c.Green() + 0.114 * c.Blue(); + pdc.SetTextForeground(lum > 140 ? wxColour("#262E30") : *wxWHITE); + pdc.DrawText(text, x0 + (w - tsz.GetWidth()) / 2, rc.y + (rc.height - tsz.GetHeight()) / 2); + } + x0 = x1; } } - } else if (!spec.is_gradient) { - // Stacked ratio bar: one solid segment per component, widths proportional to the - // published shares. Integer widths accumulate left to right; the last segment takes - // the rounding remainder so the bar always fills exactly. - std::vector shares = spec.ratios; - double total = 0.0; - for (double r : shares) - total += r; - if (shares.size() != n || total <= 0.0) { - shares.assign(n, 1.0 / n); - total = 1.0; - } - auto share_to_px = [&](double share_sum) { return rc.x + int(std::lround(share_sum / total * double(rc.width))); }; - std::vector segs(n); - int x0 = rc.x; - for (size_t i = 0; i < n; ++i) { - int x1 = rc.x + rc.width; - if (i + 1 < n) - x1 = share_to_px(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0)); - segs[i] = wxRect(x0, rc.y, std::max(1, x1 - x0), rc.height); - x0 = segs[i].GetRight() + 1; - } - - for (size_t i = 0; i < n; ++i) { - pdc.SetPen(*wxTRANSPARENT_PEN); - pdc.SetBrush(wxBrush(spec.component_colours[i])); - pdc.DrawRectangle(segs[i]); - } - pdc.SetBrush(*wxTRANSPARENT_BRUSH); - pdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1)); - pdc.DrawRectangle(rc); - - // Percent label centred in each segment wide enough to hold it. - pdc.SetFont(::Label::Body_12); - for (size_t i = 0; i < n; ++i) { - const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0))); - const wxSize tsz = pdc.GetTextExtent(text); - if (tsz.GetWidth() + FromDIP(4) > segs[i].GetWidth()) - continue; - // Label contrast follows the swatch itself, not the theme. - const wxColour& c = spec.component_colours[i]; - const double lum = 0.299 * c.Red() + 0.587 * c.Green() + 0.114 * c.Blue(); - pdc.SetTextForeground(lum > 140 ? wxColour("#262E30") : *wxWHITE); - pdc.DrawText(text, segs[i].x + (segs[i].GetWidth() - tsz.GetWidth()) / 2, rc.y + (rc.height - tsz.GetHeight()) / 2); - } } else { - // Gradient: compact "Material Ratio" over "Model Height" graph, a read-only - // miniature of the GradientCurveEditor plot. Component order matches the config; - // the second component's curve is the mirror of the first's. - const wxColour grid_color = StateColor::darkModeColorFor(wxColour(238, 238, 238)); // grey 300 - const wxColour axis_color = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 - const wxColour label_muted = StateColor::darkModeColorFor(wxColour(107, 107, 107)); - const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE); - - const int pad_left = FromDIP(34); - const int pad_right = FromDIP(10); - const int pad_top = FromDIP(18); - const int pad_bottom = FromDIP(16); - const wxRect plot(rc.x + pad_left, rc.y + pad_top, std::max(1, rc.width - pad_left - pad_right), - std::max(1, rc.height - pad_top - pad_bottom)); - - constexpr int kGridDivisions = 5; - pdc.SetPen(wxPen(grid_color, 1)); - for (int i = 0; i <= kGridDivisions; ++i) { - const int gx = plot.x + plot.width * i / kGridDivisions; - const int gy = plot.y + plot.height * i / kGridDivisions; - pdc.DrawLine(gx, plot.y, gx, plot.y + plot.height); - pdc.DrawLine(plot.x, gy, plot.x + plot.width, gy); - } - - // Axes with small filled arrowheads along the plot's left and bottom edges. - const int arrow_len = FromDIP(7); - const int arrow_half = FromDIP(3); - pdc.SetPen(wxPen(axis_color, 1)); - pdc.SetBrush(wxBrush(axis_color)); - pdc.DrawLine(plot.x, plot.y + plot.height, plot.x, plot.y); - { - wxPoint tri[3] = {wxPoint(plot.x, plot.y - arrow_len), wxPoint(plot.x - arrow_half, plot.y), - wxPoint(plot.x + arrow_half, plot.y)}; - pdc.DrawPolygon(3, tri); - } - pdc.DrawLine(plot.x, plot.y + plot.height, plot.x + plot.width, plot.y + plot.height); - { - wxPoint tri[3] = {wxPoint(plot.x + plot.width + arrow_len, plot.y + plot.height), - wxPoint(plot.x + plot.width, plot.y + plot.height - arrow_half), - wxPoint(plot.x + plot.width, plot.y + plot.height + arrow_half)}; - pdc.DrawPolygon(3, tri); - } - - wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); - pdc.SetFont(label_font); - pdc.SetTextForeground(label_muted); - pdc.DrawText(_L("Material Ratio"), plot.x + FromDIP(4), plot.y - pdc.GetTextExtent(_L("Material Ratio")).GetHeight()); - const wxString height_title = _L("Model Height"); - pdc.DrawText(height_title, plot.x + plot.width - pdc.GetTextExtent(height_title).GetWidth(), plot.y + plot.height + FromDIP(2)); - + // Gradient: read-only miniature of the GradientCurveEditor plot, drawn through the + // shared painter so it anti-aliases and keeps the square proportions and labels. + const MixedGradientTheme grad_theme{StateColor::darkModeColorFor(*wxWHITE), + StateColor::darkModeColorFor(wxColour(238, 238, 238)), + StateColor::darkModeColorFor(wxColour(107, 107, 107)), + StateColor::darkModeColorFor(wxColour(107, 107, 107)), + StateColor::darkModeColorFor(wxColour(38, 46, 48)), + StateColor::darkModeColorFor(wxColour(172, 172, 172)), + StateColor::darkModeColorFor(*wxWHITE)}; + std::vector curves; + std::vector anchors; if (spec.gradient_samples.size() >= 2 && n >= 2) { - auto curve_point = [&](double t, double ratio) { - return wxPoint(plot.x + int(std::lround(t * plot.width)), plot.y + int(std::lround((1.0 - ratio) * plot.height))); + const wxRect plot = mixed_gradient_plot_rect(rc.GetSize()); + auto lift_alpha = [](wxColour c) { + if (c.Alpha() == 0) + c.Set(c.Red(), c.Green(), c.Blue(), 150); + return c; }; - // First component's ratio solid, its mirror dashed-free twin for the other. - const wxColour& col_a = spec.component_colours[0]; - const wxColour& col_b = spec.component_colours[1]; - std::vector pts_a, pts_b; + // First component's ratio solid, its mirror twin for the other. + const wxColour col_a = lift_alpha(spec.component_colours[0]); + const wxColour col_b = lift_alpha(spec.component_colours[1]); + std::vector pts_a, pts_b; pts_a.reserve(spec.gradient_samples.size()); pts_b.reserve(spec.gradient_samples.size()); for (const auto& [t, r] : spec.gradient_samples) { - pts_a.push_back(curve_point(t, r)); - pts_b.push_back(curve_point(t, 1.0 - r)); + pts_a.push_back({plot.x + t * plot.width, plot.y + (1.0 - r) * plot.height}); + pts_b.push_back({plot.x + t * plot.width, plot.y + r * plot.height}); } - pdc.SetPen(wxPen(col_b, 2)); - for (size_t i = 0; i + 1 < pts_b.size(); ++i) - pdc.DrawLine(pts_b[i], pts_b[i + 1]); - pdc.SetPen(wxPen(col_a, 2)); - for (size_t i = 0; i + 1 < pts_a.size(); ++i) - pdc.DrawLine(pts_a[i], pts_a[i + 1]); - // Control-point anchors of the stored curve on the first component's line. - pdc.SetBrush(wxBrush(point_fill)); - pdc.SetPen(wxPen(col_a, 1)); - for (const auto& [t, r] : spec.gradient_anchors) { - const wxPoint c = curve_point(t, r); - pdc.DrawCircle(c, FromDIP(3)); - } + anchors.reserve(spec.gradient_anchors.size()); + for (const auto& [t, r] : spec.gradient_anchors) + anchors.push_back({plot.x + t * plot.width, plot.y + (1.0 - r) * plot.height}); + curves.push_back({std::move(pts_a), col_a, 4}); + curves.push_back({std::move(pts_b), col_b, 2}); } + draw_mixed_gradient_plot(pdc, rc.GetSize(), curves, anchors, grad_theme); } }); // Fixed DIP size, left-aligned: the visualization keeps its proportions no matter how the // dialog is resized (the paint handler draws into whatever client rect the panel ends up - // with, so nothing else has to change). - const int viz_h = spec.is_gradient ? 150 : (spec.tri_weights.size() == 3 ? 180 : 30); - const wxSize viz_sz(FromDIP(240), FromDIP(viz_h)); + // with, so nothing else has to change). Sizes mirror the MixedFilamentDialog controls: the + // gradient plot and triangle picker match the editor's 260x200 / 160x160, the ratio bar is + // the dialog's 27px bar plus its label line. + int viz_h; + int viz_w; + if (spec.is_gradient) { + viz_w = 260; + viz_h = 200; + } else if (spec.tri_weights.size() == 3 && spec.component_colours.size() == 3) { + viz_w = 160; + viz_h = 160; + } else { + viz_w = 240; + viz_h = spec.component_colours.size() == 2 ? 50 : 30; + } + const wxSize viz_sz(FromDIP(viz_w), FromDIP(viz_h)); viz->SetMinSize(viz_sz); viz->SetMaxSize(viz_sz); // Parented to the page right above the scroll area, so it is always shown with the tab: diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 7e1f748e36..4090bf9c1b 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -92,7 +92,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli btn->Create(this, item, "", wxBORDER_NONE); btn->SetFont(GetFont()); btn->SetTextColor( - StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal))); + StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(wxColour("#262E30"), (int) StateColor::Normal))); btn->SetBackgroundColor(StateColor()); btn->SetCornerRadius(0); btn->SetPaddingSize({TAB_BUTTON_PADDING});