UI Fixes and Polish

This commit is contained in:
Lam Wei Lun
2026-08-31 18:01:15 +08:00
parent 21c0bf795a
commit 684cd37f8d
7 changed files with 622 additions and 502 deletions

View File

@@ -1,11 +1,22 @@
#include <wx/dcmemory.h> #include <wx/dcmemory.h>
#include <wx/dcgraph.h>
#include <wx/graphics.h> #include <wx/graphics.h>
#include <wx/settings.h>
#include <wx/window.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <map>
#include <numeric>
#include <string>
#include <tuple>
#include "EncodedFilament.hpp" #include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp" #include "FilamentBitmapUtils.hpp"
#include "GUI_App.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/FilamentMixer.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
@@ -488,4 +499,378 @@ void recompute_mixed_slot_colors(std::vector<wxColour>& 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<float>(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<int>(std::lround(sz.x * kPlotLeftRatio));
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
const int y2 = static_cast<int>(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<MixedGradientCurve>& curves,
const std::vector<wxPoint2DDouble>& 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<int>(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<wxColour>& colours,
const std::vector<double>& shares)
{
const size_t n = std::min(colours.size(), shares.size());
if (n == 0 || rect.width <= 0 || rect.height <= 0)
return;
std::vector<double> 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<wxRect> 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<TriCacheKey, wxBitmap>& tri_cache()
{
static std::map<TriCacheKey, wxBitmap> cache;
return cache;
}
} // namespace
std::array<TriPoint, 3> 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<wxColour, 3>& colours,
const std::array<double, 3>& weights, const MixedTriangleTheme& theme)
{
if (size.GetWidth() <= 0 || size.GetHeight() <= 0)
return;
const std::array<TriPoint, 3> 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<double, 3>& weights,
const MixedTriangleTheme& theme)
{
const std::array<TriPoint, 3> 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 }} // namespace Slic3r::GUI

View File

@@ -5,6 +5,9 @@
#include <wx/colour.h> #include <wx/colour.h>
#include <wx/dc.h> #include <wx/dc.h>
#include <wx/gdicmn.h> #include <wx/gdicmn.h>
#include <wx/geometry.h>
#include <wx/graphics.h>
#include <array>
#include <vector> #include <vector>
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's // 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<wxColour>& ramp, const wx
void recompute_mixed_slot_colors(std::vector<wxColour>& colors, void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg); 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<wxPoint2DDouble> 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<MixedGradientCurve>& curves,
const std::vector<wxPoint2DDouble>& 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<wxColour>& colours,
const std::vector<double>& 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<TriPoint, 3> 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<wxColour, 3>& colours,
const std::array<double, 3>& 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<double, 3>& weights,
const MixedTriangleTheme& theme);
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_ #endif // slic3r_GUI_FilamentBitmapUtils_hpp_

View File

