mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-07 07:41:06 +00:00
Mix filament colours in the slicer instead of in the paint mask
Each mix in the palette now becomes a mixed filament slot, and the mask names that slot. The slicer alternates its components from one print layer to the next, so the blend is as fine as the layer height. Painting the interleave into the mesh could never be finer than the triangles, which is why color_band_mm() had to widen a band to two triangle rows and why lowering the layer height did not make the bands thinner. That path is gone: ColorMixMode, make_mix_resolver(), ColorResolveFn and the shaders' own interleave go with it. Sidebar::ensure_mixed_filament() creates or finds a slot, so a feature can ask for a blend without the modal dialog. A paint mask names at most ExtruderMax states, so the palette is capped to fit beside the physical filaments; a mix with no slot left falls back to its dominant component. The palette is built from physical filaments only - the slots it creates are extruders too, and feeding them back in produced mixes of mixes with components no blend can name. Preview colouring uses the full extruder list instead, since the bake writes slot indices; grouping against the shorter list dropped those triangles out of the mesh. Also lowers PREFER_PURE_DE from 10 to 2: ten Delta E is not a visible step but a different colour, and it turned most lookups that wanted a mix back into a plain filament.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M5.5,14.5c-1.105,0-2-3.686-2-7s.895-7,2-7" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><line x1="8.67" y1="4.67" x2="14.33" y2="10.33" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round"/><line x1="14.33" y1="4.67" x2="8.67" y2="10.33" style="fill:none;stroke:#949494;stroke-linecap:square;stroke-linejoin:round"/><path d="M3.5,13.7c-.294.511-.636.8-1,.8-1.1,0-2-3.686-2-7s.9-7,2-7c.365,0,.707.293,1,.805" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M6.727,4.5c.334-2.208,1-4,1.773-4,.354,0,.686.378.974,1" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M9.474,13.5c-.288.622-.62,1-.974,1-.77,0-1.439-1.792-1.773-4" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>
|
||||
|
After Width: | Height: | Size: 931 B |
@@ -35,13 +35,8 @@ uniform bool pure_only; // match against single filaments only (flat-c
|
||||
// per triangle on the CPU, so the preview shows the pattern the bake will print.
|
||||
uniform int palette_a[64];
|
||||
uniform int palette_b[64];
|
||||
uniform int palette_num[64];
|
||||
uniform int palette_den[64];
|
||||
uniform vec3 filament_rgb[16];
|
||||
uniform int filament_count;
|
||||
uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
|
||||
uniform float layer_height; // mm; one Z band per print layer
|
||||
uniform float dither_cell; // mm; one XY dither cell
|
||||
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;
|
||||
@@ -229,63 +224,16 @@ int nearest_palette_entry(vec3 rgb)
|
||||
}
|
||||
|
||||
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
|
||||
float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
|
||||
|
||||
// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
|
||||
// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
|
||||
// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
|
||||
// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
|
||||
// integer % (not available on every GLSL 1.10 target).
|
||||
vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
|
||||
// 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];
|
||||
int b = palette_b[index];
|
||||
if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
|
||||
if (a < 0 || a >= filament_count)
|
||||
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
|
||||
if (a == b)
|
||||
return filament_rgb[a];
|
||||
float num = float(palette_num[index]);
|
||||
float den = float(palette_den[index]);
|
||||
// Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
|
||||
if (mix_mode == 2 && abs(normal.z) >= 0.7)
|
||||
return filament_rgb[(num * 2.0 >= den) ? a : b];
|
||||
|
||||
// Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
|
||||
// blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
|
||||
// every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
|
||||
// millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
|
||||
// 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
|
||||
// so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
|
||||
// pattern is finer than this view can resolve, show what the print looks like from here, which is
|
||||
// the entry's perceptual average. The Normal view remains where the per-facet truth lives.
|
||||
float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
|
||||
float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
|
||||
float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
|
||||
if (sharp <= 0.0)
|
||||
return palette_rgb[index];
|
||||
|
||||
vec3 picked;
|
||||
if (mix_mode == 1) {
|
||||
// Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
|
||||
// 0 8 2 10
|
||||
// 12 4 14 6
|
||||
// 3 11 1 9
|
||||
// 15 7 13 5
|
||||
// is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
|
||||
// no constant arrays).
|
||||
float cell = max(dither_cell, 0.01);
|
||||
float gx = mod(floor(pos.x / cell), 4.0);
|
||||
float gy = mod(floor(pos.y / cell), 4.0);
|
||||
float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
|
||||
picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
|
||||
} else {
|
||||
// Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
|
||||
// operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
|
||||
float slot = floor(pos.z / max(layer_height, 0.01));
|
||||
float phase = mod(slot, den);
|
||||
picked = filament_rgb[(phase < num - 0.5) ? a : b];
|
||||
}
|
||||
return mix(palette_rgb[index], picked, sharp);
|
||||
return filament_rgb[a];
|
||||
}
|
||||
|
||||
void main()
|
||||
@@ -296,9 +244,6 @@ void main()
|
||||
// World millimetres throughout, like the bake - see the 140 variant.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
// World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
|
||||
// albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
|
||||
vec3 pos_fwidth = fwidth(world_pos.xyz);
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -420,6 +365,6 @@ void main()
|
||||
// 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), tex_pos, triangle_normal, pos_fwidth);
|
||||
albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
|
||||
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -94,13 +94,8 @@ uniform bool pure_only; // match against single filaments only (flat-c
|
||||
// per triangle on the CPU, so the preview shows the pattern the bake will print.
|
||||
uniform int palette_a[64];
|
||||
uniform int palette_b[64];
|
||||
uniform int palette_num[64];
|
||||
uniform int palette_den[64];
|
||||
uniform vec3 filament_rgb[16];
|
||||
uniform int filament_count;
|
||||
uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
|
||||
uniform float layer_height; // mm; one Z band per print layer
|
||||
uniform float dither_cell; // mm; one XY dither cell
|
||||
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;
|
||||
@@ -295,63 +290,16 @@ int nearest_palette_entry(vec3 rgb)
|
||||
}
|
||||
|
||||
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
|
||||
float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
|
||||
|
||||
// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
|
||||
// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
|
||||
// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
|
||||
// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
|
||||
// integer % (not available on every GLSL 1.10 target).
|
||||
vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
|
||||
// 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];
|
||||
int b = palette_b[index];
|
||||
if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
|
||||
if (a < 0 || a >= filament_count)
|
||||
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
|
||||
if (a == b)
|
||||
return filament_rgb[a];
|
||||
float num = float(palette_num[index]);
|
||||
float den = float(palette_den[index]);
|
||||
// Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
|
||||
if (mix_mode == 2 && abs(normal.z) >= 0.7)
|
||||
return filament_rgb[(num * 2.0 >= den) ? a : b];
|
||||
|
||||
// Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
|
||||
// blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
|
||||
// every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
|
||||
// millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
|
||||
// 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
|
||||
// so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
|
||||
// pattern is finer than this view can resolve, show what the print looks like from here, which is
|
||||
// the entry's perceptual average. The Normal view remains where the per-facet truth lives.
|
||||
float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
|
||||
float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
|
||||
float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
|
||||
if (sharp <= 0.0)
|
||||
return palette_rgb[index];
|
||||
|
||||
vec3 picked;
|
||||
if (mix_mode == 1) {
|
||||
// Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
|
||||
// 0 8 2 10
|
||||
// 12 4 14 6
|
||||
// 3 11 1 9
|
||||
// 15 7 13 5
|
||||
// is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
|
||||
// no constant arrays).
|
||||
float cell = max(dither_cell, 0.01);
|
||||
float gx = mod(floor(pos.x / cell), 4.0);
|
||||
float gy = mod(floor(pos.y / cell), 4.0);
|
||||
float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
|
||||
picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
|
||||
} else {
|
||||
// Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
|
||||
// operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
|
||||
float slot = floor(pos.z / max(layer_height, 0.01));
|
||||
float phase = mod(slot, den);
|
||||
picked = filament_rgb[(phase < num - 0.5) ? a : b];
|
||||
}
|
||||
return mix(palette_rgb[index], picked, sharp);
|
||||
return filament_rgb[a];
|
||||
}
|
||||
|
||||
void main()
|
||||
@@ -364,9 +312,6 @@ void main()
|
||||
// world position and perturb the world normal.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
// World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
|
||||
// albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
|
||||
vec3 pos_fwidth = fwidth(world_pos.xyz);
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -508,6 +453,6 @@ void main()
|
||||
// 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), tex_pos, triangle_normal, pos_fwidth);
|
||||
albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
|
||||
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -2283,12 +2283,11 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
//
|
||||
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
|
||||
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
|
||||
// colour changes - which is exactly what ColorResolveFn's own contract says the interleaving may
|
||||
// never be fed into. Handing it the resolved filament made every Z band boundary a crease: on an
|
||||
// upright wall that is one crease per band, so the collapse ran along those lines and left a stack
|
||||
// of horizontal slivers, each printing in a single filament. Those were the horizontal colour
|
||||
// lines in the baked result, and they also spent the triangle budget drawing a pattern the eye is
|
||||
// meant to blend away. Faces the paint excludes are skipped by the pipeline itself.
|
||||
// colour changes. A mix is one perceived colour however its components are laid down, which is why
|
||||
// it has to be the palette index here: back when this was handed a per-triangle interleave instead,
|
||||
// every band boundary read as a crease, the collapse ran along those lines and left a stack of
|
||||
// horizontal slivers, and the triangle budget went on drawing a pattern the eye is meant to blend
|
||||
// away. Faces the paint excludes are skipped by the pipeline itself.
|
||||
const TextureBake::ColorSampleFn color_sample =
|
||||
color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) {
|
||||
return color_sampler(p, n);
|
||||
@@ -2373,7 +2372,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
Vec3f normal = (b - a).cross(c - a);
|
||||
const float nl = normal.norm();
|
||||
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
|
||||
const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i];
|
||||
const int filament = palette[i];
|
||||
if (filament >= 0)
|
||||
out_color[i] = uint8_t(std::min(filament + 1, 255));
|
||||
}
|
||||
@@ -2830,8 +2829,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
Vec3f normal = (b - a).cross(c - a);
|
||||
const float nl = normal.norm();
|
||||
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
|
||||
const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal)
|
||||
: triangle_palette[i];
|
||||
const int filament = triangle_palette[i];
|
||||
if (filament >= 0)
|
||||
out_color[i] = uint8_t(std::min(filament + 1, 255));
|
||||
}
|
||||
|
||||
@@ -322,25 +322,6 @@ struct TextureDisplacementLayer
|
||||
}
|
||||
};
|
||||
|
||||
// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real
|
||||
// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by
|
||||
// interleaving two of them finely enough that the eye does the blending.
|
||||
enum class ColorMixMode : int
|
||||
{
|
||||
// Horizontal bands: which of the two filaments a point takes depends on its height, so
|
||||
// consecutive print layers alternate. This is how filament-blend prints actually work, and on a
|
||||
// vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole
|
||||
// layer is one band, so the blend disappears - that is what XYDither is for.
|
||||
ZBands = 0,
|
||||
// An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer
|
||||
// height, but its cell is around the size of one facet, so a fine mix can read as texture rather
|
||||
// than as a clean blend.
|
||||
XYDither = 1,
|
||||
// Per triangle, by its orientation: bands where the surface is upright enough for consecutive
|
||||
// layers to alternate, the checkerboard where it faces up or down and a layer would be one band.
|
||||
// The default - a flat-topped part with a mix on top gets no blend at all from bands alone.
|
||||
Auto = 2,
|
||||
};
|
||||
|
||||
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||
@@ -407,7 +388,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;
|
||||
ColorMixMode color_mix_mode = ColorMixMode::Auto;
|
||||
// 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.
|
||||
@@ -415,11 +395,9 @@ struct TextureDisplacementOptions
|
||||
|
||||
template<class Archive> void serialize(Archive &ar)
|
||||
{
|
||||
int mix_mode = int(color_mix_mode);
|
||||
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, mix_mode, color_despeckle);
|
||||
color_mix_mode = ColorMixMode(mix_mode);
|
||||
v2_relocate, color_mix_enabled, color_despeckle);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -512,17 +490,9 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
// is. See GLGizmoTextureDisplacement::make_palette_quantizer().
|
||||
using ColorQuantizeFn = std::function<int(const Vec3f &)>;
|
||||
|
||||
// Resolves a palette index plus a surface position to the filament index that position should print
|
||||
// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode.
|
||||
//
|
||||
// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour
|
||||
// criterion: that criterion asks where the **perceived** colour changes, and must not see the
|
||||
// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget
|
||||
// drawing a pattern the eye is supposed to blend away.
|
||||
using ColorResolveFn = std::function<int(int palette_index, const Vec3f &pos, const Vec3f &normal)>;
|
||||
|
||||
// One printable colour: either a loaded filament on its own, or a blend of two of them realised by
|
||||
// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job.
|
||||
// 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 perceptual average of the two
|
||||
@@ -539,9 +509,6 @@ struct TextureColorSettings
|
||||
{
|
||||
std::vector<PrintableColor> palette;
|
||||
std::vector<PrintableColor> palette_pure; // the filaments alone, for flat-colour images
|
||||
ColorMixMode mix_mode = ColorMixMode::ZBands;
|
||||
float layer_height = 0.2f; // sizes the Z bands
|
||||
float dither_cell_mm = 0.4f; // sizes the XY dither cells
|
||||
int despeckle_passes = 2;
|
||||
|
||||
bool empty() const { return palette.empty(); }
|
||||
@@ -781,9 +748,6 @@ struct TextureColorRequest
|
||||
// made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo
|
||||
// prints in single filaments while a photograph on another layer may still use mixes.
|
||||
ColorQuantizeFn quantize_pure;
|
||||
// Palette index + position -> filament. Optional: without it a palette index is taken to be a
|
||||
// filament index directly, which is the no-mixing case.
|
||||
ColorResolveFn resolve;
|
||||
// Majority-filter passes over the *perceived* colour, before any interleaving is resolved.
|
||||
//
|
||||
// Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own
|
||||
|
||||
@@ -1462,11 +1462,8 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
|
||||
// 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);
|
||||
shader->set_uniform(("palette_num" + idx).c_str(), e.num);
|
||||
shader->set_uniform(("palette_den" + idx).c_str(), e.den);
|
||||
}
|
||||
// The filaments those indices refer to, and the interleave the shader resolves a mix with - the
|
||||
// same inputs make_mix_resolver() gets, so the preview shows the pattern that prints rather than
|
||||
// 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;
|
||||
@@ -1475,9 +1472,6 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
|
||||
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()));
|
||||
}
|
||||
shader->set_uniform("mix_mode", int(mv->texture_displacement_options.color_mix_mode));
|
||||
shader->set_uniform("layer_height", color_band_mm(*mv)); // as color_settings_for()
|
||||
shader->set_uniform("dither_cell", std::max(m_subdivide_color_mm, 0.05f) * 2.f); // as color_settings_for()
|
||||
if (color_tex != nullptr) {
|
||||
shader->set_uniform("color_tex", 1);
|
||||
glsafe(::glActiveTexture(GL_TEXTURE1));
|
||||
@@ -2022,7 +2016,13 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
// 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.
|
||||
const std::vector<ColorRGBA> filaments = m_palette_filaments;
|
||||
// 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();
|
||||
@@ -4219,19 +4219,44 @@ bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
|
||||
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, /* mixing */ false);
|
||||
out.mix_mode = mv.texture_displacement_options.color_mix_mode;
|
||||
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;
|
||||
out.layer_height = color_band_mm(mv);
|
||||
// The dither cell is tied to the colour-detail target: a cell much smaller than a facet cannot be
|
||||
// drawn at all, and one much larger stops reading as a blend and starts reading as a check.
|
||||
out.dither_cell_mm = std::max(m_subdivide_color_mm, 0.05f) * 2.f;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -4244,10 +4269,15 @@ const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDispl
|
||||
const ModelVolume *mv = texture_volume();
|
||||
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
|
||||
std::vector<ColorRGBA> filaments = filament_palette();
|
||||
if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) {
|
||||
// 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_mixing = mixing;
|
||||
m_palette_cache = make_palette(m_palette_filaments, 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;
|
||||
@@ -4255,40 +4285,29 @@ const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDispl
|
||||
|
||||
std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
|
||||
{
|
||||
std::vector<ColorRGBA> palette = wxGetApp().plater()->get_extruders_colors();
|
||||
// mmu_segmentation_facets encodes the filament in a 6-bit prefix code and stops at Extruder16.
|
||||
std::vector<ColorRGBA> all = wxGetApp().plater()->get_extruders_colors();
|
||||
|
||||
// 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());
|
||||
for (size_t i = 0; i < all.size(); ++i)
|
||||
if (is_mixed == nullptr || i >= is_mixed->values.size() || !is_mixed->values[i])
|
||||
palette.push_back(all[i]);
|
||||
|
||||
// A paint mask can only name so many states, and every mix spends one beside these.
|
||||
if (palette.size() > size_t(EnforcerBlockerType::ExtruderMax))
|
||||
palette.resize(size_t(EnforcerBlockerType::ExtruderMax));
|
||||
return palette;
|
||||
}
|
||||
|
||||
float GLGizmoTextureDisplacement::color_band_mm(const ModelVolume &mv)
|
||||
{
|
||||
const float lh = print_layer_height();
|
||||
const float edge = (mv.texture_displacement_options.v2_refine_mm > 0.f) ? mv.texture_displacement_options.v2_refine_mm
|
||||
: v2_recommendation(mv).edge_mm;
|
||||
if (edge <= 0.f || lh <= 0.f)
|
||||
return lh;
|
||||
// A refined triangle of edge e stacks in rows about 0.87 * e apart (an equilateral triangle's
|
||||
// height), and a dither needs at least two rows per period to be a dither at all.
|
||||
constexpr float ROW_PER_EDGE = 0.87f;
|
||||
return lh * std::max(1.f, std::ceil(2.f * ROW_PER_EDGE * edge / lh));
|
||||
}
|
||||
|
||||
float GLGizmoTextureDisplacement::print_layer_height()
|
||||
{
|
||||
try {
|
||||
const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
|
||||
if (const ConfigOptionFloat *opt = cfg.option<ConfigOptionFloat>("layer_height"); opt != nullptr)
|
||||
if (opt->value > 1e-3)
|
||||
return float(opt->value);
|
||||
} catch (...) {
|
||||
}
|
||||
return 0.2f;
|
||||
}
|
||||
|
||||
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::make_palette(
|
||||
const std::vector<ColorRGBA> &filaments, bool mixing)
|
||||
const std::vector<ColorRGBA> &filaments, bool mixing, int max_entries)
|
||||
{
|
||||
std::vector<PaletteEntry> out;
|
||||
const int n = int(filaments.size());
|
||||
@@ -4305,7 +4324,7 @@ std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement
|
||||
const int pairs = n * (n - 1) / 2;
|
||||
int steps = 0;
|
||||
for (int s = 5; s >= 1; --s)
|
||||
if (n + pairs * s <= PALETTE_MAX_ENTRIES) {
|
||||
if (n + pairs * s <= max_entries) {
|
||||
steps = s;
|
||||
break;
|
||||
}
|
||||
@@ -4327,51 +4346,6 @@ std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement
|
||||
return out;
|
||||
}
|
||||
|
||||
ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector<PaletteEntry> &palette,
|
||||
ColorMixMode mode, float layer_height,
|
||||
float cell_mm)
|
||||
{
|
||||
if (palette.empty())
|
||||
return nullptr;
|
||||
auto entries = std::make_shared<std::vector<PaletteEntry>>(palette);
|
||||
const float band = std::max(layer_height, 0.01f);
|
||||
const float cell = std::max(cell_mm, 0.01f);
|
||||
|
||||
return [entries, mode, band, cell](int index, const Vec3f &pos, const Vec3f &normal) -> int {
|
||||
if (index < 0 || size_t(index) >= entries->size())
|
||||
return -1;
|
||||
const PaletteEntry &e = (*entries)[size_t(index)];
|
||||
if (!e.is_mix())
|
||||
return e.a;
|
||||
|
||||
// Which of the two filaments this point falls on. Both patterns are *ordered*, never random:
|
||||
// the eye blends a regular pattern into a flat colour, and turns a random one into noise.
|
||||
// Auto: bands wherever the surface is steeper than ~45 degrees - consecutive layers alternate
|
||||
// there, which is how a blend prints and reads. On a flat-facing surface a layer is one band
|
||||
// and the only way to interleave is a checkerboard across the surface, which at print scale
|
||||
// reads as a pattern rather than a colour; there the mix falls back to its dominant filament.
|
||||
const bool upright = std::abs(normal.z()) < 0.7f;
|
||||
if (mode == ColorMixMode::Auto && !upright)
|
||||
return e.num * 2 >= e.den ? e.a : e.b;
|
||||
const bool bands = mode == ColorMixMode::ZBands || mode == ColorMixMode::Auto;
|
||||
if (bands) {
|
||||
// One band per print layer. floorf, not a cast, so this stays correct below z = 0.
|
||||
const int slot = int(std::floor(pos.z() / band));
|
||||
const int phase = ((slot % e.den) + e.den) % e.den;
|
||||
return phase < e.num ? e.a : e.b;
|
||||
}
|
||||
// Ordered 4x4 Bayer over the surface, indexed by position so the pattern is stable in space
|
||||
// rather than in triangle order (which would move under any remesh, and read as noise).
|
||||
static const int BAYER[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
|
||||
const int gx = ((int(std::floor(pos.x() / cell)) % 4) + 4) % 4;
|
||||
const int gy = ((int(std::floor(pos.y() / cell)) % 4) + 4) % 4;
|
||||
// A third axis would be ideal, but the two dominant ones are enough for a surface pattern and
|
||||
// keep the cell square on the faces that matter.
|
||||
const float threshold = (float(BAYER[gy * 4 + gx]) + 0.5f) / 16.f;
|
||||
return (float(e.num) / float(e.den)) > threshold ? e.a : e.b;
|
||||
};
|
||||
}
|
||||
|
||||
ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::vector<PaletteEntry> &palette)
|
||||
{
|
||||
if (palette.empty())
|
||||
@@ -4408,10 +4382,15 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
|
||||
best_pure = int(i);
|
||||
}
|
||||
}
|
||||
// A mix is an interleave that only reads as its colour from a distance; up close
|
||||
// it is stripes. Spend it only where it buys a clearly better match than the nearest
|
||||
// single filament: ten Delta E is a visible step, less is not worth the stripes.
|
||||
constexpr float PREFER_PURE_DE = 10.f;
|
||||
// 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);
|
||||
@@ -4511,7 +4490,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
|
||||
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 (see ColorResolveFn).
|
||||
// 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.
|
||||
@@ -6069,24 +6048,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 colours "
|
||||
"prints the same either way. Off uses one filament per area."));
|
||||
if (opts.color_mix_enabled) {
|
||||
slider_label(_L("Mix by"));
|
||||
const std::string mix_z = _u8L("Layers");
|
||||
const std::string mix_xy = _u8L("Surface");
|
||||
const std::string mix_auto = _u8L("Automatic");
|
||||
const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str(), mix_auto.c_str() };
|
||||
int mix_mode = int(opts.color_mix_mode);
|
||||
ImGui::SetNextItemWidth(-card_pad);
|
||||
if (scoped_combo("##color_mix_mode", &mix_mode, mix_items, IM_ARRAYSIZE(mix_items))) {
|
||||
opts.color_mix_mode = ColorMixMode(mix_mode);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
hover_tip(_u8L("Layers: the two filaments alternate between print layers, which "
|
||||
"blends smoothly on upright surfaces but disappears on flat-facing "
|
||||
"ones, where a whole layer is a single band.\n"
|
||||
"Surface: a fine checkerboard across the surface, which works at "
|
||||
"any angle but can read as texture rather than as a blend.\n"
|
||||
"Automatic: layers on upright faces; flat-facing faces take the nearer "
|
||||
"single filament, since a checkerboard there shows as a pattern."));
|
||||
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colours from %2% filaments"),
|
||||
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
|
||||
}
|
||||
|
||||
@@ -81,7 +81,11 @@ public:
|
||||
// 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.
|
||||
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing);
|
||||
// `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
|
||||
@@ -95,9 +99,6 @@ public:
|
||||
|
||||
// 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
|
||||
// cells. See ColorResolveFn for why this is separate from the quantizer.
|
||||
static ColorResolveFn make_mix_resolver(const std::vector<PaletteEntry> &palette, ColorMixMode mode,
|
||||
float layer_height, float cell_mm);
|
||||
|
||||
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
|
||||
// height, despeckle. Empty when no layer is actually colouring.
|
||||
@@ -106,16 +107,20 @@ public:
|
||||
// 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();
|
||||
// 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 filaments, clamped to the sixteen mmu_segmentation_facets can address.
|
||||
static std::vector<ColorRGBA> filament_palette();
|
||||
// The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be
|
||||
// read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything.
|
||||
static float print_layer_height();
|
||||
// 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,
|
||||
@@ -123,7 +128,6 @@ public:
|
||||
// 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.
|
||||
float color_band_mm(const ModelVolume &mv);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -51,9 +51,6 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
|
||||
if (!m_input.color.palette_pure.empty())
|
||||
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
|
||||
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
|
||||
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
|
||||
m_input.color.dither_cell_mm);
|
||||
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
||||
color_request.out_triangle = &m_triangle_color;
|
||||
if (color_request.quantize)
|
||||
|
||||
@@ -33,9 +33,6 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
|
||||
if (!m_input.color.palette_pure.empty())
|
||||
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
|
||||
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
|
||||
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
|
||||
m_input.color.dither_cell_mm);
|
||||
color_request.despeckle_passes = m_input.color.despeckle_passes;
|
||||
color_request.out_triangle = &m_result.triangle_color;
|
||||
if (color_request.quantize)
|
||||
|
||||
@@ -938,6 +938,7 @@ struct Sidebar::priv
|
||||
wxStaticText* m_text_mixed_title{nullptr};
|
||||
ScalableButton* m_btn_mixed_add{nullptr};
|
||||
ScalableButton* m_btn_mixed_del{nullptr};
|
||||
ScalableButton* m_btn_mixed_del_all{nullptr};
|
||||
wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows
|
||||
wxPanel* m_panel_mixed_content{nullptr};
|
||||
wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments
|
||||
@@ -3451,6 +3452,11 @@ Sidebar::Sidebar(Plater *parent)
|
||||
});
|
||||
title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing()));
|
||||
|
||||
p->m_btn_mixed_del_all = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_all_filaments");
|
||||
p->m_btn_mixed_del_all->SetToolTip(_L("Remove all mixed filaments"));
|
||||
p->m_btn_mixed_del_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { remove_all_mixed_filaments(); });
|
||||
title_sizer->Add(p->m_btn_mixed_del_all, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing()));
|
||||
|
||||
p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament");
|
||||
p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament"));
|
||||
p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); });
|
||||
@@ -5120,6 +5126,58 @@ static bool create_mixed_filament_from_result(
|
||||
return true;
|
||||
}
|
||||
|
||||
int Sidebar::ensure_mixed_filament(const std::vector<unsigned int> &components, const std::vector<int> &ratios)
|
||||
{
|
||||
if (components.size() < 2 || components.size() != ratios.size())
|
||||
return -1;
|
||||
if (p->combos_filament.size() < 2)
|
||||
return -1;
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
std::vector<std::string> color_strs, names, types;
|
||||
collect_physical_filament_info(color_strs, names, types);
|
||||
|
||||
MixedFilamentResult result;
|
||||
result.components = components;
|
||||
result.ratios = ratios;
|
||||
const size_t created_at = wxGetApp().preset_bundle->filament_presets.size();
|
||||
if (!create_mixed_filament_from_result(this, result, color_strs))
|
||||
return -1;
|
||||
return int(created_at);
|
||||
}
|
||||
|
||||
void Sidebar::add_mixed_filament()
|
||||
{
|
||||
auto* plater = dynamic_cast<Plater*>(GetParent());
|
||||
@@ -5297,6 +5355,28 @@ void Sidebar::edit_mixed_filament(size_t panel_idx)
|
||||
}
|
||||
}
|
||||
|
||||
void Sidebar::remove_all_mixed_filaments()
|
||||
{
|
||||
auto *plater = dynamic_cast<Plater *>(GetParent());
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
const size_t count = plater->mixed_filament_config_indices().size();
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
// Worth a confirmation: this drops filament slots the model may be painted with, and anything
|
||||
// painted in one falls back to a plain filament.
|
||||
MessageDialog dlg(this, format_wxstr(_L("Remove all %1% mixed filaments?"), count), _L("Mixed Filament"),
|
||||
wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return;
|
||||
|
||||
// Back to front: delete_mixed_filament_at() indexes the list as it stands, so removing from the end
|
||||
// leaves the indices of everything still to go untouched.
|
||||
for (size_t i = count; i-- > 0;)
|
||||
delete_mixed_filament_at(i);
|
||||
}
|
||||
|
||||
void Sidebar::delete_mixed_filament_at(size_t panel_idx)
|
||||
{
|
||||
auto* plater = dynamic_cast<Plater*>(GetParent());
|
||||
|
||||
@@ -291,8 +291,19 @@ public:
|
||||
|
||||
// Mixed-color filament sidebar section
|
||||
void add_mixed_filament();
|
||||
// The filament slot that blends `components` (1-based physical filament indices) in `ratios`
|
||||
// (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:
|
||||
// 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);
|
||||
void delete_mixed_filament_at(size_t idx);
|
||||
// Drops every mixed filament at once, after confirming. The texture displacement gizmo can create
|
||||
// one slot per colour in its palette, so clearing them one at a time is tedious.
|
||||
void remove_all_mixed_filaments();
|
||||
void decompose_filament_color(int filament_idx);
|
||||
void recalc_filament_scroll_sizes();
|
||||
void update_mixed_filament_list();
|
||||
|
||||
Reference in New Issue
Block a user