diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 67d5aaa86ae..84f447776f9 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -17,6 +17,7 @@ #include "TextureMapping.hpp" #include "ColorSolver.hpp" #include "ImageMapRawFilamentOffsetAtlas.hpp" +#include "TriangleSelector.hpp" #include "Utils.hpp" #include "ClipperUtils.hpp" #include "libslic3r.h" @@ -32,8 +33,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -2003,6 +2007,7 @@ namespace DoExport { } // namespace DoExport static bool print_has_raw_offset_texture_zone_without_raw_data_for_gcode(const Print &print); +static std::vector collect_raw_atlas_warnings_for_gcode(const Print &print); bool GCode::is_BBL_Printer() { @@ -2051,6 +2056,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu _(L("An object is assigned to a Raw filament offset texture mapping zone, but it does not contain raw filament " "offset atlas data. Slicing will interpret its regular RGB texture channels as raw offsets."))); } + for (const std::string &warning_msg : collect_raw_atlas_warnings_for_gcode(*print)) + print->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); // check if any custom gcode contains keywords used by the gcode processor to // produce time estimation and gcode toolpaths @@ -7213,6 +7220,327 @@ static bool print_has_raw_offset_texture_zone_without_raw_data_for_gcode(const P return false; } +static void append_image_texture_zone_id_for_raw_atlas_warning_for_gcode(const TextureMappingManager &texture_mgr, + int filament_id, + std::vector &zone_ids) +{ + if (filament_id <= 0) + return; + + const unsigned int filament_id_u = unsigned(filament_id); + const TextureMappingZone *zone = texture_mgr.zone_from_id(filament_id_u); + if (zone == nullptr || !zone->enabled || zone->deleted || !zone->is_image_texture()) + return; + if (std::find(zone_ids.begin(), zone_ids.end(), filament_id_u) == zone_ids.end()) + zone_ids.emplace_back(filament_id_u); +} + +static std::vector image_texture_zone_ids_for_raw_atlas_warning_for_gcode(const Print &print, const PrintObject &print_object) +{ + const ModelObject *model_object = print_object.model_object(); + if (model_object == nullptr) + return {}; + + const TextureMappingManager &texture_mgr = print.texture_mapping_manager(); + std::vector zone_ids; + for (const ModelVolume *volume : model_object->volumes) { + if (volume == nullptr) + continue; + + for (const int filament_id : volume->get_extruders()) + append_image_texture_zone_id_for_raw_atlas_warning_for_gcode(texture_mgr, filament_id, zone_ids); + + if (volume->mmu_segmentation_facets.empty()) + continue; + + const std::vector &used_states = volume->mmu_segmentation_facets.get_data().used_states; + for (size_t state_idx = static_cast(EnforcerBlockerType::Extruder1); state_idx < used_states.size(); ++state_idx) + if (used_states[state_idx]) + append_image_texture_zone_id_for_raw_atlas_warning_for_gcode(texture_mgr, int(state_idx), zone_ids); + } + return zone_ids; +} + +static std::string format_texture_mapping_line_width_mm_for_gcode(double value) +{ + std::ostringstream stream; + stream << std::fixed << std::setprecision(3) << value; + std::string formatted = stream.str(); + while (formatted.size() > 1 && formatted.back() == '0') + formatted.pop_back(); + if (!formatted.empty() && formatted.back() == '.') + formatted.pop_back(); + return formatted + " mm"; +} + +static std::string join_texture_mapping_labels_for_gcode(const std::vector &labels) +{ + std::string out; + for (size_t idx = 0; idx < labels.size(); ++idx) { + if (idx > 0) + out += ", "; + out += labels[idx]; + } + return out; +} + +struct RawAtlasChannelWarningInfoForGCode +{ + std::string key; + std::string label; + std::array rgb { { 1.f, 1.f, 1.f } }; +}; + +static std::vector raw_atlas_channel_warning_infos_for_gcode(const ModelVolume &volume) +{ + const std::vector filaments = + image_map_raw_filaments_from_metadata_json(volume.imported_texture_raw_metadata_json, volume.imported_texture_raw_channels); + std::vector infos; + infos.reserve(filaments.size()); + for (size_t channel = 0; channel < filaments.size(); ++channel) { + const ImageMapRawFilament &filament = filaments[channel]; + const std::string key = image_map_raw_filament_channel_key(filament, channel); + std::string label = key; + if (key.size() != 1 || !image_map_raw_filament_is_standard_color(key)) { + const unsigned int slot = filament.slot != 0 ? filament.slot : unsigned(channel + 1); + label = filament.hex.empty() ? "slot " + std::to_string(slot) : "slot " + std::to_string(slot) + " " + filament.hex; + } + infos.push_back({ key, label, raw_filament_channel_color_for_gcode(filament, channel) }); + } + return infos; +} + +static std::vector collect_raw_atlas_warnings_for_gcode(const Print &print) +{ + const double active_max_line_width_mm = + std::max(0.05, print.config().texture_mapping_outer_wall_gradient_max_line_width.value); + const double active_min_line_width_mm = + std::clamp(print.config().texture_mapping_outer_wall_gradient_min_line_width.value, 0.05, active_max_line_width_mm); + const float poor_match_distance_sq = + TextureMappingManager::poor_color_match_distance() * TextureMappingManager::poor_color_match_distance(); + + std::vector warnings; + std::set seen; + for (const PrintObject *print_object : print.objects()) { + if (print_object == nullptr || print_object->model_object() == nullptr) + continue; + + const std::vector used_texture_zone_ids = + image_texture_zone_ids_for_raw_atlas_warning_for_gcode(print, *print_object); + if (used_texture_zone_ids.empty()) + continue; + + for (const ModelVolume *volume : print_object->model_object()->volumes) { + if (!model_volume_has_raw_offset_texture_data_for_gcode(volume)) + continue; + + const ImageMapRawExpectedLineWidth expected = + image_map_raw_expected_line_width_from_metadata_json(volume->imported_texture_raw_metadata_json); + if (expected.valid && expected.warn_if_differs && + (std::abs(active_min_line_width_mm - expected.min_mm) > 0.001 || + std::abs(active_max_line_width_mm - expected.max_mm) > 0.001)) { + for (const unsigned int zone_id : used_texture_zone_ids) { + std::ostringstream key_stream; + key_stream << "width|" << print_object->id().id << "|" << volume->id().id << "|" << zone_id << "|" << + std::fixed << std::setprecision(3) << expected.min_mm << "|" << expected.max_mm; + if (!seen.insert(key_stream.str()).second) + continue; + + warnings.emplace_back( + _(L("Texture mapping zone ")) + std::to_string(zone_id) + + _(L(" uses a raw filament offset atlas designed for line widths ")) + + format_texture_mapping_line_width_mm_for_gcode(expected.min_mm) + " - " + + format_texture_mapping_line_width_mm_for_gcode(expected.max_mm) + + _(L(", but current texture mapping settings use ")) + + format_texture_mapping_line_width_mm_for_gcode(active_min_line_width_mm) + " - " + + format_texture_mapping_line_width_mm_for_gcode(active_max_line_width_mm) + + _(L(". Update the texture mapping minimum/maximum outer wall line width or regenerate the raw offset atlas."))); + } + } + + const std::vector atlas_channels = + raw_atlas_channel_warning_infos_for_gcode(*volume); + if (atlas_channels.empty()) + continue; + + for (const unsigned int zone_id : used_texture_zone_ids) { + const TextureMappingZone *zone = print.texture_mapping_manager().zone_from_id(zone_id); + if (zone == nullptr || !zone->enabled || zone->deleted || !zone->is_image_texture()) + continue; + + const std::vector component_ids = + TextureMappingManager::effective_texture_component_ids(*zone, + print.config().filament_colour.size(), + print.config().filament_colour.values); + if (component_ids.empty()) + continue; + + const int filament_color_mode = std::clamp(zone->filament_color_mode, + int(TextureMappingZone::FilamentColorAny), + int(TextureMappingZone::FilamentColorRGBKW)); + std::ostringstream zone_key_stream; + zone_key_stream << "channels|" << print_object->id().id << "|" << volume->id().id << "|" << zone_id << "|" << + filament_color_mode; + if (seen.find(zone_key_stream.str()) != seen.end()) + continue; + + const std::vector target_keys = + raw_filament_color_mode_channel_keys_for_gcode(filament_color_mode, component_ids.size()); + if (!target_keys.empty()) { + std::vector atlas_labels; + std::vector missing; + std::vector unused; + atlas_labels.reserve(atlas_channels.size()); + for (const RawAtlasChannelWarningInfoForGCode &channel : atlas_channels) + atlas_labels.push_back(channel.label); + + std::vector target_to_channel(target_keys.size(), size_t(-1)); + std::vector used_channels(atlas_channels.size(), 0); + for (size_t target_idx = 0; target_idx < target_keys.size(); ++target_idx) { + for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { + if (used_channels[channel_idx] == 0 && atlas_channels[channel_idx].key == target_keys[target_idx]) { + target_to_channel[target_idx] = channel_idx; + used_channels[channel_idx] = 1; + break; + } + } + } + for (size_t target_idx = 0; target_idx < target_keys.size(); ++target_idx) { + if (target_to_channel[target_idx] != size_t(-1)) + continue; + const std::array target_rgb = + raw_filament_channel_color_for_gcode({ 0, target_keys[target_idx], std::string() }, target_idx); + size_t best_channel = atlas_channels.size(); + float best_distance_sq = std::numeric_limits::max(); + for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { + if (used_channels[channel_idx] != 0) + continue; + const float distance_sq = + raw_filament_color_distance_sq_for_gcode(target_rgb, atlas_channels[channel_idx].rgb); + if (distance_sq < best_distance_sq) { + best_distance_sq = distance_sq; + best_channel = channel_idx; + } + } + if (best_channel < atlas_channels.size() && best_distance_sq <= poor_match_distance_sq) { + target_to_channel[target_idx] = best_channel; + used_channels[best_channel] = 1; + } + } + + for (size_t target_idx = 0; target_idx < target_keys.size(); ++target_idx) + if (target_to_channel[target_idx] == size_t(-1)) + missing.push_back(target_keys[target_idx]); + for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) + if (used_channels[channel_idx] == 0) + unused.push_back(atlas_channels[channel_idx].label); + + if (missing.empty() && unused.empty()) + continue; + + seen.insert(zone_key_stream.str()); + std::string message = + _(L("Object has raw atlas data with channels [")) + join_texture_mapping_labels_for_gcode(atlas_labels) + + _(L("], but texture mapping zone ")) + std::to_string(zone_id) + _(L(" uses [")) + join_texture_mapping_labels_for_gcode(target_keys) + "]."; + if (!missing.empty()) + message += _(L(" Missing channels will use 0 offset/minimum line width: [")) + + join_texture_mapping_labels_for_gcode(missing) + "]."; + if (!unused.empty()) + message += _(L(" Unused atlas channels will be ignored: [")) + + join_texture_mapping_labels_for_gcode(unused) + "]."; + warnings.emplace_back(std::move(message)); + continue; + } + + struct GenericRawAtlasCandidateForGCode { + float distance_sq { 0.f }; + size_t component_idx { 0 }; + size_t channel_idx { 0 }; + }; + + std::vector> component_colors; + std::vector component_labels; + component_colors.reserve(component_ids.size()); + component_labels.reserve(component_ids.size()); + for (const unsigned int component_id : component_ids) { + if (component_id < 1 || component_id > print.config().filament_colour.size()) + continue; + const std::optional> parsed = + parse_texture_mapping_color_hex_for_gcode(print.config().filament_colour.get_at(size_t(component_id - 1))); + if (!parsed) + continue; + component_colors.push_back({ { (*parsed)[0], (*parsed)[1], (*parsed)[2] } }); + component_labels.push_back("F" + std::to_string(component_id)); + } + if (component_colors.empty()) + continue; + + std::vector candidates; + candidates.reserve(component_colors.size() * atlas_channels.size()); + for (size_t component_idx = 0; component_idx < component_colors.size(); ++component_idx) + for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) + candidates.push_back({ + raw_filament_color_distance_sq_for_gcode(component_colors[component_idx], atlas_channels[channel_idx].rgb), + component_idx, + channel_idx + }); + std::sort(candidates.begin(), candidates.end(), [](const GenericRawAtlasCandidateForGCode &lhs, + const GenericRawAtlasCandidateForGCode &rhs) { + return lhs.distance_sq < rhs.distance_sq; + }); + + std::vector component_to_channel(component_colors.size(), size_t(-1)); + std::vector used_components(component_colors.size(), 0); + std::vector used_channels(atlas_channels.size(), 0); + for (const GenericRawAtlasCandidateForGCode &candidate : candidates) { + if (used_components[candidate.component_idx] != 0 || used_channels[candidate.channel_idx] != 0) + continue; + component_to_channel[candidate.component_idx] = candidate.channel_idx; + used_components[candidate.component_idx] = 1; + used_channels[candidate.channel_idx] = 1; + } + + std::vector poor_matches; + std::vector missing_components; + std::vector unused_channels; + for (size_t component_idx = 0; component_idx < component_to_channel.size(); ++component_idx) { + const size_t channel_idx = component_to_channel[component_idx]; + if (channel_idx == size_t(-1)) { + missing_components.push_back(component_labels[component_idx]); + continue; + } + const float distance_sq = + raw_filament_color_distance_sq_for_gcode(component_colors[component_idx], atlas_channels[channel_idx].rgb); + if (distance_sq > poor_match_distance_sq) + poor_matches.push_back(component_labels[component_idx] + " to " + atlas_channels[channel_idx].label); + } + for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) + if (used_channels[channel_idx] == 0) + unused_channels.push_back(atlas_channels[channel_idx].label); + + if (poor_matches.empty() && missing_components.empty() && unused_channels.empty()) + continue; + + seen.insert(zone_key_stream.str()); + std::string message = + _(L("Texture mapping zone ")) + std::to_string(zone_id) + + _(L("'s filament colors may not match " + "the selected filaments in an object's raw offset atlas data.")); + if (!poor_matches.empty()) + message += _(L(" Poor color matches: ")) + join_texture_mapping_labels_for_gcode(poor_matches) + "."; + if (!missing_components.empty()) + message += _(L(" Unmatched filaments will use 0 offset/minimum line width: ")) + + join_texture_mapping_labels_for_gcode(missing_components) + "."; + if (!unused_channels.empty()) + message += _(L(" Unused atlas channels will be ignored: ")) + + join_texture_mapping_labels_for_gcode(unused_channels) + "."; + warnings.emplace_back(std::move(message)); + } + } + } + return warnings; +} + static std::array unwrap_triangle_uvs_for_sampling_for_gcode(const Vec2f &uv0, const Vec2f &uv1, const Vec2f &uv2) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index c090cf91d10..c3c40f0ab4a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,8 +1,6 @@ #include #include -#include -#include #include #include #include @@ -13,9 +11,7 @@ #include #include "ClipperUtils.hpp" -#include "ColorSolver.hpp" #include "ElephantFootCompensation.hpp" -#include "ImageMapRawFilamentOffsetAtlas.hpp" #include "I18N.hpp" #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" @@ -176,444 +172,6 @@ static std::vector collect_texture_mapping_outer_wall_gradient_line return warnings; } -static bool model_volume_has_raw_offset_texture_data(const ModelVolume *volume) -{ - return volume != nullptr && - volume->imported_texture_width > 0 && - volume->imported_texture_height > 0 && - volume->imported_texture_raw_channels > 0 && - volume->imported_texture_raw_filament_offsets.size() >= - size_t(volume->imported_texture_width) * - size_t(volume->imported_texture_height) * - size_t(volume->imported_texture_raw_channels); -} - -static std::string format_texture_mapping_line_width_mm(double value) -{ - std::ostringstream stream; - stream << std::fixed << std::setprecision(3) << value; - std::string formatted = stream.str(); - while (formatted.size() > 1 && formatted.back() == '0') - formatted.pop_back(); - if (!formatted.empty() && formatted.back() == '.') - formatted.pop_back(); - return formatted + " mm"; -} - -static bool texture_mapping_line_width_differs(double lhs, double rhs) -{ - return std::abs(lhs - rhs) > 0.001; -} - -static int texture_mapping_hex_digit(char ch) -{ - return ch >= '0' && ch <= '9' ? ch - '0' : - ch >= 'a' && ch <= 'f' ? ch - 'a' + 10 : - ch >= 'A' && ch <= 'F' ? ch - 'A' + 10 : -1; -} - -static bool parse_texture_mapping_hex_rgb(const std::string &hex, std::array &rgb) -{ - const size_t hash_pos = hex.find('#'); - const size_t start = hash_pos == std::string::npos ? 0 : hash_pos + 1; - if (start + 6 > hex.size()) - return false; - - int values[3] = { 0, 0, 0 }; - for (size_t channel = 0; channel < 3; ++channel) { - const int hi = texture_mapping_hex_digit(hex[start + channel * 2]); - const int lo = texture_mapping_hex_digit(hex[start + channel * 2 + 1]); - if (hi < 0 || lo < 0) - return false; - values[channel] = hi * 16 + lo; - } - - rgb = { - float(values[0]) / 255.f, - float(values[1]) / 255.f, - float(values[2]) / 255.f - }; - return true; -} - -static std::array raw_texture_standard_channel_rgb(const std::string &key) -{ - if (key == "C") - return { { 0.f, 1.f, 1.f } }; - if (key == "M") - return { { 1.f, 0.f, 1.f } }; - if (key == "Y") - return { { 1.f, 1.f, 0.f } }; - if (key == "K") - return { { 0.f, 0.f, 0.f } }; - if (key == "W") - return { { 1.f, 1.f, 1.f } }; - if (key == "R") - return { { 1.f, 0.f, 0.f } }; - if (key == "G") - return { { 0.f, 1.f, 0.f } }; - if (key == "B") - return { { 0.f, 0.f, 1.f } }; - return { { 1.f, 1.f, 1.f } }; -} - -static float texture_mapping_oklab_distance(const std::array &lhs, const std::array &rhs) -{ - const std::array lhs_oklab = color_solver_oklab_from_srgb(lhs); - const std::array rhs_oklab = color_solver_oklab_from_srgb(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]; - return std::sqrt(dl * dl + da * da + db * db); -} - -static std::string join_texture_mapping_labels(const std::vector &labels) -{ - std::string out; - for (size_t idx = 0; idx < labels.size(); ++idx) { - if (idx > 0) - out += ", "; - out += labels[idx]; - } - return out; -} - -static std::vector raw_filament_color_mode_channel_keys_for_warning(int filament_color_mode, size_t component_count) -{ - std::vector keys; - switch (std::clamp(filament_color_mode, int(TextureMappingZone::FilamentColorAny), int(TextureMappingZone::FilamentColorRGBKW))) { - case int(TextureMappingZone::FilamentColorRGB): - keys = { "R", "G", "B" }; - break; - case int(TextureMappingZone::FilamentColorCMY): - keys = { "C", "M", "Y" }; - break; - case int(TextureMappingZone::FilamentColorCMYK): - keys = { "C", "M", "Y", "K" }; - break; - case int(TextureMappingZone::FilamentColorCMYW): - keys = { "C", "M", "Y", "W" }; - break; - case int(TextureMappingZone::FilamentColorRGBK): - keys = { "R", "G", "B", "K" }; - break; - case int(TextureMappingZone::FilamentColorRGBW): - keys = { "R", "G", "B", "W" }; - break; - case int(TextureMappingZone::FilamentColorBW): - keys = { "K", "W" }; - break; - case int(TextureMappingZone::FilamentColorCMYKW): - keys = { "C", "M", "Y", "K", "W" }; - break; - case int(TextureMappingZone::FilamentColorRGBKW): - keys = { "R", "G", "B", "K", "W" }; - break; - default: - break; - } - if (keys.size() > component_count) - keys.resize(component_count); - return keys; -} - -struct RawTextureChannelWarningInfo -{ - size_t channel { 0 }; - std::string key; - std::string label; - std::array rgb { { 1.f, 1.f, 1.f } }; -}; - -static std::vector raw_texture_channel_warning_infos(const ModelVolume &volume) -{ - const std::vector filaments = - image_map_raw_filaments_from_metadata_json(volume.imported_texture_raw_metadata_json, volume.imported_texture_raw_channels); - std::vector infos; - infos.reserve(filaments.size()); - for (size_t channel = 0; channel < filaments.size(); ++channel) { - const ImageMapRawFilament &filament = filaments[channel]; - const std::string key = image_map_raw_filament_channel_key(filament, channel); - std::string label = key; - std::array rgb = raw_texture_standard_channel_rgb(key); - if (key.size() != 1 || !image_map_raw_filament_is_standard_color(key)) { - const unsigned int slot = filament.slot != 0 ? filament.slot : unsigned(channel + 1); - label = filament.hex.empty() ? "slot " + std::to_string(slot) : "slot " + std::to_string(slot) + " " + filament.hex; - if (!filament.hex.empty()) - parse_texture_mapping_hex_rgb(filament.hex, rgb); - } - infos.push_back({ channel, key, label, rgb }); - } - return infos; -} - -static std::vector collect_texture_mapping_raw_atlas_line_width_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 double active_max_line_width_mm = - std::max(0.05, print->config().texture_mapping_outer_wall_gradient_max_line_width.value); - const double active_min_line_width_mm = - std::clamp(print->config().texture_mapping_outer_wall_gradient_min_line_width.value, 0.05, active_max_line_width_mm); - - std::vector warnings; - std::set seen; - for (const ModelVolume *volume : model_object->volumes) { - if (!model_volume_has_raw_offset_texture_data(volume)) - continue; - - const ImageMapRawExpectedLineWidth expected = - image_map_raw_expected_line_width_from_metadata_json(volume->imported_texture_raw_metadata_json); - if (!expected.valid || !expected.warn_if_differs) - continue; - if (!texture_mapping_line_width_differs(active_min_line_width_mm, expected.min_mm) && - !texture_mapping_line_width_differs(active_max_line_width_mm, expected.max_mm)) - 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); - 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; - - std::ostringstream key_stream; - key_stream << filament_id_u << "|" << std::fixed << std::setprecision(3) << expected.min_mm << "|" << expected.max_mm; - const std::string key = key_stream.str(); - if (!seen.insert(key).second) - continue; - - warnings.emplace_back( - L("Texture mapping zone ") + std::to_string(filament_id_u) + - L(" uses a raw filament offset atlas authored for line widths ") + - format_texture_mapping_line_width_mm(expected.min_mm) + " - " + - format_texture_mapping_line_width_mm(expected.max_mm) + - L(", but current texture mapping settings use ") + - format_texture_mapping_line_width_mm(active_min_line_width_mm) + " - " + - format_texture_mapping_line_width_mm(active_max_line_width_mm) + - L(". Update the texture mapping minimum/maximum outer wall line width or regenerate the raw offset atlas.")); - } - } - - return warnings; -} - -static std::vector collect_texture_mapping_raw_atlas_channel_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; - for (const ModelVolume *volume : model_object->volumes) { - if (!model_volume_has_raw_offset_texture_data(volume)) - continue; - - const std::vector atlas_channels = raw_texture_channel_warning_infos(*volume); - if (atlas_channels.empty()) - 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); - 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 component_ids = TextureMappingManager::selected_component_ids(*zone, num_physical); - if (component_ids.empty()) - continue; - - const int filament_color_mode = std::clamp(zone->filament_color_mode, - int(TextureMappingZone::FilamentColorAny), - int(TextureMappingZone::FilamentColorRGBKW)); - const std::string zone_key = - std::to_string(filament_id_u) + "|" + std::to_string(filament_color_mode) + "|" + std::to_string(volume->id().id); - if (seen.find(zone_key) != seen.end()) - continue; - - const std::vector target_keys = - raw_filament_color_mode_channel_keys_for_warning(filament_color_mode, component_ids.size()); - if (!target_keys.empty()) { - std::vector atlas_labels; - std::vector missing; - std::vector unused; - atlas_labels.reserve(atlas_channels.size()); - for (const RawTextureChannelWarningInfo &channel : atlas_channels) - atlas_labels.push_back(channel.label); - std::vector target_to_channel(target_keys.size(), size_t(-1)); - std::vector used_channels(atlas_channels.size(), 0); - for (size_t target_idx = 0; target_idx < target_keys.size(); ++target_idx) { - for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { - if (used_channels[channel_idx] == 0 && atlas_channels[channel_idx].key == target_keys[target_idx]) { - target_to_channel[target_idx] = channel_idx; - used_channels[channel_idx] = 1; - break; - } - } - } - - const float max_match_distance = TextureMappingManager::poor_color_match_distance(); - for (size_t target_idx = 0; target_idx < target_keys.size(); ++target_idx) { - if (target_to_channel[target_idx] != size_t(-1)) - continue; - const std::array target_rgb = raw_texture_standard_channel_rgb(target_keys[target_idx]); - size_t best_channel = atlas_channels.size(); - float best_distance = std::numeric_limits::max(); - for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { - if (used_channels[channel_idx] != 0) - continue; - const float distance = texture_mapping_oklab_distance(target_rgb, atlas_channels[channel_idx].rgb); - if (distance < best_distance) { - best_distance = distance; - best_channel = channel_idx; - } - } - if (best_channel < atlas_channels.size() && best_distance <= max_match_distance) { - target_to_channel[target_idx] = best_channel; - used_channels[best_channel] = 1; - } - } - - for (size_t target_idx = 0; target_idx < target_keys.size(); ++target_idx) { - if (target_to_channel[target_idx] == size_t(-1)) - missing.push_back(target_keys[target_idx]); - } - for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { - if (used_channels[channel_idx] == 0) - unused.push_back(atlas_channels[channel_idx].label); - } - - if (missing.empty() && unused.empty()) - continue; - - seen.insert(zone_key); - std::string message = - L("Texture mapping zone ") + std::to_string(filament_id_u) + - L(" uses raw atlas channels [") + join_texture_mapping_labels(atlas_labels) + - L("], but slicing is using channels [") + join_texture_mapping_labels(target_keys) + L("]."); - if (!missing.empty()) - message += L(" Missing channels will use 0 offset/minimum line width: ") + join_texture_mapping_labels(missing) + "."; - if (!unused.empty()) - message += L(" Unused atlas channels will be ignored: ") + join_texture_mapping_labels(unused) + "."; - warnings.emplace_back(std::move(message)); - continue; - } - - struct GenericRawAtlasCandidate { - float distance { 0.f }; - size_t component_idx { 0 }; - size_t channel_idx { 0 }; - }; - - std::vector> component_colors; - std::vector component_labels; - component_colors.reserve(component_ids.size()); - component_labels.reserve(component_ids.size()); - for (const unsigned int component_id : component_ids) { - if (component_id < 1 || component_id > num_physical) - continue; - std::array rgb; - if (!parse_texture_mapping_hex_rgb(print->config().filament_colour.get_at(size_t(component_id - 1)), rgb)) - continue; - component_colors.push_back(rgb); - component_labels.push_back("F" + std::to_string(component_id)); - } - if (component_colors.empty()) - continue; - - std::vector candidates; - candidates.reserve(component_colors.size() * atlas_channels.size()); - for (size_t component_idx = 0; component_idx < component_colors.size(); ++component_idx) { - for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { - candidates.push_back({ - texture_mapping_oklab_distance(component_colors[component_idx], atlas_channels[channel_idx].rgb), - component_idx, - channel_idx - }); - } - } - std::sort(candidates.begin(), candidates.end(), [](const GenericRawAtlasCandidate &lhs, const GenericRawAtlasCandidate &rhs) { - return lhs.distance < rhs.distance; - }); - - std::vector component_to_channel(component_colors.size(), size_t(-1)); - std::vector used_components(component_colors.size(), 0); - std::vector used_channels(atlas_channels.size(), 0); - for (const GenericRawAtlasCandidate &candidate : candidates) { - if (used_components[candidate.component_idx] != 0 || used_channels[candidate.channel_idx] != 0) - continue; - component_to_channel[candidate.component_idx] = candidate.channel_idx; - used_components[candidate.component_idx] = 1; - used_channels[candidate.channel_idx] = 1; - } - - std::vector poor_matches; - std::vector missing_components; - std::vector unused_channels; - for (size_t component_idx = 0; component_idx < component_to_channel.size(); ++component_idx) { - const size_t channel_idx = component_to_channel[component_idx]; - if (channel_idx == size_t(-1)) { - missing_components.push_back(component_labels[component_idx]); - continue; - } - const float distance = texture_mapping_oklab_distance(component_colors[component_idx], atlas_channels[channel_idx].rgb); - if (distance > TextureMappingManager::poor_color_match_distance()) - poor_matches.push_back(component_labels[component_idx] + " to " + atlas_channels[channel_idx].label); - } - for (size_t channel_idx = 0; channel_idx < atlas_channels.size(); ++channel_idx) { - if (used_channels[channel_idx] == 0) - unused_channels.push_back(atlas_channels[channel_idx].label); - } - - if (poor_matches.empty() && missing_components.empty() && unused_channels.empty()) - continue; - - seen.insert(zone_key); - std::string message = - L("Texture mapping zone ") + std::to_string(filament_id_u) + - L(" uses Generic Solver with a raw filament offset atlas whose channel colors may not match the selected filaments."); - if (!poor_matches.empty()) - message += L(" Poor color matches: ") + join_texture_mapping_labels(poor_matches) + "."; - if (!missing_components.empty()) - message += L(" Unmatched filaments will use 0 offset/minimum line width: ") + - join_texture_mapping_labels(missing_components) + "."; - if (!unused_channels.empty()) - message += L(" Unused atlas channels will be ignored: ") + join_texture_mapping_labels(unused_channels) + "."; - warnings.emplace_back(std::move(message)); - } - } - - return warnings; -} - static std::vector collect_texture_mapping_vertex_color_match_warnings(const PrintObject &print_object) { const Print *print = print_object.print(); @@ -1847,10 +1405,6 @@ void PrintObject::slice_volumes() this->apply_conical_overhang(); for (const std::string &warning_msg : collect_texture_mapping_outer_wall_gradient_line_width_warnings(*this)) this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); - for (const std::string &warning_msg : collect_texture_mapping_raw_atlas_line_width_warnings(*this)) - this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_msg); - for (const std::string &warning_msg : collect_texture_mapping_raw_atlas_channel_warnings(*this)) - 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 &error_msg : collect_texture_mapping_vertex_color_mode_mismatch_errors(*this)) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp index 2d37f3cb9fa..038e9de984b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.cpp @@ -399,6 +399,7 @@ struct RawAtlasProjectionLayout std::vector filaments; std::vector channel_keys; std::vector atlas_to_target_channel; + ImageMapRawExpectedLineWidth expected_line_width_mm; }; static bool model_volume_has_raw_atlas_texture_data(const ModelVolume *volume) @@ -525,6 +526,10 @@ static bool append_object_raw_atlas_layout(const ModelObject &object, RawAtlasPr const std::vector volume_filaments = image_map_raw_filaments_from_metadata_json(volume->imported_texture_raw_metadata_json, volume->imported_texture_raw_channels); const std::vector volume_keys = image_map_raw_filament_channel_keys(volume_filaments); + const ImageMapRawExpectedLineWidth expected = + image_map_raw_expected_line_width_from_metadata_json(volume->imported_texture_raw_metadata_json); + if (expected.valid && !layout.expected_line_width_mm.valid) + layout.expected_line_width_mm = expected; if (!raw_channel_keys_are_unique(volume_keys)) { if (error != nullptr) *error = "The selected object's existing raw filament offset metadata has duplicate channels."; @@ -551,6 +556,8 @@ static bool raw_atlas_projection_layout_for_object(const ModelObject &object, if (!append_object_raw_atlas_layout(object, layout, error)) return false; + if (atlas.expected_line_width_mm.valid) + layout.expected_line_width_mm = atlas.expected_line_width_mm; const std::vector atlas_filaments = image_map_raw_filaments_for_channels(atlas.filaments, atlas.channels); @@ -597,6 +604,13 @@ static std::string raw_layout_metadata_json(uint32_t width, uint32_t height, con entry["hex"] = filament.hex; root["filaments"].push_back(std::move(entry)); } + if (layout.expected_line_width_mm.valid) { + root["expected_line_width_mm"] = { + { "min", layout.expected_line_width_mm.min_mm }, + { "max", layout.expected_line_width_mm.max_mm }, + { "warn_if_differs", layout.expected_line_width_mm.warn_if_differs } + }; + } return root.dump(); } @@ -12147,6 +12161,7 @@ void GLGizmoImageProjection::on_load(cereal::BinaryInputArchive& ar) m_raw_atlas.offsets = std::move(raw_offsets); m_raw_atlas.mask = std::move(raw_mask); m_raw_atlas.metadata_json = std::move(raw_metadata_json); + m_raw_atlas.expected_line_width_mm = image_map_raw_expected_line_width_from_metadata_json(m_raw_atlas.metadata_json); if (!m_raw_atlas.valid()) m_raw_atlas = {}; m_overlay_texture.reset();