mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 10:47:16 +00:00
UI Fixes and Polish
This commit is contained in:
@@ -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
|
||||||
@@ -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_
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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});
|
||||||
|
|||||||
Reference in New Issue
Block a user