@@ -1,4 +1,5 @@
#include "GradientCurveEditor.hpp" #include "GradientCurveEditor.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "GuiColor.hpp" #include "GuiColor.hpp"
#include "I18N.hpp" #include "I18N.hpp"
@@ -19,23 +20,13 @@ namespace GUI {
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
namespace { namespace {
// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing. // Hit / stroke (DIP). The plot-rect ratios, grid divisions, axis/arrow geometry and the
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels. // near-background outline threshold now live in FilamentBitmapUtils so the read-only Publish
constexpr double kPlotLeftRatio = 0.0316; // preview and this editor stay pixel-identical.
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).
constexpr int kHitRadius = 6; constexpr int kHitRadius = 6;
constexpr int kCurveHitRadius = 5; constexpr int kCurveHitRadius = 5;
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
constexpr int kStrokeUnselected = 2; constexpr int kStrokeUnselected = 2;
constexpr int kStrokeSelected = 4; 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() // Light-mode design tokens. Resolved through StateColor::darkModeColorFor()
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B -> // 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 kLabelMuted (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900 const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements 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 } // namespace
GradientCurveEditor::GradientCurveEditor(wxWindow* parent, GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
@@ -177,15 +162,8 @@ void GradientCurveEditor::emit_changed()
wxRect GradientCurveEditor::plot_rect() const wxRect GradientCurveEditor::plot_rect() const
{ {
const wxSize sz = GetClientSize(); // Square 1:1 plot, shared with the Publish dialog's read-only preview.
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio)); return mixed_gradient_plot_rect(GetClientSize());
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
const int y2 = static_cast<int>(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);
} }
wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const 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.SetBackground(wxBrush(bg));
raw_dc.Clear(); raw_dc.Clear();
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered // Render the plot (grid, axes, labels, curves, anchors) through the shared painter so the
// DC is the actual back buffer that gets blitted to the window. // interactive editor and the Publish dialog's read-only preview stay pixel-identical. The
wxGCDC dc(raw_dc); // curves are handed over as sub-pixel polylines and anti-alias inside the helper.
// The curve and its anchors are drawn straight on the graphics context so their std::vector<MixedGradientCurve> curves;
// coordinates stay sub-pixel accurate (see data_to_px_f). std::vector<wxPoint2DDouble> anchors;
wxGraphicsContext* gc = dc.GetGraphicsContext(); if (m_points.size() >= 2) {
auto color_for_curve = [&](int curve_idx) -> wxColour {
const wxRect rc = plot_rect(); wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
if (rc.width <= 0 || rc.height <= 0) // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
return; if (c.Alpha() == 0)
c.Set(c.Red(), c.Green(), c.Blue(), 150);
// 10x10 light grid (10 lines including outer borders, 9 equal divisions). return c;
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),
}; };
dc.DrawPolygon(3, tri);
}
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
// "Material Ratio" label still fits inside the canvas without overlapping the arrow. const int samples = std::max(128, plot_rect().width * 2);
const int x_axis_y = rc.y + rc.height; std::vector<wxPoint2DDouble> poly;
const int x_label_gap = FromDIP(4); poly.reserve(samples + 1);
const int x_edge_pad = FromDIP(6); for (int s = 0; s <= samples; ++s) {
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10); const double x = double(s) / samples;
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; const double y0 = sample_curve_y(x);
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, const double vy = to_visual_y(curve_idx, y0);
std::min(x_arrow_ideal, x_arrow_max)); poly.push_back(data_to_px_f(x, vy));
const int x_arrow_tip_x = x_arrow_tx + arrow_len; }
const int x_title_x = x_arrow_tip_x + x_label_gap; return poly;
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. // Draw unselected first so the selected curve sits on top.
// "Model Height" and "100%" share the same left x; the gap is larger than the const int other = 1 - m_selected_curve;
// axis-arrow half-base so the text never visually touches the Y-axis arrow. for (const int idx : {other, m_selected_curve}) {
const int label_left_x = y_axis_x + FromDIP(10); std::vector<wxPoint2DDouble> pts = build_polyline(idx);
dc.SetTextForeground(label_muted); if (pts.empty())
dc.DrawText(axis_y_title, label_left_x, y_title_y); continue;
curves.push_back({std::move(pts), color_for_curve(idx), idx == m_selected_curve ? kStrokeSelected : kStrokeUnselected});
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<wxPoint2DDouble> {
const int samples = std::max(128, rc.width * 2);
std::vector<wxPoint2DDouble> 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;
};
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint // Control points (selected curve only).
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which anchors.reserve(m_points.size());
// forwards it here while keeping its own cached state in sync for later dc drawing. for (size_t i = 0; i < m_points.size(); ++i) {
auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) { const double vy = to_visual_y(m_selected_curve, m_points[i].y);
dc.SetPen(wxPen(col, FromDIP(stroke_dip))); anchors.push_back(data_to_px_f(m_points[i].x, vy));
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);
} }
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) void GradientCurveEditor::on_left_down(wxMouseEvent& evt)

View File

@@ -847,23 +847,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider()
m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) {
wxBufferedPaintDC dc(m_ratio_bar); wxBufferedPaintDC dc(m_ratio_bar);
wxSize sz = m_ratio_bar->GetClientSize(); wxSize sz = m_ratio_bar->GetClientSize();
draw_mixed_ratio_blend_bar(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()),
wxColour col_a = comp_colour(0), col_b = comp_colour(1); comp_colour(0), comp_colour(1), ratio(1) / 100.0);
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());
}); });
m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { 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(); wxSize sz = m_triangle_panel->GetClientSize();
auto [v0, v1, v2] = get_vertices(); auto [v0, v1, v2] = get_vertices();
wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); // Draw the background, cached barycentric fill, outline and drag-handle marker through the
dc.SetBrush(wxBrush(tri_bg)); // shared picker painter (same geometry the read-only Publish preview uses).
dc.SetPen(*wxTRANSPARENT_PEN); draw_mixed_triangle_picker(dc, sz,
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); {comp_colour(0), comp_colour(1), comp_colour(2)},
{m_tri_wx, m_tri_wy, m_tri_wz},
wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2); {StateColor::darkModeColorFor(*wxWHITE),
StateColor::darkModeColorFor(wxColour("#CECECE")),
const bool cache_valid = m_tri_cache_bmp.IsOk() && StateColor::darkModeColorFor(wxColour("#262E30")),
m_tri_cache_size == sz && StateColor::darkModeColorFor(COLOR_LABEL_MUTED)});
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<float>(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<float>(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);
if (m_result.ratios.size() >= 3) { if (m_result.ratios.size() >= 3) {
dc.SetFont(::Label::Body_10); dc.SetFont(::Label::Body_10);

View File

@@ -170,10 +170,6 @@ private:
// Triangle picker drag point (barycentric weights) // Triangle picker drag point (barycentric weights)
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334}; 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<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr}; std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
}; };

