From 5540971e7f5d318dbd36098141c986425c8401b2 Mon Sep 17 00:00:00 2001 From: sentientstardust Date: Mon, 1 Jun 2026 14:36:38 +0100 Subject: [PATCH] Allow loading filament calibration model (top-surface coloring) --- src/libslic3r/ColorSolver.cpp | 203 ++++++- src/libslic3r/ColorSolver.hpp | 31 +- src/libslic3r/TextureMapping.cpp | 606 +++++++++++++++++++- src/libslic3r/TextureMapping.hpp | 58 +- src/libslic3r/TextureMappingContoning.cpp | 26 +- src/libslic3r/TextureMappingContoning.hpp | 1 + src/slic3r/GUI/MMUPaintedTexturePreview.cpp | 56 +- src/slic3r/GUI/Plater.cpp | 321 +++++++++-- 8 files changed, 1223 insertions(+), 79 deletions(-) diff --git a/src/libslic3r/ColorSolver.cpp b/src/libslic3r/ColorSolver.cpp index 21b2bca50cf..fc5ac7f0ad4 100644 --- a/src/libslic3r/ColorSolver.cpp +++ b/src/libslic3r/ColorSolver.cpp @@ -80,6 +80,106 @@ std::array linear_to_srgb_color(const std::array &rgb) }; } +std::array eval_linear_rgb_model(const std::vector &features, + const std::vector &coefficients) +{ + if (features.empty() || coefficients.size() != features.size() * 3) + return { 0.f, 0.f, 0.f }; + + std::array linear { 0.f, 0.f, 0.f }; + for (size_t feature_idx = 0; feature_idx < features.size(); ++feature_idx) { + const float feature = std::isfinite(features[feature_idx]) ? features[feature_idx] : 0.f; + for (size_t channel = 0; channel < 3; ++channel) { + const float coeff = coefficients[feature_idx * 3 + channel]; + if (std::isfinite(coeff)) + linear[channel] += feature * coeff; + } + } + for (float &channel : linear) + channel = clamp01(channel); + return linear_to_srgb_color(linear); +} + +std::array eval_current_linear_affine_model(const std::array &base_srgb, + const std::vector &coefficients) +{ + if (coefficients.size() != 12) + return base_srgb; + const std::array base_linear = srgb_to_linear_color(base_srgb); + return eval_linear_rgb_model({ base_linear[0], base_linear[1], base_linear[2], 1.f }, coefficients); +} + +std::array eval_alpha_effective_model(const std::vector &surface_to_deep, + const ColorSolverCalibratedStackModel &model) +{ + const size_t component_count = model.component_count; + if (component_count == 0 || model.alphas.size() < component_count || + model.coefficients_linear_rgb.size() != (component_count + 1) * 3) + return { 0.f, 0.f, 0.f }; + + std::vector features(component_count + 1, 0.f); + float remaining = 1.f; + for (uint16_t component_idx : surface_to_deep) { + const size_t component = size_t(component_idx); + if (component >= component_count) + continue; + const float alpha = std::clamp(std::isfinite(model.alphas[component]) ? model.alphas[component] : 0.f, 0.f, 0.9999f); + features[component] += remaining * alpha; + remaining *= 1.f - alpha; + if (remaining <= 1e-5f) + break; + } + features[component_count] = std::max(0.f, remaining); + return eval_linear_rgb_model(features, model.coefficients_linear_rgb); +} + +std::array eval_depth_kernel_linear_model(const std::vector &surface_to_deep, + const ColorSolverCalibratedStackModel &model) +{ + const size_t component_count = model.component_count; + const size_t kernel_count = model.taus.size(); + const size_t feature_count = 1 + component_count + component_count * kernel_count; + if (component_count == 0 || kernel_count == 0 || + model.coefficients_linear_rgb.size() != feature_count * 3) + return { 0.f, 0.f, 0.f }; + + std::vector features(feature_count, 0.f); + features[0] = 1.f; + if (!surface_to_deep.empty() && size_t(surface_to_deep.front()) < component_count) + features[1 + size_t(surface_to_deep.front())] = 1.f; + + for (size_t kernel_idx = 0; kernel_idx < kernel_count; ++kernel_idx) { + const float tau = model.taus[kernel_idx]; + const size_t base = 1 + component_count + kernel_idx * component_count; + float norm = 0.f; + for (size_t depth_idx = 0; depth_idx < surface_to_deep.size(); ++depth_idx) { + const size_t component = size_t(surface_to_deep[depth_idx]); + if (component >= component_count) + continue; + const float weight = + std::isfinite(tau) && tau > 0.f ? + std::exp(-float(depth_idx) / tau) : + 1.f; + norm += weight; + features[base + component] += weight; + } + if (norm > 1e-12f) { + const float inv_norm = 1.f / norm; + for (size_t component = 0; component < component_count; ++component) + features[base + component] *= inv_norm; + } + } + return eval_linear_rgb_model(features, model.coefficients_linear_rgb); +} + +bool calibrated_stack_model_valid_for_mix(const ColorSolverCalibratedStackModel *model, + size_t component_count) +{ + return model != nullptr && model->valid() && + (model->kind == ColorSolverCalibratedStackModelKind::CurrentLinearAffine || + model->component_count == component_count); +} + float linear_luminance(const std::array &rgb) { return 0.2126f * rgb[0] + 0.7152f * rgb[1] + 0.0722f * rgb[2]; @@ -688,17 +788,44 @@ std::array mix_ordered_stack_with_buffers(const std::vector &component_roles, - const std::vector &layer_opacities_by_depth) + const std::vector &layer_opacities_by_depth, + const ColorSolverCalibratedStackModel *calibrated_stack_model) { if (colors_with_background.empty() || surface_to_deep.empty()) return colors_with_background.empty() ? std::array{ { 0.f, 0.f, 0.f } } : colors_with_background.back(); + const size_t component_count = colors_with_background.size() - 1; + if (calibrated_stack_model_valid_for_mix(calibrated_stack_model, component_count)) { + switch (calibrated_stack_model->kind) { + case ColorSolverCalibratedStackModelKind::CurrentLinearAffine: { + const std::array base = + mix_ordered_stack_with_buffers(colors_with_background, + weights, + surface_to_deep, + layer_opacities, + mix_model, + surface_scatter, + beer_lambert_rgb_correction, + td_effective_alpha_correction, + component_roles, + layer_opacities_by_depth, + nullptr); + return eval_current_linear_affine_model(base, calibrated_stack_model->coefficients_linear_rgb); + } + case ColorSolverCalibratedStackModelKind::AlphaEffective: + return eval_alpha_effective_model(surface_to_deep, *calibrated_stack_model); + case ColorSolverCalibratedStackModelKind::DepthKernelLinear: + return eval_depth_kernel_linear_model(surface_to_deep, *calibrated_stack_model); + default: + break; + } + } + if (td_effective_alpha_correction) return mix_ordered_stack_td_effective_alpha(colors_with_background, surface_to_deep, layer_opacities, 0.f, component_roles, layer_opacities_by_depth); if (beer_lambert_rgb_correction) return mix_ordered_stack_beer_lambert_rgb(colors_with_background, surface_to_deep, layer_opacities, surface_scatter, layer_opacities_by_depth); - const size_t component_count = colors_with_background.size() - 1; weights.assign(colors_with_background.size(), 0.f); float transmission = 1.f; bool surface_layer = true; @@ -1041,7 +1168,8 @@ void append_ordered_stack_candidate(ColorSolverOrderedStackCandidateSet &c bool beer_lambert_rgb_correction, bool td_effective_alpha_correction, const std::vector &component_roles, - const std::vector &layer_opacities_by_depth) + const std::vector &layer_opacities_by_depth, + const ColorSolverCalibratedStackModel *calibrated_stack_model) { std::vector simulated_surface_to_deep; const std::vector *mix_stack = &surface_to_deep; @@ -1059,7 +1187,8 @@ void append_ordered_stack_candidate(ColorSolverOrderedStackCandidateSet &c beer_lambert_rgb_correction, td_effective_alpha_correction, component_roles, - layer_opacities_by_depth); + layer_opacities_by_depth, + calibrated_stack_model); const std::array perceptual = oklab_from_srgb(mixed); candidates.rgbs.emplace_back(mixed[0]); candidates.rgbs.emplace_back(mixed[1]); @@ -1087,6 +1216,43 @@ ColorSolverMode color_solver_mode_from_index(int mode) return ColorSolverMode(std::clamp(mode, int(ColorSolverMode::RGB), int(ColorSolverMode::OklabSoftCap4Dark4))); } +bool ColorSolverCalibratedStackModel::valid() const +{ + switch (kind) { + case ColorSolverCalibratedStackModelKind::CurrentLinearAffine: + return coefficients_linear_rgb.size() == 12; + case ColorSolverCalibratedStackModelKind::AlphaEffective: + return component_count > 0 && + alphas.size() >= component_count && + coefficients_linear_rgb.size() == (component_count + 1) * 3; + case ColorSolverCalibratedStackModelKind::DepthKernelLinear: + return component_count > 0 && + !taus.empty() && + coefficients_linear_rgb.size() == (1 + component_count + component_count * taus.size()) * 3; + default: + return false; + } +} + +std::string ColorSolverCalibratedStackModel::cache_key() const +{ + if (!valid()) + return std::string(); + std::ostringstream key; + key << "cal" << int(kind) << "n" << component_count; + auto append_values = [&key](const char *label, const std::vector &values) { + key << '|' << label; + for (float value : values) { + const float safe = std::isfinite(value) ? std::clamp(value, -1000.f, 1000.f) : 0.f; + key << ',' << int(std::lround(safe * 1000000.f)); + } + }; + append_values("a", alphas); + append_values("t", taus); + append_values("w", coefficients_linear_rgb); + return key.str(); +} + int color_solver_total_units_for_component_count(size_t component_count) { return component_count <= 4 ? 40 : (component_count == 5 ? 24 : (component_count == 6 ? 20 : 12)); @@ -1138,7 +1304,8 @@ std::array mix_color_solver_ordered_stack(const std::vector &component_roles, - const std::vector &layer_opacities_by_depth) + const std::vector &layer_opacities_by_depth, + const ColorSolverCalibratedStackModel *calibrated_stack_model) { if (component_colors.empty() || surface_to_deep.empty()) return background_rgb; @@ -1155,7 +1322,8 @@ std::array mix_color_solver_ordered_stack(const std::vector color_solver_oklab_from_srgb(const std::array &rgb) @@ -1294,7 +1462,8 @@ std::string color_solver_ordered_stack_candidate_cache_key(const std::vector &component_roles, bool beam_search_stack_expansion, - const std::vector &layer_opacities_by_depth) + const std::vector &layer_opacities_by_depth, + const ColorSolverCalibratedStackModel *calibrated_stack_model) { std::ostringstream key; key << component_colors.size(); @@ -1346,6 +1515,8 @@ std::string color_solver_ordered_stack_candidate_cache_key(const std::vectorvalid()) + key << '|' << calibrated_stack_model->cache_key(); return key.str(); } @@ -1363,7 +1534,8 @@ ColorSolverOrderedStackCandidateSet build_color_solver_ordered_stack_candidates( bool td_effective_alpha_correction, const std::vector &component_roles, bool beam_search_stack_expansion, - const std::vector &layer_opacities_by_depth) + const std::vector &layer_opacities_by_depth, + const ColorSolverCalibratedStackModel *calibrated_stack_model) { ColorSolverOrderedStackCandidateSet candidates; int simulated_depth = simulated_stack_depth > 0 ? simulated_stack_depth : stack_depth; @@ -1405,7 +1577,8 @@ ColorSolverOrderedStackCandidateSet build_color_solver_ordered_stack_candidates( beer_lambert_rgb_correction, td_effective_alpha_correction, component_roles, - layer_opacities_by_depth); + layer_opacities_by_depth, + calibrated_stack_model); return; } @@ -1459,7 +1632,8 @@ ColorSolverOrderedStackCandidateSet build_color_solver_ordered_stack_candidates( beer_lambert_rgb_correction, td_effective_alpha_correction, component_roles, - layer_opacities_by_depth); + layer_opacities_by_depth, + calibrated_stack_model); } return; } @@ -1491,7 +1665,8 @@ const ColorSolverOrderedStackCandidateSet &color_solver_ordered_stack_candidates bool td_effective_alpha_correction, const std::vector &component_roles, bool beam_search_stack_expansion, - const std::vector &layer_opacities_by_depth) + const std::vector &layer_opacities_by_depth, + const ColorSolverCalibratedStackModel *calibrated_stack_model) { const std::string key = color_solver_ordered_stack_candidate_cache_key(component_colors, @@ -1507,7 +1682,8 @@ const ColorSolverOrderedStackCandidateSet &color_solver_ordered_stack_candidates td_effective_alpha_correction, component_roles, beam_search_stack_expansion, - layer_opacities_by_depth); + layer_opacities_by_depth, + calibrated_stack_model); auto it = cache.find(key); if (it != cache.end()) return it->second; @@ -1526,7 +1702,8 @@ const ColorSolverOrderedStackCandidateSet &color_solver_ordered_stack_candidates td_effective_alpha_correction, component_roles, beam_search_stack_expansion, - layer_opacities_by_depth)).first->second; + layer_opacities_by_depth, + calibrated_stack_model)).first->second; } ColorSolverOrderedStackResult solve_color_solver_ordered_stack_result_for_target( diff --git a/src/libslic3r/ColorSolver.hpp b/src/libslic3r/ColorSolver.hpp index 5d4646dbdb9..18a709e1128 100644 --- a/src/libslic3r/ColorSolver.hpp +++ b/src/libslic3r/ColorSolver.hpp @@ -55,6 +55,25 @@ enum class ColorSolverStackComponentRole : uint8_t White = 5 }; +enum class ColorSolverCalibratedStackModelKind : uint8_t +{ + None = 0, + CurrentLinearAffine = 1, + AlphaEffective = 2, + DepthKernelLinear = 3 +}; + +struct ColorSolverCalibratedStackModel { + ColorSolverCalibratedStackModelKind kind { ColorSolverCalibratedStackModelKind::None }; + size_t component_count { 0 }; + std::vector alphas; + std::vector taus; + std::vector coefficients_linear_rgb; + + bool valid() const; + std::string cache_key() const; +}; + struct ColorSolverCandidateSet { struct KdNode { uint32_t candidate_idx { 0 }; @@ -132,7 +151,8 @@ std::array mix_color_solver_ordered_stack(const std::vector &component_roles = {}, - const std::vector &layer_opacities_by_depth = {}); + const std::vector &layer_opacities_by_depth = {}, + const ColorSolverCalibratedStackModel *calibrated_stack_model = nullptr); std::array color_solver_oklab_from_srgb(const std::array &rgb); std::array color_solver_srgb_from_oklab(const std::array &oklab); @@ -163,7 +183,8 @@ std::string color_solver_ordered_stack_candidate_cache_key(const std::vector &component_roles = {}, bool beam_search_stack_expansion = false, - const std::vector &layer_opacities_by_depth = {}); + const std::vector &layer_opacities_by_depth = {}, + const ColorSolverCalibratedStackModel *calibrated_stack_model = nullptr); ColorSolverOrderedStackCandidateSet build_color_solver_ordered_stack_candidates( const std::vector> &component_colors, const std::vector &layer_opacities, @@ -178,7 +199,8 @@ ColorSolverOrderedStackCandidateSet build_color_solver_ordered_stack_candidates( bool td_effective_alpha_correction = false, const std::vector &component_roles = {}, bool beam_search_stack_expansion = false, - const std::vector &layer_opacities_by_depth = {}); + const std::vector &layer_opacities_by_depth = {}, + const ColorSolverCalibratedStackModel *calibrated_stack_model = nullptr); const ColorSolverOrderedStackCandidateSet &color_solver_ordered_stack_candidates( ColorSolverOrderedStackCandidateCache &cache, const std::vector> &component_colors, @@ -194,7 +216,8 @@ const ColorSolverOrderedStackCandidateSet &color_solver_ordered_stack_candidates bool td_effective_alpha_correction = false, const std::vector &component_roles = {}, bool beam_search_stack_expansion = false, - const std::vector &layer_opacities_by_depth = {}); + const std::vector &layer_opacities_by_depth = {}, + const ColorSolverCalibratedStackModel *calibrated_stack_model = nullptr); ColorSolverOrderedStackResult solve_color_solver_ordered_stack_result_for_target( const ColorSolverOrderedStackCandidateSet &candidates, const std::array &target_rgb, diff --git a/src/libslic3r/TextureMapping.cpp b/src/libslic3r/TextureMapping.cpp index 5a9b74502f9..3813b54db66 100644 --- a/src/libslic3r/TextureMapping.cpp +++ b/src/libslic3r/TextureMapping.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -484,6 +485,80 @@ static nlohmann::json floats_to_json(const std::vector &values) return out; } +static std::array color_array_from_hex(const std::string &hex) +{ + const RGB rgb = parse_hex_color(hex); + return { { float(rgb.r) / 255.f, float(rgb.g) / 255.f, float(rgb.b) / 255.f } }; +} + +static std::string json_string_or_empty(const nlohmann::json &object, const std::initializer_list &keys) +{ + if (!object.is_object()) + return std::string(); + for (const char *key : keys) { + auto it = object.find(key); + if (it != object.end() && it->is_string()) + return it->get(); + } + return std::string(); +} + +static float json_float_or(const nlohmann::json &object, const std::initializer_list &keys, float fallback) +{ + if (!object.is_object()) + return fallback; + for (const char *key : keys) { + auto it = object.find(key); + if (it != object.end() && it->is_number()) + return it->get(); + } + return fallback; +} + +static std::vector json_float_vector_or_empty(const nlohmann::json &object, const std::initializer_list &keys) +{ + if (!object.is_object()) + return {}; + for (const char *key : keys) { + auto it = object.find(key); + if (it != object.end()) + return floats_from_json(*it); + } + return {}; +} + +static std::vector calibration_taus_from_json(const nlohmann::json &object) +{ + std::vector taus; + if (!object.is_object()) + return taus; + auto taus_it = object.find("taus"); + if (taus_it != object.end() && taus_it->is_array()) { + for (const nlohmann::json &item : *taus_it) + taus.emplace_back(item.is_number() ? item.get() : -1.f); + return taus; + } + auto kernels_it = object.find("depth_kernels"); + if (kernels_it != object.end() && kernels_it->is_array()) { + for (const nlohmann::json &item : *kernels_it) { + if (item.is_object()) { + auto tau_it = item.find("tau_layers"); + taus.emplace_back(tau_it != item.end() && tau_it->is_number() ? tau_it->get() : -1.f); + } + } + } + return taus; +} + +static std::string lower_ascii(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + const char lowered = char(std::tolower(c)); + return lowered == '-' || lowered == ' ' ? '_' : lowered; + }); + return value; +} + static nlohmann::json point3_to_json(const std::array &point) { return nlohmann::json::array({ point[0], point[1], point[2] }); @@ -737,6 +812,14 @@ static std::string top_surface_contoning_color_prediction_mode_name(int mode) return "rgb_beer_lambert"; if (mode == int(TextureMappingZone::ContoningColorPredictionBasicReflectance)) return "basic_reflectance"; + if (mode == int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine)) + return "calibrated_current_linear_affine"; + if (mode == int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective)) + return "calibrated_td_alpha_effective"; + if (mode == int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective)) + return "calibrated_free_alpha_effective"; + if (mode == int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear)) + return "calibrated_depth_kernel_linear"; return "default"; } @@ -754,6 +837,23 @@ static int top_surface_contoning_color_prediction_mode_from_name(std::string nam name == "basic_reflectance_no_td_correction" || name == "no_td_correction") return int(TextureMappingZone::ContoningColorPredictionBasicReflectance); + if (name == "calibrated_current_linear_affine" || + name == "calibrated_current_affine" || + name == "current_linear_affine" || + name == "current_linear_affine_global") + return int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine); + if (name == "calibrated_td_alpha_effective" || + name == "td_alpha_effective" || + name == "td_alpha_effective_global") + return int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective); + if (name == "calibrated_free_alpha_effective" || + name == "free_alpha_effective" || + name == "free_alpha_effective_global") + return int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective); + if (name == "calibrated_depth_kernel_linear" || + name == "depth_kernel_linear" || + name == "depth_kernel_linear_global") + return int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear); return TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; } @@ -1123,6 +1223,476 @@ bool TextureMappingGlobalSettings::is_generic_solver_color_mode(const std::strin return normalize_color_mode_name(mode) == "generic_solver"; } +bool TextureMappingColorCalibration::has_mode(int mode) const +{ + switch (TextureMappingZone::effective_top_surface_contoning_color_prediction_mode(mode)) { + case int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine): + return current_linear_affine.valid(); + case int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective): + return td_alpha_effective.valid(); + case int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective): + return free_alpha_effective.valid(); + case int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear): + return depth_kernel_linear.valid(); + default: + return false; + } +} + +namespace { + +static const nlohmann::json *calibration_model_json(const nlohmann::json &models, const char *base_key) +{ + if (!models.is_object()) + return nullptr; + auto it = models.find(base_key); + if (it != models.end()) + return &*it; + const std::string global_key = std::string(base_key) + "_global"; + it = models.find(global_key); + return it != models.end() ? &*it : nullptr; +} + +static ColorSolverCalibratedStackModel calibration_current_linear_affine_model(const nlohmann::json &raw) +{ + ColorSolverCalibratedStackModel model; + model.kind = ColorSolverCalibratedStackModelKind::CurrentLinearAffine; + model.coefficients_linear_rgb = raw.is_array() ? + floats_from_json(raw) : + json_float_vector_or_empty(raw, { "weights", "coefficients_linear_rgb" }); + if (!model.valid()) + model = {}; + return model; +} + +static ColorSolverCalibratedStackModel calibration_alpha_model(const nlohmann::json &raw) +{ + ColorSolverCalibratedStackModel model; + model.kind = ColorSolverCalibratedStackModelKind::AlphaEffective; + model.alphas = json_float_vector_or_empty(raw, { + "alphas", + "alphas_at_nominal_layer_height", + "learned_alphas" + }); + model.component_count = model.alphas.size(); + model.coefficients_linear_rgb = json_float_vector_or_empty(raw, { "weights", "coefficients_linear_rgb" }); + if (!model.valid()) + model = {}; + return model; +} + +static ColorSolverCalibratedStackModel calibration_depth_kernel_model(const nlohmann::json &raw, size_t fallback_component_count) +{ + ColorSolverCalibratedStackModel model; + model.kind = ColorSolverCalibratedStackModelKind::DepthKernelLinear; + model.component_count = size_t(std::max(0, raw.value("component_count", int(fallback_component_count)))); + model.taus = calibration_taus_from_json(raw); + model.coefficients_linear_rgb = json_float_vector_or_empty(raw, { "weights", "coefficients_linear_rgb" }); + if (!model.valid()) + model = {}; + return model; +} + +static TextureMappingColorCalibrationFilament calibration_filament_from_json(const std::string &fallback_name, + const nlohmann::json &raw) +{ + TextureMappingColorCalibrationFilament filament; + filament.name = json_string_or_empty(raw, { "name", "role", "id" }); + if (filament.name.empty()) + filament.name = fallback_name; + const std::string hex = json_string_or_empty(raw, { "color", "display_color_hex", "display_color" }); + if (!hex.empty()) + filament.color = color_array_from_hex(hex); + else + filament.color = point3_from_json(raw.value("display_color_srgb", nlohmann::json::array())); + filament.td_mm = json_float_or(raw, { "td_mm", "transmission_distance_mm" }, 0.f); + return filament; +} + +static std::vector calibration_component_order_from_json(const nlohmann::json &root) +{ + const nlohmann::json *order = nullptr; + auto order_it = root.find("component_order"); + if (order_it != root.end() && order_it->is_array()) + order = &*order_it; + auto applicability_it = root.find("applicability"); + if (order == nullptr && applicability_it != root.end() && applicability_it->is_object()) { + auto app_order_it = applicability_it->find("component_order"); + if (app_order_it != applicability_it->end() && app_order_it->is_array()) + order = &*app_order_it; + } + std::vector out; + if (order == nullptr) + return out; + for (const nlohmann::json &item : *order) + if (item.is_string()) + out.emplace_back(item.get()); + return out; +} + +static std::vector calibration_filaments_from_json(const nlohmann::json &root) +{ + std::vector filaments; + auto filaments_it = root.find("filaments"); + if (filaments_it == root.end()) + return filaments; + + if (filaments_it->is_array()) { + filaments.reserve(filaments_it->size()); + for (const nlohmann::json &item : *filaments_it) + filaments.emplace_back(calibration_filament_from_json(std::string(), item)); + return filaments; + } + + if (!filaments_it->is_object()) + return filaments; + + std::map by_name; + for (auto it = filaments_it->begin(); it != filaments_it->end(); ++it) + by_name.emplace(lower_ascii(it.key()), calibration_filament_from_json(it.key(), it.value())); + + for (const std::string &name : calibration_component_order_from_json(root)) { + auto it = by_name.find(lower_ascii(name)); + if (it == by_name.end()) + continue; + filaments.emplace_back(it->second); + by_name.erase(it); + } + for (const auto &item : by_name) + filaments.emplace_back(item.second); + return filaments; +} + +static const ColorSolverCalibratedStackModel &calibration_model_for_mode(const TextureMappingColorCalibration &calibration, + int mode) +{ + switch (TextureMappingZone::effective_top_surface_contoning_color_prediction_mode(mode)) { + case int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine): + return calibration.current_linear_affine; + case int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective): + return calibration.td_alpha_effective; + case int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective): + return calibration.free_alpha_effective; + default: + return calibration.depth_kernel_linear; + } +} + +struct TextureMappingCalibrationMatch { + std::vector calibration_index_by_component; + std::string warning; + bool complete { false }; +}; + +static float calibration_color_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 TextureMappingCalibrationMatch match_calibration_filaments(const TextureMappingColorCalibration &calibration, + const std::vector> &component_colors, + const std::vector &component_tds_mm) +{ + TextureMappingCalibrationMatch match; + match.calibration_index_by_component.assign(component_colors.size(), size_t(-1)); + if (component_colors.empty()) { + match.complete = true; + return match; + } + if (calibration.filaments.empty()) { + match.warning = "The calibration file does not include filament color/TD metadata."; + return match; + } + + std::vector used(calibration.filaments.size(), false); + std::vector notes; + for (size_t component_idx = 0; component_idx < component_colors.size(); ++component_idx) { + size_t best_idx = size_t(-1); + float best_score = std::numeric_limits::max(); + float best_color_distance = std::numeric_limits::max(); + for (size_t calibration_idx = 0; calibration_idx < calibration.filaments.size(); ++calibration_idx) { + if (used[calibration_idx]) + continue; + const TextureMappingColorCalibrationFilament &filament = calibration.filaments[calibration_idx]; + const float color_distance = calibration_color_distance(component_colors[component_idx], filament.color); + float td_score = 0.f; + const float component_td = component_idx < component_tds_mm.size() ? component_tds_mm[component_idx] : 0.f; + if (component_td > 0.f && filament.td_mm > 0.f) + td_score = std::min(0.12f, std::abs(component_td - filament.td_mm) / std::max({ component_td, filament.td_mm, 1.f }) * 0.12f); + const float score = color_distance + td_score; + if (score < best_score) { + best_score = score; + best_idx = calibration_idx; + best_color_distance = color_distance; + } + } + if (best_idx == size_t(-1)) + continue; + used[best_idx] = true; + match.calibration_index_by_component[component_idx] = best_idx; + + const TextureMappingColorCalibrationFilament &filament = calibration.filaments[best_idx]; + const float component_td = component_idx < component_tds_mm.size() ? component_tds_mm[component_idx] : 0.f; + if (best_color_distance > 0.16f) { + notes.emplace_back("component " + std::to_string(component_idx + 1) + + " color does not closely match calibrated " + filament.name); + } + if (component_td > 0.f && filament.td_mm > 0.f) { + const float td_delta = std::abs(component_td - filament.td_mm); + if (td_delta > std::max(1.f, std::max(component_td, filament.td_mm) * 0.35f)) { + notes.emplace_back("component " + std::to_string(component_idx + 1) + + " TD differs from calibrated " + filament.name); + } + } + } + match.complete = std::all_of(match.calibration_index_by_component.begin(), + match.calibration_index_by_component.end(), + [](size_t idx) { return idx != size_t(-1); }); + if (!match.complete) + notes.emplace_back("the calibration file does not contain enough matching filaments"); + if (!notes.empty()) { + match.warning = "Top-surface color calibration mismatch: "; + for (size_t idx = 0; idx < notes.size(); ++idx) { + if (idx > 0) + match.warning += "; "; + match.warning += notes[idx]; + } + match.warning += "."; + } + return match; +} + +static std::optional remap_calibration_model( + const ColorSolverCalibratedStackModel &source, + int mode, + const TextureMappingCalibrationMatch &match, + size_t target_component_count) +{ + if (!source.valid()) + return std::nullopt; + if (TextureMappingZone::effective_top_surface_contoning_color_prediction_mode(mode) == + int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine)) + return source; + if (!match.complete || target_component_count == 0) + return std::nullopt; + + if (source.kind == ColorSolverCalibratedStackModelKind::AlphaEffective) { + ColorSolverCalibratedStackModel out; + out.kind = ColorSolverCalibratedStackModelKind::AlphaEffective; + out.component_count = target_component_count; + out.alphas.assign(target_component_count, 0.f); + out.coefficients_linear_rgb.assign((target_component_count + 1) * 3, 0.f); + const size_t source_component_count = source.component_count; + if (source.alphas.size() < source_component_count || + source.coefficients_linear_rgb.size() != (source_component_count + 1) * 3) + return std::nullopt; + for (size_t component_idx = 0; component_idx < target_component_count; ++component_idx) { + const size_t source_idx = match.calibration_index_by_component[component_idx]; + if (source_idx >= source_component_count) + return std::nullopt; + out.alphas[component_idx] = source.alphas[source_idx]; + for (size_t channel = 0; channel < 3; ++channel) + out.coefficients_linear_rgb[component_idx * 3 + channel] = source.coefficients_linear_rgb[source_idx * 3 + channel]; + } + for (size_t channel = 0; channel < 3; ++channel) + out.coefficients_linear_rgb[target_component_count * 3 + channel] = + source.coefficients_linear_rgb[source_component_count * 3 + channel]; + return out.valid() ? std::optional(out) : std::nullopt; + } + + if (source.kind == ColorSolverCalibratedStackModelKind::DepthKernelLinear) { + ColorSolverCalibratedStackModel out; + out.kind = ColorSolverCalibratedStackModelKind::DepthKernelLinear; + out.component_count = target_component_count; + out.taus = source.taus; + out.coefficients_linear_rgb.assign((1 + target_component_count + target_component_count * out.taus.size()) * 3, 0.f); + const size_t source_component_count = source.component_count; + if (source.coefficients_linear_rgb.size() != (1 + source_component_count + source_component_count * source.taus.size()) * 3) + return std::nullopt; + auto copy_row = [&source, &out](size_t dst_row, size_t src_row) { + for (size_t channel = 0; channel < 3; ++channel) + out.coefficients_linear_rgb[dst_row * 3 + channel] = source.coefficients_linear_rgb[src_row * 3 + channel]; + }; + copy_row(0, 0); + for (size_t component_idx = 0; component_idx < target_component_count; ++component_idx) { + const size_t source_idx = match.calibration_index_by_component[component_idx]; + if (source_idx >= source_component_count) + return std::nullopt; + copy_row(1 + component_idx, 1 + source_idx); + } + for (size_t tau_idx = 0; tau_idx < out.taus.size(); ++tau_idx) { + for (size_t component_idx = 0; component_idx < target_component_count; ++component_idx) { + const size_t source_idx = match.calibration_index_by_component[component_idx]; + if (source_idx >= source_component_count) + return std::nullopt; + const size_t dst_row = 1 + target_component_count + tau_idx * target_component_count + component_idx; + const size_t src_row = 1 + source_component_count + tau_idx * source_component_count + source_idx; + copy_row(dst_row, src_row); + } + } + return out.valid() ? std::optional(out) : std::nullopt; + } + + return std::nullopt; +} + +} // namespace + +std::optional texture_mapping_parse_top_surface_color_calibration( + const std::string &json_text, + std::string *error) +{ + if (error != nullptr) + error->clear(); + if (json_text.empty()) + return std::nullopt; + + nlohmann::json root = nlohmann::json::parse(json_text, nullptr, false); + if (root.is_discarded() || !root.is_object()) { + if (error != nullptr) + *error = "The calibration file is not valid JSON."; + return std::nullopt; + } + + TextureMappingColorCalibration calibration; + calibration.display_name = json_string_or_empty(root, { "display_name", "name", "profile_id" }); + calibration.filaments = calibration_filaments_from_json(root); + const nlohmann::json models = root.contains("weights") && root["weights"].is_object() ? + root["weights"] : + root.value("models", nlohmann::json::object()); + + if (const nlohmann::json *raw = calibration_model_json(models, "current_linear_affine")) + calibration.current_linear_affine = calibration_current_linear_affine_model(*raw); + if (const nlohmann::json *raw = calibration_model_json(models, "td_alpha_effective")) + calibration.td_alpha_effective = calibration_alpha_model(*raw); + if (const nlohmann::json *raw = calibration_model_json(models, "free_alpha_effective")) + calibration.free_alpha_effective = calibration_alpha_model(*raw); + if (const nlohmann::json *raw = calibration_model_json(models, "depth_kernel_linear")) + calibration.depth_kernel_linear = calibration_depth_kernel_model(*raw, calibration.filaments.size()); + + if (!calibration.current_linear_affine.valid() && + !calibration.td_alpha_effective.valid() && + !calibration.free_alpha_effective.valid() && + !calibration.depth_kernel_linear.valid()) { + if (error != nullptr) + *error = "The calibration file does not contain any supported color prediction weights."; + return std::nullopt; + } + + return calibration; +} + +std::vector texture_mapping_top_surface_color_calibration_supported_modes(const std::string &json_text) +{ + std::vector out; + std::optional calibration = + texture_mapping_parse_top_surface_color_calibration(json_text); + if (!calibration) + return out; + const std::array modes = { + int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine), + int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective), + int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective), + int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear) + }; + for (int mode : modes) + if (calibration->has_mode(mode)) + out.emplace_back(mode); + return out; +} + +int texture_mapping_top_surface_color_calibration_first_supported_mode(const std::string &json_text) +{ + std::optional calibration = + texture_mapping_parse_top_surface_color_calibration(json_text); + if (!calibration) + return TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; + + const std::array, 4> modes = { + std::pair(int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine), "current_linear_affine"), + std::pair(int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective), "td_alpha_effective"), + std::pair(int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective), "free_alpha_effective"), + std::pair(int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear), "depth_kernel_linear") + }; + size_t best_pos = std::numeric_limits::max(); + int best_mode = TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; + for (const auto &item : modes) { + if (!calibration->has_mode(item.first)) + continue; + const size_t pos = json_text.find(std::string("\"") + item.second); + const size_t effective_pos = pos == std::string::npos ? std::numeric_limits::max() - size_t(item.first) : pos; + if (effective_pos < best_pos) { + best_pos = effective_pos; + best_mode = item.first; + } + } + return best_mode; +} + +std::string texture_mapping_top_surface_color_calibration_display_name(const std::string &json_text) +{ + std::optional calibration = + texture_mapping_parse_top_surface_color_calibration(json_text); + return calibration ? calibration->display_name : std::string(); +} + +std::string texture_mapping_top_surface_color_calibration_warning(const TextureMappingZone &zone, + const std::vector> &component_colors, + const std::vector &component_tds_mm) +{ + if (zone.top_surface_color_calibration_json.empty()) + return std::string(); + std::string error; + std::optional calibration = + texture_mapping_parse_top_surface_color_calibration(zone.top_surface_color_calibration_json, &error); + if (!calibration) + return error.empty() ? "Top-surface color calibration could not be loaded." : error; + + const TextureMappingCalibrationMatch match = + match_calibration_filaments(*calibration, component_colors, component_tds_mm); + if (!match.warning.empty()) + return match.warning; + + const int mode = TextureMappingZone::effective_top_surface_contoning_color_prediction_mode(zone.top_surface_contoning_color_prediction_mode); + if (TextureMappingZone::top_surface_contoning_color_prediction_mode_is_calibrated(mode) && + !calibration->has_mode(mode)) + return "The selected calibrated color prediction mode is not present in the loaded calibration file."; + return std::string(); +} + +std::optional texture_mapping_top_surface_color_calibrated_model( + const TextureMappingZone &zone, + int mode, + const std::vector> &component_colors, + const std::vector &component_tds_mm, + std::string *warning) +{ + if (warning != nullptr) + warning->clear(); + std::string error; + std::optional calibration = + texture_mapping_parse_top_surface_color_calibration(zone.top_surface_color_calibration_json, &error); + if (!calibration) { + if (warning != nullptr) + *warning = error; + return std::nullopt; + } + const ColorSolverCalibratedStackModel &source = calibration_model_for_mode(*calibration, mode); + if (!source.valid()) + return std::nullopt; + + const TextureMappingCalibrationMatch match = + match_calibration_filaments(*calibration, component_colors, component_tds_mm); + if (warning != nullptr) + *warning = match.warning; + return remap_calibration_model(source, mode, match, component_colors.size()); +} + bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const { constexpr float eps = 1e-6f; @@ -1238,6 +1808,8 @@ bool TextureMappingZone::operator==(const TextureMappingZone &rhs) const top_surface_contoning_td_effective_alpha_correction_enabled == rhs.top_surface_contoning_td_effective_alpha_correction_enabled && top_surface_contoning_variable_layer_height_compensation_enabled == rhs.top_surface_contoning_variable_layer_height_compensation_enabled && effective_top_surface_contoning_beam_search_stack_expansion_enabled() == rhs.effective_top_surface_contoning_beam_search_stack_expansion_enabled() && + top_surface_color_calibration_name == rhs.top_surface_color_calibration_name && + top_surface_color_calibration_json == rhs.top_surface_color_calibration_json && 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 && @@ -1660,6 +2232,12 @@ std::string TextureMappingManager::serialize_entries() zone.top_surface_contoning_variable_layer_height_compensation_enabled; texture["top_surface_contoning_beam_search_stack_expansion_enabled"] = zone.effective_top_surface_contoning_beam_search_stack_expansion_enabled(); + if (!zone.top_surface_color_calibration_json.empty()) { + texture["top_surface_color_calibration_name"] = zone.top_surface_color_calibration_name; + nlohmann::json calibration = nlohmann::json::parse(zone.top_surface_color_calibration_json, nullptr, false); + texture["top_surface_color_calibration"] = + calibration.is_discarded() ? nlohmann::json(zone.top_surface_color_calibration_json) : std::move(calibration); + } 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"] = true; @@ -1985,9 +2563,9 @@ void TextureMappingManager::load_entries(const std::string &serialized, top_surface_contoning_color_prediction_mode_from_name(color_prediction_mode_it->get()) : clamp_int(color_prediction_mode_it != texture.end() && color_prediction_mode_it->is_number_integer() ? color_prediction_mode_it->get() : - TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode, + TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode, int(TextureMappingZone::ContoningColorPredictionDefault), - int(TextureMappingZone::ContoningColorPredictionBasicReflectance)); + int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear)); zone.top_surface_contoning_beer_lambert_rgb_correction_enabled = TextureMappingZone::DefaultTopSurfaceContoningBeerLambertRgbCorrectionEnabled; zone.top_surface_contoning_td_effective_alpha_correction_enabled = @@ -1998,6 +2576,30 @@ void TextureMappingManager::load_entries(const std::string &serialized, zone.top_surface_contoning_beam_search_stack_expansion_enabled = texture.value("top_surface_contoning_beam_search_stack_expansion_enabled", TextureMappingZone::DefaultTopSurfaceContoningBeamSearchStackExpansionEnabled); + zone.top_surface_color_calibration_name = + texture.value("top_surface_color_calibration_name", std::string()); + zone.top_surface_color_calibration_json.clear(); + auto color_calibration_it = texture.find("top_surface_color_calibration"); + if (color_calibration_it != texture.end()) { + zone.top_surface_color_calibration_json = + color_calibration_it->is_string() ? + color_calibration_it->get() : + color_calibration_it->dump(); + if (zone.top_surface_color_calibration_name.empty()) + zone.top_surface_color_calibration_name = + texture_mapping_top_surface_color_calibration_display_name(zone.top_surface_color_calibration_json); + } + if (TextureMappingZone::top_surface_contoning_color_prediction_mode_is_calibrated(zone.top_surface_contoning_color_prediction_mode) && + !texture_mapping_parse_top_surface_color_calibration(zone.top_surface_color_calibration_json)) { + zone.top_surface_contoning_color_prediction_mode = + TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; + } else if (TextureMappingZone::top_surface_contoning_color_prediction_mode_is_calibrated(zone.top_surface_contoning_color_prediction_mode)) { + const std::vector modes = + texture_mapping_top_surface_color_calibration_supported_modes(zone.top_surface_color_calibration_json); + if (std::find(modes.begin(), modes.end(), zone.top_surface_contoning_color_prediction_mode) == modes.end()) + zone.top_surface_contoning_color_prediction_mode = + texture_mapping_top_surface_color_calibration_first_supported_mode(zone.top_surface_color_calibration_json); + } zone.apply_top_surface_contoning_experimental_defaults(); zone.compact_offset_mode = texture.value("compact_offset_mode", TextureMappingZone::DefaultCompactOffsetMode); zone.use_legacy_fixed_color_mode = diff --git a/src/libslic3r/TextureMapping.hpp b/src/libslic3r/TextureMapping.hpp index 07bac22031e..632d32443e2 100644 --- a/src/libslic3r/TextureMapping.hpp +++ b/src/libslic3r/TextureMapping.hpp @@ -8,10 +8,13 @@ #include #include #include +#include #include #include #include +#include "ColorSolver.hpp" + namespace Slic3r { struct TextureMappingZone @@ -93,7 +96,11 @@ struct TextureMappingZone ContoningColorPredictionDefault = 0, ContoningColorPredictionTdEffectiveAlpha = 1, ContoningColorPredictionBeerLambertRgb = 2, - ContoningColorPredictionBasicReflectance = 3 + ContoningColorPredictionBasicReflectance = 3, + ContoningColorPredictionCalibratedCurrentLinearAffine = 4, + ContoningColorPredictionCalibratedTdAlphaEffective = 5, + ContoningColorPredictionCalibratedFreeAlphaEffective = 6, + ContoningColorPredictionCalibratedDepthKernelLinear = 7 }; enum TopSurfaceContoningPolygonizationMode : uint8_t { @@ -379,7 +386,14 @@ struct TextureMappingZone return SlicerDefaultTopSurfaceContoningColorPredictionMode; return std::clamp(mode, int(ContoningColorPredictionTdEffectiveAlpha), - int(ContoningColorPredictionBasicReflectance)); + int(ContoningColorPredictionCalibratedDepthKernelLinear)); + } + + static bool top_surface_contoning_color_prediction_mode_is_calibrated(int mode) + { + const int effective_mode = effective_top_surface_contoning_color_prediction_mode(mode); + return effective_mode >= int(ContoningColorPredictionCalibratedCurrentLinearAffine) && + effective_mode <= int(ContoningColorPredictionCalibratedDepthKernelLinear); } static bool effective_top_surface_contoning_layer_phase_enabled(bool value, bool surface_anchored_stacks) @@ -488,6 +502,8 @@ struct TextureMappingZone int top_surface_contoning_color_prediction_mode = DefaultTopSurfaceContoningColorPredictionMode; bool top_surface_contoning_variable_layer_height_compensation_enabled = DefaultTopSurfaceContoningVariableLayerHeightCompensationEnabled; bool top_surface_contoning_beam_search_stack_expansion_enabled = DefaultTopSurfaceContoningBeamSearchStackExpansionEnabled; + std::string top_surface_color_calibration_name; + std::string top_surface_color_calibration_json; bool compact_offset_mode = DefaultCompactOffsetMode; bool use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; bool high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; @@ -672,7 +688,8 @@ struct TextureMappingZone mode == int(ContoningColorPredictionBeerLambertRgb); top_surface_contoning_beer_lambert_rgb_correction_enabled = beer_lambert_rgb; top_surface_contoning_td_effective_alpha_correction_enabled = - mode == int(ContoningColorPredictionTdEffectiveAlpha); + mode == int(ContoningColorPredictionTdEffectiveAlpha) || + mode == int(ContoningColorPredictionCalibratedCurrentLinearAffine); } void apply_top_surface_contoning_experimental_defaults() @@ -782,6 +799,8 @@ struct TextureMappingZone top_surface_contoning_color_prediction_mode = DefaultTopSurfaceContoningColorPredictionMode; top_surface_contoning_variable_layer_height_compensation_enabled = DefaultTopSurfaceContoningVariableLayerHeightCompensationEnabled; top_surface_contoning_beam_search_stack_expansion_enabled = DefaultTopSurfaceContoningBeamSearchStackExpansionEnabled; + top_surface_color_calibration_name.clear(); + top_surface_color_calibration_json.clear(); compact_offset_mode = DefaultCompactOffsetMode; use_legacy_fixed_color_mode = DefaultUseLegacyFixedColorMode; high_speed_image_texture_sampling = DefaultHighSpeedImageTextureSampling; @@ -834,6 +853,39 @@ struct TextureMappingZone bool operator!=(const TextureMappingZone &rhs) const { return !(*this == rhs); } }; +struct TextureMappingColorCalibrationFilament { + std::string name; + std::array color { { 0.f, 0.f, 0.f } }; + float td_mm { 0.f }; +}; + +struct TextureMappingColorCalibration { + std::string display_name; + std::vector filaments; + ColorSolverCalibratedStackModel current_linear_affine; + ColorSolverCalibratedStackModel td_alpha_effective; + ColorSolverCalibratedStackModel free_alpha_effective; + ColorSolverCalibratedStackModel depth_kernel_linear; + + bool has_mode(int mode) const; +}; + +std::optional texture_mapping_parse_top_surface_color_calibration( + const std::string &json_text, + std::string *error = nullptr); +std::vector texture_mapping_top_surface_color_calibration_supported_modes(const std::string &json_text); +int texture_mapping_top_surface_color_calibration_first_supported_mode(const std::string &json_text); +std::string texture_mapping_top_surface_color_calibration_display_name(const std::string &json_text); +std::string texture_mapping_top_surface_color_calibration_warning(const TextureMappingZone &zone, + const std::vector> &component_colors, + const std::vector &component_tds_mm); +std::optional texture_mapping_top_surface_color_calibrated_model( + const TextureMappingZone &zone, + int mode, + const std::vector> &component_colors, + const std::vector &component_tds_mm, + std::string *warning = nullptr); + struct TextureMappingPrimeTowerImage { std::vector rgba; diff --git a/src/libslic3r/TextureMappingContoning.cpp b/src/libslic3r/TextureMappingContoning.cpp index 11353776b6e..d4099825356 100644 --- a/src/libslic3r/TextureMappingContoning.cpp +++ b/src/libslic3r/TextureMappingContoning.cpp @@ -407,12 +407,17 @@ TextureMappingContoningSolver::TextureMappingContoningSolver(const TextureMappin { m_mix_model = color_solver_mix_model_from_index(zone.generic_solver_mix_model); m_td_adjustment_enabled = zone.top_surface_contoning_td_adjustment_enabled; + const int effective_color_prediction_mode = + TextureMappingZone::effective_top_surface_contoning_color_prediction_mode( + zone.top_surface_contoning_color_prediction_mode); m_td_effective_alpha_correction_enabled = - m_td_adjustment_enabled && zone.top_surface_contoning_td_effective_alpha_correction_enabled; + m_td_adjustment_enabled && + (effective_color_prediction_mode == int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha) || + effective_color_prediction_mode == int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine)); m_beer_lambert_rgb_correction_enabled = m_td_adjustment_enabled && !m_td_effective_alpha_correction_enabled && - zone.top_surface_contoning_beer_lambert_rgb_correction_enabled; + effective_color_prediction_mode == int(TextureMappingZone::ContoningColorPredictionBeerLambertRgb); m_variable_layer_height_compensation_enabled = m_td_adjustment_enabled && zone.top_surface_contoning_variable_layer_height_compensation_enabled; m_beam_search_stack_expansion_enabled = zone.effective_top_surface_contoning_beam_search_stack_expansion_enabled(); @@ -439,6 +444,17 @@ TextureMappingContoningSolver::TextureMappingContoningSolver(const TextureMappin m_background_rgb = mix_color_solver_components(m_component_colors, background_weights, m_mix_model); m_effective_transmission_distances_mm = effective_transmission_distances_mm(zone, config, m_component_ids, m_td_adjustment_enabled); + if (TextureMappingZone::top_surface_contoning_color_prediction_mode_is_calibrated(effective_color_prediction_mode)) { + std::string warning; + std::optional calibrated_model = + texture_mapping_top_surface_color_calibrated_model(zone, + effective_color_prediction_mode, + m_component_colors, + m_effective_transmission_distances_mm, + &warning); + if (calibrated_model && calibrated_model->valid()) + m_calibrated_stack_model = *calibrated_model; + } m_component_layer_opacity.reserve(m_effective_transmission_distances_mm.size()); for (float td_mm : m_effective_transmission_distances_mm) m_component_layer_opacity.emplace_back(layer_opacity_from_td(td_mm > 0.f ? td_mm : 3.f, m_layer_height_mm)); @@ -556,7 +572,8 @@ std::optional> TextureMappingContoningSolver::stack_rgb( m_beer_lambert_rgb_correction_enabled, m_td_effective_alpha_correction_enabled, m_component_roles, - depth_layer_opacities); + depth_layer_opacities, + m_calibrated_stack_model.valid() ? &m_calibrated_stack_model : nullptr); } std::vector TextureMappingContoningSolver::layer_opacities_by_depth( @@ -690,7 +707,8 @@ TextureMappingContoningStack TextureMappingContoningSolver::solve(const std::arr m_td_effective_alpha_correction_enabled, m_component_roles, m_beam_search_stack_expansion_enabled, - depth_layer_opacities); + depth_layer_opacities, + m_calibrated_stack_model.valid() ? &m_calibrated_stack_model : nullptr); } const ColorSolverOrderedStackResult solved = solve_color_solver_ordered_stack_result_for_target(*ordered_candidates, target_rgb, ColorSolverMode::OklabSoftCap4Dark4); diff --git a/src/libslic3r/TextureMappingContoning.hpp b/src/libslic3r/TextureMappingContoning.hpp index bd3d2f56149..4d28fa5e3c9 100644 --- a/src/libslic3r/TextureMappingContoning.hpp +++ b/src/libslic3r/TextureMappingContoning.hpp @@ -77,6 +77,7 @@ private: bool m_td_effective_alpha_correction_enabled { false }; bool m_variable_layer_height_compensation_enabled { false }; bool m_beam_search_stack_expansion_enabled { false }; + ColorSolverCalibratedStackModel m_calibrated_stack_model; std::vector m_component_roles; mutable std::map> m_candidates_by_depth; mutable std::shared_ptr m_candidate_mutex { std::make_shared() }; diff --git a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp index 22beb9f72c9..8c1f1aec203 100644 --- a/src/slic3r/GUI/MMUPaintedTexturePreview.cpp +++ b/src/slic3r/GUI/MMUPaintedTexturePreview.cpp @@ -106,6 +106,8 @@ struct TexturePreviewSimulationSettings float contoning_flat_surface_surface_scatter = k_contoning_preview_surface_scatter; std::array contoning_flat_surface_background_rgb { { 0.f, 0.f, 0.f } }; std::vector contoning_flat_surface_layer_opacities; + std::vector contoning_flat_surface_transmission_distances_mm; + ColorSolverCalibratedStackModel contoning_flat_surface_calibrated_stack_model; float minimum_visibility_offset_factor = 0.f; std::vector component_ids; std::vector> component_colors; @@ -588,7 +590,7 @@ float texture_preview_layer_opacity_from_td(float td_mm, float layer_height_mm) return std::clamp(1.f - std::pow(0.05f, 2.f * safe_layer_height / safe_td), 1e-4f, 0.9999f); } -std::vector contoning_flat_surface_preview_layer_opacities( +std::vector contoning_flat_surface_preview_transmission_distances( const TextureMappingZone &zone, const TexturePreviewSimulationSettings &settings) { @@ -608,11 +610,21 @@ std::vector contoning_flat_surface_preview_layer_opacities( td_mm = k_contoning_preview_inferred_black_td_mm; if (td_mm <= 0.f) td_mm = texture_preview_estimated_transmission_distance_mm(settings.component_colors[idx]); - out.emplace_back(texture_preview_layer_opacity_from_td(td_mm, settings.contoning_flat_surface_layer_height_mm)); + out.emplace_back(td_mm); } return out; } +std::vector contoning_flat_surface_preview_layer_opacities( + const TexturePreviewSimulationSettings &settings) +{ + std::vector out; + out.reserve(settings.contoning_flat_surface_transmission_distances_mm.size()); + for (const float td_mm : settings.contoning_flat_surface_transmission_distances_mm) + out.emplace_back(texture_preview_layer_opacity_from_td(td_mm, settings.contoning_flat_surface_layer_height_mm)); + return out; +} + std::array raw_offset_visibility_factors_for_texture_preview(const TextureMappingZone &zone) { const float base_outer_width_mm = @@ -2610,7 +2622,11 @@ std::array contoning_flat_surface_rgb_for_texture_preview( settings.contoning_flat_surface_surface_scatter, settings.contoning_flat_surface_beer_lambert_rgb_correction, settings.contoning_flat_surface_td_effective_alpha_correction, - settings.component_roles); + settings.component_roles, + std::vector{}, + settings.contoning_flat_surface_calibrated_stack_model.valid() ? + &settings.contoning_flat_surface_calibrated_stack_model : + nullptr); } } if (!candidates.empty()) { @@ -2763,13 +2779,16 @@ std::optional texture_preview_simulation_setti settings.contoning_flat_surface_pattern_filaments = std::max(1, std::min(stack_layers, pattern_filaments)); settings.contoning_flat_surface_td_adjustment = zone->top_surface_contoning_active() && zone->top_surface_contoning_td_adjustment_enabled; + const int effective_color_prediction_mode = + TextureMappingZone::effective_top_surface_contoning_color_prediction_mode( + zone->top_surface_contoning_color_prediction_mode); settings.contoning_flat_surface_beer_lambert_rgb_correction = settings.contoning_flat_surface_td_adjustment && - !zone->top_surface_contoning_td_effective_alpha_correction_enabled && - zone->top_surface_contoning_beer_lambert_rgb_correction_enabled; + effective_color_prediction_mode == int(TextureMappingZone::ContoningColorPredictionBeerLambertRgb); settings.contoning_flat_surface_td_effective_alpha_correction = settings.contoning_flat_surface_td_adjustment && - zone->top_surface_contoning_td_effective_alpha_correction_enabled; + (effective_color_prediction_mode == int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha) || + effective_color_prediction_mode == int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine)); settings.contoning_flat_surface_beam_search_stack_expansion = zone->effective_top_surface_contoning_beam_search_stack_expansion_enabled(); settings.contoning_flat_surface_surface_scatter = @@ -2826,12 +2845,27 @@ std::optional texture_preview_simulation_setti mix_color_solver_components(settings.component_colors, background_weights, color_solver_mix_model_from_index(settings.generic_solver_mix_model)); + settings.contoning_flat_surface_transmission_distances_mm = + contoning_flat_surface_preview_transmission_distances(*zone, settings); settings.contoning_flat_surface_layer_opacities = - contoning_flat_surface_preview_layer_opacities(*zone, settings); + contoning_flat_surface_preview_layer_opacities(settings); if (settings.contoning_flat_surface_layer_opacities.size() != settings.component_colors.size()) { settings.contoning_flat_surface_td_adjustment = false; settings.contoning_flat_surface_beer_lambert_rgb_correction = false; settings.contoning_flat_surface_td_effective_alpha_correction = false; + } else { + const int effective_color_prediction_mode = + TextureMappingZone::effective_top_surface_contoning_color_prediction_mode( + zone->top_surface_contoning_color_prediction_mode); + if (TextureMappingZone::top_surface_contoning_color_prediction_mode_is_calibrated(effective_color_prediction_mode)) { + std::optional calibrated_model = + texture_mapping_top_surface_color_calibrated_model(*zone, + effective_color_prediction_mode, + settings.component_colors, + settings.contoning_flat_surface_transmission_distances_mm); + if (calibrated_model && calibrated_model->valid()) + settings.contoning_flat_surface_calibrated_stack_model = *calibrated_model; + } } } @@ -2888,6 +2922,8 @@ size_t texture_preview_simulation_signature(const ModelVolume &model_volume, if (settings.contoning_flat_surface_td_effective_alpha_correction) for (const ColorSolverStackComponentRole role : settings.component_roles) mix(std::hash{}(int(role))); + if (settings.contoning_flat_surface_calibrated_stack_model.valid()) + mix(std::hash{}(settings.contoning_flat_surface_calibrated_stack_model.cache_key())); } mix(std::hash{}(int(std::lround(settings.contrast_pct * 100.f)))); mix(std::hash{}(int(std::lround(settings.tone_gamma * 1000.f)))); @@ -2980,7 +3016,11 @@ TexturePreviewSimulationResult build_simulated_texture_preview_result(size_t sig settings.contoning_flat_surface_beer_lambert_rgb_correction, settings.contoning_flat_surface_td_effective_alpha_correction, settings.component_roles, - settings.contoning_flat_surface_beam_search_stack_expansion) : + settings.contoning_flat_surface_beam_search_stack_expansion, + std::vector{}, + settings.contoning_flat_surface_calibrated_stack_model.valid() ? + &settings.contoning_flat_surface_calibrated_stack_model : + nullptr) : ColorSolverOrderedStackCandidateSet{}; const std::vector contoning_flat_surface_candidates = use_contoning_flat_surface_quantization && contoning_flat_surface_ordered_candidates.empty() ? diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index eca7719f606..c98c4c5c0d3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -1038,6 +1040,50 @@ static wxColour parse_texture_mapping_color(const std::string &hex) return wxColour(rgba[0], rgba[1], rgba[2], rgba[3]); } +static std::array texture_mapping_color_array(const std::string &hex) +{ + const wxColour color = parse_texture_mapping_color(hex); + return { { float(color.Red()) / 255.f, float(color.Green()) / 255.f, float(color.Blue()) / 255.f } }; +} + +static std::vector> texture_mapping_component_color_arrays(const std::vector &ids, + const std::vector &physical_colors) +{ + std::vector> out; + out.reserve(ids.size()); + for (unsigned int id : ids) { + if (id > 0 && size_t(id - 1) < physical_colors.size()) + out.emplace_back(texture_mapping_color_array(physical_colors[size_t(id - 1)])); + else + out.emplace_back(std::array{ { 0.5f, 0.5f, 0.5f } }); + } + return out; +} + +static std::vector texture_mapping_component_tds(const TextureMappingZone &zone, + const std::vector &ids) +{ + std::vector out; + out.reserve(ids.size()); + for (unsigned int id : ids) { + const size_t idx = id > 0 ? size_t(id - 1) : size_t(-1); + out.emplace_back(idx < zone.filament_transmission_distances_mm.size() ? + zone.filament_transmission_distances_mm[idx] : + 0.f); + } + return out; +} + +static wxString texture_mapping_zone_color_calibration_warning(const TextureMappingZone &zone, + const std::vector &ids, + const std::vector &physical_colors) +{ + return from_u8(texture_mapping_top_surface_color_calibration_warning( + zone, + texture_mapping_component_color_arrays(ids, physical_colors), + texture_mapping_component_tds(zone, ids))); +} + static std::string encode_texture_mapping_component_ids(const std::vector &ids) { std::string out; @@ -2101,6 +2147,14 @@ static wxString texture_mapping_contoning_color_prediction_mode_label(int mode) return _L("RGB Beer-Lambert - not recommended"); case int(TextureMappingZone::ContoningColorPredictionBasicReflectance): return _L("Basic Reflectance"); + case int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine): + return _L("Calibrated: current linear affine"); + case int(TextureMappingZone::ContoningColorPredictionCalibratedTdAlphaEffective): + return _L("Calibrated: TD alpha effective"); + case int(TextureMappingZone::ContoningColorPredictionCalibratedFreeAlphaEffective): + return _L("Calibrated: free alpha effective"); + case int(TextureMappingZone::ContoningColorPredictionCalibratedDepthKernelLinear): + return _L("Calibrated: depth kernel linear"); default: return _L("TD Effective Alpha"); } @@ -2116,34 +2170,6 @@ static wxString texture_mapping_contoning_color_prediction_default_label() return label; } -static int texture_mapping_contoning_color_prediction_mode_choice_selection(int mode) -{ - if (mode == int(TextureMappingZone::ContoningColorPredictionDefault)) - return 0; - switch (TextureMappingZone::effective_top_surface_contoning_color_prediction_mode(mode)) { - case int(TextureMappingZone::ContoningColorPredictionBeerLambertRgb): - return 2; - case int(TextureMappingZone::ContoningColorPredictionBasicReflectance): - return 3; - default: - return 1; - } -} - -static int texture_mapping_contoning_color_prediction_mode_from_choice_selection(int selection) -{ - switch (selection) { - case 1: - return int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha); - case 2: - return int(TextureMappingZone::ContoningColorPredictionBeerLambertRgb); - case 3: - return int(TextureMappingZone::ContoningColorPredictionBasicReflectance); - default: - return TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; - } -} - static int texture_mapping_contoning_polygonization_mode_choice_selection(int mode) { return TextureMappingZone::effective_top_surface_contoning_polygonization_mode(mode) == @@ -2164,6 +2190,7 @@ public: int texture_mapping_mode, int filament_color_mode, const std::vector &component_ids, + const std::vector &component_color_hexes, const std::vector &component_strengths_pct, const std::vector &component_minimum_offsets_pct, const std::vector &component_transmission_distances_mm, @@ -2227,6 +2254,8 @@ public: bool top_surface_contoning_beer_lambert_rgb_correction_enabled, bool top_surface_contoning_td_effective_alpha_correction_enabled, bool top_surface_contoning_variable_layer_height_compensation_enabled, + const std::string &top_surface_color_calibration_name, + const std::string &top_surface_color_calibration_json, const TextureMappingZoneShellUsageSummary &top_surface_contoning_shell_usage, const TextureMappingManager &texture_mapping_zones, const TextureMappingGlobalSettings &global_settings, @@ -2238,6 +2267,9 @@ public: , m_global_settings(global_settings) , m_prime_tower_image(prime_tower_image) , m_prime_tower_image_back(prime_tower_image_back) + , m_component_color_hexes(component_color_hexes) + , m_top_surface_color_calibration_name(top_surface_color_calibration_name) + , m_top_surface_color_calibration_json(top_surface_color_calibration_json) { (void) reduce_outer_surface_texture; (void) top_surface_contoning_min_feature_mm; @@ -2946,20 +2978,40 @@ public: 0, wxEXPAND | wxTOP | wxBOTTOM, gap / 2); + auto *color_calibration_row = new wxBoxSizer(wxHORIZONTAL); + color_calibration_row->Add(new wxStaticText(m_top_surface_contoning_checkboxes_panel, wxID_ANY, _L("Top-surface color calibration")), + 0, + wxALIGN_CENTER_VERTICAL | wxRIGHT, + gap); + m_top_surface_color_calibration_label = + new wxStaticText(m_top_surface_contoning_checkboxes_panel, wxID_ANY, wxEmptyString); + color_calibration_row->Add(m_top_surface_color_calibration_label, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + auto *load_top_surface_color_calibration_button = + new wxButton(m_top_surface_contoning_checkboxes_panel, wxID_ANY, _L("Load...")); + auto *clear_top_surface_color_calibration_button = + new wxButton(m_top_surface_contoning_checkboxes_panel, wxID_ANY, _L("Clear")); + color_calibration_row->Add(load_top_surface_color_calibration_button, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(2), gap / 2)); + color_calibration_row->Add(clear_top_surface_color_calibration_button, 0, wxALIGN_CENTER_VERTICAL); + contoning_checkboxes_root->Add(color_calibration_row, 0, wxEXPAND | wxTOP | wxBOTTOM, gap / 2); + m_top_surface_color_calibration_warning_text = + new wxStaticText(m_top_surface_contoning_checkboxes_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE); + m_top_surface_color_calibration_warning_text->SetForegroundColour(wxColour(180, 86, 0)); + contoning_checkboxes_root->Add(m_top_surface_color_calibration_warning_text, 0, wxEXPAND | wxBOTTOM, gap / 2); + load_top_surface_color_calibration_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { + load_top_surface_color_calibration(); + }); + clear_top_surface_color_calibration_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { + clear_top_surface_color_calibration(); + }); auto *contoning_td_correction_row = new wxBoxSizer(wxHORIZONTAL); contoning_td_correction_row->Add(new wxStaticText(m_top_surface_contoning_checkboxes_panel, wxID_ANY, _L("Color prediction mode")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); - wxArrayString contoning_td_correction_choices; - contoning_td_correction_choices.Add(texture_mapping_contoning_color_prediction_default_label()); - contoning_td_correction_choices.Add(texture_mapping_contoning_color_prediction_mode_label(int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha))); - contoning_td_correction_choices.Add(texture_mapping_contoning_color_prediction_mode_label(int(TextureMappingZone::ContoningColorPredictionBeerLambertRgb))); - contoning_td_correction_choices.Add(texture_mapping_contoning_color_prediction_mode_label(int(TextureMappingZone::ContoningColorPredictionBasicReflectance))); m_top_surface_contoning_td_correction_choice = - new wxChoice(m_top_surface_contoning_checkboxes_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, contoning_td_correction_choices); - m_top_surface_contoning_td_correction_choice->SetSelection( - texture_mapping_contoning_color_prediction_mode_choice_selection(top_surface_contoning_color_prediction_mode)); + new wxChoice(m_top_surface_contoning_checkboxes_panel, wxID_ANY); + rebuild_top_surface_contoning_color_prediction_choices(top_surface_contoning_color_prediction_mode); + update_top_surface_color_calibration_label(); contoning_td_correction_row->Add(m_top_surface_contoning_td_correction_choice, 1, wxALIGN_CENTER_VERTICAL); contoning_checkboxes_root->Add(contoning_td_correction_row, 0, wxEXPAND | wxTOP | wxBOTTOM, gap / 2); m_top_surface_contoning_variable_layer_height_compensation_checkbox = @@ -3202,6 +3254,7 @@ public: } if (m_top_surface_contoning_td_correction_choice != nullptr) { m_top_surface_contoning_td_correction_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent &) { + update_top_surface_color_calibration_label(); update_top_surface_image_options_visibility(false); queue_top_surface_contoning_message_update(true); }); @@ -3240,9 +3293,11 @@ public: if (spin == nullptr) continue; spin->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent &) { + update_top_surface_color_calibration_label(); queue_top_surface_contoning_message_update(true); }); spin->Bind(wxEVT_TEXT, [this](wxCommandEvent &) { + update_top_surface_color_calibration_label(); queue_top_surface_contoning_message_update(true); }); } @@ -3743,16 +3798,27 @@ public: } int top_surface_contoning_color_prediction_mode() const { - return m_top_surface_contoning_td_correction_choice == nullptr ? - TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode : - texture_mapping_contoning_color_prediction_mode_from_choice_selection( - m_top_surface_contoning_td_correction_choice->GetSelection()); + if (m_top_surface_contoning_td_correction_choice == nullptr) + return TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; + const int selection = m_top_surface_contoning_td_correction_choice->GetSelection(); + return selection >= 0 && size_t(selection) < m_top_surface_contoning_color_prediction_modes.size() ? + m_top_surface_contoning_color_prediction_modes[size_t(selection)] : + TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode; + } + std::string top_surface_color_calibration_name() const + { + return m_top_surface_color_calibration_name; + } + std::string top_surface_color_calibration_json() const + { + return m_top_surface_color_calibration_json; } bool top_surface_contoning_td_effective_alpha_correction_selected() const { - return TextureMappingZone::effective_top_surface_contoning_color_prediction_mode( - top_surface_contoning_color_prediction_mode()) == - int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha); + const int effective_mode = TextureMappingZone::effective_top_surface_contoning_color_prediction_mode( + top_surface_contoning_color_prediction_mode()); + return effective_mode == int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha) || + effective_mode == int(TextureMappingZone::ContoningColorPredictionCalibratedCurrentLinearAffine); } bool top_surface_contoning_surface_scatter_enabled() const { @@ -4014,6 +4080,135 @@ private: update_prime_tower_image_label(); } + std::vector> top_surface_color_calibration_component_colors() const + { + std::vector> colors; + colors.reserve(m_component_color_hexes.size()); + for (const std::string &hex : m_component_color_hexes) + colors.emplace_back(texture_mapping_color_array(hex)); + return colors; + } + + void rebuild_top_surface_contoning_color_prediction_choices(int selected_mode) + { + if (m_top_surface_contoning_td_correction_choice == nullptr) + return; + + m_top_surface_contoning_color_prediction_modes.clear(); + auto add_mode = [this](int mode) { + m_top_surface_contoning_color_prediction_modes.emplace_back(mode); + m_top_surface_contoning_td_correction_choice->Append( + mode == int(TextureMappingZone::ContoningColorPredictionDefault) ? + texture_mapping_contoning_color_prediction_default_label() : + texture_mapping_contoning_color_prediction_mode_label(mode)); + }; + + m_top_surface_contoning_td_correction_choice->Clear(); + add_mode(int(TextureMappingZone::ContoningColorPredictionDefault)); + add_mode(int(TextureMappingZone::ContoningColorPredictionTdEffectiveAlpha)); + add_mode(int(TextureMappingZone::ContoningColorPredictionBeerLambertRgb)); + add_mode(int(TextureMappingZone::ContoningColorPredictionBasicReflectance)); + for (int mode : texture_mapping_top_surface_color_calibration_supported_modes(m_top_surface_color_calibration_json)) + add_mode(mode); + + auto it = std::find(m_top_surface_contoning_color_prediction_modes.begin(), + m_top_surface_contoning_color_prediction_modes.end(), + selected_mode); + if (it == m_top_surface_contoning_color_prediction_modes.end()) + it = std::find(m_top_surface_contoning_color_prediction_modes.begin(), + m_top_surface_contoning_color_prediction_modes.end(), + int(TextureMappingZone::ContoningColorPredictionDefault)); + m_top_surface_contoning_td_correction_choice->SetSelection( + it != m_top_surface_contoning_color_prediction_modes.end() ? + int(it - m_top_surface_contoning_color_prediction_modes.begin()) : + 0); + } + + void update_top_surface_color_calibration_label() + { + if (m_top_surface_color_calibration_label != nullptr) { + wxString label = _L("No calibration loaded"); + wxString tooltip; + if (!m_top_surface_color_calibration_json.empty()) { + std::string name = m_top_surface_color_calibration_name; + if (name.empty()) + name = texture_mapping_top_surface_color_calibration_display_name(m_top_surface_color_calibration_json); + if (name.empty()) + name = "Loaded calibration"; + tooltip = from_u8(name); + wxString display = from_u8(name); + if (display.length() > 42) + display = display.Left(19) + "..." + display.Right(20); + label = display; + } + m_top_surface_color_calibration_label->SetLabel(label); + m_top_surface_color_calibration_label->SetToolTip(tooltip); + } + + if (m_top_surface_color_calibration_warning_text != nullptr) { + TextureMappingZone tmp; + tmp.top_surface_color_calibration_name = m_top_surface_color_calibration_name; + tmp.top_surface_color_calibration_json = m_top_surface_color_calibration_json; + tmp.top_surface_contoning_color_prediction_mode = top_surface_contoning_color_prediction_mode(); + const wxString warning = from_u8(texture_mapping_top_surface_color_calibration_warning( + tmp, + top_surface_color_calibration_component_colors(), + component_transmission_distances_mm())); + m_top_surface_color_calibration_warning_text->SetLabel(warning); + m_top_surface_color_calibration_warning_text->Show(!warning.empty()); + m_top_surface_color_calibration_warning_text->Wrap(FromDIP(390)); + } + Layout(); + } + + void load_top_surface_color_calibration() + { + wxFileDialog dlg(this, + _L("Load top-surface color calibration"), + wxEmptyString, + wxEmptyString, + _L("JSON files (*.json)|*.json"), + wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dlg.ShowModal() != wxID_OK) + return; + + boost::nowide::ifstream file(into_u8(dlg.GetPath()), std::ios::binary); + if (!file) { + MessageDialog msg(this, _L("Could not read the selected calibration file."), _L("Invalid calibration"), wxOK | wxICON_WARNING); + msg.ShowModal(); + return; + } + std::ostringstream ss; + ss << file.rdbuf(); + const std::string json_text = ss.str(); + std::string error; + if (!texture_mapping_parse_top_surface_color_calibration(json_text, &error)) { + MessageDialog msg(this, + error.empty() ? _L("The selected file is not a supported top-surface color calibration.") : from_u8(error), + _L("Invalid calibration"), + wxOK | wxICON_WARNING); + msg.ShowModal(); + return; + } + + m_top_surface_color_calibration_json = json_text; + m_top_surface_color_calibration_name = + boost::filesystem::path(into_u8(dlg.GetPath())).filename().string(); + const int mode = texture_mapping_top_surface_color_calibration_first_supported_mode(m_top_surface_color_calibration_json); + rebuild_top_surface_contoning_color_prediction_choices(mode); + update_top_surface_color_calibration_label(); + update_top_surface_image_options_visibility(true); + } + + void clear_top_surface_color_calibration() + { + m_top_surface_color_calibration_name.clear(); + m_top_surface_color_calibration_json.clear(); + rebuild_top_surface_contoning_color_prediction_choices(TextureMappingZone::DefaultTopSurfaceContoningColorPredictionMode); + update_top_surface_color_calibration_label(); + update_top_surface_image_options_visibility(true); + } + void reset_strengths_and_offsets() { for (wxSlider *slider : m_minimum_offset_sliders) @@ -4770,6 +4965,9 @@ private: wxCheckBox *m_top_surface_contoning_beam_search_stack_expansion_checkbox {nullptr}; wxCheckBox *m_top_surface_contoning_surface_scatter_checkbox {nullptr}; wxChoice *m_top_surface_contoning_td_correction_choice {nullptr}; + std::vector m_top_surface_contoning_color_prediction_modes; + wxStaticText *m_top_surface_color_calibration_label {nullptr}; + wxStaticText *m_top_surface_color_calibration_warning_text {nullptr}; wxCheckBox *m_top_surface_contoning_variable_layer_height_compensation_checkbox {nullptr}; wxCheckBox *m_use_legacy_fixed_color_mode_checkbox {nullptr}; wxCheckBox *m_minimum_visibility_offset_checkbox {nullptr}; @@ -4789,6 +4987,9 @@ private: TextureMappingGlobalSettings m_global_settings; TextureMappingPrimeTowerImage m_prime_tower_image; TextureMappingPrimeTowerImage m_prime_tower_image_back; + std::vector m_component_color_hexes; + std::string m_top_surface_color_calibration_name; + std::string m_top_surface_color_calibration_json; std::vector m_minimum_offset_sliders; std::vector m_minimum_offset_spins; std::vector m_strength_sliders; @@ -9128,6 +9329,21 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) _L("Preview Result Colors"), bundle->texture_mapping_global_settings.preview_simulate_colors); editor_sizer->Add(preview_colors_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto *color_calibration_warning_text = new wxStaticText(editor, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxST_NO_AUTORESIZE); + color_calibration_warning_text->SetForegroundColour(wxColour(180, 86, 0)); + editor_sizer->Add(color_calibration_warning_text, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); + auto update_color_calibration_warning_text = [this, color_calibration_warning_text, num_physical, physical_colors](const TextureMappingZone &zone) { + if (color_calibration_warning_text == nullptr) + return; + const std::vector warning_ids = zone.is_image_texture() ? + TextureMappingManager::effective_texture_component_ids(zone, num_physical, physical_colors) : + texture_mapping_selected_ids(zone, num_physical); + const wxString warning = texture_mapping_zone_color_calibration_warning(zone, warning_ids, physical_colors); + color_calibration_warning_text->SetLabel(warning); + color_calibration_warning_text->Show(!warning.empty()); + color_calibration_warning_text->Wrap(FromDIP(300)); + }; + update_color_calibration_warning_text(entry); auto *button_row = new wxBoxSizer(wxHORIZONTAL); auto *offset_btn = new wxButton(editor, wxID_ANY, _L("2D Gradient Settings")); @@ -9141,7 +9357,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) linear_gradient_stops_bar, linear_gradient_mode_choice, linear_gradient_radius_spin, linear_gradient_radius_percent_chk, surface_pattern_from_selection, linear_gradient_mode_from_selection, mode_choice, - show_direction_arrow_chk, contrast_spin, apply_zone]() { + show_direction_arrow_chk, contrast_spin, apply_zone, update_color_calibration_warning_text]() { if (mgr_ptr == nullptr) return; auto &rows = mgr_ptr->zones(); @@ -9193,6 +9409,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.show_linear_gradient_direction_arrow = show_direction_arrow_chk != nullptr && show_direction_arrow_chk->GetValue(); updated.contrast_pct = contrast_spin != nullptr ? std::clamp(float(contrast_spin->GetValue()), 25.f, 300.f) : updated.contrast_pct; updated.high_resolution_sampling = true; + update_color_calibration_warning_text(updated); apply_zone(std::move(updated)); }; @@ -9216,7 +9433,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) } evt.Skip(); }); - auto update_pattern_visibility = [this, editor_sizer, filaments_row, linear_gradient_mode_row, linear_gradient_mode_choice, linear_gradient_stops_bar, linear_gradient_row, linear_gradient_point_row, linear_gradient_buttons_row, linear_gradient_radius_row, show_direction_arrow_row, mode_row, contrast_row, preview_colors_row, offset_btn, advanced_btn, surface_choice, row, editor, update_texture_mapping_area_height, set_linear_gradient_picker_button_labels, sync_linear_gradient_stop_controls]() { + auto update_pattern_visibility = [this, editor_sizer, filaments_row, linear_gradient_mode_row, linear_gradient_mode_choice, linear_gradient_stops_bar, linear_gradient_row, linear_gradient_point_row, linear_gradient_buttons_row, linear_gradient_radius_row, show_direction_arrow_row, mode_row, contrast_row, preview_colors_row, color_calibration_warning_text, offset_btn, advanced_btn, surface_choice, row, editor, update_texture_mapping_area_height, set_linear_gradient_picker_button_labels, sync_linear_gradient_stop_controls]() { const int selection = surface_choice == nullptr ? 0 : surface_choice->GetSelection(); const bool image_texture = selection == 0; const bool linear_gradient = selection == 1; @@ -9233,6 +9450,8 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) editor_sizer->Show(mode_row, image_texture, true); editor_sizer->Show(contrast_row, image_texture, true); editor_sizer->Show(preview_colors_row, image_texture, true); + if (color_calibration_warning_text != nullptr) + editor_sizer->Show(color_calibration_warning_text, !color_calibration_warning_text->GetLabel().empty(), true); if (offset_btn != nullptr) offset_btn->Show(selection == 2); if (advanced_btn != nullptr) @@ -9306,6 +9525,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) surface_choice, contrast_spin, set_sidebar_checkbox_value, + update_color_calibration_warning_text, surface_pattern_from_selection, apply_zone, last_mode_selection](wxCommandEvent &) mutable { @@ -9388,6 +9608,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) if (filament_checks[idx] != nullptr) set_sidebar_checkbox_value(filament_checks[idx], std::find(ids.begin(), ids.end(), unsigned(idx + 1)) != ids.end()); + update_color_calibration_warning_text(updated); apply_zone(std::move(updated)); }; mode_choice->Bind(wxEVT_COMBOBOX, on_mode_choice); @@ -9733,8 +9954,11 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) strengths.reserve(ids.size()); offsets.reserve(ids.size()); transmission_distances.reserve(ids.size()); + std::vector component_color_hexes; + component_color_hexes.reserve(ids.size()); for (const unsigned int id : ids) { const size_t idx = id > 0 ? size_t(id - 1) : size_t(0); + component_color_hexes.emplace_back(idx < physical_colors.size() ? physical_colors[idx] : std::string("#808080")); strengths.emplace_back(idx < updated.filament_strengths_pct.size() ? updated.filament_strengths_pct[idx] : 100.f); offsets.emplace_back(idx < updated.filament_minimum_offsets_pct.size() ? updated.filament_minimum_offsets_pct[idx] : 0.f); transmission_distances.emplace_back(idx < updated.filament_transmission_distances_mm.size() ? @@ -9747,6 +9971,7 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.texture_mapping_mode, updated.filament_color_mode, ids, + component_color_hexes, strengths, offsets, transmission_distances, @@ -9810,6 +10035,8 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) updated.top_surface_contoning_beer_lambert_rgb_correction_enabled, updated.top_surface_contoning_td_effective_alpha_correction_enabled, updated.top_surface_contoning_variable_layer_height_compensation_enabled, + updated.top_surface_color_calibration_name, + updated.top_surface_color_calibration_json, shell_usage, bundle->texture_mapping_zones, bundle->texture_mapping_global_settings, @@ -9906,6 +10133,10 @@ void Sidebar::update_texture_mapping_panel(bool sync_manager) dlg.top_surface_contoning_td_effective_alpha_correction_enabled(); updated.top_surface_contoning_variable_layer_height_compensation_enabled = dlg.top_surface_contoning_variable_layer_height_compensation_enabled(); + updated.top_surface_color_calibration_name = + dlg.top_surface_color_calibration_name(); + updated.top_surface_color_calibration_json = + dlg.top_surface_color_calibration_json(); if (updated.top_surface_image_printing_enabled && updated.top_surface_image_printing_method == int(TextureMappingZone::TopSurfaceImageContoning)) { updated.modulation_mode = int(TextureMappingZone::ModulationPerimeterPathV2);