Compare commits

..
Author SHA1 Message Date
Hanif Koh 6d218f9abf Let Plugin Windows Be Maximized on X11
wxGTK types every wxDialog as a dialog window, and Mutter offers no
maximize for anything but a normal window, so the maximize button and
shortcut did nothing on GNOME under X11. Modeless plugin windows are now
normal windows, kept off the taskbar as before. Modal ones stay dialogs
so the window manager keeps routing focus from the main window to them.
2026-10-10 14:27:45 +08:00
24 changed files with 384 additions and 1257 deletions
@@ -29,11 +29,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
// The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform vec3 filament_rgb[16];
uniform int filament_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -208,16 +210,31 @@ int nearest_palette_entry(vec3 rgb)
best = i;
}
}
// The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// where the bake prints a single filament. Compared on the distances rather than their squares, so
// the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so
// it is only worth taking when it beats the nearest single filament by a visible step. Without it
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries
// against 30 mixes - while the bake picked a single filament for most of them, so the preview
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
best = best_pure;
return best;
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -337,11 +354,16 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour -
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -88,11 +88,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
// The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform vec3 filament_rgb[16];
uniform int filament_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -274,16 +276,31 @@ int nearest_palette_entry(vec3 rgb)
best = i;
}
}
// The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// where the bake prints a single filament. Compared on the distances rather than their squares, so
// the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so
// it is only worth taking when it beats the nearest single filament by a visible step. Without it
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries
// against 30 mixes - while the bake picked a single filament for most of them, so the preview
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
best = best_pure;
return best;
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -425,11 +442,16 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour -
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
-54
View File
@@ -18,7 +18,6 @@
#include <utility>
#include "ColorDecomposeRecipe.hpp"
#include "Config.hpp"
#include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp"
@@ -428,59 +427,6 @@ std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_componen
return ratios;
}
std::string format_mixed_components(const std::vector<unsigned int> &components)
{
std::string out;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0)
out += ",";
out += std::to_string(components[i]);
}
return out;
}
std::string format_mixed_ratios(const std::vector<int> &weights)
{
int sum = std::accumulate(weights.begin(), weights.end(), 0);
if (sum <= 0)
sum = 100;
CNumericLocalesSetter c_locale_setter;
std::string out;
for (size_t i = 0; i < weights.size(); ++i) {
if (i > 0)
out += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(weights[i]) / float(sum));
out += buf;
}
return out;
}
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights)
{
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return -1;
// Created lazily with the first mixed slot, so an older project may not have it at all.
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
const std::string comp_str = format_mixed_components(components);
const std::string ratio_str = format_mixed_ratios(weights);
for (size_t i = 0; i < is_mixed->values.size(); ++i) {
if (!is_mixed->values[i] || i >= comps->values.size() || i >= ratios->values.size())
continue;
if (gradient != nullptr && i < gradient->values.size() && gradient->values[i])
continue;
if (comps->values[i] == comp_str && ratios->values[i] == ratio_str)
return int(i);
}
return -1;
}
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
{
for (unsigned char v : is_mixed)
-18
View File
@@ -11,8 +11,6 @@
namespace Slic3r {
class ConfigBase;
// Photoshop-style gradient curve control point in [0,1] x [0,1].
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
@@ -96,22 +94,6 @@ std::vector<unsigned int> parse_mixed_components(const std::string &str);
// Normalizes so the sum equals 1.0.
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
// The text a mixed slot stores in filament_mixed_components, e.g. {1, 3} → "1,3".
std::string format_mixed_components(const std::vector<unsigned int> &components);
// The text a mixed slot stores in filament_mixed_sublayer_ratios: the weights normalised to sum to 1,
// four decimals, e.g. {1, 2} → "0.3333,0.6667". A non-positive sum is read as 100.
std::string format_mixed_ratios(const std::vector<int> &weights);
// The 0-based index of the mixed slot in `project_config` that blends `components` (1-based physical
// filaments) in `weights` at a fixed ratio, or -1 when there is none. Matched on the stored text, as
// format_mixed_components() and format_mixed_ratios() write it. A gradient slot never matches: its
// ratio drifts from layer to layer, so it is not the blend asked for even where its stored ratios
// are the same.
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights);
// Returns true if any element in is_mixed is true.
// ConfigOptionBools stores values as std::vector<unsigned char>.
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
-14
View File
@@ -247,20 +247,6 @@ void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, f
}
} // namespace
bool height_texture_has_color(const TextureDisplacementLayer &layer)
{
if (layer.empty())
return false;
{
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
const auto it = g_decoded_texture_cache.entries.find(layer.image_data.get());
if (it != g_decoded_texture_cache.entries.end() && it->second.first.lock() == layer.image_data)
return it->second.second.has_color();
}
// Not decoded yet. Decoding caches the raw image, so this happens once per image.
return decode_height_texture(layer).has_color();
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{
DecodedHeightTexture result;
+5 -17
View File
@@ -1,7 +1,6 @@
#ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_
#include <cmath>
#include <cstddef>
#include <Eigen/Core>
#include <cstdint>
@@ -388,10 +387,6 @@ struct TextureDisplacementOptions
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
// photograph or gradient in mixes. Off forces single filaments everywhere.
bool color_mix_enabled = true;
// The most mixes the palette may offer. Every mix a bake paints with becomes a mixed filament slot,
// so this is also the most slots one bake can add. The mixes themselves are picked from the
// texture's colours, those that improve the match the most coming first.
int color_mix_count = 8;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
@@ -401,7 +396,7 @@ struct TextureDisplacementOptions
{
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
v2_relocate, color_mix_enabled, color_despeckle);
}
};
@@ -485,10 +480,6 @@ struct DecodedHeightTexture
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
bool height_texture_has_color(const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
//
// Deliberately a callback rather than a function here: matching a colour to a filament is a
@@ -503,14 +494,12 @@ using ColorQuantizeFn = std::function<int(const Vec3f &)>;
// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the colour its mixed filament slot shows
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
int a = 0; // filament index
int b = 0; // the second filament; == a for a pure entry
int num = 1; // a's share of the interleave, out of `den`
int den = 1;
bool is_mix() const { return a != b; }
// a's share in percent, the form a mixed filament slot is created from.
int a_percent() const { return int(std::lround(100.0 * double(num) / double(den))); }
};
// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty
@@ -773,10 +762,9 @@ struct TextureColorRequest
float min_color_region_mm2 = 0.5f;
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
// the texture". The +1 lines up with EnforcerBlockerType, where 0 is NONE ("use the volume's own
// filament"): where every palette entry is a filament, these go straight to a TriangleSelector. A
// palette with mixes maps each index to the mix's filament slot first (see
// GLGizmoTextureDisplacement::palette_filaments()).
// the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE
// ("use the volume's own filament") and 1..16 are Extruder1..16, so the caller can hand these
// straight to a TriangleSelector without a second mapping table.
std::vector<uint8_t> *out_triangle = nullptr;
};
+1 -3
View File
@@ -113,7 +113,7 @@ Vec3f GLGizmoPainterBase::get_tilt_up_direction() const
return build_plate_tilt_up_direction().cast<float>();
}
void GLGizmoPainterBase::render_triangles(const Selection& selection, const ModelVolume* skip) const
void GLGizmoPainterBase::render_triangles(const Selection& selection) const
{
auto* shader = wxGetApp().get_shader("mm_gouraud");
if (!shader)
@@ -135,8 +135,6 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection, const Mode
continue;
++mesh_id;
if (mv == skip)
continue;
Transform3d trafo_matrix;
if (m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView) {
+2 -8
View File
@@ -48,9 +48,6 @@ public:
virtual ~TriangleSelectorGUI() = default;
virtual void render(ImGuiWrapper* imgui, const Transform3d& matrix);
// The seed-fill contour alone, as render() last built it - for a gizmo that draws over the selector
// and has to put the contour back on top.
void render_paint_contour(const Transform3d& matrix);
//void render(const Transform3d& matrix) { this->render(nullptr, matrix); }
void set_wireframe_needed(bool need_wireframe) { m_need_wireframe = need_wireframe; }
bool get_wireframe_needed() { return m_need_wireframe; }
@@ -93,6 +90,7 @@ protected:
GLModel m_paint_contour;
void update_paint_contour();
void render_paint_contour(const Transform3d& matrix);
bool m_need_wireframe {false};
};
@@ -233,8 +231,7 @@ public:
bool on_mouse(const wxMouseEvent &mouse_event) override;
protected:
// Draws every model part's selector, except `skip`'s when given.
virtual void render_triangles(const Selection& selection, const ModelVolume* skip = nullptr) const;
virtual void render_triangles(const Selection& selection) const;
void render_cursor();
void render_cursor_circle();
void render_cursor_sphere(const Transform3d& trafo) const;
@@ -331,9 +328,6 @@ protected:
TriangleSelector::ClippingPlane get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const;
// True while a paint or erase stroke is under way.
bool is_painting() const { return m_button_down != Button::None; }
private:
std::vector<std::vector<ProjectedMousePosition>> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector<Transform3d> &trafo_matrices) const;
@@ -8,7 +8,6 @@
#include "ColorSpaceConvert.hpp"
#include "libslic3r/AABBTreeIndirect.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/Model.hpp"
@@ -70,7 +69,6 @@
#include <array>
#include <cmath>
#include <limits>
#include <numeric>
#include <queue>
#include <set>
#include <vector>
@@ -259,54 +257,11 @@ TriangleSelector::TriangleSplittingData remap_texture_paint_spatial(
// entry to fill.
constexpr int PALETTE_LUT_EDGE = 24;
// The shaded preview shader's palette arrays. The palette itself stays within the paint mask's
// EnforcerBlockerType::ExtruderMax states, since every entry has to become a filament.
// Ceiling on the printable palette, which bounds that fill cost (and the shader's uniform array).
constexpr int PALETTE_MAX_ENTRIES = 64;
// A mix is an interleave that only reads as its colour from a distance; up close it is stripes. So it
// is spent only where it beats the nearest single filament by this much (CIEDE2000). Two is about
// where a side-by-side difference stops being arguable; a margin of ten already turns most of a
// greyscale ramp - the shape a height texture actually traces - back into single filaments. The
// quantizer, the mix ranking and the shaded preview shader all apply it, so they agree on where a mix
// is used.
constexpr float PREFER_PURE_DE = 2.f;
// mix_targets(): the most pixels read per layer, and the histogram bins kept over all layers. Together
// they bound rank_mixes() to candidates x MIX_TARGET_BINS colour differences, the same order as filling
// the quantizer's lookup cube.
constexpr size_t MIX_TARGET_SAMPLES = size_t(1) << 20;
constexpr size_t MIX_TARGET_BINS = 256;
// rank_mixes() stops once the best remaining mix would improve the match by less than this, in
// CIEDE2000 averaged over every pixel of the colouring layers (see mix_targets()): a mix that only
// touches a few stray pixels is not worth a filament slot.
constexpr float MIN_MIX_GAIN = 0.05f;
// The project's mixed filament slots, one string each, as the palette cache compares them: anything
// that changes which of them a mix can reuse changes this.
std::vector<std::string> mixed_slot_signature(const DynamicPrintConfig &project_config)
{
std::vector<std::string> out;
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return out;
for (size_t i = 0; i < is_mixed->values.size(); ++i)
if (is_mixed->values[i])
out.push_back(std::to_string(i) + ':' + (i < comps->values.size() ? comps->values[i] : std::string()) + '|' +
(i < ratios->values.size() ? ratios->values[i] : std::string()) + '|' +
(gradient != nullptr && i < gradient->values.size() && gradient->values[i] ? "g" : ""));
return out;
}
// Whether two palettes would draw and print the same.
bool same_palette(const std::vector<PrintableColor> &l, const std::vector<PrintableColor> &r)
{
return std::equal(l.begin(), l.end(), r.begin(), r.end(), [](const PrintableColor &x, const PrintableColor &y) {
return x.a == y.a && x.b == y.b && x.num == y.num && x.den == y.den && x.rgb == y.rgb;
});
}
// Ceiling on the filaments the palette's entries can refer to (the shaded preview shader's filament_rgb[]);
// mmu segmentation stops at Extruder16 anyway.
constexpr int PALETTE_MAX_FILAMENTS = 16;
// sRGB (0..1) <-> CIELAB, D65. Exactly what the preview shader's srgb_to_lab() computes, so the CPU
// quantizer, the mixed-palette entries and the per-fragment preview all match in the same space.
@@ -503,11 +458,6 @@ std::string GLGizmoTextureDisplacement::on_get_name() const
return _u8L("Texture displacement");
}
bool GLGizmoTextureDisplacement::on_is_activable() const
{
return m_parent.get_canvas_type() != GLCanvas3D::CanvasAssembleView && GLGizmoPainterBase::on_is_activable();
}
void GLGizmoTextureDisplacement::on_shutdown()
{
m_parent.toggle_model_objects_visibility(true);
@@ -515,25 +465,10 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_shaded_preview_glmodel.reset();
m_paint_overlay_glmodel.reset();
m_paint_overlay_dirty = false;
m_painted_colors = PaintedColors{};
m_painted_colors_key.clear();
m_painted_colors_glmodel.reset();
m_painted_colors_drawn_key.clear();
m_painted_colors_runs.clear();
m_seed_fill_last_mesh_id = -1; // a hover from this session must not count in the next
// Any preview still in flight is superseded: raising the shared counter makes it abort at its next
// progress poll, and its completion handler then finds nothing to do.
m_preview_generation->fetch_add(1);
m_preview_job_pending = false;
// The palette caches hold the last volume's images; a closed gizmo should not keep them alive.
m_palette_cache.clear();
m_palette_quantizer = nullptr;
m_palette_pure_quantizer = nullptr;
m_palette_filaments.clear();
m_palette_images.clear();
m_mix_ranking.reset();
m_palette_slots.clear();
m_palette_changed = false;
m_uvcheck_glmodel.reset();
m_wireframe_overlay_glmodel.reset();
m_wireframe_overlay_vcount = 0;
@@ -592,17 +527,22 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
glsafe(::glEnable(GL_DEPTH_TEST));
// Once anything is painted, m_preview_glmodel holds the true displaced result (same algorithm
// Bake uses). The untouched original topology (what render_triangles() draws) coincides exactly
// with it everywhere except the displaced area, so both can be drawn: the real preview geometry
// first, then the selectors' highlight with a small depth bias so it wins the depth test on the
// coincident surface. Where the surface has actually been displaced, the raised preview geometry
// legitimately occludes the flat highlight - that visible relief is itself the "this is painted"
// indicator in that area.
// Bake uses). The untouched original topology (what render_triangles() draws) coincides
// exactly with it everywhere except the painted/displaced area, so both are drawn: the real
// preview geometry first, then the usual selection-highlight overlay with a small depth bias
// so it wins the depth test on the coincident (unpainted) surface - keeping the familiar
// enforcer/blocker highlight for precise brush editing there. Where the surface has actually
// been displaced, the raised preview geometry legitimately occludes the flat overlay - that
// visible relief is itself the "this is painted" indicator in that area.
//
// The shaded preview never actually moves geometry (it only shades), so its depth is identical to
// the highlight's *everywhere*: the depth-biased opaque highlight would win the depth test across the
// whole surface and hide the shading entirely. So render_triangles() leaves the textured volume out
// there, and the translucent tint drawn further down is the paint feedback instead.
// The shaded preview is different: it never actually moves geometry (it only shades), so
// its depth is identical to the overlay's *everywhere*, not just in the unpainted area - the
// depth-biased opaque overlay would win the depth test across the whole surface and hide the relief
// shading entirely. So render_triangles() is skipped for it. What is *not* skipped is
// render_paint_overlay(): leaving the shading as the only paint feedback meant a stroke that
// erased paint, or added it with no texture picked, changed nothing on screen until the whole
// preview rebuilt at stroke end - and in the true-displacement view the opaque overlay is hidden
// by the raised surface for the same reason. The translucent tint covers both cases.
// Coalesced shaded-preview rebuild from an in-progress UV island drag (see on_island_edited): done here, at
// most once per drawn frame, rather than synchronously in the UV canvas's mouse-move handler.
if (m_use_shaded_preview && m_shaded_preview_dirty) {
@@ -639,34 +579,24 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// Hide the real volume only when something is actually going to be drawn in its place; otherwise
// put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather
// than per branch below.
ModelVolume *mv = texture_volume();
m_parent.toggle_model_objects_visibility(true);
if ((use_shaded || use_true_preview) && mv != nullptr)
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
m_c->selection_info()->get_active_instance(), mv);
if (use_shaded || use_true_preview) {
if (ModelVolume *mv = texture_volume())
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
m_c->selection_info()->get_active_instance(), mv);
}
// Over a preview, the paint feedback - the selectors' highlight in the Normal view, the tint in both -
// is drawn only while a mouse button is down: during a stroke, a fill click included. Between strokes
// it would cover the preview: the highlight buries it under a flat plane wherever the relief does not
// rise, and the tint washes it green. The debug view shows a captured stage rather than a preview, so
// it keeps the feedback.
const bool paint_feedback = is_painting() || m_debug_stage >= 0;
// Whether the textured volume's selector - and with it a fill tool's contour - is drawn this frame.
bool textured_selector_drawn = true;
if (use_shaded) {
render_shaded_preview_mesh();
// The shaded mesh is the textured volume alone, so the other model parts are still the selectors' to draw.
render_triangles(selection, mv);
textured_selector_drawn = false;
} else if (use_true_preview) {
render_preview_mesh();
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f));
// Without paint feedback, only the other model parts: render_preview_mesh() draws the textured one.
render_triangles(selection, paint_feedback ? nullptr : mv);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
textured_selector_drawn = paint_feedback;
if (show_paint_overlay) {
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f));
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
}
} else {
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check
@@ -677,10 +607,6 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
render_triangles(selection);
}
// The model's colour paint, over whichever surface was drawn, left out where that surface's preview shows
// paint of its own: every layer's for the Normal mesh, the active layer's otherwise.
const bool painted_colors_drawn = show_paint_overlay && m_debug_stage < 0 && render_painted_colors(use_true_preview);
// Every other layer's paint, in muted grey, so all layers stay visible while one of them is edited. Drawn
// before the active layer's tint so that one reads on top where the two overlap.
if (show_paint_overlay)
@@ -689,32 +615,10 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// The translucent paint tint. Needed in the shaded view because the opaque highlight above is
// skipped there, and in the true-displacement view because the displaced surface rises *above*
// the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both
// cases leaving an erase stroke with no visible effect until the next full preview rebuild. With no
// preview, the highlight is the selectors' own, except during a stroke over the colour paint: the paint
// there is not flushed yet, so the colours are still drawn over the stroke's highlight.
const bool preview_drawn = use_shaded || use_true_preview;
if (show_paint_overlay && (preview_drawn ? paint_feedback : painted_colors_drawn && is_painting()))
// cases leaving an erase stroke with no visible effect until the next full preview rebuild.
if (show_paint_overlay && (use_shaded || use_true_preview))
render_paint_overlay(m_paint_overlay_glmodel);
// A fill tool's contour is drawn with the selectors, under the colour paint drawn since - which covers it
// on a steep face, where the paint's slope-scaled offset outruns the contour's fixed one. Put it back on top,
// at the depth its first draw stored, hence LEQUAL. Only where the selector was drawn this frame: drawing it
// is what rebuilds the contour. The tool test matters because the base keeps the last hovered mesh when the
// tool changes.
const bool fill_tool = m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL ||
(m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER);
const int textured_mesh_id = texture_volume_raycaster_index();
if (painted_colors_drawn && textured_selector_drawn && fill_tool && textured_mesh_id >= 0 &&
textured_mesh_id == m_seed_fill_last_mesh_id && size_t(textured_mesh_id) < m_triangle_selectors.size()) {
const ModelObject *mo = m_c->selection_info()->model_object();
GLint depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &depth_func));
glsafe(::glDepthFunc(GL_LEQUAL));
m_triangle_selectors[size_t(textured_mesh_id)]->render_paint_contour(
mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix());
glsafe(::glDepthFunc(GLenum(depth_func)));
}
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
{
@@ -1591,9 +1495,10 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("patch_center", m_shaded_patch_center);
shader->set_uniform("patch_axis", m_shaded_patch_axis);
// The printable palette, in RGB for display and in Lab for the match. Uploaded rather than matched
// on the CPU because the quantization is per fragment here. Count 0 means "no layer is colouring",
// and the shader keeps the model's own colour for every fragment.
// The filament palette the mesh's per-triangle indices refer to. Count 0 means "no layer is
// colouring", and the shader keeps the model's own colour for every fragment.
// The printable palette, in RGB for display and in Lab for the match. Uploaded rather than
// matched on the CPU because the quantization is per fragment here.
const GLTexture *color_tex = get_layer_color_texture(*layer);
const int palette_count =
(color_tex != nullptr) ? int(std::min(m_shaded_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0;
@@ -1601,17 +1506,24 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("has_color_tex", color_tex != nullptr);
// A flat-colour image is matched against single filaments only, as the bake does.
shader->set_uniform("pure_only", color_tex != nullptr && analyze_texture_detail(*layer).flat_colors);
shader->set_uniform("prefer_pure_de", PREFER_PURE_DE);
for (int i = 0; i < palette_count; ++i) {
const PaletteEntry &e = m_shaded_preview_palette[size_t(i)];
const std::string idx = "[" + std::to_string(i) + "]";
// An entry's colour is what it prints as: its filament's, or for a mix its mixed filament slot's.
shader->set_uniform(("palette_rgb" + idx).c_str(), e.rgb);
shader->set_uniform(("palette_lab" + idx).c_str(), srgb_to_lab(e.rgb));
// Only so the shader can tell a mix (a != b) from a single filament.
// How the entry prints: its filament, or for a mix the two it interleaves and in what ratio.
shader->set_uniform(("palette_a" + idx).c_str(), e.a);
shader->set_uniform(("palette_b" + idx).c_str(), e.b);
}
// The filaments those indices refer to, and the interleave the shader resolves a mix with - the
// the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from.
const int filament_count =
(palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0;
shader->set_uniform("filament_count", filament_count);
for (int i = 0; i < filament_count; ++i) {
const ColorRGBA &c = m_palette_filaments[size_t(i)];
shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b()));
}
if (color_tex != nullptr) {
shader->set_uniform("color_tex", 1);
glsafe(::glActiveTexture(GL_TEXTURE1));
@@ -1705,150 +1617,6 @@ void GLGizmoTextureDisplacement::rebuild_other_paint_overlay()
m_other_paint_glmodel.set_color(ColorRGBA(0.55f, 0.58f, 0.60f, 0.35f));
}
std::vector<size_t> GLGizmoTextureDisplacement::PaintedColors::outside(const std::vector<bool> &excluded) const
{
std::vector<size_t> out;
out.reserve(source.size());
for (size_t i = 0; i < source.size(); ++i)
if (size_t(source[i]) >= excluded.size() || !excluded[size_t(source[i])])
out.push_back(i);
return out;
}
GLGizmoTextureDisplacement::PaintedColors GLGizmoTextureDisplacement::painted_colors(const TriangleMesh &mesh,
const TriangleSelector::TriangleSplittingData &paint)
{
PaintedColors out;
TriangleSelector selector(mesh);
selector.deserialize(paint, false);
for (const EnforcerBlockerType state : TriangleSelector::extract_used_facet_states(paint)) {
if (state == EnforcerBlockerType::NONE)
continue;
std::vector<int> source;
const indexed_triangle_set part = selector.get_facets_strict(state, &source);
// Every state comes back over the same vertex array, only the triangles differ.
if (out.facets.vertices.empty())
out.facets.vertices = part.vertices;
out.facets.indices.insert(out.facets.indices.end(), part.indices.begin(), part.indices.end());
out.source.insert(out.source.end(), source.begin(), source.end());
out.state.resize(out.facets.indices.size(), int(state));
}
return out;
}
void GLGizmoTextureDisplacement::rebuild_painted_colors(bool whole_stack)
{
const ModelVolume *mv = texture_volume();
const bool shown = mv != nullptr && any_layer_colors(*mv) && !mv->mmu_segmentation_facets.empty();
// The sub-triangles, keyed on what they were read from: the volume, its mesh and the paint.
std::string key;
if (shown)
key = std::to_string(mv->id().id) + ":" + std::to_string(reinterpret_cast<uintptr_t>(mv->mesh_ptr().get())) + ":" +
std::to_string(mv->mmu_segmentation_facets.timestamp());
if (key != m_painted_colors_key) {
m_painted_colors_key = std::move(key);
m_painted_colors = shown ? painted_colors(mv->mesh(), mv->mmu_segmentation_facets.get_data()) : PaintedColors{};
m_painted_colors_drawn_key.clear();
m_painted_colors_glmodel.reset();
m_painted_colors_runs.clear();
}
if (m_painted_colors.state.empty())
return;
// The part drawn, keyed on whose paint is left out and on that paint.
std::string drawn_key = m_painted_colors_key + (whole_stack ? std::string(":all") : ":" + std::to_string(m_active_layer_slot));
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers)
if (l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
drawn_key += "|" + std::to_string(l.slot) + "@" + std::to_string(mv->texture_displacement_facet(l.slot).timestamp());
if (drawn_key == m_painted_colors_drawn_key)
return;
m_painted_colors_drawn_key = std::move(drawn_key);
m_painted_colors_glmodel.reset();
m_painted_colors_runs.clear();
// Whole model triangles, as the facets record what they touch: a triangle the paint only partly covers
// is left to the preview.
std::vector<bool> excluded(mv->mesh().its.indices.size(), false);
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) {
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS) || (!whole_stack && l.slot != m_active_layer_slot))
continue;
for (const TriangleSelector::TriangleBitStreamMapping &m : mv->texture_displacement_facet(l.slot).get_data().triangles_to_split)
if (m.triangle_idx >= 0 && size_t(m.triangle_idx) < excluded.size())
excluded[size_t(m.triangle_idx)] = true;
}
// One model, its triangles in filament order (painted_colors() groups them), drawn a range per filament.
const std::vector<size_t> kept = m_painted_colors.outside(excluded);
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
init_data.reserve_vertices(kept.size() * 3);
init_data.reserve_indices(kept.size() * 3);
unsigned n = 0;
for (const size_t t : kept) {
const int state = m_painted_colors.state[t];
if (m_painted_colors_runs.empty() || m_painted_colors_runs.back().first != state)
m_painted_colors_runs.push_back({ state, { size_t(n), size_t(n) } });
for (int i = 0; i < 3; ++i)
init_data.add_vertex(m_painted_colors.facets.vertices[size_t(m_painted_colors.facets.indices[t][i])]);
init_data.add_triangle(n, n + 1, n + 2);
n += 3;
m_painted_colors_runs.back().second.second = size_t(n);
}
if (!init_data.is_empty())
m_painted_colors_glmodel.init_from(std::move(init_data));
}
bool GLGizmoTextureDisplacement::render_painted_colors(bool whole_stack)
{
rebuild_painted_colors(whole_stack);
const ModelObject *mo = m_c->selection_info()->model_object();
const ModelVolume *mv = texture_volume();
GLShaderProgram *shader = wxGetApp().get_shader("mm_gouraud");
if (mo == nullptr || mv == nullptr || shader == nullptr || !m_painted_colors_glmodel.is_initialized())
return false;
const Selection &selection = m_parent.get_selection();
const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
const Camera &camera = wxGetApp().plater()->get_camera();
const Transform3d &view_matrix = camera.get_view_matrix();
const Matrix3d normal_matrix = trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
const std::vector<ColorRGBA> colors = wxGetApp().plater()->get_extruders_colors();
shader->start_using();
// Set up as render_triangles() sets it up, so the colours are lit, clipped and slope-marked exactly as the
// neutral surface they cover.
const ClippingPlaneDataWrapper clp_data = get_clipping_plane_data();
shader->set_uniform("clipping_plane", clp_data.clp_dataf);
shader->set_uniform("z_range", clp_data.z_range);
shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
shader->set_uniform("view_normal_matrix", Matrix3d(view_matrix.matrix().block(0, 0, 3, 3) * normal_matrix));
shader->set_uniform("volume_world_matrix", trafo_matrix);
shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.);
shader->set_uniform("slope.actived", m_parent.is_using_slope());
shader->set_uniform("slope.volume_world_normal_matrix", Matrix3f(normal_matrix.cast<float>()));
shader->set_uniform("slope.normal_z", float(-std::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg))));
shader->set_uniform("slope.up_direction", get_tilt_up_direction());
shader->set_uniform("show_wireframe", false);
// Pulled forward and without depth writes, as the tint is (see render_paint_overlay()). The tint is
// drawn after this, so it shows on top.
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.f, -2.f));
glsafe(::glDepthMask(GL_FALSE));
bool drawn = false;
for (const auto &[state, range] : m_painted_colors_runs)
if (state >= 1 && size_t(state) <= colors.size()) {
m_painted_colors_glmodel.set_color(adjust_color_for_rendering(colors[size_t(state - 1)]));
m_painted_colors_glmodel.render(range, shader);
drawn = true;
}
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
shader->stop_using();
return drawn;
}
void GLGizmoTextureDisplacement::rebuild_paint_overlay()
{
m_paint_overlay_glmodel.reset();
@@ -1903,12 +1671,11 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay)
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
// Translucent, and pulled toward the camera so it wins the depth test against the coincident
// shaded surface - and against the selectors' highlight at -1 in the Normal view, by the full depth
// unit OpenGL guarantees to tell apart. Depth writes are off: this is a tint, and letting it own the
// depth buffer would make the wireframe and seam overlays drawn after it fight with geometry that is
// not really there. Blending is already enabled by render_painter_gizmo().
// shaded surface. Depth writes are off: this is a tint, and letting it own the depth buffer would
// make the wireframe and seam overlays drawn after it fight with geometry that is not really
// there. Blending is already enabled by render_painter_gizmo().
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.f, -2.f));
glsafe(::glPolygonOffset(-1.5f, -1.5f));
glsafe(::glDepthMask(GL_FALSE));
overlay.render();
glsafe(::glDepthMask(GL_TRUE));
@@ -2224,9 +1991,6 @@ void GLGizmoTextureDisplacement::rebuild_preview()
// finishes after the job queued below - and, since the counter is shared with the worker, that
// job also notices mid-run and aborts rather than computing a result nobody will use.
m_preview_generation->fetch_add(1);
// Everything rebuilt from here on reads the current palette. Cleared before any of the early returns
// below, which would otherwise leave it set and re-run this every frame.
m_palette_changed = false;
update_uv_editor();
rebuild_shaded_preview_mesh();
rebuild_paint_overlay();
@@ -2297,21 +2061,25 @@ void GLGizmoTextureDisplacement::queue_preview_job()
input.volume_to_world = texture_displacement_volume_to_world(*mv);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
// Captured here rather than read in the handler: the palette is main-thread state, and the preview
// has to be grouped against the same palette it was computed with, not whatever it is by the time
// the result lands.
// Captured here rather than read in the handler: get_extruders_colors() is main-thread state and
// the preview has to be grouped against the same palette it was computed with, not whatever is
// loaded by the time it lands.
input.color = color_settings_for(*mv);
// The result names a palette entry per triangle (index + 1), so these are the colours to draw it in:
// a single filament's own, or for a mix the colour its slot will show once a bake creates it.
std::vector<ColorRGBA> entry_colors;
entry_colors.reserve(input.color.palette.size());
for (const PrintableColor &e : input.color.palette)
entry_colors.emplace_back(e.rgb.x(), e.rgb.y(), e.rgb.z(), 1.f);
// The filament list the result's indices refer to, captured with the job rather than read back
// when it lands - loading a filament meanwhile must not recolour a preview computed against a
// different list.
// Every extruder, not the palette's physical-only list: the bake writes the filament it resolved
// to, and a mix resolves to a *mixed filament slot*, which is an extruder past the physical ones.
// Grouping against the shorter list dropped every triangle carrying such a slot out of the mesh
// entirely - the relief vanished and left only the few triangles that happened to print in a plain
// filament. The palette still has to be built from physical filaments alone (see filament_palette()),
// which is why these two are not the same list.
const std::vector<ColorRGBA> filaments = wxGetApp().plater()->get_extruders_colors();
m_preview_job_running = true;
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation,
[this, entry_colors](TextureDisplacementPreviewResult result, uint64_t result_generation) {
[this, filaments](TextureDisplacementPreviewResult result, uint64_t result_generation) {
indexed_triangle_set its = std::move(result.mesh);
m_preview_job_running = false;
if (result_generation != m_preview_generation->load()) {
@@ -2325,11 +2093,14 @@ void GLGizmoTextureDisplacement::queue_preview_job()
} else {
m_preview_glmodel.reset();
m_preview_color_runs.clear();
if (result.triangle_color.size() == its.indices.size() && !entry_colors.empty()) {
if (result.triangle_color.size() == its.indices.size() && !filaments.empty()) {
// Group by *filament*, not by palette entry: what the bake wrote is the resolved
// filament, interleaving already applied, so this shows the real banding rather
// than the flat average the eye will turn it into.
indexed_triangle_set sorted;
sorted.vertices = its.vertices;
sorted.indices.reserve(its.indices.size());
for (int want = 0; want <= int(entry_colors.size()); ++want) {
for (int want = 0; want <= int(filaments.size()); ++want) {
const size_t first = sorted.indices.size();
for (size_t i = 0; i < its.indices.size(); ++i)
if (int(result.triangle_color[i]) == want)
@@ -2338,7 +2109,7 @@ void GLGizmoTextureDisplacement::queue_preview_job()
continue;
m_preview_color_runs.push_back(
{ { first * 3, sorted.indices.size() * 3 },
want == 0 ? GLVolume::NEUTRAL_COLOR : entry_colors[size_t(want - 1)] });
want == 0 ? GLVolume::NEUTRAL_COLOR : filaments[size_t(want - 1)] });
}
m_preview_glmodel.init_from(sorted);
} else {
@@ -3595,9 +3366,6 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
const ModelObject *mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
// still on: a new set of selectors has none.
m_seed_fill_last_mesh_id = -1;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(GLVolume::NEUTRAL_COLOR);
@@ -4498,101 +4266,84 @@ TextureDisplacementFacetsData GLGizmoTextureDisplacement::facets_data_of(const M
bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
{
for (const TextureDisplacementLayer &layer : mv.texture_displacement_layers)
if (layer.color_enabled && !layer.empty() && height_texture_has_color(layer))
if (layer.color_enabled && !layer.empty() && decode_height_texture(layer).has_color())
return true;
return false;
}
void GLGizmoTextureDisplacement::bind_mixes_to_filament_slots(std::vector<PaletteEntry> &palette)
{
Sidebar *sidebar = &wxGetApp().plater()->sidebar();
if (sidebar == nullptr)
return;
for (PaletteEntry &e : palette) {
if (!e.is_mix())
continue;
// Components are 1-based in the config; the ratios are percentages summing to 100, which is the
// form create_mixed_filament_from_result() normalises from.
const int a_pct = int(std::lround(100.0 * double(e.num) / double(e.den)));
const int slot = sidebar->ensure_mixed_filament({ unsigned(e.a + 1), unsigned(e.b + 1) },
{ a_pct, 100 - a_pct });
if (slot >= 0) {
e.a = e.b = slot;
e.num = e.den = 1;
} else {
// No room for another slot. Collapse to the component that dominates the blend, which is what
// the old per-triangle path did on a surface it could not band anyway.
const int dominant = (e.num * 2 >= e.den) ? e.a : e.b;
e.a = e.b = dominant;
e.num = e.den = 1;
}
}
}
TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv)
{
TextureColorSettings out;
if (!any_layer_colors(mv))
return out; // nothing is colouring: every colour path stays switched off
out.palette = cached_palette();
out.palette_pure = make_palette(m_palette_filaments, {});
out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false, PALETTE_MAX_ENTRIES);
// Done here rather than in cached_palette(): this runs when a preview or a bake is queued, off a
// user action, while that one is also touched from the render path - and creating filament slots
// there would mutate the project mid-frame.
bind_mixes_to_filament_slots(out.palette);
out.despeckle_passes = mv.texture_displacement_options.color_despeckle;
return out;
}
const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDisplacement::cached_palette()
{
// Rebuilt only when something it depends on actually changes. The shaded preview rebuilds on every
// paint stroke and the panel asks every frame, while ranking the mixes and filling the quantizer's
// lookup cube each take tens of milliseconds - paying that per stroke is the difference between
// painting that keeps up and painting that stutters.
//
// Two levels. The ranking depends only on the filaments and the images, so dragging the count, or a
// bake creating slots, re-picks from it without ranking again; the palette and its quantizers depend
// on that pick as well.
const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
const int mix_count = mv != nullptr ? std::max(0, mv->texture_displacement_options.color_mix_count) : 0;
// Rebuilt only when the loaded filaments or the mixing setting actually change. The shaded preview
// rebuilds on every paint stroke and the subdivide preview on every slider frame, and filling the
// quantizer's lookup cube for a 64-entry palette is tens of milliseconds - paying that per stroke
// is the difference between painting that keeps up and painting that stutters.
const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
std::vector<ColorRGBA> filaments = filament_palette();
// The images themselves rather than their addresses, so a freed and reallocated image can never
// pass for the old one. Whether one has colour, and whether its colours are flat, follows from it.
std::vector<std::shared_ptr<std::vector<unsigned char>>> images;
if (mv != nullptr)
for (const TextureDisplacementLayer &layer : mv->texture_displacement_layers)
if (layer.color_enabled && !layer.empty())
images.push_back(layer.image_data);
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images;
if (ranking_stale) {
// Every mix costs a filament slot once they are bound to one, and the mask can name only so many
// states, so the palette has to leave room beside the physical filaments it already counts.
const int cap = int(EnforcerBlockerType::ExtruderMax);
if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing ||
cap != m_palette_cap) {
m_palette_filaments = std::move(filaments);
m_palette_images = std::move(images);
m_mix_ranking.reset();
}
if (mixing && !m_mix_ranking)
m_mix_ranking = rank_mixes(m_palette_filaments, mix_targets(mv->texture_displacement_layers),
int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
// Which mixes the project can still print: those it has a fixed slot for, plus as many new ones as
// there are free slots.
const PresetBundle &bundle = *wxGetApp().preset_bundle;
const int free_slots = std::max(0, int(EnforcerBlockerType::ExtruderMax) - int(bundle.filament_presets.size()));
const std::vector<std::string> slots = mixed_slot_signature(bundle.project_config);
if (ranking_stale || m_palette_cache.empty() || mixing != m_palette_mixing || mix_count != m_palette_mix_count ||
free_slots != m_palette_free_slots || slots != m_palette_slots) {
m_palette_mixing = mixing;
m_palette_mix_count = mix_count;
m_palette_free_slots = free_slots;
m_palette_slots = slots;
std::vector<PaletteEntry> mixes;
if (mixing && m_mix_ranking)
mixes = pick_mixes(*m_mix_ranking, mix_count, free_slots, [&bundle](const PaletteEntry &e) {
return find_fixed_mixed_filament(bundle.project_config, {unsigned(e.a + 1), unsigned(e.b + 1)},
{e.a_percent(), 100 - e.a_percent()}) >= 0;
});
std::vector<PaletteEntry> palette = make_palette(m_palette_filaments, mixes);
// The previews keep what they were drawn with, so a palette that really changed under them -
// a filament or a mixed slot edited in the sidebar - has to send them round again.
m_palette_changed = m_palette_changed || (!m_palette_cache.empty() && !same_palette(palette, m_palette_cache));
m_palette_cache = std::move(palette);
m_palette_quantizer = nullptr;
m_palette_pure_quantizer = nullptr;
m_palette_mixing = mixing;
m_palette_cap = cap;
m_palette_cache = make_palette(m_palette_filaments, mixing, cap);
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
}
return m_palette_cache;
}
std::pair<ColorQuantizeFn, ColorQuantizeFn> GLGizmoTextureDisplacement::palette_quantizers()
{
cached_palette();
if (!m_palette_quantizer) {
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
const bool has_mixes = m_palette_cache.size() > m_palette_filaments.size();
m_palette_pure_quantizer = has_mixes ? make_palette_quantizer(make_palette(m_palette_filaments, {})) : m_palette_quantizer;
}
return { m_palette_quantizer, m_palette_pure_quantizer };
}
std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
{
std::vector<ColorRGBA> all = wxGetApp().plater()->get_extruders_colors();
// Physical filaments only. A mix bakes into a mixed filament slot of its own, and those slots are
// extruders too - mixing them again would hand a blend components naming a virtual slot, where it
// can only name physical ones ("Mixed filament has invalid or mismatched components"). Mixed slots
// are kept after the physical ones, so filament i here is extruder i.
// Physical filaments only. The mixes this palette produces each become a mixed filament slot of
// their own (see bind_mixes_to_filament_slots()), and those slots are extruders too - so taking the
// list as it comes meant the next rebuild mixed *them* again, and handed components naming a
// virtual slot to a blend that can only name physical ones. That is what left entries reading
// "filament 1 plus nothing" and raised "Mixed filament has invalid or mismatched components".
const auto *is_mixed = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
std::vector<ColorRGBA> palette;
palette.reserve(all.size());
@@ -4606,211 +4357,44 @@ std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
return palette;
}
std::vector<GLGizmoTextureDisplacement::MixTarget> GLGizmoTextureDisplacement::mix_targets(
const std::vector<TextureDisplacementLayer> &layers)
{
// Each layer's bins, weighted by their share of that layer's pixels.
std::vector<std::vector<MixTarget>> per_layer;
for (const TextureDisplacementLayer &layer : layers) {
if (!layer.color_enabled || layer.empty() || analyze_texture_detail(layer).flat_colors)
continue;
TextureDisplacementLayer raw = layer;
raw.smoothing = 0.f;
const DecodedHeightTexture tex = decode_height_texture(raw);
if (!tex.has_color())
continue;
// 16 levels per channel, each bin keeping the mean of the colours that fell in it: a coarse grid
// to gather on, without snapping every colour to a bin corner.
struct Bin
{
double r = 0., g = 0., b = 0.;
uint32_t n = 0;
};
std::vector<Bin> bins(size_t(16 * 16 * 16));
// Past the budget, one pixel from each run of `stride`, at an offset jittered by a fixed-seed
// generator. A fixed step would sample a lattice that a striped texture can line up with, so
// that only one of its colours is ever seen; jittered, stripes of any period or orientation are
// sampled in proportion, and the same image still always gives the same targets. The offset
// comes from the generator's high bits: a power-of-two LCG's low bits repeat every 2, 4, 8...
// steps, and the stride is a power of two for exactly the images large enough to need this.
const size_t npx = size_t(tex.width) * size_t(tex.height);
const size_t stride = std::max<size_t>(1, npx / MIX_TARGET_SAMPLES);
uint64_t state = 0x9E3779B97F4A7C15ull;
size_t sampled = 0;
for (size_t start = 0; start < npx; start += stride) {
state = state * 6364136223846793005ull + 1442695040888963407ull;
const size_t i = start + size_t((uint64_t(uint32_t(state >> 32)) * uint64_t(stride)) >> 32);
if (i >= npx)
break;
const uint8_t *px = &tex.rgb[i * 3];
Bin &bin = bins[size_t(px[0] >> 4) * 256 + size_t(px[1] >> 4) * 16 + size_t(px[2] >> 4)];
bin.r += px[0];
bin.g += px[1];
bin.b += px[2];
++bin.n;
++sampled;
}
std::vector<MixTarget> targets;
for (const Bin &bin : bins)
if (bin.n > 0) {
const double inv = 1. / (255. * double(bin.n));
targets.push_back({ srgb_to_lab(Vec3f(float(bin.r * inv), float(bin.g * inv), float(bin.b * inv))),
float(double(bin.n) / double(sampled)) });
}
per_layer.push_back(std::move(targets));
}
// The layers weigh the same and together 1, settled before the pruning below: what rank_mixes() sums
// over the kept bins is then a mean over every pixel of the colouring layers, with the pixels of a
// dropped bin counted as no better off.
std::vector<MixTarget> out;
for (std::vector<MixTarget> &targets : per_layer)
for (MixTarget &t : targets) {
t.weight /= float(per_layer.size());
out.push_back(t);
}
if (out.size() > MIX_TARGET_BINS) {
std::partial_sort(out.begin(), out.begin() + MIX_TARGET_BINS, out.end(),
[](const MixTarget &l, const MixTarget &r) { return l.weight > r.weight; });
out.resize(MIX_TARGET_BINS);
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::rank_mixes(
const std::vector<ColorRGBA> &filaments, const std::vector<MixTarget> &targets, int limit)
{
const int n = int(filaments.size());
if (n < 2 || targets.empty() || limit <= 0)
return {};
// Every pair at every short-cycle ratio, coloured as its slot will be.
std::vector<std::string> hex(filaments.size());
for (int i = 0; i < n; ++i)
hex[size_t(i)] = encode_color(filaments[size_t(i)]);
std::vector<PaletteEntry> candidates;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
for (int den = 2; den <= 6; ++den)
for (int num = 1; num < den; ++num) {
if (std::gcd(num, den) != 1)
continue; // 2/4 is 1/2, already there
PaletteEntry e{ Vec3f::Zero(), i, j, num, den };
ColorRGB blended;
if (!decode_color(blend_color_multi({ hex[size_t(i)], hex[size_t(j)] },
{ e.a_percent(), 100 - e.a_percent() }),
blended))
continue;
e.rgb = Vec3f(blended.r(), blended.g(), blended.b());
candidates.push_back(e);
}
// How far each target is from the nearest single filament, and from every candidate.
const size_t nt = targets.size(), nc = candidates.size();
std::vector<Vec3f> filament_lab(size_t(n), Vec3f::Zero());
for (int i = 0; i < n; ++i)
filament_lab[size_t(i)] = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()));
const auto de = [](const Vec3f &l, const Vec3f &r) { return DeltaE00(l.x(), l.y(), l.z(), r.x(), r.y(), r.z()); };
std::vector<float> pure_d(nt, std::numeric_limits<float>::max());
for (size_t t = 0; t < nt; ++t)
for (const Vec3f &lab : filament_lab)
pure_d[t] = std::min(pure_d[t], de(targets[t].lab, lab));
std::vector<float> dist(nc * nt);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nc), [&](const tbb::blocked_range<size_t> &range) {
for (size_t c = range.begin(); c < range.end(); ++c) {
const Vec3f lab = srgb_to_lab(candidates[c].rgb);
for (size_t t = 0; t < nt; ++t)
dist[c * nt + t] = de(targets[t].lab, lab);
}
});
// Greedy: each round takes the candidate that lowers the weighted error the most. A candidate only
// counts where it beats the single filament by PREFER_PURE_DE, since everywhere else the quantizer
// picks the filament anyway.
std::vector<float> current = pure_d;
std::vector<char> taken(nc, 0);
std::vector<PaletteEntry> out;
const auto counts = [&](size_t c, size_t t) {
const float d = dist[c * nt + t];
return d < current[t] && d <= pure_d[t] - PREFER_PURE_DE;
};
while (int(out.size()) < limit) {
size_t best = nc;
double best_gain = 0.;
for (size_t c = 0; c < nc; ++c) {
if (taken[c])
continue;
double gain = 0.;
for (size_t t = 0; t < nt; ++t)
if (counts(c, t))
gain += double(targets[t].weight) * double(current[t] - dist[c * nt + t]);
if (gain > best_gain) {
best_gain = gain;
best = c;
}
}
if (best == nc || best_gain < double(MIN_MIX_GAIN))
break;
taken[best] = 1;
out.push_back(candidates[best]);
for (size_t t = 0; t < nt; ++t)
if (counts(best, t))
current[t] = dist[best * nt + t];
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::pick_mixes(
const std::vector<PaletteEntry> &ranking, int count, int free_slots, const std::function<bool(const PaletteEntry &)> &reusable)
{
std::vector<PaletteEntry> out;
for (const PaletteEntry &e : ranking) {
if (int(out.size()) >= count)
break;
// Out of free slots, a later mix that already has one still fits.
if (reusable && reusable(e)) {
out.push_back(e);
} else if (free_slots > 0) {
out.push_back(e);
--free_slots;
}
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::make_palette(
const std::vector<ColorRGBA> &filaments, const std::vector<PaletteEntry> &mixes)
const std::vector<ColorRGBA> &filaments, bool mixing, int max_entries)
{
std::vector<PaletteEntry> out;
out.reserve(filaments.size() + mixes.size());
for (int i = 0; i < int(filaments.size()); ++i)
out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()), i, i, 1, 1 });
out.insert(out.end(), mixes.begin(), mixes.end());
return out;
}
const int n = int(filaments.size());
for (int i = 0; i < n; ++i)
out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()),
i, i, 1, 1 });
if (!mixing || n < 2)
return out;
std::vector<int> GLGizmoTextureDisplacement::palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix)
{
std::vector<char> used(palette.size(), 0);
for (const uint8_t v : triangle_color)
if (v > 0 && size_t(v) <= palette.size())
used[size_t(v) - 1] = 1;
// How many intermediate steps each pair gets, chosen so the whole palette stays under
// PALETTE_MAX_ENTRIES. Fewer filaments means more room for mixes, which is also what you want:
// with two filaments the mixes are the only way to get anywhere, and with sixteen there is little
// point mixing at all. `den` is also the band/dither repeat, so a small one is a short pattern.
const int pairs = n * (n - 1) / 2;
int steps = 0;
for (int s = 5; s >= 1; --s)
if (n + pairs * s <= max_entries) {
steps = s;
break;
}
if (steps == 0)
return out;
const int den = steps + 1;
std::vector<int> out(palette.size(), -1);
for (size_t i = 0; i < palette.size(); ++i) {
const PaletteEntry &e = palette[i];
if (!e.is_mix()) {
out[i] = e.a;
} else if (used[i]) {
const int slot = slot_for_mix ? slot_for_mix(e) : -1;
// No room for another slot: the component that dominates the blend is the nearest the print
// can come.
out[i] = slot >= 0 ? slot : (e.num * 2 >= e.den ? e.a : e.b);
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j) {
const Vec3f lab_i = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()));
const Vec3f lab_j = srgb_to_lab(Vec3f(filaments[size_t(j)].r(), filaments[size_t(j)].g(), filaments[size_t(j)].b()));
for (int k = 1; k <= steps; ++k) {
// k/den of filament i, the rest of j - averaged in Lab, which is what the eye does
// when the two are interleaved too finely to resolve.
const float t = float(k) / float(den);
out.push_back({ lab_to_srgb(lab_i * t + lab_j * (1.f - t)), i, j, k, den });
}
}
}
return out;
}
@@ -4850,7 +4434,15 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
best_pure = int(i);
}
}
// A mix only where it clearly beats the nearest single filament, see PREFER_PURE_DE.
// A mix is an interleave that only reads as its colour from a distance; up close it is
// stripes. So it is spent only where it buys a better match than the nearest single
// filament - but "better" was set at ten Delta E, which is not a visible step, it is a
// different colour. Measured over the whole cube that threshold turned 94% of the
// lookups that wanted a mix back into a pure filament, leaving 38%; along a greyscale
// ramp, the shape a height texture actually traces, it cut 80% to 66%. Two Delta E is
// about where a side-by-side difference stops being arguable, which is the right place
// to start paying for stripes.
constexpr float PREFER_PURE_DE = 2.f;
if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE)
best = best_pure;
(*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best);
@@ -4869,7 +4461,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
const std::vector<PrintableColor> &palette, const DisplacementProgressFn &progress,
BakeStageRecorder *debug)
{
TextureDisplacementPrepareResult out;
@@ -4947,13 +4539,10 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// Colour boundaries need triangles of their own - the chord test cannot see them, since
// the height field is perfectly smooth across a change of filament.
ColorFieldSampler color;
// A flat-colour layer is matched against single filaments only, as the bake does, so its
// boundaries are refined where the bake will actually change filament.
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
make_palette_quantizer(color_settings.palette_pure));
// The refinement follows perceived colour: a mix is one colour here, however the slicer
// interleaves its filaments layer by layer.
if (params.subdiv_color_edge_mm > 0.f && !palette.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette));
// Note the sampler is built on the *quantizer* alone - the refinement follows perceived
// colour, never the interleaving that realises a mix - the slicer does that per layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
// to); in plain adaptive mode the target edge length is the only criterion, so the floor
// must not be allowed to silently override a target the user set below it.
@@ -5241,9 +4830,9 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
// Same colour criterion Apply will use, so the previewed wireframe is the mesh that commits.
ColorFieldSampler color;
if (m_subdivide_color_mm > 0.f && any_layer_colors(*mv)) {
const auto [quantize, quantize_pure] = palette_quantizers();
color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets, quantize,
quantize_pure);
cached_palette(); // refreshes m_palette_quantizer if the filaments changed
color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets,
m_palette_quantizer);
}
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
@@ -5755,12 +5344,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
return;
ModelVolume *mv = texture_volume();
// The palette also follows project state nobody tells this gizmo about - the filaments and mixed
// slots in the sidebar - so it is checked once a frame, and a change re-runs the previews.
cached_palette();
if (m_palette_changed)
m_preview_params_dirty = true;
float scale = m_parent.get_scale();
#ifdef WIN32
int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
@@ -6544,15 +6127,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"of filaments can cover a photo or a gradient. An image of flat colors "
"prints the same either way. Off uses one filament per area."));
if (opts.color_mix_enabled) {
cached_palette(); // brings m_palette_filaments up to date
// Every mix can become a filament slot, and there are only so many beside the
// physical filaments.
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created."));
// After the slider, so a change shows in the same frame.
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
@@ -16,15 +16,11 @@
#include "libslic3r/Color.hpp"
#include <cstddef>
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "admesh/stl.h"
#include <cstdint>
#include <functional>
#include "libslic3r/Point.hpp"
#include <imgui.h>
#include <map>
#include <memory>
#include <optional>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
#include <string>
#include <vector>
@@ -62,7 +58,7 @@ public:
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings,
const std::vector<PrintableColor> &palette,
const DisplacementProgressFn &progress,
// Optional step capture: receives the mesh
// after the remesh and after the refinement,
@@ -74,43 +70,22 @@ public:
using PaletteEntry = PrintableColor;
// One colour mixes are meant to reach: a bin of the textures' colour histogram, in CIELAB, and how
// much of the image it covers.
struct MixTarget
{
Vec3f lab = Vec3f::Zero();
float weight = 0.f;
};
// The colours worth mixing for: those of the colouring layers in `layers` whose image is not made of
// flat colours (TextureDetail::flat_colors - those print in single filaments only, so no mix could
// serve them). The layers weigh the same and, before only the heaviest bins are kept (which is what
// bounds rank_mixes()), together 1. Read from the image as imported, as analyze_texture_detail()
// does, so the Smoothing slider does not move the palette around.
static std::vector<MixTarget> mix_targets(const std::vector<TextureDisplacementLayer> &layers);
// Mixes of pairs of `filaments`, best first and at most `limit` of them. Each is the one that most
// improves the match to `targets` given those ranked before it, counted only where it beats the
// nearest single filament by the quantizer's prefer-pure margin - which is where the quantizer will
// actually pick it. Stops early once another mix would make no noticeable difference, so a texture
// the filaments already cover gets few mixes or none.
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can
// address), plus - when `mixing` - every pair of them at evenly spaced ratios.
//
// Ratios are the short-cycle ones (k/d for d up to 6): the slicer interleaves a mix layer by layer,
// and a long cycle prints as visible bands rather than as a colour. A mix's `rgb` is the colour its
// mixed filament slot will show (blend_color_multi(), as the sidebar computes it), so the match, the
// preview and the slot all agree on what the mix looks like.
static std::vector<PaletteEntry> rank_mixes(const std::vector<ColorRGBA> &filaments,
const std::vector<MixTarget> &targets, int limit);
// The first `count` mixes of `ranking` the project can give a filament slot to. A mix `reusable`
// reports as already having a fixed slot costs nothing; any other uses up one of `free_slots`, and
// is skipped once they run out - so the palette never offers a colour a bake could not print.
static std::vector<PaletteEntry> pick_mixes(const std::vector<PaletteEntry> &ranking, int count, int free_slots,
const std::function<bool(const PaletteEntry &)> &reusable);
// The printable palette: the loaded filaments, entry i being filament i, followed by `mixes`.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments,
const std::vector<PaletteEntry> &mixes);
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and
// blue banded together read as a desaturated grey-green, and that is what the preview must promise
// - blending them subtractively would show a green the printer cannot produce this way.
//
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them.
// `max_entries` bounds the whole palette. It is normally the quantizer's own limit, but when the
// mixes become filament slots it has to be the paint mask's instead: a mask can name only
// EnforcerBlockerType::ExtruderMax states, and every mix now occupies one of them.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing,
int max_entries);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
@@ -122,49 +97,40 @@ public:
// to a worker thread and outlives the palette it was built from.
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
// The filament (0-based) each entry of `palette` prints in, by palette index: a single filament is
// itself, a mix is the slot `slot_for_mix` returns for it. Only the mixes `triangle_color` actually
// uses (palette index + 1 per triangle, 0 for none) are asked for, since asking is what creates a
// slot. A mix that gets no slot (-1) prints in its dominant component; one nothing uses maps to -1.
//
// This is what the bake writes into the paint: a palette index is a filament only for the single
// filaments, while a mix's slot can sit anywhere among the project's mixed slots.
static std::vector<int> palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix);
// Turns a palette index plus a position into the filament to print there, interleaving the two
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
// Everything the jobs need to colour with, for the current volume: the palette, its single-filament
// part, and the despeckle passes. Empty when no layer is actually colouring. Read-only: no filament
// slot is created here, only when a bake commits (see palette_filaments()).
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
// height, despeckle. Empty when no layer is actually colouring.
TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current volume and project, rebuilt only when what it depends on
// changes - see the definition for why that caching is not optional.
// The printable palette for the current filaments and mixing setting, rebuilt only when either
// actually changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette();
// The quantizers for the cached palette and for its single filaments alone (flat-colour images),
// filled on first use. Only the subdivide preview matches colours on this thread - the jobs build
// their own from the palette they capture - so a palette rebuild, such as every step of a count
// drag, costs no lookup cube unless that preview asks for one.
std::pair<ColorQuantizeFn, ColorQuantizeFn> palette_quantizers();
std::vector<PaletteEntry> m_palette_cache;
ColorQuantizeFn m_palette_quantizer;
ColorQuantizeFn m_palette_pure_quantizer;
// Set when a rebuild changed the palette the previews were drawn with; cleared by rebuild_preview().
bool m_palette_changed = false;
// What the cache was built from.
std::vector<ColorRGBA> m_palette_filaments;
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
bool m_palette_mixing = false;
int m_palette_mix_count = 0;
int m_palette_free_slots = 0;
std::vector<std::string> m_palette_slots; // the project's mixed slots
// Turns every mix in `palette` into a mixed filament slot and rewrites the entry to name that slot
// as a plain filament, so nothing downstream has to know a mix is involved: is_mix() goes false and
// the resolver simply returns it. The per-layer interleaving then happens in the slicer, where it is
// not limited by how fine the mesh is. Entries whose slot could not be created (the paint-state cap)
// fall back to the nearer of the two components.
void bind_mixes_to_filament_slots(std::vector<PaletteEntry> &palette);
std::vector<PaletteEntry> m_palette_cache;
std::vector<ColorRGBA> m_palette_filaments;
int m_palette_cap = 0; // the max_entries m_palette_cache was built with
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded physical filaments, clamped to the states mmu_segmentation_facets can address.
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette();
// The Z band height, in mm. One print layer is the ideal, but the interleave is realised per
// *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle
// grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour,
// it does so in the print too, not only on screen. The refinement edge is chosen from the model's
// diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of
// layers at least two facet rows tall: still exact on the printer, and representable by the mesh
// that has to carry it. Used by both the bake settings and the preview shader, so the two agree.
// The Normal preview's triangles, grouped by the palette entry they will print in. Colour is per facet
// and the palette is small, so the mesh is uploaded once with its index buffer
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
// colour attribute, and so no change to GLModel's vertex layouts.
//
@@ -182,21 +148,6 @@ public:
// and so whether the colour criterion and the mmu write ever run.
static bool any_layer_colors(const ModelVolume &mv);
// The model's own colour paint (mmu_segmentation_facets) as the gizmo draws it over its surface: the
// sub-triangles painted in a filament, grouped by that filament. NONE - the volume's own filament - is
// left out, so those triangles keep the gizmo's neutral, as they do in the preview.
struct PaintedColors
{
indexed_triangle_set facets; // over the paint's whole vertex array
std::vector<int> source; // per triangle of `facets`: the model triangle it lies in
std::vector<int> state; // per triangle of `facets`: its filament state, 1-based
// The triangles of `facets` outside the model triangles `excluded` marks, in order. A model
// triangle past the end of `excluded` is not excluded.
std::vector<size_t> outside(const std::vector<bool> &excluded) const;
};
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
@@ -206,9 +157,6 @@ public:
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
// with the main canvas's instance transform. The base alone would let the keyboard shortcut open it there.
bool on_is_activable() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
@@ -727,9 +675,13 @@ private:
bool m_shaded_preview_dirty = false;
GLModel m_shaded_preview_glmodel;
// Translucent tint over the active layer's painted triangles: the paint feedback over a preview (see
// render_painter_gizmo()). Cheap (the painted patch only), translucent so the preview shows through,
// and rebuilt live during a stroke.
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used
// in either preview mode - it is coincident with the surface and simply covers it - so the only
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
// stroke.
GLModel m_paint_overlay_glmodel;
// The islands selected in the UV editor, tinted on the model so the pane's selection can be seen
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
@@ -747,23 +699,6 @@ private:
GLModel m_other_paint_glmodel;
std::string m_other_paint_key;
void rebuild_other_paint_overlay();
// The model's colour paint, drawn over the surface so the colours a bake wrote stay visible - the
// canvas draws no volume while a paint gizmo is open, and the selectors hold only displacement paint.
// Left out wherever the preview on screen shows paint of its own (`whole_stack`: every layer's, as the
// Normal preview does; otherwise the active layer's), since an opaque overlay there would hide that
// preview. Only while a layer colours: it is the colour workflow's result, and every other paint gizmo
// shows the model neutral.
//
// Two levels: the paint's sub-triangles, which take a selector over the whole mesh and change only with
// the paint itself, and the part drawn, which follows every flushed stroke.
PaintedColors m_painted_colors;
std::string m_painted_colors_key;
GLModel m_painted_colors_glmodel;
std::string m_painted_colors_drawn_key;
std::vector<std::pair<int, std::pair<size_t, size_t>>> m_painted_colors_runs; // filament state, index range
void rebuild_painted_colors(bool whole_stack);
// False when there was nothing to draw.
bool render_painted_colors(bool whole_stack);
// Whether render_shaded_preview_mesh() would actually draw something. Checked before the real volume
// is hidden: with no layer, no texture or no shader the shaded path draws nothing, and hiding the
// volume for it left the model invisible.
@@ -777,9 +712,11 @@ private:
int m_shaded_projection_mode = 0;
Vec3f m_shaded_patch_center = Vec3f::Zero();
Vec3f m_shaded_patch_axis = Vec3f::UnitZ();
// The palette the fast preview matches each fragment against, captured with its mesh. Empty when the
// active layer is not colouring, which is what tells the shader to fall back to the model's own
// colour. Every entry carries the colour it prints in, so drawing it needs nothing else.
// The palette the fast preview's per-triangle filament indices were built against, captured when
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
// into the mesh can never be resolved against a different set of filaments than they were computed
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
std::vector<PaletteEntry> m_shaded_preview_palette;
// GPU island drag: while an island is dragged in the UV editor, the displacement mesh is baked once (with
@@ -4,7 +4,6 @@
#include <functional>
#include <utility>
#include <string>
#include <vector>
#include "libslic3r/TextureDisplacement.hpp"
#include <exception>
#include <cstddef>
@@ -115,17 +114,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
if (volume == nullptr)
return;
// The filament each palette entry prints in. This is where a mix becomes a mixed filament slot,
// and only a mix the bake actually painted with: the preview never creates one, so the project
// gains only the slots this result needs. Done before anything below names them - adding a slot
// runs ModelVolume::update_extruder_count(), which clamps paint above the old filament count.
Sidebar &sidebar = plater->sidebar();
const std::vector<int> filament_of = GLGizmoTextureDisplacement::palette_filaments(
m_input.color.palette, m_triangle_color, [&sidebar](const PrintableColor &mix) {
return sidebar.ensure_mixed_filament({ unsigned(mix.a + 1), unsigned(mix.b + 1) },
{ mix.a_percent(), 100 - mix.a_percent() });
});
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
@@ -141,11 +129,9 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
if (!existing.bitstream.empty())
selector.deserialize(existing, false);
for (size_t i = 0; i < m_triangle_color.size(); ++i) {
const size_t entry = size_t(m_triangle_color[i]);
if (entry > 0 && entry <= filament_of.size() && filament_of[entry - 1] >= 0)
selector.set_facet(int(i), EnforcerBlockerType(filament_of[entry - 1] + 1));
}
for (size_t i = 0; i < m_triangle_color.size(); ++i)
if (m_triangle_color[i] > 0)
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i]));
volume->mmu_segmentation_facets.set(selector);
}
@@ -56,9 +56,8 @@ private:
TriangleMesh m_result;
// What the bake spent, for the message it leaves behind when the budget capped the detail.
TextureBakeStats m_stats;
// Per triangle of m_result: the palette entry to print it in, as its index + 1 (0 = leave alone).
// Empty unless a layer asked for colour. See TextureColorRequest; finalize() turns these into
// filaments.
// Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value
// (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest.
std::vector<uint8_t> m_triangle_color;
std::function<void()> m_on_finished;
};
@@ -59,7 +59,7 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
m_input.color,
m_input.color.palette,
// Preparation is roughly half the run; the bake
// takes the progress bar from there.
[&report](int pct) { return report(1 + pct / 2); },
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.params, m_input.color,
m_input.params, m_input.color.palette,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
@@ -26,13 +26,13 @@ struct TextureDisplacementPreviewInput
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
// is and the two cannot disagree. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity();
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
TextureColorSettings color;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
// triangle (its index + 1; 0 means "no colour from the texture").
// A preview result: the displaced mesh, and - when the input carried a palette - one filament index
// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture").
struct TextureDisplacementPreviewResult
{
indexed_triangle_set mesh;
+50 -6
View File
@@ -5048,15 +5048,35 @@ static bool create_mixed_filament_from_result(
is_mixed_opt->values[new_idx] = true;
}
std::string comp_str;
for (size_t i = 0; i < result.components.size(); ++i) {
if (i > 0) comp_str += ",";
comp_str += std::to_string(result.components[i]);
}
{
auto* comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
while (comp_opt->values.size() <= new_idx) comp_opt->values.push_back(std::string{});
comp_opt->values[new_idx] = format_mixed_components(result.components);
comp_opt->values[new_idx] = comp_str;
}
int ratio_sum = 0;
for (int r : result.ratios) ratio_sum += r;
if (ratio_sum <= 0) ratio_sum = 100;
std::string ratio_str;
{
CNumericLocalesSetter c_locale_setter;
for (size_t i = 0; i < result.ratios.size(); ++i) {
if (i > 0) ratio_str += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum);
ratio_str += buf;
}
}
{
auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
while (ratios_opt->values.size() <= new_idx) ratios_opt->values.push_back(std::string{});
ratios_opt->values[new_idx] = format_mixed_ratios(result.ratios);
ratios_opt->values[new_idx] = ratio_str;
}
if (!project_config.option("filament_mixed_gradient"))
@@ -5116,12 +5136,36 @@ int Sidebar::ensure_mixed_filament(const std::vector<unsigned int> &components,
return -1;
if (p->combos_filament.size() < 2)
return -1;
if (std::accumulate(ratios.begin(), ratios.end(), 0) <= 0)
// Normalise the way create_mixed_filament_from_result() stores them, so the comparison below sees
// the same text the config holds rather than two spellings of one blend.
int ratio_sum = 0;
for (const int r : ratios)
ratio_sum += r;
if (ratio_sum <= 0)
return -1;
if (const int existing = find_fixed_mixed_filament(wxGetApp().preset_bundle->project_config, components, ratios);
existing >= 0)
return existing;
std::string comp_str, ratio_str;
{
CNumericLocalesSetter c_locale_setter;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0) { comp_str += ","; ratio_str += ","; }
comp_str += std::to_string(components[i]);
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(ratios[i]) / float(ratio_sum));
ratio_str += buf;
}
}
const auto &project_config = wxGetApp().preset_bundle->project_config;
const auto *is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed_opt != nullptr && comp_opt != nullptr && ratios_opt != nullptr)
for (size_t i = 0; i < is_mixed_opt->values.size(); ++i)
if (is_mixed_opt->values[i] && i < comp_opt->values.size() && i < ratios_opt->values.size() &&
comp_opt->values[i] == comp_str && ratios_opt->values[i] == ratio_str)
return int(i);
if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax))
return -1;
+3 -4
View File
@@ -292,12 +292,11 @@ public:
// Mixed-color filament sidebar section
void add_mixed_filament();
// The filament slot that blends `components` (1-based physical filament indices) in `ratios`
// (percentages) at a fixed ratio, creating it when no existing fixed mixed slot already describes
// that blend (see find_fixed_mixed_filament()). Returns the 0-based filament index, or -1 when the
// paint-state cap leaves no room for another one.
// (percentages), creating it when no existing mixed slot already describes that blend. Returns the
// 0-based filament index, or -1 when the paint-state cap leaves no room for another one.
//
// Exists so a feature that needs a blend can ask for one without going through the modal dialog:
// a texture displacement bake turns each mix it painted with into a slot, which is what moves the
// the texture displacement gizmo turns each mix in its palette into a slot, which is what moves the
// interleaving from its own paint mask to the slicer, where it happens per layer.
int ensure_mixed_filament(const std::vector<unsigned int> &components, const std::vector<int> &ratios);
void edit_mixed_filament(size_t idx);
+15
View File
@@ -13,6 +13,10 @@
#include <wx/gdicmn.h>
#include <wx/webview.h>
#ifdef __WXGTK__
#include <gtk/gtk.h>
#endif
namespace Slic3r { namespace GUI {
WebDialog::WebDialog(wxWindow* parent,
@@ -52,6 +56,17 @@ WebDialog::WebDialog(wxWindow* parent,
Bind(wxEVT_CLOSE_WINDOW, &WebDialog::on_close_window, this);
}
void WebDialog::use_normal_window_type()
{
#ifdef __WXGTK__
// wxGTK types every wxDialog as a dialog, and Mutter offers no maximize for anything but a
// normal window. Mutter also stops hiding a normal window from the taskbar, so keep it hidden
// as a dialog with a parent is everywhere else.
gtk_window_set_type_hint(GTK_WINDOW(m_widget), GDK_WINDOW_TYPE_HINT_NORMAL);
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(m_widget), TRUE);
#endif
}
void WebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
+4
View File
@@ -62,6 +62,10 @@ public:
bool is_open() const { return m_open; }
// Lets a modeless window be maximized on X11; call before Show(). Modal ones stay dialogs so the
// window manager keeps routing focus from the parent to them.
void use_normal_window_type();
// The payload submitted via window.orca.submit() (modal use), if any.
const std::optional<nlohmann::json>& result() const { return m_result; }
+1
View File
@@ -399,6 +399,7 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
if (UiRegistry::instance().is_open(new_id))
dlg->Destroy();
} else {
dlg->use_normal_window_type();
dlg->Show();
}
});
-36
View File
@@ -255,39 +255,3 @@ TEST_CASE("blend_color_multi weights components", "[FilamentMixer]")
REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8);
}
}
TEST_CASE("format_mixed_ratios normalises weights to four decimals", "[FilamentMixer]")
{
REQUIRE(format_mixed_components({1, 3}) == "1,3");
REQUIRE(format_mixed_ratios({50, 50}) == "0.5000,0.5000");
REQUIRE(format_mixed_ratios({1, 2}) == "0.3333,0.6667");
REQUIRE(format_mixed_ratios({1, 1}) == format_mixed_ratios({50, 50}));
}
TEST_CASE("find_fixed_mixed_filament reuses only a fixed slot of the same blend", "[FilamentMixer]")
{
// Physical slots 0 and 1; slot 2 blends them 50:50 as a gradient, slot 3 at a fixed 50:50.
DynamicPrintConfig cfg;
cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true, true}));
cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "1,2", "1,2"}));
cfg.set_key_value("filament_mixed_sublayer_ratios",
new ConfigOptionStrings({"", "", format_mixed_ratios({50, 50}), format_mixed_ratios({50, 50})}));
cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, true, false}));
SECTION("The fixed slot is found, whatever scale the weights are given at") {
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == 3);
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {1, 1}) == 3);
}
SECTION("A gradient slot with the same components and ratios is not a match") {
cfg.option<ConfigOptionBools>("filament_is_mixed")->values[3] = false;
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == -1);
}
SECTION("A project without the gradient key still matches its fixed slots") {
cfg.erase("filament_mixed_gradient");
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == 2);
}
SECTION("Another ratio or another component order is a different blend") {
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {1, 2}) == -1);
REQUIRE(find_fixed_mixed_filament(cfg, {2, 1}, {50, 50}) == -1);
}
}
@@ -68,28 +68,6 @@ static std::shared_ptr<std::vector<unsigned char>> make_flat_gray_png(uint8_t va
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// The same round trip for a flat colour image, which decode_height_texture() reads through its colour path.
static std::shared_ptr<std::vector<unsigned char>> make_flat_rgb_png(uint8_t r, uint8_t g, uint8_t b, size_t w = 4, size_t h = 4)
{
std::vector<uint8_t> rgb;
for (size_t i = 0; i < w * h; ++i)
rgb.insert(rgb.end(), { r, g, b });
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_rgb_to_file(tmp_path.string(), w, h, rgb));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a
// step, which is precisely the relief the post-process smoothing exists to round off.
static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t w = 16, size_t h = 16)
@@ -2157,20 +2135,3 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
CHECK(second <= single * 5 / 4);
}
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
{
TextureDisplacementLayer gray;
gray.image_data = make_flat_gray_png(128);
CHECK_FALSE(height_texture_has_color(gray));
TextureDisplacementLayer color;
color.image_data = make_flat_rgb_png(200, 40, 10);
// Before the image is decoded and after, and whatever the smoothing.
CHECK(height_texture_has_color(color));
CHECK(decode_height_texture(color).has_color());
CHECK(height_texture_has_color(color));
color.smoothing = 0.5f;
CHECK(height_texture_has_color(color));
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
}
-1
View File
@@ -36,7 +36,6 @@ add_executable(${_TEST_NAME}_tests
test_plugin_audit.cpp
test_plugin_json_depth.cpp
test_shortcuts.cpp
test_texture_color_palette.cpp
test_file_url.cpp
test_user_manager.cpp
../fff_print/test_helpers.cpp
@@ -1,294 +0,0 @@
// The texture displacement gizmo's palette helpers live in libslic3r_gui; this is the suite that links it.
// Same Windows include prologue as test_filament_bitmap_utils.cpp (wx pulls in <windows.h>; keep
// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers).
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/Color.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/Utils/ColorSpaceConvert.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Gizmo = Slic3r::GUI::GLGizmoTextureDisplacement;
using Entry = Gizmo::PaletteEntry;
using MixTarget = Gizmo::MixTarget;
namespace {
const ColorRGBA BLACK{ 0.f, 0.f, 0.f, 1.f };
const ColorRGBA WHITE{ 1.f, 1.f, 1.f, 1.f };
const ColorRGBA RED{ 1.f, 0.f, 0.f, 1.f };
const ColorRGBA BLUE{ 0.f, 0.f, 1.f, 1.f };
const ColorRGBA YELLOW{ 1.f, 1.f, 0.f, 1.f };
MixTarget target(const Vec3f &rgb, float weight)
{
MixTarget t;
RGB2Lab(rgb.x(), rgb.y(), rgb.z(), &t.lab.x(), &t.lab.y(), &t.lab.z());
t.weight = weight;
return t;
}
// The colour a mixed slot of these two filaments shows, as the sidebar computes it.
Vec3f slot_color(const ColorRGBA &a, const ColorRGBA &b, int a_percent)
{
ColorRGB c;
REQUIRE(decode_color(blend_color_multi({ encode_color(a), encode_color(b) }, { a_percent, 100 - a_percent }), c));
return Vec3f(c.r(), c.g(), c.b());
}
// A colour image layer, through Slic3r's own PNG writer so decode_height_texture() reads it the way it
// reads an imported texture.
TextureDisplacementLayer color_layer(int w, int h, const std::vector<uint8_t> &rgb)
{
ScopedTemporaryFile png(".png");
REQUIRE(png::write_rgb_to_file(png.string(), size_t(w), size_t(h), rgb));
std::ifstream in(png.string(), std::ios::binary);
std::vector<unsigned char> bytes{ std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>() };
REQUIRE_FALSE(bytes.empty());
TextureDisplacementLayer layer;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::move(bytes));
layer.color_enabled = true;
return layer;
}
// A red/green ramp over a fixed blue: colours spread over many bins, so the image is not flat-colour.
TextureDisplacementLayer gradient_layer()
{
const int n = 64;
std::vector<uint8_t> rgb;
for (int y = 0; y < n; ++y)
for (int x = 0; x < n; ++x) {
rgb.push_back(uint8_t(x * 4));
rgb.push_back(uint8_t(y * 4));
rgb.push_back(128);
}
return color_layer(n, n, rgb);
}
// A 2048x1100 image, over mix_targets()' sampling budget, that is pure red wherever `red(x, y)` holds and
// elsewhere a gradient spread over far more than eight coarse bins, so the image is not flat-colour.
template<class RedFn> TextureDisplacementLayer striped_layer(RedFn red)
{
const int w = 2048, h = 1100;
std::vector<uint8_t> rgb;
rgb.reserve(size_t(w) * size_t(h) * 3);
for (int y = 0; y < h; ++y)
for (int x = 0; x < w; ++x) {
const bool is_red = red(x, y);
rgb.push_back(is_red ? 255 : uint8_t(x * 255 / w));
rgb.push_back(is_red ? 0 : uint8_t(y * 255 / h));
rgb.push_back(is_red ? 0 : 128);
}
return color_layer(w, h, rgb);
}
// How much of the targets' weight is pure red.
float red_weight(const std::vector<MixTarget> &targets)
{
Vec3f red;
RGB2Lab(1.f, 0.f, 0.f, &red.x(), &red.y(), &red.z());
float weight = 0.f;
for (const MixTarget &t : targets)
if ((t.lab - red).norm() < 3.f)
weight += t.weight;
return weight;
}
} // namespace
TEST_CASE("mix targets cover each colouring photo layer once and skip the rest", "[TextureColorPalette][TextureDisplacement]")
{
TextureDisplacementLayer photo = gradient_layer();
const std::vector<MixTarget> targets = Gizmo::mix_targets({ photo });
REQUIRE_FALSE(targets.empty());
float total = 0.f;
for (const MixTarget &t : targets)
total += t.weight;
REQUIRE_THAT(total, WithinAbs(1., 1e-4));
SECTION("A layer that does not colour gives no targets") {
photo.color_enabled = false;
REQUIRE(Gizmo::mix_targets({ photo }).empty());
}
SECTION("A flat-colour image prints in single filaments, so it gives no targets") {
const TextureDisplacementLayer flat = color_layer(8, 8, std::vector<uint8_t>(8 * 8 * 3, 200));
REQUIRE(Gizmo::mix_targets({ flat }).empty());
}
}
TEST_CASE("mix targets of a large image weigh each colour by its share, whatever its stripes", "[TextureColorPalette][TextureDisplacement]")
{
// Sampled rather than read in full, these must not line up with the samples: a fixed sampling step
// sees only one phase of a stripe pattern, and a generator whose offsets repeat sees only some.
SECTION("Red on every other column") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int) { return x % 2 == 0; }) })),
WithinAbs(1. / 2., 0.03));
}
SECTION("Red on every fourth column") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int) { return x % 4 == 0; }) })),
WithinAbs(1. / 4., 0.03));
}
SECTION("Red on diagonals") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int y) { return (x - y) % 3 == 0; }) })),
WithinAbs(1. / 3., 0.03));
}
}
TEST_CASE("the mix ranked first is the one the image needs most", "[TextureColorPalette][TextureDisplacement]")
{
// The whole image is exactly the colour of a 1:1 black/white slot.
const std::vector<MixTarget> targets = { target(slot_color(BLACK, WHITE, 50), 1.f) };
const std::vector<Entry> ranked = Gizmo::rank_mixes({ BLACK, WHITE }, targets, 1);
REQUIRE(ranked.size() == 1);
CHECK(ranked.front().a == 0);
CHECK(ranked.front().b == 1);
CHECK(ranked.front().num * 2 == ranked.front().den);
}
TEST_CASE("an image the filaments already match ranks no mixes", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<MixTarget> targets = { target(Vec3f(1.f, 0.f, 0.f), 0.5f), target(Vec3f(0.f, 0.f, 1.f), 0.5f) };
REQUIRE(Gizmo::rank_mixes({ RED, BLUE }, targets, 8).empty());
}
TEST_CASE("ranked mixes stay within the limit and show their slot's colour", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<ColorRGBA> filaments = { RED, BLUE, YELLOW };
const std::vector<MixTarget> targets = Gizmo::mix_targets({ gradient_layer() });
for (const int limit : { 1, 3 }) {
const std::vector<Entry> ranked = Gizmo::rank_mixes(filaments, targets, limit);
CHECK(int(ranked.size()) <= limit);
CHECK_FALSE(ranked.empty());
}
const std::vector<Entry> ranked = Gizmo::rank_mixes(filaments, targets, 6);
for (const Entry &e : ranked) {
REQUIRE(e.is_mix());
const Vec3f expected = slot_color(filaments[size_t(e.a)], filaments[size_t(e.b)], e.a_percent());
CHECK_THAT(e.rgb.x(), WithinAbs(expected.x(), 1e-6));
CHECK_THAT(e.rgb.y(), WithinAbs(expected.y(), 1e-6));
CHECK_THAT(e.rgb.z(), WithinAbs(expected.z(), 1e-6));
}
}
TEST_CASE("picked mixes never need more slots than the project has free", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<Entry> ranking = { { Vec3f::Zero(), 0, 1, 1, 2 }, { Vec3f::Zero(), 0, 1, 1, 3 }, { Vec3f::Zero(), 0, 1, 2, 3 } };
const auto reusable_second = [](const Entry &e) { return e.num == 1 && e.den == 3; };
SECTION("The count caps the pick") {
REQUIRE(Gizmo::pick_mixes(ranking, 2, 10, nullptr).size() == 2);
}
SECTION("With no free slot only a mix that already has one is kept") {
const std::vector<Entry> picked = Gizmo::pick_mixes(ranking, 3, 0, reusable_second);
REQUIRE(picked.size() == 1);
CHECK(picked.front().den == 3);
CHECK(picked.front().num == 1);
}
SECTION("A reusable mix costs no free slot") {
const std::vector<Entry> picked = Gizmo::pick_mixes(ranking, 3, 1, reusable_second);
REQUIRE(picked.size() == 2);
CHECK(picked[0].den == 2);
CHECK(picked[1].den == 3);
}
}
TEST_CASE("a baked mix paints the slot it was given, wherever that slot sits", "[TextureColorPalette][TextureDisplacement]")
{
// Two filaments, then two mixes of them. Palette index 2 is a mix, but its slot need not be
// filament 2: a project that already holds other mixed slots puts it further along.
const std::vector<Entry> palette = Gizmo::make_palette({ BLACK, WHITE }, { { Vec3f::Zero(), 0, 1, 1, 2 }, { Vec3f::Zero(), 0, 1, 1, 3 } });
REQUIRE(palette.size() == 4);
// Filament 0, the 1:1 mix twice, nothing: the 1:2 mix is never used.
const std::vector<uint8_t> triangle_color = { 1, 3, 3, 0 };
std::vector<Entry> asked;
const auto slot_seven = [&asked](const Entry &e) {
asked.push_back(e);
return 7;
};
const std::vector<int> filament = Gizmo::palette_filaments(palette, triangle_color, slot_seven);
REQUIRE(filament.size() == 4);
CHECK(filament[0] == 0);
CHECK(filament[1] == 1);
CHECK(filament[2] == 7);
CHECK(filament[3] == -1);
// Asking creates a slot, so an unused mix is never asked for.
REQUIRE(asked.size() == 1);
CHECK(asked.front().den == 2);
SECTION("A mix that gets no slot prints in its dominant component") {
const std::vector<int> fallback = Gizmo::palette_filaments(palette, { 3, 4 }, [](const Entry &) { return -1; });
CHECK(fallback[2] == 0); // 1:1 - the first component
CHECK(fallback[3] == 1); // 1 part black in 3 - white dominates
}
}
TEST_CASE("the model's colour paint is drawn by filament, and only where no layer paint covers it", "[TextureColorPalette][TextureDisplacement]")
{
// A strip of four triangles: filament 2 on the first and last, filament 5 on the second, and the third
// left to the volume's own filament.
indexed_triangle_set strip;
strip.vertices = { Vec3f(0, 0, 0), Vec3f(1, 0, 0), Vec3f(0, 1, 0), Vec3f(1, 1, 0), Vec3f(0, 2, 0), Vec3f(1, 2, 0) };
strip.indices = { stl_triangle_vertex_indices(0, 1, 2), stl_triangle_vertex_indices(1, 3, 2), stl_triangle_vertex_indices(2, 3, 4),
stl_triangle_vertex_indices(3, 5, 4) };
const TriangleMesh mesh(strip);
TriangleSelector paint(mesh);
paint.set_facet(0, EnforcerBlockerType(2));
paint.set_facet(1, EnforcerBlockerType(5));
paint.set_facet(3, EnforcerBlockerType(2));
const Gizmo::PaintedColors colors = Gizmo::painted_colors(mesh, paint.serialize());
// Grouped by filament; the triangle in the volume's own filament is never drawn.
REQUIRE(colors.facets.indices.size() == 3);
REQUIRE(colors.source.size() == 3);
REQUIRE(colors.state.size() == 3);
CHECK(colors.state == std::vector<int>{ 2, 2, 5 });
CHECK(colors.source == std::vector<int>{ 0, 3, 1 });
SECTION("A model triangle a layer's paint covers is left to the preview") {
std::vector<bool> excluded(mesh.its.indices.size(), false);
excluded[3] = true;
const std::vector<size_t> kept = colors.outside(excluded);
REQUIRE(kept.size() == 2);
CHECK(colors.source[kept[0]] == 0);
CHECK(colors.source[kept[1]] == 1);
}
SECTION("A mask shorter than the model leaves the rest drawn") {
CHECK(colors.outside({ true }).size() == 2);
}
}