View File

@@ -31,6 +31,7 @@
#include <memory> #include <memory>
#include <numeric> #include <numeric>
#include <tuple> #include <tuple>
#include <utility>
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
namespace { namespace {
@@ -423,7 +424,7 @@ PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_
spec.tri_weights = spec.ratios; // the picker's barycentric shares spec.tri_weights = spec.ratios; // the picker's barycentric shares
} else { } else {
const Slic3r::GradientCurve curve = mixed_gradient_curve(full, slot); const Slic3r::GradientCurve curve = mixed_gradient_curve(full, slot);
constexpr int kSamples = 64; constexpr int kSamples = 256;
for (int i = 0; i <= kSamples; ++i) { for (int i = 0; i <= kSamples; ++i) {
const double t = double(i) / kSamples; const double t = double(i) / kSamples;
spec.gradient_samples.emplace_back(t, sample_gradient_curve(curve, t)); 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 = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetFont(Label::Body_14);
section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour());
// The mixed tabs carry full swatch compositions: give them extra room to breathe so // The mixed tabs carry full swatch compositions: give them a touch more room than the
// neighbouring compositions do not read as one long row (must precede AppendItem). // filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem).
section.mixed_tabs->SetItemSpace(FromDIP(5)); section.mixed_tabs->SetItemSpace(FromDIP(3));
page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2));
section.mixed_tabs->Hide(); 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); auto* viz = new wxPanel(category.page, wxID_ANY);
viz->SetBackgroundStyle(wxBG_STYLE_PAINT); 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<TriCache>();
// Theme colours and DIP metrics are resolved inside the paint handler so dark-mode toggles // 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. // and DPI changes are picked up on the next repaint without any explicit listener. The
viz->Bind(wxEVT_PAINT, [this, panel = viz, spec, tri_cache](wxPaintEvent&) { // 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); const wxColour bg = StateColor::darkModeColorFor(*wxWHITE);
wxBufferedPaintDC pdc(panel); wxBufferedPaintDC pdc(panel);
pdc.SetBackground(wxBrush(bg)); 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(); const size_t n = spec.component_colours.size();
if (spec.tri_weights.size() == 3 && n == 3 && !spec.is_gradient) { if (spec.tri_weights.size() == 3 && n == 3 && !spec.is_gradient) {
// Ternary mix: a read-only miniature of the MixedFilamentDialog's triangle picker. // Ternary mix: read-only miniature of the MixedFilamentDialog's triangle picker.
// Per-pixel barycentric fill is cached into a bitmap keyed on size + colours; the const MixedTriangleTheme tri_theme{StateColor::darkModeColorFor(*wxWHITE),
// marker and labels are redrawn on top every paint. StateColor::darkModeColorFor(wxColour("#CECECE")),
const wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); StateColor::darkModeColorFor(wxColour("#262E30")),
const wxColour outline = StateColor::darkModeColorFor(wxColour("#CECECE")); StateColor::darkModeColorFor(wxColour(107, 107, 107))};
const wxColour ring = StateColor::darkModeColorFor(wxColour("#262E30")); draw_mixed_triangle_picker(pdc, rc.GetSize(), {spec.component_colours[0], spec.component_colours[1], spec.component_colours[2]},
const wxColour label_c = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 {spec.tri_weights[0], spec.tri_weights[1], spec.tri_weights[2]}, tri_theme);
const double margin_dip = 24.0; draw_mixed_triangle_labels(pdc, rc.GetSize(), {spec.tri_weights[0], spec.tri_weights[1], spec.tri_weights[2]}, tri_theme);
auto& cache = *tri_cache; } else if (!spec.is_gradient) {
// Ratio bar. A 2-component slot matches the MixedFilamentDialog's continuous blend +
auto vertices_for = [&](const wxSize& sz) -> std::tuple<TriPoint, TriPoint, TriPoint> { // divider (with its two end labels); wider non-gradient mixes (rare) fall back to one
const double pw = sz.GetWidth(), ph = sz.GetHeight(); // solid segment per component.
const int margin = FromDIP(int(margin_dip)); if (n == 2) {
const double avail = std::min(pw, ph) - 2.0 * margin; const int bar_h = FromDIP(27);
const double side = avail; draw_mixed_ratio_blend_bar(pdc, wxRect(rc.x, rc.y, rc.width, bar_h), spec.component_colours[0],
const double tri_h = side * std::sqrt(3.0) / 2.0; spec.component_colours[1], spec.ratios[1]);
const double cx = pw / 2.0; // Read-only twin of the dialog's left/right percentage labels.
const double top_y = (ph - tri_h) / 2.0; pdc.SetFont(::Label::Body_12);
return {{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}; 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)));
pdc.SetFont(::Label::Body_12); const int lab_y = rc.y + bar_h + FromDIP(2);
const wxColour& c0 = spec.component_colours[0]; pdc.DrawText(la, rc.x, lab_y);
const wxColour& c1 = spec.component_colours[1]; pdc.DrawText(lb, rc.x + rc.width - pdc.GetTextExtent(lb).GetWidth(), lab_y);
const wxColour& c2 = spec.component_colours[2]; } else {
draw_mixed_ratio_segments(pdc, rc, spec.component_colours, spec.ratios);
if (!cache.bmp.IsOk() || cache.sz != rc.GetSize() || cache.c0 != c0 || cache.c1 != c1 || cache.c2 != c2) { // Percent label centred in each segment wide enough to hold it.
auto [v0, v1, v2] = vertices_for(rc.GetSize()); std::vector<double> shares = spec.ratios;
cache.bmp = wxBitmap(rc.width, rc.height, 32); double total = 0.0;
wxMemoryDC mdc(cache.bmp); for (double r : shares)
mdc.SetBrush(wxBrush(tri_bg)); total += r;
mdc.SetPen(*wxTRANSPARENT_PEN); if (total <= 0.0) {
mdc.DrawRectangle(0, 0, rc.width, rc.height); shares.assign(n, 1.0 / n);
total = 1.0;
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})); pdc.SetFont(::Label::Body_12);
const int min_x = int(std::min({v0.x, v1.x, v2.x})); int x0 = rc.x;
const int max_x = int(std::max({v0.x, v1.x, v2.x})); for (size_t i = 0; i < n; ++i) {
for (int py = min_y; py <= max_y; ++py) const int x1 = (i + 1 < n)
for (int px = min_x; px <= max_x; ++px) { ? rc.x + int(std::lround(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0) / total * double(rc.width)))
const TriPoint p = {double(px), double(py)}; : rc.x + rc.width;
if (!tri_contains(p, v0, v1, v2)) const int w = std::max(1, x1 - x0);
continue; const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0)));
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<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(), static_cast<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(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)));
const wxSize tsz = pdc.GetTextExtent(text); const wxSize tsz = pdc.GetTextExtent(text);
const TriPoint vtx = (i == 0) ? v0 : (i == 1) ? v1 : v2; if (tsz.GetWidth() + FromDIP(4) <= w) {
int lx = int(vtx.x - tsz.GetWidth() / 2.0); const wxColour& c = spec.component_colours[i];
int ly = (i == 0) ? int(vtx.y - tsz.GetHeight()) : int(vtx.y + FromDIP(3)); const double lum = 0.299 * c.Red() + 0.587 * c.Green() + 0.114 * c.Blue();
ly = std::clamp(ly, 0, rc.height - tsz.GetHeight()); pdc.SetTextForeground(lum > 140 ? wxColour("#262E30") : *wxWHITE);
lx = std::clamp(lx, 0, rc.width - tsz.GetWidth()); pdc.DrawText(text, x0 + (w - tsz.GetWidth()) / 2, rc.y + (rc.height - tsz.GetHeight()) / 2);
pdc.SetTextForeground(label_c); }
pdc.DrawText(text, lx, ly); 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<double> 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<wxRect> 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 { } else {
// Gradient: compact "Material Ratio" over "Model Height" graph, a read-only // Gradient: read-only miniature of the GradientCurveEditor plot, drawn through the
// miniature of the GradientCurveEditor plot. Component order matches the config; // shared painter so it anti-aliases and keeps the square proportions and labels.
// the second component's curve is the mirror of the first's. const MixedGradientTheme grad_theme{StateColor::darkModeColorFor(*wxWHITE),
const wxColour grid_color = StateColor::darkModeColorFor(wxColour(238, 238, 238)); // grey 300 StateColor::darkModeColorFor(wxColour(238, 238, 238)),
const wxColour axis_color = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 StateColor::darkModeColorFor(wxColour(107, 107, 107)),
const wxColour label_muted = StateColor::darkModeColorFor(wxColour(107, 107, 107)); StateColor::darkModeColorFor(wxColour(107, 107, 107)),
const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE); StateColor::darkModeColorFor(wxColour(38, 46, 48)),
StateColor::darkModeColorFor(wxColour(172, 172, 172)),
const int pad_left = FromDIP(34); StateColor::darkModeColorFor(*wxWHITE)};
const int pad_right = FromDIP(10); std::vector<MixedGradientCurve> curves;
const int pad_top = FromDIP(18); std::vector<wxPoint2DDouble> anchors;
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));
if (spec.gradient_samples.size() >= 2 && n >= 2) { if (spec.gradient_samples.size() >= 2 && n >= 2) {
auto curve_point = [&](double t, double ratio) { const wxRect plot = mixed_gradient_plot_rect(rc.GetSize());
return wxPoint(plot.x + int(std::lround(t * plot.width)), plot.y + int(std::lround((1.0 - ratio) * plot.height))); 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. // First component's ratio solid, its mirror twin for the other.
const wxColour& col_a = spec.component_colours[0]; const wxColour col_a = lift_alpha(spec.component_colours[0]);
const wxColour& col_b = spec.component_colours[1]; const wxColour col_b = lift_alpha(spec.component_colours[1]);
std::vector<wxPoint> pts_a, pts_b; std::vector<wxPoint2DDouble> pts_a, pts_b;
pts_a.reserve(spec.gradient_samples.size()); pts_a.reserve(spec.gradient_samples.size());
pts_b.reserve(spec.gradient_samples.size()); pts_b.reserve(spec.gradient_samples.size());
for (const auto& [t, r] : spec.gradient_samples) { for (const auto& [t, r] : spec.gradient_samples) {
pts_a.push_back(curve_point(t, r)); pts_a.push_back({plot.x + t * plot.width, plot.y + (1.0 - r) * plot.height});
pts_b.push_back(curve_point(t, 1.0 - r)); 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. // Control-point anchors of the stored curve on the first component's line.
pdc.SetBrush(wxBrush(point_fill)); anchors.reserve(spec.gradient_anchors.size());
pdc.SetPen(wxPen(col_a, 1)); for (const auto& [t, r] : spec.gradient_anchors)
for (const auto& [t, r] : spec.gradient_anchors) { anchors.push_back({plot.x + t * plot.width, plot.y + (1.0 - r) * plot.height});
const wxPoint c = curve_point(t, r); curves.push_back({std::move(pts_a), col_a, 4});
pdc.DrawCircle(c, FromDIP(3)); 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 // 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 // dialog is resized (the paint handler draws into whatever client rect the panel ends up
// with, so nothing else has to change). // with, so nothing else has to change). Sizes mirror the MixedFilamentDialog controls: the
const int viz_h = spec.is_gradient ? 150 : (spec.tri_weights.size() == 3 ? 180 : 30); // gradient plot and triangle picker match the editor's 260x200 / 160x160, the ratio bar is
const wxSize viz_sz(FromDIP(240), FromDIP(viz_h)); // 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->SetMinSize(viz_sz);
viz->SetMaxSize(viz_sz); viz->SetMaxSize(viz_sz);
// Parented to the page right above the scroll area, so it is always shown with the tab: // Parented to the page right above the scroll area, so it is always shown with the tab:

View File

@@ -92,7 +92,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli
btn->Create(this, item, "", wxBORDER_NONE); btn->Create(this, item, "", wxBORDER_NONE);
btn->SetFont(GetFont()); btn->SetFont(GetFont());
btn->SetTextColor( 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->SetBackgroundColor(StateColor());
btn->SetCornerRadius(0); btn->SetCornerRadius(0);
btn->SetPaddingSize({TAB_BUTTON_PADDING}); btn->SetPaddingSize({TAB_BUTTON_PADDING});