diff --git a/src/libslic3r/ColorSolver.cpp b/src/libslic3r/ColorSolver.cpp index b9410d09b43..666aafb6870 100644 --- a/src/libslic3r/ColorSolver.cpp +++ b/src/libslic3r/ColorSolver.cpp @@ -362,6 +362,11 @@ std::array mix_color_solver_components(const std::vector color_solver_oklab_from_srgb(const std::array &rgb) +{ + return oklab_from_srgb(rgb); +} + std::string color_solver_candidate_cache_key(const std::vector> &component_colors, ColorSolverMixModel mix_model, int total_units) diff --git a/src/libslic3r/ColorSolver.hpp b/src/libslic3r/ColorSolver.hpp index 15656098c12..02ffd36a171 100644 --- a/src/libslic3r/ColorSolver.hpp +++ b/src/libslic3r/ColorSolver.hpp @@ -82,6 +82,7 @@ std::array mix_color_solver_components(const std::vector mix_color_solver_components(const std::vector> &component_colors, const std::vector &weights, ColorSolverMixModel mix_model); +std::array color_solver_oklab_from_srgb(const std::array &rgb); std::string color_solver_candidate_cache_key(const std::vector> &component_colors, ColorSolverMixModel mix_model, diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c0169516679..ae96627fa9a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7314,11 +7314,13 @@ static const GCodeUVTextureTriangleCache &uv_texture_triangle_cache_for_gcode( return it->second; } -static float color_distance_sq_for_gcode(const std::array &lhs, const std::array &rhs) +static float perceptual_color_distance_sq_for_gcode(const std::array &lhs, const std::array &rhs) { - const float dr = lhs[0] - rhs[0]; - const float dg = lhs[1] - rhs[1]; - const float db = lhs[2] - rhs[2]; + const std::array lhs_oklab = oklab_from_srgb_for_gcode(lhs); + const std::array rhs_oklab = oklab_from_srgb_for_gcode(rhs); + const float dr = lhs_oklab[0] - rhs_oklab[0]; + const float dg = lhs_oklab[1] - rhs_oklab[1]; + const float db = lhs_oklab[2] - rhs_oklab[2]; return dr * dr + dg * dg + db * db; } @@ -7337,7 +7339,7 @@ static std::vector best_matching_component_indices_for_semantic_colors_f do { float error = 0.f; for (size_t role_idx = 0; role_idx < semantic_colors.size(); ++role_idx) - error += color_distance_sq_for_gcode(component_colors[permutation[role_idx]], semantic_colors[role_idx]); + error += perceptual_color_distance_sq_for_gcode(component_colors[permutation[role_idx]], semantic_colors[role_idx]); if (error < best_error) { best_error = error; diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 158f3c2ed3a..1e9242c94ac 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -248,6 +248,71 @@ static std::vector collect_texture_mapping_vertex_color_match_warni }; } +static std::vector collect_texture_mapping_filament_color_match_warnings(const PrintObject &print_object) +{ + const Print *print = print_object.print(); + if (print == nullptr) + return {}; + + const ModelObject *model_object = print_object.model_object(); + if (model_object == nullptr) + return {}; + + const size_t num_physical = print->config().filament_colour.size(); + if (num_physical == 0) + return {}; + + std::vector warnings; + std::set seen_zone_ids; + for (const ModelVolume *volume : model_object->volumes) { + if (volume == nullptr) + continue; + + const std::vector used_extruders = volume->get_extruders(); + for (const int filament_id : used_extruders) { + if (filament_id <= 0) + continue; + + const unsigned int filament_id_u = unsigned(filament_id); + if (seen_zone_ids.find(filament_id_u) != seen_zone_ids.end()) + continue; + seen_zone_ids.insert(filament_id_u); + + const TextureMappingZone *zone = print->texture_mapping_manager().zone_from_id(filament_id_u); + if (zone == nullptr || !zone->enabled || zone->deleted || !zone->is_image_texture()) + continue; + + const std::vector matches = + TextureMappingManager::texture_component_color_matches(*zone, num_physical, print->config().filament_colour.values); + if (matches.empty()) + continue; + + std::vector poor_matches; + for (const TextureMappingColorMatch &match : matches) { + if (match.perceptual_distance <= TextureMappingManager::poor_color_match_distance()) + continue; + poor_matches.emplace_back("F" + std::to_string(match.filament_id) + " for " + match.expected_color_name); + } + if (poor_matches.empty()) + continue; + + std::string detail; + for (size_t idx = 0; idx < poor_matches.size(); ++idx) { + if (idx > 0) + detail += ", "; + detail += poor_matches[idx]; + } + + warnings.emplace_back( + L("Filaments used in Texture Mapping zone ") + + std::to_string(filament_id_u) + L(" do not appear to match the expected colors: ") + detail + + L(".")); + } + } + + return warnings; +} + static const char *vertex_color_mode_name_for_error(int filament_color_mode) { switch (filament_color_mode) { @@ -1404,6 +1469,8 @@ void PrintObject::slice_volumes() this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); for (const std::string &warning_msg : collect_texture_mapping_vertex_color_match_warnings(*this)) this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); + for (const std::string &warning_msg : collect_texture_mapping_filament_color_match_warnings(*this)) + this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); for (const std::string &error_msg : collect_texture_mapping_vertex_color_mode_mismatch_errors(*this)) this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, error_msg); diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index c5aff6a1445..93b3eea8a3a 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -22,6 +22,7 @@ namespace { constexpr unsigned int TextureMappingZoneIdBase = 99; constexpr unsigned int MaxTextureMappingZoneId = 255; constexpr const char *TextureMappingGapDisplayColor = "#8C8C8C"; +constexpr float TextureMappingPoorColorMatchDistance = 0.22f; struct RGB { int r = 0; @@ -379,11 +380,22 @@ static RGB filament_color(unsigned int id, const std::vector &filam return {}; } -static float color_distance_sq(const RGB &lhs, const RGB &rhs) +static std::array rgb_to_srgb01(const RGB &color) { - const float dr = float(lhs.r - rhs.r); - const float dg = float(lhs.g - rhs.g); - const float db = float(lhs.b - rhs.b); + return { + float(std::clamp(color.r, 0, 255)) / 255.f, + float(std::clamp(color.g, 0, 255)) / 255.f, + float(std::clamp(color.b, 0, 255)) / 255.f + }; +} + +static float perceptual_color_distance_sq(const RGB &lhs, const RGB &rhs) +{ + const std::array lhs_oklab = color_solver_oklab_from_srgb(rgb_to_srgb01(lhs)); + const std::array rhs_oklab = color_solver_oklab_from_srgb(rgb_to_srgb01(rhs)); + const float dr = lhs_oklab[0] - rhs_oklab[0]; + const float dg = lhs_oklab[1] - rhs_oklab[1]; + const float db = lhs_oklab[2] - rhs_oklab[2]; return dr * dr + dg * dg + db * db; } @@ -405,6 +417,24 @@ static std::vector semantic_colors(int filament_color_mode) } } +static std::vector semantic_color_names(int filament_color_mode) +{ + switch (clamp_int(filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorRGBKW))) { + case int(TextureMappingZone::FilamentColorRGB): return {"Red", "Green", "Blue"}; + case int(TextureMappingZone::FilamentColorCMY): return {"Cyan", "Magenta", "Yellow"}; + case int(TextureMappingZone::FilamentColorCMYK): return {"Cyan", "Magenta", "Yellow", "Black"}; + case int(TextureMappingZone::FilamentColorCMYW): return {"Cyan", "Magenta", "Yellow", "White"}; + case int(TextureMappingZone::FilamentColorRGBK): return {"Red", "Green", "Blue", "Black"}; + case int(TextureMappingZone::FilamentColorRGBW): return {"Red", "Green", "Blue", "White"}; + case int(TextureMappingZone::FilamentColorBW): return {"Black", "White"}; + case int(TextureMappingZone::FilamentColorCMYKW): return {"Cyan", "Magenta", "Yellow", "Black", "White"}; + case int(TextureMappingZone::FilamentColorRGBKW): return {"Red", "Green", "Blue", "Black", "White"}; + default: return {}; + } +} + static std::vector ids_from_json(const nlohmann::json &value, size_t num_physical) { std::vector ids; @@ -1412,7 +1442,7 @@ std::vector TextureMappingManager::effective_texture_component_ids for (unsigned int id = 1; id <= num_physical; ++id) { if (id < used.size() && used[id]) continue; - const float distance = color_distance_sq(filament_color(id, filament_colours), target); + const float distance = perceptual_color_distance_sq(filament_color(id, filament_colours), target); if (distance < best_distance) { best_distance = distance; best_id = id; @@ -1448,7 +1478,7 @@ std::vector TextureMappingManager::effective_texture_component_ids const unsigned int id = selected[i]; if (selected_used[i] || id < 1 || id > num_physical) continue; - const float distance = color_distance_sq(filament_color(id, filament_colours), role); + const float distance = perceptual_color_distance_sq(filament_color(id, filament_colours), role); if (distance < best_distance) { best_distance = distance; best_selected = i; @@ -1502,6 +1532,42 @@ bool TextureMappingManager::auto_adjust_texture_component_ids(TextureMappingZone return true; } +std::vector TextureMappingManager::texture_component_color_matches( + const TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours) +{ + if (!zone.enabled || zone.deleted || !zone.is_image_texture() || num_physical == 0) + return {}; + + const size_t expected = expected_component_count(zone.texture_mapping_mode, zone.filament_color_mode); + if (expected == 0) + return {}; + + const std::vector roles = semantic_colors(zone.filament_color_mode); + const std::vector role_names = semantic_color_names(zone.filament_color_mode); + if (roles.size() != expected || role_names.size() != expected) + return {}; + + const std::vector component_ids = effective_texture_component_ids(zone, num_physical, filament_colours); + const size_t count = std::min(expected, component_ids.size()); + std::vector matches; + matches.reserve(count); + for (size_t idx = 0; idx < count; ++idx) { + const unsigned int id = component_ids[idx]; + if (id < 1 || id > num_physical) + continue; + const float distance = std::sqrt(perceptual_color_distance_sq(filament_color(id, filament_colours), roles[idx])); + matches.push_back({id, role_names[idx], distance}); + } + return matches; +} + +float TextureMappingManager::poor_color_match_distance() +{ + return TextureMappingPoorColorMatchDistance; +} + float TextureMappingManager::max_component_surface_offset_mm(float reference_width_mm) { const float safe_reference = std::max(0.05f, std::abs(reference_width_mm)); diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index b20840660db..ef96ab00832 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -272,6 +272,13 @@ struct TextureMappingGlobalSettings static bool is_generic_solver_color_mode(const std::string &mode); }; +struct TextureMappingColorMatch +{ + unsigned int filament_id = 0; + std::string expected_color_name; + float perceptual_distance = 0.f; +}; + class TextureMappingManager { public: @@ -325,6 +332,10 @@ public: static bool auto_adjust_texture_component_ids(TextureMappingZone &zone, size_t num_physical, const std::vector &filament_colours); + static std::vector texture_component_color_matches(const TextureMappingZone &zone, + size_t num_physical, + const std::vector &filament_colours); + static float poor_color_match_distance(); static std::vector default_offset_distances(size_t component_count, float reference_width_mm = 0.4f); static std::vector default_offset_angles(size_t component_count);