Compare commits

..
Author SHA1 Message Date
Hanif Koh 057d21bb53 Remove using namespace std from json_diff.hpp
The directive sat at global scope in a header that DeviceManager.hpp
includes, so most of the GUI compiled with all of std in the global
namespace. 42 files had come to rely on it, mostly for string, vector
and unordered_map, four of them for the ""sv and ""ms literals.

Those sites are qualified. GCodeViewer.cpp spelled the type as
std::vector<::string>, which only resolved through the directive. The
files that use the ""sv and ""ms literals get a file-scope
"using namespace std::string_view_literals;" or
"using namespace std::chrono_literals;", as other sources already do.
2026-10-07 14:55:09 +08:00
21 changed files with 334 additions and 330 deletions
+4 -6
View File
@@ -4,17 +4,15 @@ OrcaSlicer — open-source C++17 3D slicer. wxWidgets GUI, CMake build system.
## Build Commands
Build the Release configuration unless asked otherwise.
```bash
# macOS
cmake --build build/arm64 --config Release --target all --
cmake --build build/arm64 --config RelWithDebInfo --target all --
# Linux
cmake --build build --config Release --target all --
cmake --build build --config RelWithDebInfo --target all --
# Windows
cmake --build . --config Release --target ALL_BUILD -- -m
# Windows (replace %build_type% with Debug/Release/RelWithDebInfo)
cmake --build . --config %build_type% --target ALL_BUILD -- -m
```
## Testing
@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 931 B

@@ -29,13 +29,19 @@ 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)
// 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.
// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b,
// num parts of a in every den, and the print shows that interleave rather than the entry's average
// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does
// 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;
@@ -223,16 +229,63 @@ 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`. 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)
// 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)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
int b = palette_b[index];
if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
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);
}
void main()
@@ -243,6 +296,9 @@ 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;
@@ -364,6 +420,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));
albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -88,13 +88,19 @@ 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)
// 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.
// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b,
// num parts of a in every den, and the print shows that interleave rather than the entry's average
// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does
// 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;
@@ -289,16 +295,63 @@ 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`. 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)
// 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)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
int b = palette_b[index];
if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
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);
}
void main()
@@ -311,6 +364,9 @@ 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;
@@ -452,6 +508,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));
albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -551,13 +551,9 @@ DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool h
// Rebuild from the surviving faces, with per-face normals.
TriSoup &out = result.geometry;
if (!face_color.empty())
result.face_color.reserve(active_faces);
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
if (!face_color.empty())
result.face_color.push_back(f < face_color.size() ? face_color[f] : -1);
const Vec3f a = pos[size_t(faces[f * 3])].cast<float>();
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast<float>();
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast<float>();
@@ -46,13 +46,6 @@ using DecimateProgressFn = std::function<bool(double fraction)>;
struct DecimateResult
{
TriSoup geometry;
// One entry per output face, carried from the `face_color` handed in: a colour difference is a
// crease, so no collapse ever merges two faces of different colour and every survivor keeps exactly
// the colour it came with. Empty when no `face_color` was given.
//
// This is what lets the caller colour the simplified mesh by *provenance* rather than by sampling it
// again: the input colours were masked by the paint on the fine mesh, where that mask is exact.
std::vector<int> face_color;
// The locked faces alone met the target, so it was unreachable without touching preserved
// geometry.
bool locked_over_budget = false;
+1 -11
View File
@@ -42,7 +42,7 @@ TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &fac
return out;
}
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector<int> *face_color)
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
{
indexed_triangle_set out;
const size_t n = soup.pos.size();
@@ -54,22 +54,12 @@ indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector<in
if (map.inserted())
out.vertices.push_back(soup.pos[i]);
}
const bool track_color = face_color != nullptr && !face_color->empty();
std::vector<int> kept_color;
if (track_color)
kept_color.reserve(face_color->size());
for (size_t t = 0; t + 2 < n; t += 3) {
// Welded-together corners carry no area.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
if (track_color) {
const size_t src = t / 3;
kept_color.push_back(src < face_color->size() ? (*face_color)[src] : -1);
}
}
if (track_color)
*face_color = std::move(kept_color);
return out;
}
@@ -15,10 +15,7 @@ namespace TextureBake {
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded = {});
// Welds at the geometry grid.
// `face_color`, when given, is read as one entry per soup triangle and rewritten to match the output.
// Welding can leave a triangle with no area, and those are dropped here, so the two would otherwise
// fall out of step.
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector<int> *face_color = nullptr);
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
} // namespace TextureBake
} // namespace Slic3r
@@ -290,34 +290,6 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
return result;
}
// Colour per face, taken here and carried from here on. This is the only point where the paint mask
// is exact: `exclude_weight` says which faces the paint left out, and the mesh is still the refined
// one the displacement produced. Everything downstream (the collapse, the T-junction repair) carries
// these along rather than sampling again, and the caller uses them as they are.
//
// It also gives the collapse its crease criterion: an edge between two colours is never collapsed
// across, which is what keeps a survivor's colour well defined.
if (color_sample) {
const size_t nf = displaced.triangle_count();
result.face_color.assign(nf, -1);
const bool have_w = !displaced.exclude_weight.empty();
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
// Unpainted faces take no colour at all, which is what stops the texture appearing on
// surfaces the paint never covered.
if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
continue; // stays FACE_UNPAINTED
const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
const int sampled = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
// Painted either way. The sampler expects a point on the base surface and these are on
// the displaced one, so off the patch by more than its tolerance it simply says "no
// colour" - which must not be confused with "not painted".
result.face_color[t] = (sampled >= 0) ? sampled : FACE_NO_COLOUR;
}
});
}
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
std::vector<int> parent = std::move(sub.face_parent_id);
const size_t displaced_before_decimate = displaced.triangle_count();
@@ -351,8 +323,24 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
// unless the budget was lowered until decimation had to run. Only collapses costing less than
// harvest_tol are taken, so this does not reach the relief.
const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0;
std::vector<int> &face_color = result.face_color;
if (over_budget || harvest_only) {
// Colour per face on the fine mesh, so colour boundaries become creases the collapse
// respects. Excluded (unpainted) faces take no colour.
std::vector<int> face_color;
if (color_sample) {
const size_t nf = displaced.triangle_count();
face_color.assign(nf, -1);
const bool have_w = !displaced.exclude_weight.empty();
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
for (size_t t = r.begin(); t < r.end(); ++t) {
if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
continue;
const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
}
});
}
// Harvesting alone is asked for by handing it the count it already has: nothing is then
// over the target, so the loop only ever pops collapses under the tolerance.
const size_t before = displaced.triangle_count();
@@ -362,7 +350,6 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
result.locked_over_budget = dec.locked_over_budget;
result.budget_limited = result.simplified = dec.target_cost_detail;
displaced = std::move(dec.geometry);
face_color = std::move(dec.face_color);
lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested");
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count()
<< (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")";
@@ -390,7 +377,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
if (mode == PipelineMode::Export && parent.empty()) {
displaced = resolve_t_junctions(displaced, {}, &result.face_color);
displaced = resolve_t_junctions(displaced);
lap("repair", displaced);
}
@@ -108,27 +108,9 @@ using PipelineProgressFn = std::function<bool(const char *stage, double fraction
// colour-boundary creases. Only consulted when the mesh is over budget.
using ColorSampleFn = std::function<int(const Vec3f &centroid, const Vec3f &normal)>;
// Sentinels for PipelineResult::face_color.
static constexpr int FACE_UNPAINTED = -1; // the paint did not cover this face's origin
static constexpr int FACE_NO_COLOUR = -2; // painted, but the sampler returned nothing at this point
struct PipelineResult
{
TriSoup geometry;
// One entry per output face, carried through decimation, the T-junction repair and the weld.
// FACE_UNPAINTED means the paint never covered the geometry this face came from; anything else means
// it did, and is the palette index `color_sample` returned there (FACE_NO_COLOUR when it returned
// none). The distinction matters: the sampler answers for points on the *base* surface, and these
// are sampled on the displaced one, so a painted face can easily come back without a colour. Only
// the painted/unpainted split is reliable here, and that is what a caller should use it for.
//
// Empty unless the caller gave a `color_sample`.
//
// A caller that needs per-face colour must use this rather than sampling the result again. The
// result is displaced geometry: a point on it is no longer where its base surface was, so matching
// it back by proximity colours whatever base surface happens to be nearest - which on a part thinner
// than the relief depth is the *opposite* face, picking up the texture meant for the painted one.
std::vector<int> face_color;
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
@@ -72,8 +72,7 @@ size_t count_area_slivers(const TriSoup &geometry)
return n;
}
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
std::vector<int> *face_color)
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
{
const size_t n_tri = geometry.triangle_count();
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
@@ -99,12 +98,7 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
// the on-edge-vertex topology the pass below closes.
std::vector<std::array<int, 3>> faces;
// Parallel to `faces` throughout, so a split or a dropped degenerate keeps the two in step.
const bool track_color = face_color != nullptr && !face_color->empty();
std::vector<int> colors;
faces.reserve(n_tri);
if (track_color)
colors.reserve(n_tri);
for (size_t t = 0; t < n_tri; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
if (a == b || b == c || a == c)
@@ -114,8 +108,6 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
continue;
faces.push_back({ a, b, c });
if (track_color)
colors.push_back(t < face_color->size() ? (*face_color)[t] : -1);
}
for (int iter = 0; iter < opts.max_iters; ++iter) {
@@ -174,16 +166,11 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
break;
std::vector<std::array<int, 3>> next;
std::vector<int> next_colors;
next.reserve(faces.size() + splits.size() * 2);
if (track_color)
next_colors.reserve(next.capacity());
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto it = splits.find(fi);
if (it == splits.end()) {
next.push_back(faces[fi]);
if (track_color)
next_colors.push_back(colors[fi]);
continue;
}
const auto &f = faces[fi];
@@ -208,18 +195,11 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
seq.push_back(sp.a);
}
for (size_t s = 0; s + 1 < seq.size(); ++s) {
for (size_t s = 0; s + 1 < seq.size(); ++s)
next.push_back({ seq[s], seq[s + 1], apex });
if (track_color)
next_colors.push_back(colors[fi]); // every piece of a split face keeps its colour
}
}
faces.swap(next);
if (track_color)
colors.swap(next_colors);
}
if (track_color)
*face_color = std::move(colors);
TriSoup out;
out.pos.reserve(faces.size() * 3);
@@ -43,12 +43,7 @@ struct RepairOptions
int max_iters = 16;
};
// `face_color`, when given, is read as one entry per input face and rewritten to match the output: a
// face split to close a T-junction hands its colour to every piece, and a degenerate face dropped on
// the way takes its entry with it. Without this the caller would have no way to keep a per-face colour
// across this pass, which changes the triangle count.
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {},
std::vector<int> *face_color = nullptr);
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r
+11 -22
View File
@@ -2284,11 +2284,12 @@ 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. 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.
// 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.
const TextureBake::ColorSampleFn color_sample =
color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) {
return color_sampler(p, n);
@@ -2314,7 +2315,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
stats->triangles_budget = result.triangles_budget;
stats->budget_limited = result.budget_limited;
}
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry, &result.face_color);
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry);
if (out.indices.empty())
return mesh;
if (flip_normals)
@@ -2335,8 +2336,6 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
max_depth = std::max(max_depth, std::abs(layer.depth_mm));
const float relief_tol = max_depth + paint_tol;
std::vector<int> palette(out.indices.size(), -1);
const std::vector<int> &face_mask = result.face_color;
const bool have_face_mask = face_mask.size() == out.indices.size();
tbb::parallel_for(tbb::blocked_range<size_t>(0, out.indices.size()), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i) {
const stl_triangle_vertex_indices &t = out.indices[i];
@@ -2352,19 +2351,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
// reason; this path was the inconsistent one.
Vec3f foot = centroid, base_n = Vec3f::UnitZ();
const float d2 = painted_closest(centroid, &foot, &base_n);
// Which faces may be coloured comes from the pipeline, which recorded it on the
// refined mesh where the paint mask is exact, and carried it through the collapse,
// the T-junction repair and the weld. Proximity cannot answer this: a displaced face
// is no longer where its base was, so on a part thinner than the relief depth the
// nearest painted surface to the *opposite* face is the painted one, and the texture
// appeared there too. Only the position to sample at still comes from the base
// surface, for the projection reason above.
if (have_face_mask) {
if (face_mask[i] == TextureBake::FACE_UNPAINTED)
continue;
} else if (!all_painted && d2 >= relief_tol * relief_tol) {
if (!all_painted && d2 >= relief_tol * relief_tol)
continue;
}
palette[i] = sampler(foot, base_n);
}
});
@@ -2386,7 +2374,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 = palette[i];
const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i];
if (filament >= 0)
out_color[i] = uint8_t(std::min(filament + 1, 255));
}
@@ -2843,7 +2831,8 @@ 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 = triangle_palette[i];
const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal)
: triangle_palette[i];
if (filament >= 0)
out_color[i] = uint8_t(std::min(filament + 1, 255));
}
+38 -2
View File
@@ -321,6 +321,25 @@ 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().
@@ -387,6 +406,7 @@ 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.
@@ -394,9 +414,11 @@ 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, color_despeckle);
v2_relocate, color_mix_enabled, mix_mode, color_despeckle);
color_mix_mode = ColorMixMode(mix_mode);
}
};
@@ -489,9 +511,17 @@ 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, which the slicer does per print layer. Plain data, so it can be captured into a job.
// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
@@ -508,6 +538,9 @@ 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(); }
@@ -747,6 +780,9 @@ 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,8 +1462,11 @@ 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;
@@ -1472,6 +1475,9 @@ 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));
@@ -2016,13 +2022,7 @@ 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.
// 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();
const std::vector<ColorRGBA> filaments = m_palette_filaments;
m_preview_job_running = true;
auto &worker = wxGetApp().plater()->get_ui_job_worker();
@@ -4219,44 +4219,19 @@ 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, 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.palette_pure = make_palette(m_palette_filaments, /* mixing */ false);
out.mix_mode = mv.texture_displacement_options.color_mix_mode;
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;
}
@@ -4269,15 +4244,10 @@ 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();
// 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) {
if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) {
m_palette_filaments = std::move(filaments);
m_palette_mixing = mixing;
m_palette_cap = cap;
m_palette_cache = make_palette(m_palette_filaments, mixing, cap);
m_palette_cache = make_palette(m_palette_filaments, mixing);
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
}
return m_palette_cache;
@@ -4285,29 +4255,40 @@ const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDispl
std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
{
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.
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.
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, int max_entries)
const std::vector<ColorRGBA> &filaments, bool mixing)
{
std::vector<PaletteEntry> out;
const int n = int(filaments.size());
@@ -4324,7 +4305,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 <= max_entries) {
if (n + pairs * s <= PALETTE_MAX_ENTRIES) {
steps = s;
break;
}
@@ -4346,6 +4327,51 @@ 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())
@@ -4382,15 +4408,10 @@ 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. 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;
// 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;
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);
@@ -4490,7 +4511,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 - the slicer does that per layer.
// colour, never the interleaving that realises a mix (see ColorResolveFn).
// "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.
@@ -6048,6 +6069,24 @@ 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) {
slider_label(_L("Mix by"));
const std::string mix_z = _u8L("Layers");
const std::string mix_xy = _u8L_CONTEXT("Surface", "Texture Displacement");
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 colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
@@ -81,11 +81,7 @@ 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.
// `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);
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing);
// 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
@@ -99,6 +95,9 @@ 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.
@@ -107,20 +106,16 @@ 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,
@@ -128,6 +123,7 @@ 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
@@ -53,6 +53,9 @@ 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)
@@ -32,6 +32,9 @@ 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)
-80
View File
@@ -935,7 +935,6 @@ struct Sidebar::priv
StaticLine* 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
@@ -3450,11 +3449,6 @@ 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(); });
@@ -5128,58 +5122,6 @@ 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());
@@ -5357,28 +5299,6 @@ 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());
-11
View File
@@ -292,19 +292,8 @@ 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();
+1 -1
View File
@@ -82,7 +82,7 @@ using json = nlohmann::json;
namespace Slic3r {
namespace {
constexpr const char* ORCA_DEFAULT_API_URL = "https://api.orcaslicer.com";
constexpr const char* ORCA_DEFAULT_API_URL = "api.orcaslicer.com";
constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.orcaslicer.com";
constexpr const char* ORCA_DEFAULT_CLOUD_URL = "https://cloud.orcaslicer.com";
// Orca: This is a public key with no secret, used to identify the client application to the backend.