Compare commits

..
Author SHA1 Message Date
Ian Chua 15aa9fe6c3 fix: avoid substring denies in audit path keywords 2026-10-07 15:11:55 +08:00
58 changed files with 467 additions and 442 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
+2 -2
View File
@@ -974,8 +974,8 @@ bool AMSMaterialsSetting::Show(bool show)
static void _collect_filament_info(const wxString& shown_name,
const Preset& filament,
std::unordered_map<wxString, wxString>& query_filament_vendors,
std::unordered_map<wxString, wxString>& query_filament_types)
unordered_map<wxString, wxString>& query_filament_vendors,
unordered_map<wxString, wxString>& query_filament_types)
{
query_filament_vendors[shown_name] = filament.config.get_filament_vendor();
query_filament_types[shown_name] = filament.config.get_filament_type();
+3 -3
View File
@@ -856,14 +856,14 @@ void AuxiliaryPanel::Split(const std::string &src, const std::string &separator,
dest.clear();
index = str.find_first_of(separator, start);
do {
if (index != std::string::npos) {
if (index != string::npos) {
substring = str.substr(start, index - start);
dest.push_back(substring);
start = index + separator.size();
index = str.find(separator, start);
if (start == std::string::npos) break;
if (start == string::npos) break;
}
} while (index != std::string::npos);
} while (index != string::npos);
// the last part
substring = str.substr(start);
@@ -1307,7 +1307,7 @@ void CalibrationPresetPage::stripWhiteSpace(std::string& str)
{
if (str == "") { return; }
std::string::iterator cur_it;
string::iterator cur_it;
cur_it = str.begin();
while (cur_it != str.end()) {
@@ -2597,7 +2597,7 @@ void CalibrationPresetPage::update_multi_extruder_filament_combobox(const std::s
int ams_id_int = 0;
try {
if (!ams_id.empty())
ams_id_int = std::stoi(ams_id.c_str());
ams_id_int = stoi(ams_id.c_str());
} catch (...) {}
@@ -2685,7 +2685,7 @@ void CalibrationPresetPage::update_filament_combobox(std::string ams_id)
int ams_id_int = 0;
try {
if (!ams_id.empty())
ams_id_int = std::stoi(ams_id.c_str());
ams_id_int = stoi(ams_id.c_str());
} catch (...) {}
+4 -4
View File
@@ -347,7 +347,7 @@ static std::string get_curr_timestmp()
// return timestampString;
}
static void get_filament_compatible_printer(Preset* preset, std::vector<std::string>& printers)
static void get_filament_compatible_printer(Preset* preset, vector<std::string>& printers)
{
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset->config.option("compatible_printers"));
if (compatible_printers == nullptr) return;
@@ -1581,7 +1581,7 @@ void CreateFilamentPresetDialog::sort_printer_by_nozzle(std::vector<std::pair<st
{
std::unordered_map<std::string, float> nozzle_diameter = nozzle_diameter_map;
std::sort(printer_name_to_filament_preset.begin(), printer_name_to_filament_preset.end(),
[&nozzle_diameter](const std::pair<std::string, T> &a, const std::pair<std::string, T> &b) {
[&nozzle_diameter](const std::pair<string, T> &a, const std::pair<string, T> &b) {
size_t nozzle_index_a = a.first.find(" nozzle");
size_t nozzle_index_b = b.first.find(" nozzle");
if (nozzle_index_a == std::string::npos || nozzle_index_b == std::string::npos) return a.first < b.first;
@@ -3143,7 +3143,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
wxArrayString CreatePrinterPresetDialog::printer_preset_sort_with_nozzle_diameter(const VendorProfile &vendor_profile, float nozzle_diameter)
{
std::vector<std::pair<float, std::string>> preset_sort;
std::vector<pair<float, std::string>> preset_sort;
auto get_nozzle_size_for_printer_model = [this](const std::string & model_name) -> size_t {
auto iter = m_printer_name_to_preset.find(model_name);
@@ -4964,7 +4964,7 @@ wxBoxSizer *CreatePresetForPrinterDialog::create_selected_filament_preset_sizer(
m_selected_printer->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) {
wxString printer_name = m_selected_printer->GetStringSelection();
std::unordered_map<std::string, std::vector<std::shared_ptr<Preset>>>::iterator filament_iter = m_printer_compatible_filament_presets.find(into_u8(printer_name));
std::unordered_map<string, std::vector<std::shared_ptr<Preset>>>::iterator filament_iter = m_printer_compatible_filament_presets.find(into_u8(printer_name));
if (m_printer_compatible_filament_presets.end() != filament_iter) {
filament_choice_to_filament_preset.clear();
wxArrayString filament_choices;
+1 -1
View File
@@ -160,7 +160,7 @@ DevAms::~DevAms()
m_trays.clear();
}
static std::unordered_map<int, wxString> s_ams_display_formats = {
static unordered_map<int, wxString> s_ams_display_formats = {
{DevAms::AMS, "AMS-%d"},
{DevAms::AMS_LITE, "AMS Lite-%d"},
{DevAms::N3F, "AMS 2 PRO-%d"},
@@ -250,7 +250,7 @@ int DevNozzleMappingCtrl::CtrlGetAutoNozzleMappingV1(Slic3r::GUI::Plater* plater
void DevNozzleMappingCtrl::ParseAutoNozzleMapping(const json& print_jj)
{
if (print_jj.contains("command") && print_jj["command"].get<std::string>() == "get_auto_nozzle_mapping") {
if (print_jj.contains("command") && print_jj["command"].get<string>() == "get_auto_nozzle_mapping") {
if (print_jj.contains("sequence_id") && print_jj["sequence_id"] == m_sequence_id) {
Clear();
DevJsonValParser::ParseVal(print_jj, "result", m_result);
@@ -426,7 +426,7 @@ void DevNozzleSystem::ClearNozzles()
// ---- parsing ----------------------------------------------------------------------------------------
static std::unordered_map<std::string, NozzleFlowType> _str2_nozzle_flow_type = {
static unordered_map<string, NozzleFlowType> _str2_nozzle_flow_type = {
{"S", NozzleFlowType::S_FLOW},
{"H", NozzleFlowType::H_FLOW},
{"A", NozzleFlowType::S_FLOW},
@@ -436,7 +436,7 @@ static std::unordered_map<std::string, NozzleFlowType> _str2_nozzle_flow_type =
{"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow
};
static std::unordered_map<std::string, NozzleType> _str2_nozzle_type = {
static unordered_map<string, NozzleType> _str2_nozzle_type = {
{"00", NozzleType::ntStainlessSteel},
{"01", NozzleType::ntHardenedSteel},
{"05", NozzleType::ntTungstenCarbide}
+1 -1
View File
@@ -368,7 +368,7 @@ wxString DeviceErrorDialog::parse_error_level(int error_code)
}
}
static const std::unordered_set<std::string> s_jump_liveview_error_codes = { "0300-8003", "0300-8002", "0300-800A"};
static const std::unordered_set<string> s_jump_liveview_error_codes = { "0300-8003", "0300-8002", "0300-800A"};
wxString DeviceErrorDialog::show_error_code(int error_code)
{
if (m_error_code == error_code) { return wxEmptyString;}
+1 -2
View File
@@ -110,7 +110,6 @@ using json = nlohmann::json;
class wxWindow;
namespace fs = boost::filesystem;
using namespace std::chrono_literals;
#define CALI_DEBUG
#define MINUTE_30 1800000 //ms
@@ -3104,7 +3103,7 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
DevFirmwareVersionInfo ver_info;
ver_info.name = (*it)["name"].get<std::string>();
if ((*it).contains("product_name"))
ver_info.product_name = wxString::FromUTF8((*it)["product_name"].get<std::string>());
ver_info.product_name = wxString::FromUTF8((*it)["product_name"].get<string>());
if ((*it).contains("sw_ver"))
ver_info.sw_ver = (*it)["sw_ver"].get<std::string>();
if ((*it).contains("sw_new_ver"))
+3 -3
View File
@@ -294,8 +294,8 @@ wxDataViewItem EditGCodeDialog::add_presets_placeholders()
// Orca: create subgroups from the pages of the tabs
auto init_from_tab = [this, full_config](wxDataViewItem parent, Tab* tab, const std::set<std::string>& preset_keys){
std::set extra_keys(preset_keys);
auto init_from_tab = [this, full_config](wxDataViewItem parent, Tab* tab, const set<string>& preset_keys){
set extra_keys(preset_keys);
for (const auto& page : tab->m_pages) {
// ORCA: Pull icons from tabs for subgroups, icons are hidden on tabs
std::string icon_name = "empty"; // use empty icon if not defined
@@ -549,7 +549,7 @@ void ParamsNode::RefreshSearch(const wxString& search_text)
if (GetEnabledChildren().empty())
if (auto pos = text.find(search_text); IsParamNode() && pos != wxString::npos) {
m_highlight_index = make_unique<std::pair<int, int>>(pos, search_text.Len());
m_highlight_index = make_unique<pair<int, int>>(pos, search_text.Len());
Enable();
} else {
Disable();
+4 -4
View File
@@ -702,11 +702,11 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
set_value(str, true);
}
} else if (m_opt.opt_key == "sparse_infill_rotate_template" || m_opt.opt_key == "solid_infill_rotate_template") {
std::string ustr(str.utf8_string());
string ustr(str.utf8_string());
if (!ConfigOptionFloats::validate_string(ustr)) {
std::string v;
string v;
std::smatch match;
std::string ps = (m_opt.opt_key == "sparse_infill_rotate_template") ?
string ps = (m_opt.opt_key == "sparse_infill_rotate_template") ?
u8"[BT][!]?|[#][\\d]+[!]?|[+\\-]?[\\d.]+[%]?[*]?[\\d]*[/NnZz$LlUuQq~^|#]?[+\\-]?[\\d.]*[%#\'\"cm]?[m]?[BT]?[!*]?" :
u8"[#][\\d]+[!]?|[+\\-]?[\\d.]+[%]?[*]?[\\d]*[/NnZz$LlUuQq~^|#]?[+\\-]?[\\d.]*[%#\'\"cm]?[m]?[!*]?";
@@ -729,7 +729,7 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
}
break;
} else if (m_opt.opt_key == "extra_solid_infills") {
std::string ustr(str.utf8_string());
string ustr(str.utf8_string());
// New rule: accept either interval form (N or N#K) or explicit list (e.g. 1,7,9), with optional quotes.
const std::regex rx_interval(u8R"(^\s*['"]?\s*\d+\s*(?:#\s*\d*)?\s*['"]?\s*$)");
// List entries may be plain numbers or number with optional #K count, e.g., 5, 9#2, 18
+1 -3
View File
@@ -94,8 +94,6 @@
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/PartPlate.hpp"
using namespace std::string_view_literals;
namespace Slic3r { class PrintBase; }
@@ -3186,7 +3184,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
float ams_item_height = 0;
float filament_group_item_align_width = 0;
{
float three_words_width = imgui.calc_text_size("ABC"sv).x;
float three_words_width = imgui.calc_text_size(std::string_view("ABC")).x;
const int line_capacity = 4;
for (const auto& extruder_filaments : {m_left_extruder_filament,m_right_extruder_filament })
-2
View File
@@ -177,8 +177,6 @@ extern wxPopupWindow* wxCurrentPopupWindow;
#endif
#endif
using namespace std::string_view_literals;
static constexpr const float TRACKBALLSIZE = 0.8f;
static Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.906f, 0.906f, 0.906f, 1.0f };
+3 -3
View File
@@ -9040,7 +9040,7 @@ std::map<std::string, std::string> GUI_App::get_delete_cache_presets_lock()
void GUI_App::process_delete_presets()
{
std::map<std::string, std::string> delete_cache_presets = get_delete_cache_presets_lock();
std::map<string, string> delete_cache_presets = get_delete_cache_presets_lock();
for (auto it = delete_cache_presets.begin(); it != delete_cache_presets.end();) {
if (it->first.empty()) continue;
std::string del_setting_id = it->first;
@@ -10068,7 +10068,7 @@ bool is_soluble_filament(int extruder_id)
return support_option->get_at(0);
};
bool has_filaments(const std::vector<std::string>& model_filaments) {
bool has_filaments(const std::vector<string>& model_filaments) {
auto &filament_presets = Slic3r::GUI::wxGetApp().preset_bundle->filament_presets;
if (!Slic3r::GUI::wxGetApp().plater()) return false;
auto model_objects = Slic3r::GUI::wxGetApp().plater()->model().objects;
@@ -10103,7 +10103,7 @@ bool is_support_filament(int extruder_id, bool strict_check)
Slic3r::ConfigOptionBools *support_option = dynamic_cast<Slic3r::ConfigOptionBools *>(filament->config.option("filament_is_support"));
if(!strict_check &&(filament_type == "PETG" || filament_type == "PLA")) {
std::vector<std::string> model_filaments;
std::vector<string> model_filaments;
if (filament_type == "PETG")
model_filaments.emplace_back("PLA");
else {
+2 -2
View File
@@ -1108,7 +1108,7 @@ void ObjectList::update_name_column_width() const
}
}
GetColumn(colName)->SetWidth(std::max(0, client_size.x - (others_width)*em));
GetColumn(colName)->SetWidth(max(0, client_size.x - (others_width)*em));
}
void ObjectList::set_filament_column_hidden(const bool hide) const
@@ -3970,7 +3970,7 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D
if (config->opt_float("layer_height") == object_cfg->opt_float("layer_height")) {
SettingsFactory::Bundle new_cat_options;
for (auto cat_opt : cat_options) {
std::vector<std::string> temp;
std::vector<string> temp;
for (auto value : cat_opt.second) {
if (value != "layer_height")
temp.push_back(value);
@@ -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
@@ -44,8 +44,6 @@
#define MAX_NUM 9999.99
#define MAX_SIZE std::string_view{"9999.99"}
using namespace std::string_view_literals;
namespace Slic3r
{
namespace GUI
+7 -7
View File
@@ -33,7 +33,7 @@ static const char* HMS_LOCAL_IMG_PATH = "hms/local_image";
// the local HMS info
// Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for
static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E"};
static unordered_set<string> package_dev_id_types {"094", "239", "093", "22E"};
// Orca: HMS should be disabled when stealth mode is on or networking is not installed
static bool should_disable_hms()
@@ -345,17 +345,17 @@ wxString HMSQuery::query_hms_msg(const std::string& dev_id, const std::string& l
return _query_hms_msg(dev_id.substr(0, 3), long_error_code, lang_code);
}
std::string HMSQuery::get_dev_id_type(const MachineObject* obj) const
string HMSQuery::get_dev_id_type(const MachineObject* obj) const
{
if (obj)
{
return obj->get_dev_id().substr(0, 3);
}
return std::string();
return string();
}
wxString HMSQuery::_query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code)
wxString HMSQuery::_query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code)
{
if (long_error_code.empty())
{
@@ -426,9 +426,9 @@ wxString HMSQuery::_query_hms_msg(const std::string& dev_id_type, const std::str
return wxEmptyString;
}
bool HMSQuery::_is_internal_error(const std::string &dev_id_type,
const std::string &error_code,
const std::string &lang_code)
bool HMSQuery::_is_internal_error(const string &dev_id_type,
const string &error_code,
const string &lang_code)
{
init_hms_info(dev_id_type);
auto iter = m_hms_info_jsons.find(dev_id_type);
+1 -1
View File
@@ -335,7 +335,7 @@ void PrintJob::process(Ctl &ctl)
auto origin_profile_id = model_info->metadata_items.find(BBL_DESIGNER_PROFILE_ID_TAG);
if (origin_profile_id != model_info->metadata_items.end()) {
try {
params.origin_profile_id = std::stoi(origin_profile_id->second.c_str());
params.origin_profile_id = stoi(origin_profile_id->second.c_str());
}
catch(...) {}
}
@@ -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)
+5 -5
View File
@@ -536,12 +536,12 @@ void NotificationManager::PopNotification::count_lines()
return;
// handle with marks
if (pos_start == std::string::npos && pos_end == std::string::npos) {
if (pos_start == string::npos && pos_end == string::npos) {
pos_start = text.find(error_start);
if (pos_start != std::string::npos) {
if (pos_start != string::npos) {
text.erase(pos_start, error_start.length());
pos_end = text.find(error_end);
if (pos_end != std::string::npos) {
if (pos_end != string::npos) {
text.erase(pos_end, error_end.length());
}
}
@@ -675,7 +675,7 @@ void NotificationManager::PopNotification::bbl_render_block_notif_text(ImGuiWrap
if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
if (pos_start != std::string::npos && pos_end != std::string::npos && m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) {
if (pos_start != string::npos && pos_end != string::npos && m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) {
push_style_color(ImGuiCol_Text, m_ErrorColor, m_state == EState::FadingOut, m_current_fade_opacity);
imgui.text(line.c_str());
ImGui::PopStyleColor();
@@ -737,7 +737,7 @@ void NotificationManager::PopNotification::render_text(ImGuiWrapper& imgui, cons
if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
if (pos_start != std::string::npos && pos_end != std::string::npos&& m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) {
if (pos_start != string::npos && pos_end != string::npos&& m_endlines[i] - line.length() >= pos_start && m_endlines[i] <= pos_end) {
push_style_color(ImGuiCol_Text, m_ErrorColor, m_state == EState::FadingOut, m_current_fade_opacity);
imgui.text(line.c_str());
ImGui::PopStyleColor();
+4 -4
View File
@@ -86,7 +86,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
{
std::time_t t = std::time(0);
std::stringstream buf;
buf << std::put_time(std::localtime(&t), "%a_%b_%d_%H_%M_%S/");
buf << put_time(std::localtime(&t), "%a_%b_%d_%H_%M_%S/");
m_timestamp = buf.str();
SetBackgroundColour(*wxWHITE);
@@ -402,7 +402,7 @@ std::string PartSkipDialog::create_tmp_path()
return tmp_path;
}
bool PartSkipDialog::is_local_file_existed(const std::vector<std::string> &local_paths)
bool PartSkipDialog::is_local_file_existed(const std::vector<string> &local_paths)
{
for (auto path : local_paths) {
if (!std::filesystem::exists(path)) { return false; }
@@ -703,8 +703,8 @@ void PartSkipDialog::InitDialogUI()
m_parts_state.clear();
m_parts_name.clear();
std::string pick_img = m_local_paths[0];
std::string slice_info = m_local_paths[2];
string pick_img = m_local_paths[0];
string slice_info = m_local_paths[2];
m_switch_drag_btn->SetIcon("canvas_drag");
m_switch_drag_btn->SetBackgroundColor(*wxWHITE);
+3 -3
View File
@@ -139,11 +139,11 @@ private:
boost::shared_ptr<PrinterFileSystem> m_file_sys;
std::string m_timestamp;
std::string m_tmp_path;
std::vector<std::string> m_local_paths;
std::vector<std::string> m_target_paths;
std::vector<string> m_local_paths;
std::vector<string> m_target_paths;
std::string create_tmp_path();
bool is_local_file_existed(const std::vector<std::string> &local_paths);
bool is_local_file_existed(const std::vector<string> &local_paths);
void DownloadPartsFile();
void OnFileSystemEvent(wxCommandEvent &event);
+4 -84
View File
@@ -357,7 +357,7 @@ wxDEFINE_EVENT(EVT_NOTICE_FULL_SCREEN_CHANGED, IntEvent);
#define PRINTER_PANEL_RADIUS (6) // ORCA
#define BTN_SYNC_SIZE (wxSize(FromDIP(96), FromDIP(98)))
static std::string get_diameter_string(float diameter)
static string get_diameter_string(float diameter)
{
std::ostringstream stream; // ORCA ensure 0.25 returned as 0.25. previous code returned as 0.2 because of std::setprecision(1)
stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably
@@ -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());
@@ -6403,7 +6323,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn)
}
// BBS:Record consumables information before synchronization
std::vector<std::string> color_before_sync;
std::vector<string> color_before_sync;
std::vector<bool> is_support_before;
DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config;
ConfigOptionStrings* color_opt = project_config.option<ConfigOptionStrings>("filament_colour");
@@ -12842,7 +12762,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
Vec3d old_plate_pos = old_plate->get_center_origin();
// BBS: Save the model in the current platelist
std::vector<std::vector<int> > plate_object;
std::vector<vector<int> > plate_object;
for (size_t i = 0; i < old_plate_list.get_plate_count(); ++i) {
PartPlate* plate = old_plate_list.get_plate(i);
std::vector<int> obj_idxs;
@@ -18055,7 +17975,7 @@ bool Plater::load_files(const wxArrayString& filenames)
// Orca: Iters through given paths and imports files from zip then remove zip from paths
// returns true if zip files were found
auto handle_zips = [this](std::vector<fs::path>& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly
auto handle_zips = [this](vector<fs::path>& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly
bool res = false;
for (auto it = paths.begin(); it != paths.end();) {
if (boost::algorithm::iends_with(it->string(), ".zip")) {
-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();
+2 -2
View File
@@ -378,7 +378,7 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
auto current_setting = app_config->get(param);
if (!current_setting.empty()) {
auto compare = [current_setting](std::string possible_setting) { return current_setting == possible_setting; };
auto compare = [current_setting](string possible_setting) { return current_setting == possible_setting; };
auto iterator = find_if(config_name_index.begin(), config_name_index.end(), compare);
if (iterator != config_name_index.end())
current_index = static_cast<unsigned int>(iterator - config_name_index.begin());
@@ -1714,7 +1714,7 @@ void PreferencesDialog::create_items()
g_sizer->Add(create_item_title(_L("Project")), 1, wxEXPAND);
std::vector<wxString> projectLoadSettingsBehaviourOptions = {_L("Load All"), _L("Ask When Relevant"), _L("Always Ask"), _L("Load Geometry Only")};
std::vector<std::string> projectLoadSettingsConfigOptions = { OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL, OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT, OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY };
std::vector<string> projectLoadSettingsConfigOptions = { OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL, OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT, OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY };
auto item_project_load = create_item_combobox(_L("Load behavior"), _L("Should printer/filament/process settings be loaded when opening a 3MF file?"), SETTING_PROJECT_LOAD_BEHAVIOUR, projectLoadSettingsBehaviourOptions, projectLoadSettingsConfigOptions);
g_sizer->Add(item_project_load);
+1 -1
View File
@@ -239,7 +239,7 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
}
bool has_content = false;
for (const std::string& v : {
for (const string& v : {
update_type,
license,
model_name,
+4 -4
View File
@@ -182,7 +182,7 @@ std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_t
static int s_nozzle_mapping_last_request_time = 0;
std::vector<wxString> SelectMachineDialog::MACHINE_BED_TYPE_STRING;
std::vector<std::string> SelectMachineDialog::MachineBedTypeString;
std::vector<string> SelectMachineDialog::MachineBedTypeString;
void SelectMachineDialog::init_machine_bed_types()
{
if (MACHINE_BED_TYPE_STRING.size() == 0) {
@@ -2748,8 +2748,8 @@ void SelectMachineDialog::on_ok_btn(wxCommandEvent &event)
std::vector<ConfirmBeforeSendInfo> confirm_text;
// check more than one using in same external spool
std::unordered_set<std::string> main_external_spool_filas;
std::unordered_set<std::string> deputy_external_spool_filas;
std::unordered_set<string> main_external_spool_filas;
std::unordered_set<string> deputy_external_spool_filas;
for (const auto& mapping_info : m_ams_mapping_result) {
if (mapping_info.ams_id == VIRTUAL_AMS_MAIN_ID_STR){
main_external_spool_filas.insert(mapping_info.filament_id);
@@ -5239,7 +5239,7 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_)
/*Check high temperture slicing*/
if (m_print_type == PrintFromType::FROM_NORMAL) {
std::set<std::string> high_temp_filaments;
std::set<string> high_temp_filaments;
std::unordered_set<int> known_fila_soften_extruders;
std::unordered_set<int> unknown_fila_soften_extruders;
auto preset_full_config = wxGetApp().preset_bundle->full_config();
+1 -1
View File
@@ -337,7 +337,7 @@ private:
wxColour m_colour_bold_color{wxColour(38, 46, 48)};
StateColor m_btn_bg_enable;
std::unordered_map<std::string, PrintOption*> m_checkbox_list;
std::unordered_map<string, PrintOption*> m_checkbox_list;
std::list<PrintOption*> m_checkbox_list_order;
std::shared_ptr<int> m_token = std::make_shared<int>(0);
+1 -1
View File
@@ -126,7 +126,7 @@ void SendToPrinterDialog::stripWhiteSpace(std::string& str)
{
if (str == "") { return; }
std::string::iterator cur_it;
string::iterator cur_it;
cur_it = str.begin();
while (cur_it != str.end()) {
+2 -2
View File
@@ -171,7 +171,7 @@ public:
std::string current_nozzle_loc = "";
wxColour m_colour;
std::string m_file_name;
string m_file_name;
bool m_ams_loading{false};
void doRender(wxDC &dc);
ExtruderImage(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
@@ -244,7 +244,7 @@ protected:
{
wxString local_image_url; //local image path
std::string img_url_paths; // oss url path
std::vector<wxPanel *> image_broad;
vector<wxPanel *> image_broad;
bool is_selected;
bool is_uploaded; // load
wxBoxSizer * image_tb_broad = nullptr;
+2 -2
View File
@@ -1576,7 +1576,7 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err
}
}
std::vector<int> map_extruders = {1, 0};
vector<int> map_extruders = {1, 0};
// The default two extruders are left, right, but the order of the extruders on the machine is right, left.
std::vector<std::string> flow_type_of_machine;
@@ -1676,7 +1676,7 @@ void SyncAmsInfoDialog::stripWhiteSpace(std::string &str)
{
if (str == "") { return; }
std::string::iterator cur_it;
string::iterator cur_it;
cur_it = str.begin();
while (cur_it != str.end()) {
+1 -1
View File
@@ -2257,7 +2257,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
wxGetApp().plater()->update();
}
std::string opt_key_without_idx = opt_key.substr(0, opt_key.find('#'));
string opt_key_without_idx = opt_key.substr(0, opt_key.find('#'));
if (opt_key_without_idx == "long_retractions_when_cut") {
unsigned char activate = boost::any_cast<unsigned char>(value);
+5 -5
View File
@@ -1682,13 +1682,13 @@ int GuideFrame::SaveProfileData()
return 0;
}
void StringReplace(std::string &strBase, std::string strSrc, std::string strDes)
void StringReplace(string &strBase, string strSrc, string strDes)
{
std::string::size_type pos = 0;
std::string::size_type srcLen = strSrc.size();
std::string::size_type desLen = strDes.size();
string::size_type pos = 0;
string::size_type srcLen = strSrc.size();
string::size_type desLen = strDes.size();
pos = strBase.find(strSrc, pos);
while ((pos != std::string::npos)) {
while ((pos != string::npos)) {
strBase.replace(pos, srcLen, strDes);
pos = strBase.find(strSrc, (pos + desLen));
}
+1 -1
View File
@@ -1052,7 +1052,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
/*update humidity popup*/
if (m_percent_humidity_dry_popup->IsShown())
{
std::string target_id = m_percent_humidity_dry_popup->get_owner_ams_id();
string target_id = m_percent_humidity_dry_popup->get_owner_ams_id();
for (const auto& the_info : ams_info)
{
if (target_id == the_info.ams_id)
+2 -2
View File
@@ -63,7 +63,7 @@ protected:
std::string m_dev_id;
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
std::vector<std::pair<std::string, std::string>> pair_id;
std::vector<std::pair<string, string>> pair_id;
int m_total_ext_count = 1;
AMSextruder *m_extruder{nullptr};
@@ -181,7 +181,7 @@ public:
std::tuple<bool, bool> isFilaSwitchReady();
void show_switcher_status(bool show);
void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
void UpdatePassRoad(string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
void CreateAms();
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
+3 -3
View File
@@ -634,7 +634,7 @@ void AMSextruderImage::doRender(wxDC &dc)
}
AMSextruderImage::AMSextruderImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos)
AMSextruderImage::AMSextruderImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos)
{
wxWindow::Create(parent, id, pos, size);
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
@@ -823,7 +823,7 @@ void SwitcherImage::doRender(wxDC &dc)
Layout();
}
SwitcherImage::SwitcherImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos)
SwitcherImage::SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos)
{
wxWindow::Create(parent, id, pos, size);
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
@@ -899,7 +899,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
}
/*return true if something is updated*/
bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name)
bool AMSextruder::updateNozzleNum(int nozzle_num, const string& series_name)
{
if (m_nozzle_num == nozzle_num && m_series_name == series_name) return false;
m_series_name = series_name;
+6 -6
View File
@@ -393,10 +393,10 @@ public:
bool m_show_state = {false};
wxColour m_colour;
ScalableBitmap m_ams_extruder;
std::string m_file_name;
string m_file_name;
bool m_ams_loading{ false };
void doRender(wxDC &dc);
AMSextruderImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition);
AMSextruderImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition);
~AMSextruderImage();
};
@@ -444,10 +444,10 @@ public:
bool m_show_state = {false};
wxColour m_colour;
ScalableBitmap m_switcher;
std::string m_file_name;
string m_file_name;
// bool m_ams_loading{ false };
void doRender(wxDC &dc);
SwitcherImage(wxWindow *parent, wxWindowID id, std::string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition);
SwitcherImage(wxWindow *parent, wxWindowID id, string file_name, const wxSize& size, const wxPoint &pos = wxDefaultPosition);
~SwitcherImage();
};
@@ -456,7 +456,7 @@ class AMSextruder : public wxWindow
{
private:
int m_nozzle_num = -1;
std::string m_series_name;
string m_series_name;
public:
void TurnOn(wxColour col);
@@ -466,7 +466,7 @@ public:
void msw_rescale();
void has_ams(bool hams) {m_has_vams = hams; Refresh();};
void no_ams_mode(bool mode) {m_none_ams_mode = mode; Refresh();};
bool updateNozzleNum(int nozzle_num, const std::string& series_name = std::string());
bool updateNozzleNum(int nozzle_num, const std::string& series_name = string());
bool m_none_ams_mode{true};
bool m_has_vams{false};
+3
View File
@@ -7,6 +7,9 @@
#include "nlohmann/json.hpp"
using namespace std;
class json_diff
{
private:
+25 -6
View File
@@ -346,8 +346,20 @@ std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
// must never be able to reach a secret, a certificate, or a configuration file just because
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is
// granted as a read-only allowed root).
return {"secret", "cert", "conf"};
// granted as a read-only allowed root). Match as whole path components, not substrings, so
// imports such as numpy/__config__.py and stdlib configparser.py remain usable.
return {"secret", "secrets", "cert", "certs", "certificate", "certificates", "conf", "config"};
}
static bool has_denied_config_extension(std::string name)
{
const size_t stream_pos = name.find(':');
if (stream_pos != std::string::npos)
name.erase(stream_pos);
const boost::filesystem::path path(name);
const std::string extension = path.extension().string();
return extension == ".conf" || extension == ".ini";
}
bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const
@@ -372,7 +384,7 @@ bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& c
continue;
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
for (const auto& keyword : m_denied_path_keywords) {
if (name.find(keyword) != std::string::npos)
if (name == keyword || (keyword == "conf" && has_denied_config_extension(name)))
return true;
}
}
@@ -796,7 +808,8 @@ bool persist_permission(const std::string& plugin_key,
int report_denied(PluginAuditManager& mgr,
const std::string& event_name,
const AuditDecision& decision)
const AuditDecision& decision,
const std::string& target = {})
{
AuditViolation violation;
violation.plugin_key = mgr.current_plugin();
@@ -804,7 +817,13 @@ int report_denied(PluginAuditManager& mgr,
violation.reason = decision.reason;
mgr.report_violation(violation);
PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission");
std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission";
if (!decision.reason.empty())
message += ": " + decision.reason;
if (!target.empty())
message += ": " + target;
PyErr_SetString(PyExc_PermissionError, message.c_str());
return -1;
}
@@ -916,7 +935,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
if (fs_category) {
for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"});
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}, target);
}
}
+8 -11
View File
@@ -4,7 +4,6 @@
// Via pybind11 so this file requests the same python3xx.lib as everything else.
#include <boost/filesystem/path.hpp>
#include <pybind11/conduit/wrap_include_python_h.h>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
@@ -103,22 +102,20 @@ public:
bool is_denied_filename(const boost::filesystem::path& candidate) const;
// --- denied-path-keyword registry ---
// Keywords that categorically deny a path if ANY of its components (directory or file
// name), not just the base name, contains one case-insensitively -- e.g. a "secrets"
// subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can
// otherwise reach, including inside an allowed root. This is intentionally broader and
// fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule
// out whole classes of sensitive paths (secrets, certificates, config) rather than name
// specific known files, at the cost of over-blocking an unrelated name that happens to
// contain the keyword -- the fail-safe direction, same rationale as is_denied_filename.
// Keywords that categorically deny a path if ANY component matches one case-insensitively --
// e.g. a "secrets" subfolder, a "certificates" folder, a "conf"/"config" directory, or a
// .conf/.ini file anywhere the plugin can otherwise reach, including inside an allowed root.
// This is broader than the exact-name is_denied_filename registry, but it is not a substring
// match: importable modules such as numpy/__config__.py, configparser.py, sysconfig.py, or
// user folders such as "Conference" and "Concert" are unrelated names and must stay promptable.
void add_denied_path_keyword(const std::string& keyword);
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
// same set without a live interpreter.
static std::vector<std::string> default_denied_path_keywords();
// True when any component of candidate's (canonicalized) path contains a registered
// keyword, case-insensitively.
// True when any component of candidate's (canonicalized) path matches a registered keyword,
// case-insensitively. A registered "conf" keyword also blocks .conf/.ini file components.
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
+12 -3
View File
@@ -222,18 +222,19 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword",
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem")));
}
SECTION("a 'conf'/'config' directory or file component is denied")
SECTION("a 'conf'/'config' directory or config file component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.ini")));
}
SECTION("matching is case-insensitive")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/Certificates/ca.pem")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/PLUGIN.CONF")));
}
SECTION("matching is not limited to the base name -- any ancestor component counts")
@@ -245,6 +246,14 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword",
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/numpy/__config__.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/numpy/_core/_ufunc_config.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/configparser.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/sysconfig.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/Lib/logging/config.py")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/python/packages/cp312/certifi/cacert.pem")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/users/Conference/output.txt")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/users/Concert/output.txt")));
}
SECTION("an empty path is not denied")