diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index 334a5650d1..42bf84cd6c 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -305,42 +305,140 @@ void sharpen_rgb(DecodedHeightTexture &tex, float amount) } } -// Two levels, on luma. For line art and logos, where the useful result is exactly two filaments and -// anything in between is speckle. -void apply_threshold(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer) +// Reduces the picture to at most `count` colours, by k-means in RGB. +// +// "At most": two of the chosen colours can still match the same filament, in which case the print +// uses fewer. The guarantee runs the right way round, which is the one that matters - asking for +// three colours cannot produce four. +// +// k-means rather than a fixed palette or an even split of the luma range, because the useful +// question is which colours this picture is actually made of. A tartan asks for its own handful of +// greens and reds; posterizing the range would hand it eight shades of mud instead. The clustering +// runs over a coarse histogram, not over the pixels: at 1024 x 1024 that is 4096 weighted points +// instead of a million, which is what makes it cheap enough to sit on a slider. +void reduce_colors(DecodedHeightTexture &tex, int count) { - if (!tex.has_color() || !layer.threshold_enabled) + if (!tex.has_color() || count < 2 || count > 64) return; - const float cut = std::clamp(layer.threshold, 0.f, 1.f) * 255.f; - const size_t n = size_t(tex.width) * size_t(tex.height); + const size_t n = size_t(tex.width) * size_t(tex.height); + + // 16 levels per channel, each bin keeping the mean of what fell in it, so a colour is not + // snapped to a bin corner before the clustering has had a say. + struct Bin { double r = 0, g = 0, b = 0; uint32_t n = 0; }; + std::vector bins(size_t(16 * 16 * 16)); for (size_t i = 0; i < n; ++i) { - const float luma = 0.2126f * float(tex.rgb[i * 3]) + 0.7152f * float(tex.rgb[i * 3 + 1]) + - 0.0722f * float(tex.rgb[i * 3 + 2]); - const uint8_t v = luma >= cut ? 255 : 0; - tex.rgb[i * 3] = tex.rgb[i * 3 + 1] = tex.rgb[i * 3 + 2] = v; + const uint8_t r = tex.rgb[i * 3], g = tex.rgb[i * 3 + 1], b = tex.rgb[i * 3 + 2]; + Bin &bin = bins[(size_t(r >> 4) << 8) | (size_t(g >> 4) << 4) | size_t(b >> 4)]; + bin.r += r; bin.g += g; bin.b += b; ++bin.n; } + struct Point { float c[3]; double w; }; + std::vector pts; + pts.reserve(bins.size()); + for (const Bin &bin : bins) + if (bin.n > 0) + pts.push_back({ { float(bin.r / bin.n), float(bin.g / bin.n), float(bin.b / bin.n) }, double(bin.n) }); + if (pts.size() <= size_t(count)) + return; // already no more colours than asked for + + // Seeded the k-means++ way, deterministically: the heaviest point first, then each next one is + // whichever point is furthest from everything chosen so far, weighted by how much of the image + // it is. Picking the N fullest bins instead would put every centre inside the one dominant + // colour of a picture that has one. + std::vector> centre; + { + size_t best = 0; + for (size_t i = 1; i < pts.size(); ++i) + if (pts[i].w > pts[best].w) + best = i; + centre.push_back({ pts[best].c[0], pts[best].c[1], pts[best].c[2] }); + std::vector d2(pts.size(), std::numeric_limits::max()); + while (int(centre.size()) < count) { + const auto &last = centre.back(); + double top = -1.0; + size_t pick = 0; + for (size_t i = 0; i < pts.size(); ++i) { + const double dr = pts[i].c[0] - last[0], dg = pts[i].c[1] - last[1], db = pts[i].c[2] - last[2]; + d2[i] = std::min(d2[i], dr * dr + dg * dg + db * db); + if (const double score = d2[i] * pts[i].w; score > top) { + top = score; + pick = i; + } + } + if (top <= 0.0) + break; // fewer distinct colours than asked for + centre.push_back({ pts[pick].c[0], pts[pick].c[1], pts[pick].c[2] }); + } + } + + // Lloyd's algorithm over the weighted bins. Twelve passes: on a histogram this coarse it has + // stopped moving well before that, and a fixed count keeps the slider's cost predictable. + const int k = int(centre.size()); + for (int iter = 0; iter < 12; ++iter) { + std::vector> sum(size_t(k), { 0.0, 0.0, 0.0 }); + std::vector wsum(size_t(k), 0.0); + for (const Point &p : pts) { + int best = 0; + double bd = std::numeric_limits::max(); + for (int c = 0; c < k; ++c) { + const double dr = p.c[0] - centre[size_t(c)][0], dg = p.c[1] - centre[size_t(c)][1], + db = p.c[2] - centre[size_t(c)][2]; + if (const double d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; } + } + for (int ch = 0; ch < 3; ++ch) + sum[size_t(best)][size_t(ch)] += double(p.c[ch]) * p.w; + wsum[size_t(best)] += p.w; + } + for (int c = 0; c < k; ++c) + if (wsum[size_t(c)] > 0.0) + for (int ch = 0; ch < 3; ++ch) + centre[size_t(c)][size_t(ch)] = float(sum[size_t(c)][size_t(ch)] / wsum[size_t(c)]); + } + + // Every texel takes its nearest centre. Parallel by rows, as the tonal pass is. + const size_t rows = size_t(tex.height), cols = size_t(tex.width); + tbb::parallel_for(tbb::blocked_range(0, rows), [&](const tbb::blocked_range &range) { + for (size_t y = range.begin(); y < range.end(); ++y) + for (size_t x = 0; x < cols; ++x) { + uint8_t *px = tex.rgb.data() + (y * cols + x) * 3; + int best = 0; + float bd = std::numeric_limits::max(); + for (int c = 0; c < k; ++c) { + const float dr = float(px[0]) - centre[size_t(c)][0], dg = float(px[1]) - centre[size_t(c)][1], + db = float(px[2]) - centre[size_t(c)][2]; + if (const float d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; } + } + for (int ch = 0; ch < 3; ++ch) + px[size_t(ch)] = uint8_t(std::lround(std::clamp(centre[size_t(best)][size_t(ch)], 0.f, 255.f))); + } + }); } -// Keys out the colour the picture's corners share, so a logo on a white card prints as the logo and -// the object's own colour shows through the rest. +// Keys one colour out, so a logo on a white card prints as the logo and the object's own colour +// shows through the rest. // -// The corners rather than a colour the user picks: a background that does not reach all four of them -// is not a background, and asking for an eyedropper would be a second interaction for the case this -// exists to make one click. The median of the four is used so a single stray corner cannot decide it. +// The corners are the default because a background that does not reach all four of them is not a +// background, and that makes the common case one click; the median of the four is taken so a single +// stray corner cannot decide it. The panel's dropper and swatch override it when the picture needs +// something else keyed out. void remove_background(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer) { if (!tex.has_color() || !layer.bg_remove || tex.width < 2 || tex.height < 2) return; const size_t n = size_t(tex.width) * size_t(tex.height); - const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width), - n - 1 }; float key[3]; - for (int c = 0; c < 3; ++c) { - std::array v{}; - for (int i = 0; i < 4; ++i) - v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]); - std::sort(v.begin(), v.end()); - key[c] = 0.5f * (v[1] + v[2]); + if (layer.bg_from_corners) { + const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width), + n - 1 }; + for (int c = 0; c < 3; ++c) { + std::array v{}; + for (int i = 0; i < 4; ++i) + v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]); + std::sort(v.begin(), v.end()); + key[c] = 0.5f * (v[1] + v[2]); + } + } else { + for (int c = 0; c < 3; ++c) + key[c] = std::clamp(layer.bg_color[c], 0.f, 1.f) * 255.f; } // Euclidean in 0..255 RGB, against a tolerance scaled to the diagonal of that cube, so the // slider reads as "how different from the background still counts as background". @@ -361,7 +459,7 @@ bool has_color_adjustments(const TextureDisplacementLayer &layer) { return layer.brightness != 0.f || layer.contrast != 0.f || layer.saturation != 1.f || layer.gamma != 1.f || layer.color_invert || layer.hue != 0.f || layer.sharpness > 0.f || - layer.threshold_enabled || layer.bg_remove; + layer.color_count >= 2 || layer.bg_remove; } void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer) @@ -439,7 +537,7 @@ void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacemen // thresholded image would only ring along the one edge it has left, and the key has to read the // colours the user is actually looking at. sharpen_rgb(tex, layer.sharpness); - apply_threshold(tex, layer); + reduce_colors(tex, layer.color_count); remove_background(tex, layer); } diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index c22a1fae97..d6c189ffa7 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -301,15 +301,20 @@ struct TextureDisplacementLayer float contrast = 0.f; // -1 .. 1, pivoted on mid grey; -1 flattens to grey, 1 doubles the slope float hue = 0.f; // -180 .. 180 degrees, rotated about the grey axis at constant luma float sharpness = 0.f; // 0 .. 1 unsharp mask amount, 0 is off - // Line art and logos: everything lighter than `threshold` becomes white and the rest black, so the - // picture matches exactly two filaments instead of speckling across several. - bool threshold_enabled = false; - float threshold = 0.5f; // 0 .. 1, on Rec. 709 luma + // How many colours the picture is allowed to use, 0 for as many as it likes. Reduced by k-means + // in RGB before the filaments are matched, so the result uses at most this many of them: 2 for a + // logo that should be two flat colours, 4 or 5 for a poster, off for a photograph. + int color_count = 0; // 0, or 2 .. 12 // Leaves the background of a picture unprinted, so the object's own colour shows through instead // of a slab of white behind a logo. Texels the source image marked transparent always count; - // `bg_remove` additionally keys out whatever colour the picture's corners share. + // `bg_remove` additionally keys out one colour. bool bg_remove = false; float bg_tolerance = 0.15f; // 0 .. 1, distance from the keyed colour that still counts as background + // The colour keyed out. Taken from the median of the picture's four corners while + // `bg_from_corners` is set, which is right for nearly every logo on a flat card; the panel's + // dropper and swatch clear it and put a colour of the user's own here. + bool bg_from_corners = true; + float bg_color[3] = { 1.f, 1.f, 1.f }; float saturation = 1.f; // 0 .. 2, 1 leaves it alone, 0 is grayscale float gamma = 1.f; // 0.2 .. 5, 1 leaves it alone; above 1 lifts the midtones bool color_invert = false; // negative of the colour, as opposed to `invert` which flips relief @@ -324,8 +329,8 @@ struct TextureDisplacementLayer static_cast(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness, - contrast, saturation, gamma, color_invert, hue, sharpness, threshold_enabled, threshold, - bg_remove, bg_tolerance); + contrast, saturation, gamma, color_invert, hue, sharpness, color_count, bg_remove, + bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]); } template void load(Archive &ar) { @@ -338,7 +343,7 @@ struct TextureDisplacementLayer island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert, hue, sharpness, - threshold_enabled, threshold, bg_remove, bg_tolerance); + color_count, bg_remove, bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]); image_data = blob.empty() ? nullptr : std::make_shared>(blob.begin(), blob.end()); tile_method = static_cast(tile_method_int); projection_method = static_cast(projection_method_int); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp index 10a57b6a5f..9e0db210cb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoImage.cpp @@ -18,6 +18,7 @@ #include "slic3r/GUI/Jobs/Worker.hpp" #include +#include #include #include #include @@ -259,6 +260,17 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo hover_tip(tip_text); return clicked; }; + // The integer twin of float_row, for the colour count. + const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, + const char *format) -> bool { + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + m_imgui->text(label); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX()); + return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp); + }; // Framed icon toggle, the displacement panel's one cut down to what this window needs. const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, const wxString &label, const wxString &tip_text) -> bool { @@ -356,14 +368,41 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo const float aspect = std::max(0.05f, layer_texture_aspect(*layer)); const float box_h = std::clamp(std::round(panel_w / aspect), std::round(panel_w * 0.35f), std::round(panel_w * 1.1f)); + const ImVec2 box_min = ImGui::GetCursorScreenPos(); + bool box_hit = false; if (shown != nullptr) { - if (ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h), - ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0)) + box_hit = ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h), + ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0); + } else { + box_hit = ImGui::Button("?##thumb", ImVec2(panel_w, box_h)); + } + hover_tip(m_image_bg_picking ? + _u8L("Click the part of the picture that is its background.") : + _u8L("Choose a different picture. Where you placed it and how it is corrected stay as " + "they are.")); + if (box_hit) { + if (!m_image_bg_picking) { open_library(layer->slot); - } else if (ImGui::Button("?##thumb", ImVec2(panel_w, box_h))) { - open_library(layer->slot); + } else { + // Read straight out of the decoded image rather than off the screen: the preview is + // scaled, and it already has the corrections applied, so a screen read would key the + // corrected colour against itself. + const ImVec2 rel((ImGui::GetIO().MousePos.x - box_min.x) / panel_w, + (ImGui::GetIO().MousePos.y - box_min.y) / box_h); + const DecodedHeightTexture &tex = decode_height_texture(*layer); + if (tex.has_color()) { + const int px = std::clamp(int(rel.x * float(tex.width)), 0, tex.width - 1); + const int py = std::clamp(int(rel.y * float(tex.height)), 0, tex.height - 1); + const size_t i = (size_t(py) * size_t(tex.width) + size_t(px)) * 3; + for (int c = 0; c < 3; ++c) + layer->bg_color[c] = float(tex.rgb[i + size_t(c)]) / 255.f; + layer->bg_from_corners = false; + layer->bg_remove = true; + m_preview_params_dirty = true; + } + m_image_bg_picking = false; + } } - hover_tip(_u8L("Choose a different picture. Where you placed it and how it is corrected stay as they are.")); const std::string name = layer->name.empty() ? _u8L("Image") : layer->name; ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w); ImGui::TextDisabled("%s", name.c_str()); @@ -534,27 +573,79 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo "than in a photo: a soft edge between two colors quantizes into a ragged band of " "both, and sharpening first makes it land in one place.")); - // Two filaments, on purpose. The case this exists for is a logo or line art, where anything - // in between the two reads as speckle rather than as shading. - if (m_imgui->bbl_checkbox(_L("Two colors"), layer->threshold_enabled)) - m_preview_params_dirty = true; - hover_tip(_u8L("Forces the picture to light and dark only, so it prints in exactly two filaments. " - "For a logo or line art, where the in-between grays come out as speckle.")); - if (layer->threshold_enabled) { - m_preview_params_dirty |= float_row("##threshold", _L("Split at"), &layer->threshold, 0.05f, 0.95f, "%.2f", false); - hover_tip(_u8L("Where the split falls. Lower keeps more of the picture on the light side, higher " - "on the dark one.")); + // How many colours the picture may use. 2 is a logo in two flat colours, 4 or 5 a poster, + // off a photograph. This is not the same as the displacement tool's "Mixed colors", which + // caps how many mixed filament slots a bake may create rather than what the picture uses. + { + int count = layer->color_count; + if (int_row("##colors", _L("Colors"), &count, 0, 12, count == 0 ? _u8L("Off").c_str() : "%d")) { + // 1 is not a picture, so the slider steps over it on the way out of Off. + layer->color_count = count == 1 ? 2 : count; + m_preview_params_dirty = true; + } + hover_tip(_u8L("Reduces the picture to this many colors before your filaments are matched to " + "it, so it prints in at most that many. The colors are the ones the picture is " + "actually made of, not an even split of the range. Off leaves it alone.")); } if (m_imgui->bbl_checkbox(_L("Remove background"), layer->bg_remove)) m_preview_params_dirty = true; hover_tip(_u8L("Leaves the background unprinted, so the object's own color shows through instead of " - "a slab of white behind a logo. The color is taken from the picture's four corners. " - "A picture that is already transparent needs none of this: those parts are left out " - "either way.")); + "a slab of white behind a logo. A picture that is already transparent needs none of " + "this: those parts are left out either way.")); if (layer->bg_remove) { + // Which colour is being removed, shown rather than described: the swatch is also a + // picker, and the dropper beside it takes the colour from the preview above. + const float x0 = ImGui::GetCursorPosX(); + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Color")); + ImGui::SameLine(); + ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w)); + float shown_bg[3] = { layer->bg_color[0], layer->bg_color[1], layer->bg_color[2] }; + if (layer->bg_from_corners) { + // Nothing has been picked, so show what the corners actually gave: the same median + // the bake will key on, not a placeholder white. + const DecodedHeightTexture &tex = decode_height_texture(*layer); + if (tex.has_color() && tex.width > 1 && tex.height > 1) { + const size_t n = size_t(tex.width) * size_t(tex.height); + const size_t corner[4] = { 0, size_t(tex.width) - 1, + (size_t(tex.height) - 1) * size_t(tex.width), n - 1 }; + for (int c = 0; c < 3; ++c) { + std::array v{}; + for (int i = 0; i < 4; ++i) + v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]); + std::sort(v.begin(), v.end()); + shown_bg[c] = 0.5f * (v[1] + v[2]) / 255.f; + } + } + } + ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX() - icon_sm - gap_s); + if (ImGui::ColorEdit3("##bg_color", shown_bg, + ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + for (int c = 0; c < 3; ++c) + layer->bg_color[c] = shown_bg[c]; + layer->bg_from_corners = false; + m_preview_params_dirty = true; + } + hover_tip(layer->bg_from_corners ? + _u8L("Taken from the four corners of the picture, which is right for a logo on a " + "plain card. Click to choose a color yourself instead.") : + _u8L("The color being removed. Click to change it.")); + ImGui::SameLine(0.f, gap_s); + if (icon_toggle(930, "texture_displacement_adjust.svg", m_image_bg_picking, _L("Pick from the picture"), + _L("Pick from the picture - then click the background in the preview above."))) + m_image_bg_picking = !m_image_bg_picking; + if (!layer->bg_from_corners) { + ImGui::SameLine(0.f, gap_s); + if (m_imgui->button(_u8L("Corners"))) { + layer->bg_from_corners = true; + m_preview_params_dirty = true; + } + hover_tip(_u8L("Back to taking the color from the picture's four corners.")); + } + m_preview_params_dirty |= float_row("##bg_tol", _L("Tolerance"), &layer->bg_tolerance, 0.01f, 0.6f, "%.2f", false); - hover_tip(_u8L("How different from the corners still counts as background. Raise it when a " + hover_tip(_u8L("How different from that color still counts as background. Raise it when a " "shadow or a gradient is left behind; lower it when it starts eating the picture.")); } @@ -569,10 +660,12 @@ void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, flo layer->hue = 0.f; layer->gamma = 1.f; layer->sharpness = 0.f; - layer->threshold_enabled = false; - layer->threshold = 0.5f; + layer->color_count = 0; layer->bg_remove = false; layer->bg_tolerance = 0.15f; + layer->bg_from_corners = true; + layer->bg_color[0] = layer->bg_color[1] = layer->bg_color[2] = 1.f; + m_image_bg_picking = false; layer->color_invert = false; m_preview_params_dirty = true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index 241976f50d..b23693ea78 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -160,21 +160,24 @@ public: struct ColorAdjustKey { float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f, hue = 0.f; - float sharpness = 0.f, threshold = 0.5f, bg_tolerance = 0.15f; - bool invert = false, threshold_on = false, bg_remove = false; + float sharpness = 0.f, bg_tolerance = 0.15f; + float bg_r = 1.f, bg_g = 1.f, bg_b = 1.f; + int color_count = 0; + bool invert = false, bg_remove = false, bg_from_corners = true; bool operator==(const ColorAdjustKey &o) const { return brightness == o.brightness && contrast == o.contrast && saturation == o.saturation && gamma == o.gamma && hue == o.hue && sharpness == o.sharpness && - threshold == o.threshold && bg_tolerance == o.bg_tolerance && invert == o.invert && - threshold_on == o.threshold_on && bg_remove == o.bg_remove; + bg_tolerance == o.bg_tolerance && bg_r == o.bg_r && bg_g == o.bg_g && bg_b == o.bg_b && + color_count == o.color_count && invert == o.invert && bg_remove == o.bg_remove && + bg_from_corners == o.bg_from_corners; } bool operator!=(const ColorAdjustKey &o) const { return !(*this == o); } static ColorAdjustKey of(const TextureDisplacementLayer &l) { - return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue, - l.sharpness, l.threshold, l.bg_tolerance, - l.color_invert, l.threshold_enabled, l.bg_remove }; + return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue, + l.sharpness, l.bg_tolerance, l.bg_color[0], l.bg_color[1], l.bg_color[2], + l.color_count, l.color_invert, l.bg_remove, l.bg_from_corners }; } }; std::vector m_palette_adjusts; // alongside m_palette_images, one per entry @@ -324,6 +327,9 @@ private: // render_image_input_window() draws over the canvas. bool m_file_drag_over = false; void render_image_drop_hint(); + // Set while the panel's background dropper is armed: the next click on the picture preview takes + // the colour under it instead of opening the library. + bool m_image_bg_picking = false; // Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace. // The order matters and is the whole reason this is one button - a height map can only move // existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop