From 396cca82bb34b1d384dcd85c475dd4dab22a9228 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Sun, 17 May 2026 10:38:45 +0100 Subject: [PATCH] Add 'paint by color' utility. Add minimum visibility offset --- resources/profiles/OrcaFilamentLibrary.json | 2 +- resources/profiles/Prusa.json | 2 +- resources/profiles/Snapmaker.json | 2 +- resources/profiles_template/Template.json | 2 +- resources/shaders/110/mm_gouraud.fs | 8 + resources/shaders/140/mm_gouraud.fs | 8 + .../ImageMapRawFilamentOffsetAtlas.cpp | 2 + .../ImageMapRawFilamentOffsetAtlas.hpp | 2 + src/libslic3r/TextureMapping.cpp | 12 + src/libslic3r/TextureMapping.hpp | 6 + src/libslic3r/TriangleSelector.cpp | 157 ++++ src/libslic3r/TriangleSelector.hpp | 20 + .../GUI/Gizmos/GLGizmoMmuSegmentation.cpp | 714 +++++++++++++++++- .../GUI/Gizmos/GLGizmoMmuSegmentation.hpp | 52 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 11 +- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp | 6 + src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 2 +- src/slic3r/GUI/MMUPaintedTexturePreview.cpp | 46 +- src/slic3r/GUI/Plater.cpp | 58 ++ 19 files changed, 1096 insertions(+), 16 deletions(-) diff --git a/resources/profiles/OrcaFilamentLibrary.json b/resources/profiles/OrcaFilamentLibrary.json index 2a6d58b495f..95ca97be7cd 100644 --- a/resources/profiles/OrcaFilamentLibrary.json +++ b/resources/profiles/OrcaFilamentLibrary.json @@ -1,6 +1,6 @@ { "name": "OrcaFilamentLibrary", - "version": "02.03.02.60", + "version": "02.03.02.61", "force_update": "0", "description": "Orca Filament Library", "filament_list": [ diff --git a/resources/profiles/Prusa.json b/resources/profiles/Prusa.json index cd52d66c9e4..68983911cca 100644 --- a/resources/profiles/Prusa.json +++ b/resources/profiles/Prusa.json @@ -1,6 +1,6 @@ { "name": "Prusa", - "version": "02.03.02.60", + "version": "02.03.02.61", "force_update": "0", "description": "Prusa configurations", "machine_model_list": [ diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index f3a494da74e..0dd05217c5b 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.03.02.60", + "version": "02.03.02.61", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ diff --git a/resources/profiles_template/Template.json b/resources/profiles_template/Template.json index d06e385c1e1..030d56a8979 100644 --- a/resources/profiles_template/Template.json +++ b/resources/profiles_template/Template.json @@ -1,6 +1,6 @@ { "name": "Template", - "version": "01.07.00.02", + "version": "01.07.00.03", "force_update": "0", "description": "Template configurations", "machine_model_list": [], diff --git a/resources/shaders/110/mm_gouraud.fs b/resources/shaders/110/mm_gouraud.fs index 20a32c610a6..185da965af3 100644 --- a/resources/shaders/110/mm_gouraud.fs +++ b/resources/shaders/110/mm_gouraud.fs @@ -174,6 +174,14 @@ void main() color = color * 0.5 + LightRed * 0.5; alpha = 1.0; } + } else if (slope.preview_mode == 4) { + float preview_luma = dot(slope.highlight_color.rgb, vec3(0.2126, 0.7152, 0.0722)); + vec3 preview_accent = preview_luma > 0.75 ? vec3(0.02, 0.08, 0.24) : vec3(1.0, 1.0, 1.0); + float preview_check = slopePreviewChecker(world_pos.xyz, transformed_normal); + vec3 preview_color_a = slope.highlight_color.rgb * 0.78 + preview_accent * 0.22; + vec3 preview_color_b = slope.highlight_color.rgb * 0.25 + preview_accent * 0.75; + color = mix(preview_color_a, preview_color_b, preview_check); + alpha = max(alpha, slope.highlight_color.a); } else { int effective_state = slope.current_state == 0 ? slope.base_state : slope.current_state; if (slopeOverrideMatches(effective_state) && slopePreviewMatches(smooth_world_normal_z)) { diff --git a/resources/shaders/140/mm_gouraud.fs b/resources/shaders/140/mm_gouraud.fs index 81c8d5b7729..f1b9447a5c4 100644 --- a/resources/shaders/140/mm_gouraud.fs +++ b/resources/shaders/140/mm_gouraud.fs @@ -176,6 +176,14 @@ void main() color = color * 0.5 + LightRed * 0.5; alpha = 1.0; } + } else if (slope.preview_mode == 4) { + float preview_luma = dot(slope.highlight_color.rgb, vec3(0.2126, 0.7152, 0.0722)); + vec3 preview_accent = preview_luma > 0.75 ? vec3(0.02, 0.08, 0.24) : vec3(1.0, 1.0, 1.0); + float preview_check = slopePreviewChecker(world_pos.xyz, transformed_normal); + vec3 preview_color_a = slope.highlight_color.rgb * 0.78 + preview_accent * 0.22; + vec3 preview_color_b = slope.highlight_color.rgb * 0.25 + preview_accent * 0.75; + color = mix(preview_color_a, preview_color_b, preview_check); + alpha = max(alpha, slope.highlight_color.a); } else { int effective_state = slope.current_state == 0 ? slope.base_state : slope.current_state; if (slopeOverrideMatches(effective_state) && slopePreviewMatches(smooth_world_normal_z)) { diff --git a/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp index 098791a1404..0c3ee72e2f1 100644 --- a/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp +++ b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.cpp @@ -1,3 +1,5 @@ +// original author: sentientstardust + #include "ImageMapRawFilamentOffsetAtlas.hpp" #include diff --git a/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp index 9fe71656f30..36d67ee8960 100644 --- a/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp +++ b/src/libslic3r/ImageMapRawFilamentOffsetAtlas.hpp @@ -1,3 +1,5 @@ +// original author: sentientstardust + #ifndef slic3r_ImageMapRawFilamentOffsetAtlas_hpp_ #define slic3r_ImageMapRawFilamentOffsetAtlas_hpp_ diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index 76ec66d1d0a..1783d89a652 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -797,6 +797,8 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const compact_offset_mode == rhs.compact_offset_mode && use_legacy_fixed_color_mode == rhs.use_legacy_fixed_color_mode && high_speed_image_texture_sampling == rhs.high_speed_image_texture_sampling && + minimum_visibility_offset_enabled == rhs.minimum_visibility_offset_enabled && + std::abs(minimum_visibility_offset_pct - rhs.minimum_visibility_offset_pct) <= eps && generic_solver_lookup_mode == rhs.generic_solver_lookup_mode && generic_solver_mode == rhs.generic_solver_mode && generic_solver_mix_model == rhs.generic_solver_mix_model && @@ -1044,6 +1046,9 @@ std::string TextureMappingManager::serialize_entries() texture["compact_offset_mode"] = zone.compact_offset_mode; texture["use_legacy_fixed_color_mode"] = zone.use_legacy_fixed_color_mode; texture["high_speed_image_texture_sampling"] = zone.high_speed_image_texture_sampling; + texture["minimum_visibility_offset_enabled"] = zone.minimum_visibility_offset_enabled; + texture["minimum_visibility_offset_pct"] = + std::clamp(finite_or(zone.minimum_visibility_offset_pct, TextureMappingZone::DefaultMinimumVisibilityOffsetPct), 0.f, 100.f); texture["generic_solver_lookup"] = generic_solver_lookup_mode_name(zone.generic_solver_lookup_mode); texture["generic_solver_mode"] = generic_solver_mode_name(zone.generic_solver_mode); texture["generic_solver_mix_model"] = generic_solver_mix_model_name(zone.generic_solver_mix_model); @@ -1174,6 +1179,13 @@ void TextureMappingManager::load_entries(const std::string &serialized, texture.value("use_legacy_fixed_color_mode", TextureMappingZone::DefaultUseLegacyFixedColorMode); zone.high_speed_image_texture_sampling = texture.value("high_speed_image_texture_sampling", TextureMappingZone::DefaultHighSpeedImageTextureSampling); + zone.minimum_visibility_offset_enabled = + texture.value("minimum_visibility_offset_enabled", TextureMappingZone::DefaultMinimumVisibilityOffsetEnabled); + zone.minimum_visibility_offset_pct = + std::clamp(finite_or(texture.value("minimum_visibility_offset_pct", TextureMappingZone::DefaultMinimumVisibilityOffsetPct), + TextureMappingZone::DefaultMinimumVisibilityOffsetPct), + 0.f, + 100.f); zone.generic_solver_lookup_mode = generic_solver_lookup_mode_from_name(texture.value("generic_solver_lookup", std::string("closest_mix"))); const auto generic_solver_mode_it = texture.find("generic_solver_mode"); diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index ef96ab00832..9dcf3a355f2 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -97,6 +97,8 @@ struct TextureMappingZone static constexpr bool DefaultCompactOffsetMode = true; static constexpr bool DefaultUseLegacyFixedColorMode = false; static constexpr bool DefaultHighSpeedImageTextureSampling = true; + static constexpr bool DefaultMinimumVisibilityOffsetEnabled = false; + static constexpr float DefaultMinimumVisibilityOffsetPct = 30.f; static constexpr int DefaultGenericSolverLookupMode = int(GenericSolverClosestMix); static constexpr int DefaultGenericSolverMode = int(GenericSolverV2); static constexpr int DefaultGenericSolverMixModel = int(GenericSolverPigmentPainter); @@ -139,6 +141,8 @@ struct TextureMappingZone bool compact_offset_mode = DefaultCompactOffsetMode; bool use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; bool high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; + bool minimum_visibility_offset_enabled = DefaultMinimumVisibilityOffsetEnabled; + float minimum_visibility_offset_pct = DefaultMinimumVisibilityOffsetPct; int generic_solver_lookup_mode = DefaultGenericSolverLookupMode; int generic_solver_mode = DefaultGenericSolverMode; int generic_solver_mix_model = DefaultGenericSolverMixModel; @@ -182,6 +186,8 @@ struct TextureMappingZone compact_offset_mode = DefaultCompactOffsetMode; use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; + minimum_visibility_offset_enabled = DefaultMinimumVisibilityOffsetEnabled; + minimum_visibility_offset_pct = DefaultMinimumVisibilityOffsetPct; generic_solver_lookup_mode = DefaultGenericSolverLookupMode; generic_solver_mode = DefaultGenericSolverMode; generic_solver_mix_model = DefaultGenericSolverMixModel; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a562f15bdde..4deb990b258 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -20,6 +20,38 @@ static Vec3f normalized_or_default(const Vec3f &normal, const Vec3f &fallback) return len > float(EPSILON) ? normal / len : fallback; } +static Vec3f barycentric_for_triangle_sample(const indexed_triangle_set &its, int tri_idx, const Vec3f &point) +{ + if (tri_idx < 0 || tri_idx >= int(its.indices.size())) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + + const stl_triangle_vertex_indices &source_indices = its.indices[size_t(tri_idx)]; + if (source_indices[0] < 0 || source_indices[1] < 0 || source_indices[2] < 0 || + source_indices[0] >= int(its.vertices.size()) || + source_indices[1] >= int(its.vertices.size()) || + source_indices[2] >= int(its.vertices.size())) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + + const Vec3f &a = its.vertices[size_t(source_indices[0])]; + const Vec3f &b = its.vertices[size_t(source_indices[1])]; + const Vec3f &c = its.vertices[size_t(source_indices[2])]; + const Vec3f v0 = b - a; + const Vec3f v1 = c - a; + const Vec3f v2 = point - a; + const float d00 = v0.dot(v0); + const float d01 = v0.dot(v1); + const float d11 = v1.dot(v1); + const float d20 = v2.dot(v0); + const float d21 = v2.dot(v1); + const float denom = d00 * d11 - d01 * d01; + if (std::abs(denom) <= float(EPSILON)) + return Vec3f(1.f / 3.f, 1.f / 3.f, 1.f / 3.f); + + const float v = (d11 * d20 - d01 * d21) / denom; + const float w = (d00 * d21 - d01 * d20) / denom; + return Vec3f(1.f - v - w, v, w); +} + static std::vector> its_corner_normals(const indexed_triangle_set &its) { constexpr float crease_dot = 0.5f; @@ -1255,6 +1287,131 @@ bool TriangleSelector::apply_state_by_smooth_world_normal(const Transform3d &tra return changed; } +bool TriangleSelector::apply_state_by_triangle_sampler_recursive( + int facet_idx, + const Vec3i32 &neighbors, + const Transform3f &trafo, + EnforcerBlockerType new_state, + const std::function &predicate, + float max_world_edge_sqr, + int max_depth, + bool clear_non_matching) +{ + if (facet_idx < 0 || facet_idx >= int(m_triangles.size()) || !m_triangles[size_t(facet_idx)].valid()) + return false; + + Triangle *tr = &m_triangles[size_t(facet_idx)]; + if (tr->is_split()) { + bool changed = false; + const int child_count = tr->number_of_split_sides() + 1; + for (int child_idx = 0; child_idx < child_count; ++child_idx) { + changed |= apply_state_by_triangle_sampler_recursive(tr->children[size_t(child_idx)], + child_neighbors(*tr, neighbors, child_idx), + trafo, + new_state, + predicate, + max_world_edge_sqr, + max_depth, + clear_non_matching); + tr = &m_triangles[size_t(facet_idx)]; + } + return changed; + } + + const EnforcerBlockerType current_state = tr->get_state(); + std::array sample_points; + sample_points[0] = m_vertices[size_t(tr->verts_idxs[0])].v; + sample_points[1] = m_vertices[size_t(tr->verts_idxs[1])].v; + sample_points[2] = m_vertices[size_t(tr->verts_idxs[2])].v; + sample_points[3] = (sample_points[0] + sample_points[1] + sample_points[2]) / 3.f; + + std::array matches; + int match_count = 0; + for (size_t sample_idx = 0; sample_idx < sample_points.size(); ++sample_idx) { + const Vec3f barycentric = barycentric_for_triangle_sample(m_mesh.its, tr->source_triangle, sample_points[sample_idx]); + matches[sample_idx] = predicate(current_state, size_t(std::max(tr->source_triangle, 0)), sample_points[sample_idx], barycentric); + if (matches[sample_idx]) + ++match_count; + } + + if (match_count == 0) { + const EnforcerBlockerType next_state = clear_non_matching ? EnforcerBlockerType::NONE : current_state; + if (next_state != current_state) { + tr->set_state(next_state); + return true; + } + return false; + } + if (match_count == int(matches.size())) { + if (current_state != new_state) { + tr->set_state(new_state); + return true; + } + return false; + } + + if (max_depth > 0 && triangle_max_world_edge_sqr(*tr, trafo) > max_world_edge_sqr && + split_triangle_for_world_edge_limit(facet_idx, neighbors, trafo, max_world_edge_sqr)) { + bool changed = false; + tr = &m_triangles[size_t(facet_idx)]; + const int child_count = tr->number_of_split_sides() + 1; + for (int child_idx = 0; child_idx < child_count; ++child_idx) { + changed |= apply_state_by_triangle_sampler_recursive(tr->children[size_t(child_idx)], + child_neighbors(*tr, neighbors, child_idx), + trafo, + new_state, + predicate, + max_world_edge_sqr, + max_depth - 1, + clear_non_matching); + tr = &m_triangles[size_t(facet_idx)]; + } + return changed; + } + + const EnforcerBlockerType next_state = matches[3] ? new_state : (clear_non_matching ? EnforcerBlockerType::NONE : current_state); + if (next_state != current_state) { + tr->set_state(next_state); + return true; + } + return false; +} + +bool TriangleSelector::apply_state_by_triangle_sampler( + const Transform3d &trafo_no_translate, + EnforcerBlockerType new_state, + const std::function &predicate, + float max_world_edge, + int max_depth, + bool clear_non_matching) +{ + if (!predicate) + return false; + + const Transform3f trafo = trafo_no_translate.cast(); + const float max_world_edge_sqr = std::max(max_world_edge, float(EPSILON)) * std::max(max_world_edge, float(EPSILON)); + bool changed = false; + + for (int facet_idx = 0; facet_idx < m_orig_size_indices; ++facet_idx) { + changed |= apply_state_by_triangle_sampler_recursive(facet_idx, + m_neighbors[size_t(facet_idx)], + trafo, + new_state, + predicate, + max_world_edge_sqr, + max_depth, + clear_non_matching); + } + + if (changed) { + for (int facet_idx = 0; facet_idx < m_orig_size_indices; ++facet_idx) + if (m_triangles[size_t(facet_idx)].valid() && m_triangles[size_t(facet_idx)].is_split()) + remove_useless_children(facet_idx); + } + + return changed; +} + // called by select_patch()->select_triangle()...select_triangle() // to decide which sides of the triangle to split and to actually split it calling set_division() and perform_split(). void TriangleSelector::split_triangle(int facet_idx, const Vec3i32 &neighbors) diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 3e2a86de299..f33bbe347c9 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -358,6 +358,15 @@ public: float max_world_edge, int max_depth, bool clear_non_matching); + bool apply_state_by_triangle_sampler(const Transform3d &trafo_no_translate, + EnforcerBlockerType new_state, + const std::function &predicate, + float max_world_edge, + int max_depth, + bool clear_non_matching); // Clear everything and make the tree empty. void reset(); @@ -514,6 +523,17 @@ private: float max_world_edge_sqr, int max_depth, bool clear_non_matching); + bool apply_state_by_triangle_sampler_recursive(int facet_idx, + const Vec3i32 &neighbors, + const Transform3f &trafo, + EnforcerBlockerType new_state, + const std::function &predicate, + float max_world_edge_sqr, + int max_depth, + bool clear_non_matching); void remove_useless_children(int facet_idx); // No hidden meaning. Triangles are meant. bool is_facet_clipped(int facet_idx, const ClippingPlane &clp) const; int push_triangle(int a, int b, int c, int source_triangle, EnforcerBlockerType state = EnforcerBlockerType{0}); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 038e9de984b..78f4ca5cf15 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -140,6 +140,17 @@ struct TrueColorRgbDataConversionResult bool canceled = false; }; +struct ColorAutoPaintPreviewTask +{ + const ModelObject *object = nullptr; + const ModelVolume *volume = nullptr; + TriangleSelector::TriangleSplittingData selector_data; + std::vector ebt_colors; + Transform3d trafo_no_translate = Transform3d::Identity(); + EnforcerBlockerType max_ebt = EnforcerBlockerType::NONE; + bool valid = false; +}; + struct GLGizmoTrueColorPainting::RgbDataConversionState { std::atomic_bool cancel { false }; @@ -192,10 +203,19 @@ void GLGizmoMmuSegmentation::on_opening() show_notification_extruders_limit_exceeded(); } +GLGizmoMmuSegmentation::~GLGizmoMmuSegmentation() +{ + cancel_color_auto_paint_preview_worker(); +} + void GLGizmoMmuSegmentation::on_shutdown() { m_show_slope_auto_paint_overlay = false; m_slope_auto_paint_preview_active = false; + m_show_color_auto_paint_overlay = false; + m_color_auto_paint_preview_active = false; + cancel_color_auto_paint_preview_worker(); + m_color_auto_paint_preview_selectors.clear(); m_parent.use_slope(false); m_parent.toggle_model_objects_visibility(true); } @@ -309,6 +329,20 @@ static wxString slope_auto_paint_filament_label(unsigned int filament_id) wxString::Format(_L("Filament %u"), filament_id); } +static std::vector mmu_segmentation_selector_colors_for_volume(const ModelVolume &volume, + const std::vector &extruder_colors) +{ + if (extruder_colors.empty()) + return {}; + + const unsigned int extruder_id = volume.extruder_id() > 0 ? unsigned(volume.extruder_id()) : 1u; + const size_t extruder_color_idx = extruder_color_index_for_filament_id(extruder_id, extruder_colors.size()); + std::vector colors; + colors.emplace_back(extruder_colors[extruder_color_idx]); + colors.insert(colors.end(), extruder_colors.begin(), extruder_colors.end()); + return colors; +} + static bool slope_auto_paint_matches_normal(const SlopeAutoPaintSettings &settings, float world_normal_z) { constexpr float normal_epsilon = 0.0001f; @@ -3749,6 +3783,126 @@ static ColorRGBA sample_volume_color_source(const ModelVolume &volume, return fallback_color != nullptr ? *fallback_color : ColorRGBA(1.f, 1.f, 1.f, 1.f); } +static ColorRGBA composite_color_over_background(const ColorRGBA &color, const ColorRGBA &background) +{ + const float alpha = std::clamp(color.a(), 0.f, 1.f); + return ColorRGBA(color.r() * alpha + background.r() * (1.f - alpha), + color.g() * alpha + background.g() * (1.f - alpha), + color.b() * alpha + background.b() * (1.f - alpha), + 1.f); +} + +static std::optional sample_color_auto_paint_source(const ModelObject *object, + const ModelVolume &volume, + const VolumeColorSource &source, + size_t tri_idx, + const Vec3f &point, + const Vec3f &barycentric) +{ + const ColorRGBA configured_background = + configured_texture_mapping_background_color_for_volume(volume).value_or(managed_color_data_background_color(object)); + + if (source.rgb_background_color) { + ColorRGBA color = *source.rgb_background_color; + if (std::optional sampled = sample_rgb_color_facets(source.rgb_facets, + source.rgb_by_source_triangle, + int(tri_idx), + point)) + color = *sampled; + ColorRGBA background = *source.rgb_background_color; + background.a(1.f); + return composite_color_over_background(color, background); + } + + const indexed_triangle_set &its = volume.mesh().its; + if (model_volume_has_bakeable_image_texture_data(&volume) && tri_idx < volume.imported_texture_uv_valid.size()) { + const size_t uv_offset = tri_idx * 6; + if (volume.imported_texture_uv_valid[tri_idx] != 0 && uv_offset + 5 < volume.imported_texture_uvs_per_face.size()) { + const Vec2f uv0(volume.imported_texture_uvs_per_face[uv_offset + 0], volume.imported_texture_uvs_per_face[uv_offset + 1]); + const Vec2f uv1(volume.imported_texture_uvs_per_face[uv_offset + 2], volume.imported_texture_uvs_per_face[uv_offset + 3]); + const Vec2f uv2(volume.imported_texture_uvs_per_face[uv_offset + 4], volume.imported_texture_uvs_per_face[uv_offset + 5]); + return composite_color_over_background(sample_texture_rgba_for_face_bake(volume.imported_texture_rgba, + volume.imported_texture_width, + volume.imported_texture_height, + uv0, + uv1, + uv2, + barycentric), + configured_background); + } + } + + if (volume.imported_vertex_colors_rgba.size() == its.vertices.size() && tri_idx < its.indices.size()) { + const stl_triangle_vertex_indices &tri = its.indices[tri_idx]; + if (tri[0] >= 0 && tri[1] >= 0 && tri[2] >= 0 && + size_t(tri[0]) < volume.imported_vertex_colors_rgba.size() && + size_t(tri[1]) < volume.imported_vertex_colors_rgba.size() && + size_t(tri[2]) < volume.imported_vertex_colors_rgba.size()) { + const ColorRGBA c0 = unpack_vertex_color_rgba_for_conversion(volume.imported_vertex_colors_rgba[size_t(tri[0])]); + const ColorRGBA c1 = unpack_vertex_color_rgba_for_conversion(volume.imported_vertex_colors_rgba[size_t(tri[1])]); + const ColorRGBA c2 = unpack_vertex_color_rgba_for_conversion(volume.imported_vertex_colors_rgba[size_t(tri[2])]); + const ColorRGBA sampled(c0.r() * barycentric.x() + c1.r() * barycentric.y() + c2.r() * barycentric.z(), + c0.g() * barycentric.x() + c1.g() * barycentric.y() + c2.g() * barycentric.z(), + c0.b() * barycentric.x() + c1.b() * barycentric.y() + c2.b() * barycentric.z(), + c0.a() * barycentric.x() + c1.a() * barycentric.y() + c2.a() * barycentric.z()); + return composite_color_over_background(sampled, configured_background); + } + } + + return std::nullopt; +} + +static std::array color_auto_paint_oklab(const ColorRGBA &color) +{ + return color_solver_oklab_from_srgb({ std::clamp(color.r(), 0.f, 1.f), + std::clamp(color.g(), 0.f, 1.f), + std::clamp(color.b(), 0.f, 1.f) }); +} + +static float color_auto_paint_max_oklab_distance() +{ + static const float max_distance = []() { + const std::array corners = { + ColorRGBA(0.f, 0.f, 0.f, 1.f), + ColorRGBA(0.f, 0.f, 1.f, 1.f), + ColorRGBA(0.f, 1.f, 0.f, 1.f), + ColorRGBA(0.f, 1.f, 1.f, 1.f), + ColorRGBA(1.f, 0.f, 0.f, 1.f), + ColorRGBA(1.f, 0.f, 1.f, 1.f), + ColorRGBA(1.f, 1.f, 0.f, 1.f), + ColorRGBA(1.f, 1.f, 1.f, 1.f) + }; + float distance = 0.f; + for (const ColorRGBA &lhs : corners) { + const std::array lhs_oklab = color_auto_paint_oklab(lhs); + for (const ColorRGBA &rhs : corners) { + const std::array rhs_oklab = color_auto_paint_oklab(rhs); + const float dl = lhs_oklab[0] - rhs_oklab[0]; + const float da = lhs_oklab[1] - rhs_oklab[1]; + const float db = lhs_oklab[2] - rhs_oklab[2]; + distance = std::max(distance, std::sqrt(dl * dl + da * da + db * db)); + } + } + return std::max(distance, 1.f); + }(); + return max_distance; +} + +static bool color_auto_paint_matches_target(const ColorRGBA &sample, const ColorAutoPaintSettings &settings) +{ + const float tolerance_pct = std::clamp(settings.tolerance_pct, 0.f, 100.f); + if (tolerance_pct >= 100.f) + return true; + + const std::array sample_oklab = color_auto_paint_oklab(sample); + const std::array target_oklab = color_auto_paint_oklab(settings.target_color); + const float dl = sample_oklab[0] - target_oklab[0]; + const float da = sample_oklab[1] - target_oklab[1]; + const float db = sample_oklab[2] - target_oklab[2]; + const float tolerance = tolerance_pct * 0.01f * color_auto_paint_max_oklab_distance(); + return dl * dl + da * da + db * db <= tolerance * tolerance; +} + static bool snapshot_has_bakeable_image_texture_data(const TrueColorRgbDataConversionVolumeSnapshot &snapshot) { return !snapshot.its.vertices.empty() && @@ -8646,6 +8800,70 @@ bool GLGizmoMmuSegmentation::should_render_triangle_texture_preview() const return !m_slope_auto_paint_preview_active; } +void GLGizmoMmuSegmentation::render_extra_triangle_overlays(int mesh_id, + const Transform3d &matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane) const +{ + (void)view_matrix; + (void)projection_matrix; + (void)z_range; + (void)clipping_plane; + if (!m_color_auto_paint_preview_active || + mesh_id < 0 || + size_t(mesh_id) >= m_color_auto_paint_preview_selectors.size() || + m_color_auto_paint_preview_selectors[size_t(mesh_id)] == nullptr) + return; + + GLShaderProgram *shader = wxGetApp().get_current_shader(); + if (shader == nullptr) + return; + + const size_t target_color_idx = + extruder_color_index_for_filament_id(m_color_auto_paint_preview_settings.target_filament_id, m_extruders_colors.size()); + ColorRGBA highlight_color = m_extruders_colors.empty() ? + ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : + m_extruders_colors[target_color_idx]; + highlight_color.a(0.95f); + const Matrix3f normal_matrix = static_cast(matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); + const std::array empty_mask { 0.f, 0.f, 0.f, 0.f }; + + GLint depth_func = GL_LESS; + GLfloat polygon_offset_factor = 0.f; + GLfloat polygon_offset_units = 0.f; + GLboolean polygon_offset_fill_enabled = glIsEnabled(GL_POLYGON_OFFSET_FILL); + glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &depth_func)); + glsafe(::glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &polygon_offset_factor)); + glsafe(::glGetFloatv(GL_POLYGON_OFFSET_UNITS, &polygon_offset_units)); + + shader->set_uniform("slope.actived", true); + shader->set_uniform("slope.volume_world_normal_matrix", normal_matrix); + shader->set_uniform("slope.normal_z", 0.f); + shader->set_uniform("slope.preview_mode", 4); + shader->set_uniform("slope.top_z", 1.f); + shader->set_uniform("slope.bottom_z", -1.f); + shader->set_uniform("slope.highlight_color", highlight_color); + shader->set_uniform("slope.override_all", true); + shader->set_uniform("slope.current_state", 0); + shader->set_uniform("slope.base_state", 1); + shader->set_uniform("slope.override_mask0", empty_mask); + shader->set_uniform("slope.override_mask1", empty_mask); + shader->set_uniform("slope.override_mask2", empty_mask); + shader->set_uniform("slope.override_mask3", empty_mask); + + glsafe(::glDepthFunc(GL_LEQUAL)); + glsafe(::glEnable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glPolygonOffset(-1.f, -1.f)); + m_color_auto_paint_preview_selectors[size_t(mesh_id)]->render(m_imgui, matrix); + glsafe(::glPolygonOffset(polygon_offset_factor, polygon_offset_units)); + if (!polygon_offset_fill_enabled) + glsafe(::glDisable(GL_POLYGON_OFFSET_FILL)); + glsafe(::glDepthFunc(depth_func)); + shader->set_uniform("slope.actived", false); +} + void GLGizmoMmuSegmentation::render_slope_auto_paint_overlay() { if (!m_show_slope_auto_paint_overlay) @@ -8742,6 +8960,7 @@ void GLGizmoMmuSegmentation::render_slope_auto_paint_overlay() changed |= render_angle(_L("Angle from bottom"), "##slope_bottom_angle", m_slope_auto_paint_settings.bottom_angle_deg); ImGui::Separator(); + m_imgui->text(_L("Mask:")); bool override_all = m_slope_auto_paint_settings.override_all; if (m_imgui->bbl_checkbox(_L("All"), override_all)) { if (override_all) { @@ -8831,6 +9050,10 @@ void GLGizmoMmuSegmentation::open_slope_auto_paint_overlay() if (m_c->selection_info()->model_object() == nullptr) return; + m_show_color_auto_paint_overlay = false; + m_color_auto_paint_overlay_positioned = false; + clear_color_auto_paint_preview(); + m_slope_auto_paint_settings = SlopeAutoPaintSettings(); m_slope_auto_paint_settings.target_filament_id = m_selected_extruder_idx < m_display_filament_ids.size() ? m_display_filament_ids[m_selected_extruder_idx] : @@ -8909,9 +9132,10 @@ bool GLGizmoMmuSegmentation::apply_slope_auto_paint(const SlopeAutoPaintSettings EnforcerBlockerType current_state, float world_normal_z) { if (!slope_auto_paint_matches_normal(settings, world_normal_z)) return false; - const unsigned int current_filament_id = current_state == EnforcerBlockerType::NONE ? - base_filament_id : - static_cast(current_state); + const unsigned int current_filament_id = + current_state == EnforcerBlockerType::NONE ? + base_filament_id : + static_cast(current_state); return settings.override_all || override_ids.count(current_filament_id) != 0; }, SlopeAutoPaintMaxEdgeMm, @@ -8924,6 +9148,446 @@ bool GLGizmoMmuSegmentation::apply_slope_auto_paint(const SlopeAutoPaintSettings return changed; } +void GLGizmoMmuSegmentation::render_color_auto_paint_overlay() +{ + if (!m_show_color_auto_paint_overlay) + return; + + if (!selected_object_has_color_auto_paint_source()) { + m_show_color_auto_paint_overlay = false; + clear_color_auto_paint_preview(); + return; + } + + std::vector filament_ids = m_display_filament_ids; + if (filament_ids.empty()) + filament_ids.emplace_back(1u); + + std::vector filament_labels; + std::vector filament_colors; + filament_labels.reserve(filament_ids.size()); + filament_colors.reserve(filament_ids.size()); + for (const unsigned int filament_id : filament_ids) { + filament_labels.emplace_back(into_u8(slope_auto_paint_filament_label(filament_id))); + const size_t color_idx = extruder_color_index_for_filament_id(filament_id, m_extruders_colors.size()); + filament_colors.emplace_back(m_extruders_colors.empty() ? ColorRGBA(0.15f, 0.65f, 0.6f, 1.f) : m_extruders_colors[color_idx]); + } + + if (std::find(filament_ids.begin(), filament_ids.end(), m_color_auto_paint_settings.target_filament_id) == filament_ids.end()) + m_color_auto_paint_settings.target_filament_id = filament_ids.front(); + + if (!m_color_auto_paint_overlay_positioned) { + const Size canvas_size = m_parent.get_canvas_size(); + const float window_width = m_imgui->scaled(22.f); + const float window_x = std::max(m_imgui->scaled(8.f), + float(canvas_size.get_width()) - window_width - m_imgui->scaled(24.f)); + ImGui::SetNextWindowPos(ImVec2(window_x, m_parent.get_main_toolbar_height() + m_imgui->scaled(16.f)), + ImGuiCond_Always); + m_color_auto_paint_overlay_positioned = true; + } + + m_imgui->push_common_window_style(m_parent.get_scale()); + ImGui::SetNextWindowSize(ImVec2(m_imgui->scaled(22.f), 0.f), ImGuiCond_FirstUseEver); + bool open = true; + const int flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; + m_imgui->begin(_L("Paint by color"), &open, flags); + + bool changed = false; + const float label_width = m_imgui->scaled(7.5f); + const float item_width = m_imgui->scaled(11.f); + + size_t target_idx = 0; + for (size_t idx = 0; idx < filament_ids.size(); ++idx) { + if (filament_ids[idx] == m_color_auto_paint_settings.target_filament_id) { + target_idx = idx; + break; + } + } + const size_t old_target_idx = target_idx; + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Paint with")); + ImGui::SameLine(label_width); + ImGui::PushItemWidth(item_width); + render_extruders_combo("##color_auto_paint_target", filament_labels, filament_colors, target_idx); + ImGui::PopItemWidth(); + if (target_idx != old_target_idx && target_idx < filament_ids.size()) { + m_color_auto_paint_settings.target_filament_id = filament_ids[target_idx]; + changed = true; + } + + std::array target_color = { + std::clamp(m_color_auto_paint_settings.target_color.r(), 0.f, 1.f), + std::clamp(m_color_auto_paint_settings.target_color.g(), 0.f, 1.f), + std::clamp(m_color_auto_paint_settings.target_color.b(), 0.f, 1.f) + }; + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Color")); + ImGui::SameLine(label_width); + ImGui::PushItemWidth(item_width); + ImGuiColorEditFlags color_flags = ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_InputRGB | + ImGuiColorEditFlags_NoInputs; + if (ImGui::ColorEdit3("##color_auto_paint_picker", target_color.data(), color_flags)) { + m_color_auto_paint_settings.target_color = ColorRGBA(target_color[0], target_color[1], target_color[2], 1.f); + changed = true; + } + ImGui::PopItemWidth(); + + ImGui::AlignTextToFramePadding(); + m_imgui->text(_L("Tolerance")); + ImGui::SameLine(label_width); + ImGui::PushItemWidth(item_width); + if (ImGui::SliderFloat("##color_auto_paint_tolerance", &m_color_auto_paint_settings.tolerance_pct, 0.f, 100.f, "%.0f%%")) { + m_color_auto_paint_settings.tolerance_pct = std::clamp(m_color_auto_paint_settings.tolerance_pct, 0.f, 100.f); + changed = true; + } + ImGui::PopItemWidth(); + + ImGui::Separator(); + m_imgui->text(_L("Mask:")); + bool override_all = m_color_auto_paint_settings.override_all; + if (m_imgui->bbl_checkbox(_L("All"), override_all)) { + if (override_all) { + m_color_auto_paint_settings.override_all = true; + m_color_auto_paint_settings.override_filament_ids.clear(); + changed = true; + } else { + override_all = true; + } + } + + const float row_height = ImGui::GetTextLineHeightWithSpacing(); + const float override_height = std::min(row_height * (float(filament_ids.size()) + 0.5f), row_height * 6.5f); + ImGui::BeginChild("##color_auto_paint_override_scroll", ImVec2(0.f, override_height), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); + for (size_t idx = 0; idx < filament_ids.size(); ++idx) { + const unsigned int filament_id = filament_ids[idx]; + const bool currently_selected = !m_color_auto_paint_settings.override_all && + std::find(m_color_auto_paint_settings.override_filament_ids.begin(), + m_color_auto_paint_settings.override_filament_ids.end(), + filament_id) != m_color_auto_paint_settings.override_filament_ids.end(); + bool selected = currently_selected; + ImGui::PushID(int(filament_id)); + if (ImGui::Checkbox("##override", &selected)) { + if (selected) { + if (m_color_auto_paint_settings.override_all) { + m_color_auto_paint_settings.override_all = false; + m_color_auto_paint_settings.override_filament_ids.clear(); + } + if (std::find(m_color_auto_paint_settings.override_filament_ids.begin(), + m_color_auto_paint_settings.override_filament_ids.end(), + filament_id) == m_color_auto_paint_settings.override_filament_ids.end()) + m_color_auto_paint_settings.override_filament_ids.emplace_back(filament_id); + } else { + auto &override_ids = m_color_auto_paint_settings.override_filament_ids; + override_ids.erase(std::remove(override_ids.begin(), override_ids.end(), filament_id), override_ids.end()); + if (override_ids.empty()) + m_color_auto_paint_settings.override_all = true; + } + changed = true; + } + ImGui::SameLine(); + ImGuiColorEditFlags swatch_flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | + ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; + ImGui::ColorButton("##swatch", ImGuiWrapper::to_ImVec4(filament_colors[idx]), swatch_flags, + ImVec2(ImGui::GetTextLineHeight() * 1.5f, ImGui::GetTextLineHeight())); + ImGui::SameLine(); + ImGui::Text("%s", filament_labels[idx].c_str()); + ImGui::PopID(); + } + ImGui::EndChild(); + + if (changed) { + update_color_auto_paint_preview(m_color_auto_paint_settings); + } else if (!m_color_auto_paint_preview_active && !m_color_auto_paint_preview_update_pending) { + update_color_auto_paint_preview(m_color_auto_paint_settings); + } + + ImGui::Separator(); + bool close_overlay = false; + const bool preview_processing = m_color_auto_paint_preview_update_pending || m_color_auto_paint_preview_worker_running; + m_imgui->push_confirm_button_style(); + m_imgui->disabled_begin(preview_processing); + if (m_imgui->bbl_button(_L("Apply"))) { + m_color_auto_paint_preview_update_pending = false; + ++m_color_auto_paint_preview_generation; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Paint by color", UndoRedo::SnapshotType::GizmoAction); + if (apply_color_auto_paint(m_color_auto_paint_settings, false)) { + update_model_object(); + m_parent.set_as_dirty(); + } + close_overlay = true; + } + m_imgui->disabled_end(); + m_imgui->pop_confirm_button_style(); + ImGui::SameLine(); + m_imgui->push_cancel_button_style(); + if (m_imgui->bbl_button(_L("Cancel"))) + close_overlay = true; + m_imgui->pop_cancel_button_style(); + if (preview_processing) { + ImGui::SameLine(); + m_imgui->text(_L("Processing...")); + } + + m_imgui->end(); + m_imgui->pop_common_window_style(); + + if (!open || close_overlay) { + m_show_color_auto_paint_overlay = false; + m_color_auto_paint_overlay_positioned = false; + clear_color_auto_paint_preview(); + } +} + +void GLGizmoMmuSegmentation::open_color_auto_paint_overlay() +{ + if (m_c->selection_info()->model_object() == nullptr || !selected_object_has_color_auto_paint_source()) + return; + + m_show_slope_auto_paint_overlay = false; + m_slope_auto_paint_overlay_positioned = false; + clear_slope_auto_paint_preview(); + + m_color_auto_paint_settings = ColorAutoPaintSettings(); + m_color_auto_paint_settings.target_filament_id = m_selected_extruder_idx < m_display_filament_ids.size() ? + m_display_filament_ids[m_selected_extruder_idx] : + 1u; + if (!m_display_filament_ids.empty() && + std::find(m_display_filament_ids.begin(), m_display_filament_ids.end(), m_color_auto_paint_settings.target_filament_id) == + m_display_filament_ids.end()) + m_color_auto_paint_settings.target_filament_id = m_display_filament_ids.front(); + const size_t color_idx = + extruder_color_index_for_filament_id(m_color_auto_paint_settings.target_filament_id, m_extruders_colors.size()); + if (!m_extruders_colors.empty()) { + m_color_auto_paint_settings.target_color = m_extruders_colors[color_idx]; + m_color_auto_paint_settings.target_color.a(1.f); + } + + m_show_color_auto_paint_overlay = true; + m_color_auto_paint_overlay_positioned = false; + update_color_auto_paint_preview(m_color_auto_paint_settings); +} + +bool GLGizmoMmuSegmentation::apply_color_auto_paint_to_selector(const ModelObject &object, + const ModelVolume &volume, + TriangleSelector &selector, + const Transform3d &trafo_no_translate, + const ColorAutoPaintSettings &settings, + bool clear_non_matching) const +{ + const VolumeColorSource source = build_volume_color_source(volume); + const std::unordered_set override_ids(settings.override_filament_ids.begin(), settings.override_filament_ids.end()); + const unsigned int base_filament_id = volume.extruder_id() > 0 ? static_cast(volume.extruder_id()) : 1u; + + return selector.apply_state_by_triangle_sampler( + trafo_no_translate, + static_cast(settings.target_filament_id), + [&object, &volume, &source, &settings, &override_ids, base_filament_id]( + EnforcerBlockerType current_state, size_t source_triangle, const Vec3f &point, const Vec3f &barycentric) { + const unsigned int current_filament_id = current_state == EnforcerBlockerType::NONE ? + base_filament_id : + static_cast(current_state); + if (!settings.override_all && override_ids.count(current_filament_id) == 0) + return false; + + const std::optional color = + sample_color_auto_paint_source(&object, volume, source, source_triangle, point, barycentric); + return color && color_auto_paint_matches_target(*color, settings); + }, + SlopeAutoPaintMaxEdgeMm, + SlopeAutoPaintMaxDepth, + clear_non_matching); +} + +void GLGizmoMmuSegmentation::update_color_auto_paint_preview(const ColorAutoPaintSettings &settings) +{ + m_color_auto_paint_requested_settings = settings; + m_color_auto_paint_preview_update_pending = true; + ++m_color_auto_paint_preview_generation; + start_color_auto_paint_preview_worker(); + m_parent.schedule_extra_frame(0); +} + +void GLGizmoMmuSegmentation::start_color_auto_paint_preview_worker() +{ + if (m_color_auto_paint_preview_worker_running) + return; + + if (m_color_auto_paint_preview_thread.joinable()) + m_color_auto_paint_preview_thread.join(); + + ModelObject *mo = m_c->selection_info()->model_object(); + const Selection &selection = m_parent.get_selection(); + const bool active = mo != nullptr && + !m_triangle_selectors.empty() && + selection.get_instance_idx() >= 0 && + selection.get_instance_idx() < int(mo->instances.size()) && + selected_object_has_color_auto_paint_source(); + if (!active) { + m_color_auto_paint_preview_selectors.clear(); + m_color_auto_paint_preview_active = false; + m_color_auto_paint_preview_update_pending = false; + m_parent.set_as_dirty(); + m_parent.schedule_extra_frame(0); + return; + } + + const Transform3d instance_trafo_not_translate = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + mo->instances[selection.get_instance_idx()]->get_assemble_transformation().get_matrix_no_offset() : + mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix_no_offset(); + + std::vector tasks; + int selector_idx = -1; + for (const ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + + ++selector_idx; + ColorAutoPaintPreviewTask task; + if (selector_idx < 0 || + size_t(selector_idx) >= m_triangle_selectors.size() || + m_triangle_selectors[size_t(selector_idx)] == nullptr) { + tasks.emplace_back(std::move(task)); + continue; + } + + std::vector ebt_colors = mmu_segmentation_selector_colors_for_volume(*mv, m_extruders_colors); + if (ebt_colors.empty()) { + tasks.emplace_back(std::move(task)); + continue; + } + + task.object = mo; + task.volume = mv; + task.selector_data = m_triangle_selectors[size_t(selector_idx)]->serialize(); + task.ebt_colors = std::move(ebt_colors); + task.trafo_no_translate = instance_trafo_not_translate * mv->get_matrix_no_offset(); + task.max_ebt = (EnforcerBlockerType)std::min(m_extruders_colors.size(), (size_t)EnforcerBlockerType::ExtruderMax); + task.valid = true; + tasks.emplace_back(std::move(task)); + } + + const uint64_t generation = m_color_auto_paint_preview_generation; + const ColorAutoPaintSettings settings = m_color_auto_paint_requested_settings; + m_color_auto_paint_preview_worker_running = true; + m_color_auto_paint_preview_thread = std::thread([this, generation, settings, tasks = std::move(tasks)]() mutable { + auto selectors = std::make_shared>>(); + selectors->reserve(tasks.size()); + for (const ColorAutoPaintPreviewTask &task : tasks) { + if (!task.valid || task.object == nullptr || task.volume == nullptr) { + selectors->emplace_back(); + continue; + } + + std::unique_ptr preview_selector = + std::make_unique(task.volume->mesh(), task.volume, task.ebt_colors, 0.2f); + preview_selector->deserialize(task.selector_data, false, task.max_ebt); + preview_selector->set_none_state_rendered(false); + preview_selector->set_texture_preview_needed(false); + preview_selector->set_wireframe_needed(false); + apply_color_auto_paint_to_selector(*task.object, + *task.volume, + *preview_selector, + task.trafo_no_translate, + settings, + true); + preview_selector->request_update_render_data(true); + selectors->emplace_back(std::move(preview_selector)); + } + + wxGetApp().CallAfter([this, generation, settings, selectors]() mutable { + finish_color_auto_paint_preview_update(generation, settings, true, std::move(*selectors)); + }); + }); +} + +void GLGizmoMmuSegmentation::finish_color_auto_paint_preview_update( + uint64_t generation, + const ColorAutoPaintSettings &settings, + bool active, + std::vector> &&selectors) +{ + if (m_color_auto_paint_preview_thread.joinable()) + m_color_auto_paint_preview_thread.join(); + m_color_auto_paint_preview_worker_running = false; + + if (generation == m_color_auto_paint_preview_generation && m_show_color_auto_paint_overlay) { + m_color_auto_paint_preview_selectors = std::move(selectors); + m_color_auto_paint_preview_settings = settings; + m_color_auto_paint_preview_active = active; + m_color_auto_paint_preview_update_pending = false; + m_parent.set_as_dirty(); + m_parent.schedule_extra_frame(0); + } + + if (m_color_auto_paint_preview_update_pending && m_show_color_auto_paint_overlay) + start_color_auto_paint_preview_worker(); +} + +void GLGizmoMmuSegmentation::cancel_color_auto_paint_preview_worker() +{ + ++m_color_auto_paint_preview_generation; + m_color_auto_paint_preview_update_pending = false; + if (m_color_auto_paint_preview_thread.joinable()) + m_color_auto_paint_preview_thread.join(); + m_color_auto_paint_preview_worker_running = false; +} + +void GLGizmoMmuSegmentation::clear_color_auto_paint_preview() +{ + ++m_color_auto_paint_preview_generation; + m_color_auto_paint_preview_active = false; + m_color_auto_paint_preview_update_pending = false; + m_color_auto_paint_preview_selectors.clear(); + m_parent.set_as_dirty(); + m_parent.schedule_extra_frame(0); +} + +bool GLGizmoMmuSegmentation::apply_color_auto_paint(const ColorAutoPaintSettings &settings, bool preview) +{ + if (preview) { + update_color_auto_paint_preview(settings); + return false; + } + + ModelObject *mo = m_c->selection_info()->model_object(); + if (mo == nullptr || m_triangle_selectors.empty()) + return false; + + const Selection &selection = m_parent.get_selection(); + if (selection.get_instance_idx() < 0 || selection.get_instance_idx() >= int(mo->instances.size())) + return false; + + const Transform3d instance_trafo_not_translate = m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + mo->instances[selection.get_instance_idx()]->get_assemble_transformation().get_matrix_no_offset() : + mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix_no_offset(); + + bool changed = false; + int selector_idx = -1; + for (const ModelVolume *mv : mo->volumes) { + if (!mv->is_model_part()) + continue; + + ++selector_idx; + if (selector_idx < 0 || size_t(selector_idx) >= m_triangle_selectors.size() || + m_triangle_selectors[size_t(selector_idx)] == nullptr) + continue; + + const Transform3d trafo_matrix_not_translate = instance_trafo_not_translate * mv->get_matrix_no_offset(); + const bool selector_changed = + apply_color_auto_paint_to_selector(*mo, + *mv, + *m_triangle_selectors[size_t(selector_idx)], + trafo_matrix_not_translate, + settings, + false); + m_triangle_selectors[size_t(selector_idx)]->request_update_render_data(true); + changed |= selector_changed; + } + + return changed; +} + void GLGizmoMmuSegmentation::data_changed(bool is_serializing) { GLGizmoPainterBase::data_changed(is_serializing); @@ -9490,6 +10154,25 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott if (ImGui::IsItemHovered()) m_imgui->tooltip(_L("Paint color regions by surface slope with a live preview."), max_tooltip_width); + const bool can_paint_by_color = selected_object_has_color_auto_paint_source(); + m_imgui->disabled_begin(!can_paint_by_color); + if (m_imgui->button(_L("Paint by color"))) { + if (m_show_color_auto_paint_overlay) { + m_show_color_auto_paint_overlay = false; + m_color_auto_paint_overlay_positioned = false; + clear_color_auto_paint_preview(); + } else { + open_color_auto_paint_overlay(); + } + } + if (ImGui::IsItemHovered()) { + if (can_paint_by_color) + m_imgui->tooltip(_L("Paint color regions by matching the visible source color with a live preview."), max_tooltip_width); + else + m_imgui->tooltip(_L("This object does not have RGBA, image texture, or vertex color data."), max_tooltip_width); + } + m_imgui->disabled_end(); + ImGui::Separator(); const bool has_painted_regions = selected_object_has_painted_regions(); @@ -9624,6 +10307,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott ImGuiWrapper::pop_toolbar_style(); render_slope_auto_paint_overlay(); + render_color_auto_paint_overlay(); } @@ -9676,6 +10360,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors() const ModelObject *mo = m_c->selection_info()->model_object(); m_triangle_selectors.clear(); m_slope_auto_paint_preview_active = false; + m_color_auto_paint_preview_active = false; + ++m_color_auto_paint_preview_generation; + m_color_auto_paint_preview_update_pending = false; + m_color_auto_paint_preview_selectors.clear(); m_volumes_extruder_idxs.clear(); // Don't continue when extruders colors are not initialized @@ -9833,6 +10521,24 @@ bool GLGizmoMmuSegmentation::selected_object_has_painted_regions() const return false; } +bool GLGizmoMmuSegmentation::selected_object_has_color_auto_paint_source() const +{ + const ModelObject *object = m_c->selection_info()->model_object(); + if (object == nullptr) + return false; + + for (const ModelVolume *volume : object->volumes) { + if (volume == nullptr || !volume->is_model_part()) + continue; + const indexed_triangle_set &its = volume->mesh().its; + if (!volume->texture_mapping_color_facets.empty() || + model_volume_has_bakeable_image_texture_data(volume) || + volume->imported_vertex_colors_rgba.size() == its.vertices.size()) + return true; + } + return false; +} + void GLGizmoMmuSegmentation::open_obj_vertex_color_mapping_dialog() { ModelObject *object = m_c->selection_info()->model_object(); @@ -10420,7 +11126,7 @@ PainterGizmoType GLGizmoTrueColorPainting::get_painter_type() const std::string GLGizmoTrueColorPainting::on_get_name() const { - return _u8L("True Color Painting"); + return _u8L("RGB Color Painting"); } bool GLGizmoTrueColorPainting::on_is_selectable() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp index 22237c6a5f1..31d09c69d8d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp @@ -45,6 +45,15 @@ struct SlopeAutoPaintSettings std::vector override_filament_ids; }; +struct ColorAutoPaintSettings +{ + ColorRGBA target_color = ColorRGBA(1.f, 1.f, 1.f, 1.f); + float tolerance_pct = 5.f; + unsigned int target_filament_id = 1; + bool override_all = true; + std::vector override_filament_ids; +}; + class GLMmSegmentationGizmo3DScene { public: @@ -105,7 +114,7 @@ class GLGizmoMmuSegmentation : public GLGizmoPainterBase { public: GLGizmoMmuSegmentation(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - ~GLGizmoMmuSegmentation() override = default; + ~GLGizmoMmuSegmentation() override; void render_painter_gizmo() override; @@ -187,6 +196,29 @@ private: bool apply_slope_auto_paint(const SlopeAutoPaintSettings &settings, bool preview); void set_render_triangle_slope_uniforms(GLShaderProgram *shader, const ModelVolume *model_volume, const Matrix3f &normal_matrix) const override; bool should_render_triangle_texture_preview() const override; + void open_color_auto_paint_overlay(); + void render_color_auto_paint_overlay(); + void update_color_auto_paint_preview(const ColorAutoPaintSettings &settings); + void start_color_auto_paint_preview_worker(); + void finish_color_auto_paint_preview_update(uint64_t generation, + const ColorAutoPaintSettings &settings, + bool active, + std::vector> &&selectors); + void cancel_color_auto_paint_preview_worker(); + void clear_color_auto_paint_preview(); + bool apply_color_auto_paint(const ColorAutoPaintSettings &settings, bool preview); + bool apply_color_auto_paint_to_selector(const ModelObject &object, + const ModelVolume &volume, + TriangleSelector &selector, + const Transform3d &trafo_no_translate, + const ColorAutoPaintSettings &settings, + bool clear_non_matching) const; + void render_extra_triangle_overlays(int mesh_id, + const Transform3d &matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane) const override; // Filament remapping methods void remap_filament_assignments(); @@ -196,6 +228,7 @@ private: bool selected_object_has_bakeable_image_texture_data() const; bool selected_object_has_texture_mapping_color_data() const; bool selected_object_has_painted_regions() const; + bool selected_object_has_color_auto_paint_source() const; void open_obj_vertex_color_mapping_dialog(); void bake_selected_object_image_texture_to_vertex_colors(); void convert_selected_object_vertex_colors_to_texture_mapping_colors(); @@ -215,6 +248,17 @@ private: bool m_show_slope_auto_paint_overlay = false; bool m_slope_auto_paint_overlay_positioned = false; bool m_slope_auto_paint_preview_active = false; + ColorAutoPaintSettings m_color_auto_paint_settings; + ColorAutoPaintSettings m_color_auto_paint_preview_settings; + bool m_show_color_auto_paint_overlay = false; + bool m_color_auto_paint_overlay_positioned = false; + bool m_color_auto_paint_preview_active = false; + bool m_color_auto_paint_preview_update_pending = false; + bool m_color_auto_paint_preview_worker_running = false; + uint64_t m_color_auto_paint_preview_generation = 0; + ColorAutoPaintSettings m_color_auto_paint_requested_settings; + std::thread m_color_auto_paint_preview_thread; + std::vector> m_color_auto_paint_preview_selectors; }; class GLGizmoTrueColorPainting : public GLGizmoPainterBase @@ -241,9 +285,9 @@ protected: wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override; - std::string get_gizmo_entering_text() const override { return "Entering true color painting"; } - std::string get_gizmo_leaving_text() const override { return "Leaving true color painting"; } - std::string get_action_snapshot_name() const override { return "True color painting editing"; } + std::string get_gizmo_entering_text() const override { return "Entering RGB color painting"; } + std::string get_gizmo_leaving_text() const override { return "Leaving RGB color painting"; } + std::string get_action_snapshot_name() const override { return "RGB color painting editing"; } private: enum class ColorInputMode : int diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index 661fb3fa37d..109197663fe 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -164,8 +164,9 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const const Camera& camera = wxGetApp().plater()->get_camera(); const Transform3d& view_matrix = camera.get_view_matrix(); + const Transform3d& projection_matrix = camera.get_projection_matrix(); shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + shader->set_uniform("projection_matrix", projection_matrix); const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose(); shader->set_uniform("view_normal_matrix", view_normal_matrix); @@ -178,10 +179,16 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const if (should_render_triangle_texture_preview()) m_triangle_selectors[mesh_id]->render_texture_preview(trafo_matrix, view_matrix, - camera.get_projection_matrix(), + projection_matrix, clp_data.z_range, clp_data.clp_dataf); shader->start_using(); + render_extra_triangle_overlays(mesh_id, + trafo_matrix, + view_matrix, + projection_matrix, + clp_data.z_range, + clp_data.clp_dataf); if (is_left_handed) glsafe(::glFrontFace(GL_CCW)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp index 911c64a3b91..dcdc6241683 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp @@ -254,6 +254,12 @@ protected: virtual void render_triangles(const Selection& selection) const; virtual void set_render_triangle_slope_uniforms(GLShaderProgram *shader, const ModelVolume *model_volume, const Matrix3f &normal_matrix) const; virtual bool should_render_triangle_texture_preview() const { return true; } + virtual void render_extra_triangle_overlays(int mesh_id, + const Transform3d &matrix, + const Transform3d &view_matrix, + const Transform3d &projection_matrix, + const std::array &z_range, + const std::array &clipping_plane) const {} void render_cursor(); void render_cursor_circle(); void render_cursor_sphere(const Transform3d& trafo) const; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 3a9426f3455..51af4e6b380 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -1489,7 +1489,7 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type) case GLGizmosManager::EType::MmSegmentation: return "Color Region Painting"; case GLGizmosManager::EType::TrueColorPainting: - return "True Color Painting"; + return "RGB Color Painting"; case GLGizmosManager::EType::ImageProjection: return "Project image to model surface"; case GLGizmosManager::EType::FuzzySkin: diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp index 3b705e4ccb5..a13cd9f2796 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -78,6 +78,7 @@ struct TexturePreviewSimulationSettings int generic_solver_lookup_mode = int(TextureMappingZone::GenericSolverClosestMix); int generic_solver_mode = int(TextureMappingZone::GenericSolverV2); int generic_solver_mix_model = TextureMappingZone::DefaultGenericSolverMixModel; + float minimum_visibility_offset_factor = 0.f; std::vector component_ids; std::vector> component_colors; std::vector component_strength_factors; @@ -324,6 +325,28 @@ float clamp01(float value) return std::clamp(value, 0.f, 1.f); } +float apply_minimum_visibility_offset(float value, float offset) +{ + const float safe_value = clamp01(value); + const float safe_offset = clamp01(offset); + if (safe_offset <= k_epsilon) + return safe_value; + if (safe_value <= safe_offset) + return 0.f; + const float denominator = 1.f - safe_offset; + if (denominator <= k_epsilon) + return 0.f; + return clamp01((safe_value - safe_offset) / denominator); +} + +void apply_minimum_visibility_offset(std::vector &weights, float offset) +{ + if (offset <= k_epsilon) + return; + for (float &weight : weights) + weight = apply_minimum_visibility_offset(weight, offset); +} + float texture_preview_config_float(const char *key, float fallback) { if (GUI::wxGetApp().preset_bundle == nullptr) @@ -1624,6 +1647,7 @@ std::vector component_weights_for_texture_preview(const TexturePreviewSim for (size_t idx = 0; idx < desired.size() && idx < settings.component_strength_factors.size(); ++idx) desired[idx] = clamp01(desired[idx] * settings.component_strength_factors[idx]); + apply_minimum_visibility_offset(desired, settings.minimum_visibility_offset_factor); return desired; } @@ -1636,10 +1660,17 @@ std::vector raw_offset_print_width_weights_for_texture_preview(const Text const float strength = idx < settings.component_strength_factors.size() ? settings.component_strength_factors[idx] : 1.f; const float minimum = idx < settings.component_minimum_offset_factors.size() ? settings.component_minimum_offset_factors[idx] : 0.f; const float adjusted_visibility = clamp01(minimum + clamp01(raw_weights[idx]) * strength * (1.f - minimum)); + if (settings.minimum_visibility_offset_factor > k_epsilon) { + width_factors[idx] = apply_minimum_visibility_offset(adjusted_visibility, settings.minimum_visibility_offset_factor); + continue; + } width_factors[idx] = clamp01(settings.raw_offset_base_visibility_factor + settings.raw_offset_visibility_range_factor * adjusted_visibility); } + if (settings.minimum_visibility_offset_factor > k_epsilon) + return width_factors; + const auto min_width = std::min_element(width_factors.begin(), width_factors.end()); if (min_width == width_factors.end()) return {}; @@ -1732,6 +1763,13 @@ std::optional texture_preview_simulation_setti int(TextureMappingZone::GenericSolverLegacy), int(TextureMappingZone::GenericSolverV2)); settings.generic_solver_mix_model = TextureMappingZone::DefaultGenericSolverMixModel; + settings.minimum_visibility_offset_factor = zone->minimum_visibility_offset_enabled ? + std::clamp((std::isfinite(zone->minimum_visibility_offset_pct) ? + zone->minimum_visibility_offset_pct : + TextureMappingZone::DefaultMinimumVisibilityOffsetPct) / 100.f, + 0.f, + 1.f) : + 0.f; settings.component_ids = TextureMappingManager::effective_texture_component_ids(*zone, num_physical, physical_colors); if (settings.component_ids.empty()) return std::nullopt; @@ -1792,6 +1830,7 @@ size_t texture_preview_simulation_signature(const ModelVolume &model_volume, mix(std::hash{}(settings.generic_solver_mix_model)); mix(std::hash{}(int(std::lround(settings.contrast_pct * 100.f)))); mix(std::hash{}(int(std::lround(settings.tone_gamma * 1000.f)))); + mix(std::hash{}(int(std::lround(settings.minimum_visibility_offset_factor * 100000.f)))); for (const unsigned int id : settings.component_ids) mix(std::hash{}(id)); for (const auto &color : settings.component_colors) { @@ -1896,7 +1935,10 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig float activity = 0.f; for (const float weight : component_weights) activity = std::max(activity, clamp01(weight)); - if (use_raw_offsets && activity <= k_epsilon && !component_weights.empty()) { + if (use_raw_offsets && + activity <= k_epsilon && + settings.minimum_visibility_offset_factor <= k_epsilon && + !component_weights.empty()) { std::fill(component_weights.begin(), component_weights.end(), 1.f); activity = 1.f; } @@ -3869,6 +3911,8 @@ size_t texture_preview_settings_signature(size_t num_physical, const TextureMapp signature_mix(std::hash{}(zone.nonlinear_offset_adjustment ? 1 : 0)); signature_mix(std::hash{}(zone.compact_offset_mode ? 1 : 0)); signature_mix(std::hash{}(zone.use_legacy_fixed_color_mode ? 1 : 0)); + signature_mix(std::hash{}(zone.minimum_visibility_offset_enabled ? 1 : 0)); + signature_mix_float(zone.minimum_visibility_offset_pct, 100.f); signature_mix(std::hash{}(zone.generic_solver_lookup_mode)); signature_mix(std::hash{}(zone.generic_solver_mode)); signature_mix(std::hash{}(TextureMappingZone::DefaultGenericSolverMixModel)); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d2064802189..ede93a4eda5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1079,6 +1079,8 @@ public: bool compact_offset_mode, bool use_legacy_fixed_color_mode, bool high_speed_image_texture_sampling, + bool minimum_visibility_offset_enabled, + float minimum_visibility_offset_pct, int generic_solver_lookup_mode, int generic_solver_mode, int generic_solver_mix_model, @@ -1349,6 +1351,27 @@ public: m_generic_solver_lookup_choice->SetToolTip(_L("Controls how the fallback Generic Solver picks from precomputed filament color mixes.")); generic_solver_row->Add(m_generic_solver_lookup_choice, 1, wxALIGN_CENTER_VERTICAL); experimental_box->Add(generic_solver_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + auto *minimum_visibility_offset_row = new wxBoxSizer(wxHORIZONTAL); + m_minimum_visibility_offset_checkbox = new wxCheckBox(experimental_page, wxID_ANY, _L("Minimum visibility offset")); + m_minimum_visibility_offset_checkbox->SetValue(minimum_visibility_offset_enabled); + minimum_visibility_offset_row->Add(m_minimum_visibility_offset_checkbox, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + m_minimum_visibility_offset_spin = new wxSpinCtrl(experimental_page, + wxID_ANY, + wxEmptyString, + wxDefaultPosition, + wxSize(FromDIP(70), -1), + wxSP_ARROW_KEYS | wxALIGN_RIGHT, + 0, + 100, + std::clamp(int(std::lround(minimum_visibility_offset_pct)), 0, 100)); + minimum_visibility_offset_row->Add(m_minimum_visibility_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap / 2); + m_minimum_visibility_offset_units = new wxStaticText(experimental_page, wxID_ANY, _L("%")); + minimum_visibility_offset_row->Add(m_minimum_visibility_offset_units, 0, wxALIGN_CENTER_VERTICAL); + m_minimum_visibility_offset_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &) { + update_minimum_visibility_offset_visibility(true); + }); + experimental_box->Add(minimum_visibility_offset_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, gap); + update_minimum_visibility_offset_visibility(false); experimental_root->Add(experimental_box, 0, wxEXPAND | wxALL, gap); m_prime_tower_mapping_enabled_checkbox = new wxCheckBox(global_page, wxID_ANY, _L("Enable prime tower image mapping")); @@ -1531,6 +1554,18 @@ public: bool compact_offset_mode() const { return m_compact_offset_mode_checkbox && m_compact_offset_mode_checkbox->GetValue(); } bool use_legacy_fixed_color_mode() const { return m_use_legacy_fixed_color_mode_checkbox && m_use_legacy_fixed_color_mode_checkbox->GetValue(); } bool high_speed_image_texture_sampling() const { return m_high_speed_image_texture_sampling_checkbox == nullptr || m_high_speed_image_texture_sampling_checkbox->GetValue(); } + bool minimum_visibility_offset_enabled() const + { + return m_minimum_visibility_offset_checkbox && m_minimum_visibility_offset_checkbox->GetValue(); + } + + float minimum_visibility_offset_pct() const + { + const int value = m_minimum_visibility_offset_spin != nullptr ? + m_minimum_visibility_offset_spin->GetValue() : + int(TextureMappingZone::DefaultMinimumVisibilityOffsetPct); + return float(std::clamp(value, 0, 100)); + } int generic_solver_lookup_mode() const { return m_generic_solver_lookup_choice ? @@ -1791,6 +1826,22 @@ private: } } + void update_minimum_visibility_offset_visibility(bool fit_dialog) + { + const bool enabled = m_minimum_visibility_offset_checkbox != nullptr && m_minimum_visibility_offset_checkbox->GetValue(); + if (m_minimum_visibility_offset_spin != nullptr) + m_minimum_visibility_offset_spin->Show(enabled); + if (m_minimum_visibility_offset_units != nullptr) + m_minimum_visibility_offset_units->Show(enabled); + if (!fit_dialog) + return; + update_options_book_min_size(); + if (GetSizer() != nullptr) { + Layout(); + Fit(); + } + } + wxChoice *m_options_tab_choice {nullptr}; wxSimplebook *m_options_book {nullptr}; wxChoice *m_texture_mapping_mode_choice {nullptr}; @@ -1809,6 +1860,9 @@ private: wxCheckBox *m_compact_offset_mode_checkbox {nullptr}; wxCheckBox *m_use_legacy_fixed_color_mode_checkbox {nullptr}; wxCheckBox *m_high_speed_image_texture_sampling_checkbox {nullptr}; + wxCheckBox *m_minimum_visibility_offset_checkbox {nullptr}; + wxSpinCtrl *m_minimum_visibility_offset_spin {nullptr}; + wxStaticText *m_minimum_visibility_offset_units {nullptr}; wxChoice *m_generic_solver_lookup_choice {nullptr}; wxChoice *m_generic_solver_mode_choice {nullptr}; wxChoice *m_generic_solver_mix_model_choice {nullptr}; @@ -5913,6 +5967,8 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.compact_offset_mode, updated.use_legacy_fixed_color_mode, updated.high_speed_image_texture_sampling, + updated.minimum_visibility_offset_enabled, + updated.minimum_visibility_offset_pct, updated.generic_solver_lookup_mode, updated.generic_solver_mode, updated.generic_solver_mix_model, @@ -5945,6 +6001,8 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.compact_offset_mode = dlg.compact_offset_mode(); updated.use_legacy_fixed_color_mode = dlg.use_legacy_fixed_color_mode(); updated.high_speed_image_texture_sampling = dlg.high_speed_image_texture_sampling(); + updated.minimum_visibility_offset_enabled = dlg.minimum_visibility_offset_enabled(); + updated.minimum_visibility_offset_pct = dlg.minimum_visibility_offset_pct(); updated.generic_solver_lookup_mode = dlg.generic_solver_lookup_mode(); updated.generic_solver_mode = dlg.generic_solver_mode(); updated.generic_solver_mix_model = dlg.generic_solver_mix_model();