Make sure expected_line_width_mm is stored when using image projection
This commit is contained in:
@@ -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 <chrono>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
@@ -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<std::string> 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<unsigned int> &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<unsigned int> 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<unsigned int> 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<bool> &used_states = volume->mmu_segmentation_facets.get_data().used_states;
|
||||
for (size_t state_idx = static_cast<size_t>(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<std::string> &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<float, 3> rgb { { 1.f, 1.f, 1.f } };
|
||||
};
|
||||
|
||||
static std::vector<RawAtlasChannelWarningInfoForGCode> raw_atlas_channel_warning_infos_for_gcode(const ModelVolume &volume)
|
||||
{
|
||||
const std::vector<ImageMapRawFilament> filaments =
|
||||
image_map_raw_filaments_from_metadata_json(volume.imported_texture_raw_metadata_json, volume.imported_texture_raw_channels);
|
||||
std::vector<RawAtlasChannelWarningInfoForGCode> 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<std::string> 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<std::string> warnings;
|
||||
std::set<std::string> seen;
|
||||
for (const PrintObject *print_object : print.objects()) {
|
||||
if (print_object == nullptr || print_object->model_object() == nullptr)
|
||||
continue;
|
||||
|
||||
const std::vector<unsigned int> 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<RawAtlasChannelWarningInfoForGCode> 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<unsigned int> 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<std::string> target_keys =
|
||||
raw_filament_color_mode_channel_keys_for_gcode(filament_color_mode, component_ids.size());
|
||||
if (!target_keys.empty()) {
|
||||
std::vector<std::string> atlas_labels;
|
||||
std::vector<std::string> missing;
|
||||
std::vector<std::string> unused;
|
||||
atlas_labels.reserve(atlas_channels.size());
|
||||
for (const RawAtlasChannelWarningInfoForGCode &channel : atlas_channels)
|
||||
atlas_labels.push_back(channel.label);
|
||||
|
||||
std::vector<size_t> target_to_channel(target_keys.size(), size_t(-1));
|
||||
std::vector<uint8_t> 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<float, 3> 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<float>::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<std::array<float, 3>> component_colors;
|
||||
std::vector<std::string> 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<std::array<float, 4>> 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<GenericRawAtlasCandidateForGCode> 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<size_t> component_to_channel(component_colors.size(), size_t(-1));
|
||||
std::vector<uint8_t> used_components(component_colors.size(), 0);
|
||||
std::vector<uint8_t> 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<std::string> poor_matches;
|
||||
std::vector<std::string> missing_components;
|
||||
std::vector<std::string> 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<Vec2f, 3> unwrap_triangle_uvs_for_sampling_for_gcode(const Vec2f &uv0,
|
||||
const Vec2f &uv1,
|
||||
const Vec2f &uv2)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
@@ -13,9 +11,7 @@
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#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<std::string> 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<float, 3> &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<float, 3> 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<float, 3> &lhs, const std::array<float, 3> &rhs)
|
||||
{
|
||||
const std::array<float, 3> lhs_oklab = color_solver_oklab_from_srgb(lhs);
|
||||
const std::array<float, 3> 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<std::string> &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<std::string> raw_filament_color_mode_channel_keys_for_warning(int filament_color_mode, size_t component_count)
|
||||
{
|
||||
std::vector<std::string> 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<float, 3> rgb { { 1.f, 1.f, 1.f } };
|
||||
};
|
||||
|
||||
static std::vector<RawTextureChannelWarningInfo> raw_texture_channel_warning_infos(const ModelVolume &volume)
|
||||
{
|
||||
const std::vector<ImageMapRawFilament> filaments =
|
||||
image_map_raw_filaments_from_metadata_json(volume.imported_texture_raw_metadata_json, volume.imported_texture_raw_channels);
|
||||
std::vector<RawTextureChannelWarningInfo> 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<float, 3> 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<std::string> 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<std::string> warnings;
|
||||
std::set<std::string> 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<int> 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<std::string> 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<std::string> warnings;
|
||||
std::set<std::string> seen;
|
||||
for (const ModelVolume *volume : model_object->volumes) {
|
||||
if (!model_volume_has_raw_offset_texture_data(volume))
|
||||
continue;
|
||||
|
||||
const std::vector<RawTextureChannelWarningInfo> atlas_channels = raw_texture_channel_warning_infos(*volume);
|
||||
if (atlas_channels.empty())
|
||||
continue;
|
||||
|
||||
const std::vector<int> 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<unsigned int> 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<std::string> target_keys =
|
||||
raw_filament_color_mode_channel_keys_for_warning(filament_color_mode, component_ids.size());
|
||||
if (!target_keys.empty()) {
|
||||
std::vector<std::string> atlas_labels;
|
||||
std::vector<std::string> missing;
|
||||
std::vector<std::string> unused;
|
||||
atlas_labels.reserve(atlas_channels.size());
|
||||
for (const RawTextureChannelWarningInfo &channel : atlas_channels)
|
||||
atlas_labels.push_back(channel.label);
|
||||
std::vector<size_t> target_to_channel(target_keys.size(), size_t(-1));
|
||||
std::vector<uint8_t> 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<float, 3> target_rgb = raw_texture_standard_channel_rgb(target_keys[target_idx]);
|
||||
size_t best_channel = atlas_channels.size();
|
||||
float best_distance = std::numeric_limits<float>::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<std::array<float, 3>> component_colors;
|
||||
std::vector<std::string> 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<float, 3> 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<GenericRawAtlasCandidate> 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<size_t> component_to_channel(component_colors.size(), size_t(-1));
|
||||
std::vector<uint8_t> used_components(component_colors.size(), 0);
|
||||
std::vector<uint8_t> 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<std::string> poor_matches;
|
||||
std::vector<std::string> missing_components;
|
||||
std::vector<std::string> 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<std::string> 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))
|
||||
|
||||
@@ -399,6 +399,7 @@ struct RawAtlasProjectionLayout
|
||||
std::vector<ImageMapRawFilament> filaments;
|
||||
std::vector<std::string> channel_keys;
|
||||
std::vector<size_t> 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<ImageMapRawFilament> volume_filaments =
|
||||
image_map_raw_filaments_from_metadata_json(volume->imported_texture_raw_metadata_json, volume->imported_texture_raw_channels);
|
||||
const std::vector<std::string> 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<ImageMapRawFilament> 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();
|
||||
|
||||
Reference in New Issue
Block a user