diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 1708928bba..4727938a69 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -77,10 +77,10 @@ jobs: notes_content="" for notes_file in \ - "doc/changelogs/CHANGELOG_v${version}.md" \ "doc/changelogs/RELEASE_NOTES_v${version}.md" \ - "CHANGELOG_v${version}.md" \ - "RELEASE_NOTES_v${version}.md"; do + "doc/changelogs/CHANGELOG_v${version}.md" \ + "RELEASE_NOTES_v${version}.md" \ + "CHANGELOG_v${version}.md"; do if [ -f "$notes_file" ]; then notes_content=$(cat "$notes_file") break diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 34fd0b44e8..5723920f89 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -861,6 +861,601 @@ static std::vector build_weighted_gradient_sequence(const std::vec return sequence; } +static unsigned int decode_manual_pattern_preview_token(char token, unsigned int component_a, unsigned int component_b, size_t num_physical) +{ + unsigned int extruder_id = 0; + if (token == '1') + extruder_id = component_a; + else if (token == '2') + extruder_id = component_b; + else if (token >= '3' && token <= '9') + extruder_id = unsigned(token - '0'); + + return (extruder_id >= 1 && extruder_id <= num_physical) ? extruder_id : 0; +} + +static std::vector build_grouped_manual_pattern_preview_sequence(const std::string &pattern, + unsigned int component_a, + unsigned int component_b, + size_t num_physical, + size_t wall_loops) +{ + std::vector sequence; + if (num_physical == 0) + return sequence; + + const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern); + if (normalized.empty()) + return sequence; + + const std::vector groups = split_manual_pattern_groups(normalized); + if (groups.empty()) + return sequence; + + if (groups.size() == 1) { + sequence.reserve(normalized.size()); + for (const char token : normalized) { + const unsigned int extruder_id = + decode_manual_pattern_preview_token(token, component_a, component_b, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + return sequence; + } + + constexpr size_t k_max_preview_cycle = 48; + size_t cycle = 1; + for (const std::string &group : groups) { + if (group.empty()) + continue; + cycle = std::lcm(cycle, group.size()); + if (cycle >= k_max_preview_cycle) { + cycle = k_max_preview_cycle; + break; + } + } + + const size_t preview_wall_loops = std::max(1, wall_loops == 0 ? groups.size() : wall_loops); + sequence.reserve(preview_wall_loops * cycle); + for (size_t layer_idx = 0; layer_idx < cycle; ++layer_idx) { + for (size_t wall_idx = 0; wall_idx < preview_wall_loops; ++wall_idx) { + const std::string &group = groups[std::min(wall_idx, groups.size() - 1)]; + if (group.empty()) + continue; + const char token = group[layer_idx % group.size()]; + const unsigned int extruder_id = + decode_manual_pattern_preview_token(token, component_a, component_b, num_physical); + if (extruder_id != 0) + sequence.emplace_back(extruder_id); + } + } + + return sequence; +} + +static std::pair effective_pair_preview_ratios(int percent_b) +{ + const int mix_b = std::clamp(percent_b, 0, 100); + int ratio_a = 1; + int ratio_b = 0; + + if (mix_b >= 100) { + ratio_a = 0; + ratio_b = 1; + } else if (mix_b > 0) { + const int pct_b = mix_b; + const int pct_a = 100 - pct_b; + const bool b_is_major = pct_b >= pct_a; + const int major_pct = b_is_major ? pct_b : pct_a; + const int minor_pct = b_is_major ? pct_a : pct_b; + const int major_layers = + std::max(1, int(std::lround(double(major_pct) / double(std::max(1, minor_pct))))); + ratio_a = b_is_major ? 1 : major_layers; + ratio_b = b_is_major ? major_layers : 1; + } + + if (ratio_a > 0 && ratio_b > 0) { + const int g = std::gcd(ratio_a, ratio_b); + if (g > 1) { + ratio_a /= g; + ratio_b /= g; + } + } + + return { std::max(0, ratio_a), std::max(0, ratio_b) }; +} + +static std::vector build_effective_pair_preview_sequence(unsigned int component_a, + unsigned int component_b, + int percent_b, + bool limit_cycle) +{ + std::vector sequence; + if (component_a == 0 || component_b == 0 || component_a == component_b) + return sequence; + + auto [ratio_a, ratio_b] = effective_pair_preview_ratios(percent_b); + constexpr int k_max_cycle = 24; + if (limit_cycle && ratio_a > 0 && ratio_b > 0 && ratio_a + ratio_b > k_max_cycle) { + const double scale = double(k_max_cycle) / double(ratio_a + ratio_b); + ratio_a = std::max(1, int(std::round(double(ratio_a) * scale))); + ratio_b = std::max(1, int(std::round(double(ratio_b) * scale))); + } + if (ratio_a == 0 && ratio_b == 0) + ratio_a = 1; + + const int cycle = std::max(1, ratio_a + ratio_b); + sequence.reserve(size_t(cycle)); + for (int pos = 0; pos < cycle; ++pos) { + const int b_before = (pos * ratio_b) / cycle; + const int b_after = ((pos + 1) * ratio_b) / cycle; + sequence.emplace_back((b_after > b_before) ? component_b : component_a); + } + return sequence; +} + +static std::string blend_display_color_from_sequence(const std::vector &colors, + size_t num_physical, + const std::vector &sequence, + const std::string &fallback) +{ + if (colors.empty() || sequence.empty() || num_physical == 0) + return fallback; + + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + std::vector> color_percents; + color_percents.reserve(num_physical); + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] == 0 || id > colors.size()) + continue; + color_percents.emplace_back(colors[id - 1], int(counts[id])); + } + if (color_percents.empty()) + return fallback; + + if (color_percents.size() == 1) + return color_percents.front().first; + + return MixedFilamentManager::blend_color_multi(color_percents); +} + +static std::vector build_local_z_preview_pass_heights(double nominal_layer_height, + double lower_bound, + double upper_bound, + double preferred_a_height, + double preferred_b_height, + int mix_b_percent, + int max_sublayers_limit) +{ + if (nominal_layer_height <= EPSILON) + return {}; + + const double base_height = nominal_layer_height; + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + const size_t max_passes_limit = max_sublayers_limit >= 2 ? size_t(max_sublayers_limit) : size_t(0); + + auto fit_pass_heights_to_interval = [](std::vector &passes, double total_height, double local_lo, double local_hi) { + if (passes.empty() || total_height <= EPSILON) + return false; + + const auto within = [local_lo, local_hi](double value) { + return value >= local_lo - 1e-6 && value <= local_hi + 1e-6; + }; + + double sum = 0.0; + for (const double h : passes) + sum += h; + + double delta = total_height - sum; + if (std::abs(delta) > 1e-6) { + if (delta > 0.0) { + for (double &h : passes) { + if (delta <= 1e-6) + break; + const double room = local_hi - h; + if (room <= 1e-6) + continue; + const double take = std::min(room, delta); + h += take; + delta -= take; + } + } else { + for (auto it = passes.rbegin(); it != passes.rend() && delta < -1e-6; ++it) { + const double room = *it - local_lo; + if (room <= 1e-6) + continue; + const double take = std::min(room, -delta); + *it -= take; + delta += take; + } + } + } + + if (std::abs(delta) > 1e-6) + return false; + return std::all_of(passes.begin(), passes.end(), within); + }; + + auto build_uniform = [&fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit]() { + std::vector out; + size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); + size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); + size_t pass_count = min_passes; + + if (max_passes >= min_passes) { + const double target_step = 0.5 * (lo + hi); + const size_t target_passes = + size_t(std::max(1.0, std::llround(base_height / std::max(target_step, EPSILON)))); + pass_count = std::clamp(target_passes, min_passes, max_passes); + } + + if (max_passes_limit > 0 && pass_count > max_passes_limit) + pass_count = max_passes_limit; + + if (pass_count == 1 && base_height >= 2.0 * lo - EPSILON && max_passes >= 2) + pass_count = 2; + + if (pass_count <= 1) { + out.emplace_back(base_height); + return out; + } + + out.assign(pass_count, base_height / double(pass_count)); + double accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + if (!fit_pass_heights_to_interval(out, base_height, lo, hi) && max_passes_limit == 0) { + out.assign(pass_count, base_height / double(pass_count)); + accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + } + return out; + }; + + auto build_alternating = [&build_uniform, &fit_pass_heights_to_interval, base_height, lo, hi, max_passes_limit](double gradient_h_a, double gradient_h_b) { + if (base_height < 2.0 * lo - EPSILON) + return std::vector{ base_height }; + + const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); + const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); + + size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); + if ((min_passes % 2) != 0) + ++min_passes; + + size_t max_passes = size_t(std::max(2.0, std::floor((base_height + EPSILON) / lo))); + if ((max_passes % 2) != 0) + --max_passes; + if (max_passes_limit > 0) { + size_t capped_limit = std::max(2, max_passes_limit); + if ((capped_limit % 2) != 0) + --capped_limit; + if (capped_limit >= 2) + max_passes = std::min(max_passes, capped_limit); + } + if (max_passes < 2) + return build_uniform(); + if (min_passes > max_passes) + min_passes = max_passes; + if (min_passes < 2) + min_passes = 2; + if ((min_passes % 2) != 0) + ++min_passes; + if (min_passes > max_passes) + return build_uniform(); + + const double target_step = 0.5 * (lo + hi); + size_t target_passes = + size_t(std::max(2.0, std::llround(base_height / std::max(target_step, EPSILON)))); + if ((target_passes % 2) != 0) { + const size_t round_up = (target_passes < max_passes) ? (target_passes + 1) : max_passes; + const size_t round_down = (target_passes > min_passes) ? (target_passes - 1) : min_passes; + if (round_up > max_passes) + target_passes = round_down; + else if (round_down < min_passes) + target_passes = round_up; + else + target_passes = ((round_up - target_passes) <= (target_passes - round_down)) ? round_up : round_down; + } + target_passes = std::clamp(target_passes, min_passes, max_passes); + + bool has_best = false; + std::vector best_passes; + double best_ratio_error = 0.0; + size_t best_pass_distance = 0; + double best_max_height = 0.0; + size_t best_pass_count = 0; + + for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { + const size_t pair_count = pass_count / 2; + if (pair_count == 0) + continue; + const double pair_h = base_height / double(pair_count); + + const double h_a_min = std::max(lo, pair_h - hi); + const double h_a_max = std::min(hi, pair_h - lo); + if (h_a_min > h_a_max + EPSILON) + continue; + + const double h_a = std::clamp(pair_h * ratio_a, h_a_min, h_a_max); + const double h_b = pair_h - h_a; + + std::vector out; + out.reserve(pass_count); + for (size_t pair_idx = 0; pair_idx < pair_count; ++pair_idx) { + out.emplace_back(h_a); + out.emplace_back(h_b); + } + if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) + continue; + + const double ratio_actual = (h_a + h_b > EPSILON) ? (h_a / (h_a + h_b)) : 0.5; + const double ratio_error = std::abs(ratio_actual - ratio_a); + const size_t pass_distance = + (pass_count > target_passes) ? (pass_count - target_passes) : (target_passes - pass_count); + const double max_height = std::max(h_a, h_b); + + const bool better_ratio = !has_best || (ratio_error + 1e-6 < best_ratio_error); + const bool similar_ratio = has_best && std::abs(ratio_error - best_ratio_error) <= 1e-6; + const bool better_distance = similar_ratio && (pass_distance < best_pass_distance); + const bool similar_distance = similar_ratio && (pass_distance == best_pass_distance); + const bool better_max_height = similar_distance && (max_height + 1e-6 < best_max_height); + const bool similar_max_height = similar_distance && std::abs(max_height - best_max_height) <= 1e-6; + const bool better_pass_count = similar_max_height && (pass_count > best_pass_count); + + if (better_ratio || better_distance || better_max_height || better_pass_count) { + has_best = true; + best_passes = std::move(out); + best_ratio_error = ratio_error; + best_pass_distance = pass_distance; + best_max_height = max_height; + best_pass_count = pass_count; + } + } + + return has_best ? best_passes : build_uniform(); + }; + + if (preferred_a_height > EPSILON || preferred_b_height > EPSILON) { + std::vector cadence_unit; + if (preferred_a_height > EPSILON) + cadence_unit.push_back(std::clamp(preferred_a_height, lo, hi)); + if (preferred_b_height > EPSILON) + cadence_unit.push_back(std::clamp(preferred_b_height, lo, hi)); + + if (!cadence_unit.empty()) { + std::vector out; + out.reserve(size_t(std::ceil(base_height / lo)) + 2); + + double z_used = 0.0; + size_t idx = 0; + size_t guard = 0; + while (z_used + cadence_unit[idx] < base_height - EPSILON && guard++ < 100000) { + out.push_back(cadence_unit[idx]); + z_used += cadence_unit[idx]; + idx = (idx + 1) % cadence_unit.size(); + } + + const double remainder = base_height - z_used; + if (remainder > EPSILON) + out.push_back(remainder); + + if (fit_pass_heights_to_interval(out, base_height, lo, hi) && + (max_passes_limit == 0 || out.size() <= max_passes_limit)) + return out; + } + + if (preferred_a_height > EPSILON && preferred_b_height > EPSILON) + return build_alternating(preferred_a_height, preferred_b_height); + return build_uniform(); + } + + const int mix_b = std::clamp(mix_b_percent, 0, 100); + const double pct_b = double(mix_b) / 100.0; + const double pct_a = 1.0 - pct_b; + const double gradient_h_a = lo + pct_a * (hi - lo); + const double gradient_h_b = lo + pct_b * (hi - lo); + return build_alternating(gradient_h_a, gradient_h_b); +} + +static double mixed_filament_reference_nozzle_mm(unsigned int component_a, + unsigned int component_b, + const std::vector &nozzle_diameters) +{ + std::vector samples; + samples.reserve(2); + + auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) { + if (component_id >= 1 && component_id <= nozzle_diameters.size()) + samples.emplace_back(std::max(0.05, nozzle_diameters[size_t(component_id - 1)])); + }; + + append_if_valid(component_a); + append_if_valid(component_b); + + if (samples.empty()) + return 0.4; + return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size()); +} + +int mixed_filament_effective_local_z_preview_mix_b_percent(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings) +{ + if (!preview_settings.local_z_mode) + return std::clamp(mf.mix_b_percent, 0, 100); + + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); + if (!normalized_pattern.empty() || mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) + return std::clamp(mf.mix_b_percent, 0, 100); + + const std::vector gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, 9); + if (gradient_ids.size() >= 3) + return std::clamp(mf.mix_b_percent, 0, 100); + + const std::vector pass_heights = build_local_z_preview_pass_heights(preview_settings.nominal_layer_height, + preview_settings.mixed_lower_bound, + preview_settings.mixed_upper_bound, + preview_settings.preferred_a_height, + preview_settings.preferred_b_height, + mf.mix_b_percent, + 0); + if (pass_heights.empty()) + return std::clamp(mf.mix_b_percent, 0, 100); + + double expected_h_a = preview_settings.preferred_a_height; + double expected_h_b = preview_settings.preferred_b_height; + if (expected_h_a <= EPSILON && expected_h_b <= EPSILON) { + const int mix_b = std::clamp(mf.mix_b_percent, 0, 100); + const double pct_b = double(mix_b) / 100.0; + const double pct_a = 1.0 - pct_b; + const double lo = std::max(0.01, preview_settings.mixed_lower_bound); + const double hi = std::max(lo, preview_settings.mixed_upper_bound); + expected_h_a = lo + pct_a * (hi - lo); + expected_h_b = lo + pct_b * (hi - lo); + } + + auto choose_start_with_component_a = [](const std::vector &passes, double local_expected_h_a, double local_expected_h_b) { + double err_ab = 0.0; + double err_ba = 0.0; + for (size_t pass_i = 0; pass_i < passes.size(); ++pass_i) { + const double expected_ab = (pass_i % 2) == 0 ? local_expected_h_a : local_expected_h_b; + const double expected_ba = (pass_i % 2) == 0 ? local_expected_h_b : local_expected_h_a; + err_ab += std::abs(passes[pass_i] - expected_ab); + err_ba += std::abs(passes[pass_i] - expected_ba); + } + if (err_ab + 1e-6 < err_ba) + return true; + if (err_ba + 1e-6 < err_ab) + return false; + return local_expected_h_a >= local_expected_h_b; + }; + + const bool start_with_a = choose_start_with_component_a(pass_heights, expected_h_a, expected_h_b); + double total_a = 0.0; + double total_b = 0.0; + for (size_t pass_i = 0; pass_i < pass_heights.size(); ++pass_i) { + const bool even_pass = (pass_i % 2) == 0; + const bool pass_is_a = even_pass ? start_with_a : !start_with_a; + if (pass_is_a) + total_a += pass_heights[pass_i]; + else + total_b += pass_heights[pass_i]; + } + + const double total = total_a + total_b; + if (total <= EPSILON) + return std::clamp(mf.mix_b_percent, 0, 100); + return std::clamp(int(std::lround(100.0 * total_b / total)), 0, 100); +} + +bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled) +{ + if (!bias_mode_enabled) + return false; + if (preview_settings.local_z_mode) + return false; + if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) + return false; + if (!MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern).empty()) + return false; + if (decode_gradient_component_ids(mf.gradient_component_ids, 9).size() >= 3) + return false; + return mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b; +} + +std::pair mixed_filament_apparent_pair_percentages(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings, + const std::vector &nozzle_diameters, + bool bias_mode_enabled) +{ + const int base_b = mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings); + if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) + return { 100 - base_b, base_b }; + + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + const int apparent_b = MixedFilamentManager::apparent_mix_b_percent(base_b, + mf.component_a_surface_offset, + mf.component_b_surface_offset, + float(reference_nozzle_mm)); + return { 100 - apparent_b, apparent_b }; +} + +std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context) +{ + constexpr const char *fallback = "#26A69A"; + if (context.num_physical == 0 || context.physical_colors.empty()) + return fallback; + + if (mixed_filament_supports_bias_apparent_color(entry, context.preview_settings, context.component_bias_enabled) && + entry.component_a >= 1 && entry.component_b >= 1 && + entry.component_a <= context.num_physical && entry.component_b <= context.num_physical && + entry.component_a <= context.physical_colors.size() && entry.component_b <= context.physical_colors.size()) { + const auto [apparent_pct_a, apparent_pct_b] = + mixed_filament_apparent_pair_percentages(entry, context.preview_settings, context.nozzle_diameters, context.component_bias_enabled); + return MixedFilamentManager::blend_color( + context.physical_colors[entry.component_a - 1], + context.physical_colors[entry.component_b - 1], + apparent_pct_a, + apparent_pct_b); + } + + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); + if (!normalized_pattern.empty()) { + const std::vector sequence = build_grouped_manual_pattern_preview_sequence( + normalized_pattern, entry.component_a, entry.component_b, context.num_physical, context.preview_settings.wall_loops); + if (!sequence.empty()) + return blend_display_color_from_sequence(context.physical_colors, context.num_physical, sequence, fallback); + } + + if (entry.distribution_mode != int(MixedFilament::Simple)) { + const std::vector gradient_ids = decode_gradient_component_ids(entry.gradient_component_ids, context.num_physical); + if (gradient_ids.size() >= 3) { + const std::vector gradient_weights = + decode_gradient_component_weights(entry.gradient_component_weights, gradient_ids.size()); + const std::vector sequence = build_weighted_gradient_sequence( + gradient_ids, gradient_weights.empty() ? std::vector(gradient_ids.size(), 1) : gradient_weights); + if (!sequence.empty()) + return blend_display_color_from_sequence(context.physical_colors, context.num_physical, sequence, fallback); + } + } + + const int effective_mix_b = mixed_filament_effective_local_z_preview_mix_b_percent(entry, context.preview_settings); + const bool same_layer_mode = entry.distribution_mode == int(MixedFilament::SameLayerPointillisme); + const std::vector pair_sequence = + build_effective_pair_preview_sequence(entry.component_a, entry.component_b, effective_mix_b, same_layer_mode); + if (!pair_sequence.empty()) + return blend_display_color_from_sequence(context.physical_colors, context.num_physical, pair_sequence, fallback); + + if (entry.component_a == 0 || entry.component_b == 0 || + entry.component_a > context.num_physical || entry.component_b > context.num_physical || + entry.component_a > context.physical_colors.size() || entry.component_b > context.physical_colors.size()) { + return fallback; + } + + const int mix_b = std::clamp(entry.mix_b_percent, 0, 100); + return MixedFilamentManager::blend_color( + context.physical_colors[entry.component_a - 1], + context.physical_colors[entry.component_b - 1], + 100 - mix_b, + mix_b); +} + // --------------------------------------------------------------------------- // MixedFilamentManager // --------------------------------------------------------------------------- @@ -1651,54 +2246,16 @@ int MixedFilamentManager::apparent_mix_b_percent(int mix_b_percent, void MixedFilamentManager::refresh_display_colors(const std::vector &filament_colours) { - for (MixedFilament &mf : m_mixed) { - const std::vector gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, filament_colours.size()); - if (mf.distribution_mode != int(MixedFilament::Simple) && gradient_ids.size() >= 3) { - const std::vector gradient_weights = - decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size()); - const std::vector gradient_sequence = - build_weighted_gradient_sequence(gradient_ids, - gradient_weights.empty() ? std::vector(gradient_ids.size(), 1) : gradient_weights); - if (gradient_sequence.empty()) { - mf.display_color = "#26A69A"; - continue; - } + MixedFilamentDisplayContext context = m_display_context; + context.num_physical = filament_colours.size(); + context.physical_colors = filament_colours; + if (context.preview_settings.wall_loops == 0) + context.preview_settings.wall_loops = 1; + if (context.nozzle_diameters.size() < context.num_physical) + context.nozzle_diameters.resize(context.num_physical, 0.4); - std::vector counts(gradient_ids.size(), 0); - for (const unsigned int id : gradient_sequence) { - auto it = std::find(gradient_ids.begin(), gradient_ids.end(), id); - if (it != gradient_ids.end()) - ++counts[size_t(it - gradient_ids.begin())]; - } - std::vector> color_percents; - color_percents.reserve(gradient_ids.size()); - for (size_t i = 0; i < gradient_ids.size(); ++i) { - const int wi = std::max(0, counts[i]); - if (wi == 0) - continue; - color_percents.emplace_back(filament_colours[gradient_ids[i] - 1], wi); - } - mf.display_color = blend_color_multi(color_percents); - continue; - } - if (mf.component_a == 0 || mf.component_b == 0 || - mf.component_a > filament_colours.size() || mf.component_b > filament_colours.size()) { - mf.display_color = "#26A69A"; - continue; - } - const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); - const int ratio_b = - (mf.distribution_mode != int(MixedFilament::SameLayerPointillisme) && - normalized_pattern.empty() && - gradient_ids.size() < 3) ? - apparent_mix_b_percent(mf.mix_b_percent, mf.component_a_surface_offset, mf.component_b_surface_offset) : - clamp_int(mf.mix_b_percent, 0, 100); - const int ratio_a = std::max(0, 100 - ratio_b); - mf.display_color = blend_color( - filament_colours[mf.component_a - 1], - filament_colours[mf.component_b - 1], - ratio_a, ratio_b); - } + for (MixedFilament &mf : m_mixed) + mf.display_color = compute_mixed_filament_display_color(mf, context); } size_t MixedFilamentManager::enabled_count() const @@ -1719,4 +2276,17 @@ std::vector MixedFilamentManager::display_colors() const return colors; } +void MixedFilamentManager::set_display_context(const MixedFilamentDisplayContext &context) +{ + m_display_context = context; + if (m_display_context.num_physical == 0 || m_display_context.num_physical < m_display_context.physical_colors.size()) + m_display_context.num_physical = m_display_context.physical_colors.size(); + if (m_display_context.preview_settings.wall_loops == 0) + m_display_context.preview_settings.wall_loops = 1; + if (m_display_context.nozzle_diameters.size() < m_display_context.num_physical) + m_display_context.nozzle_diameters.resize(m_display_context.num_physical, 0.4); + if (!m_display_context.physical_colors.empty()) + refresh_display_colors(m_display_context.physical_colors); +} + } // namespace Slic3r diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 6b4702c877..83088e5e07 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -112,6 +112,37 @@ struct MixedFilament bool operator!=(const MixedFilament &rhs) const { return !(*this == rhs); } }; +struct MixedFilamentPreviewSettings +{ + double nominal_layer_height { 0.2 }; + double mixed_lower_bound { 0.04 }; + double mixed_upper_bound { 0.16 }; + double preferred_a_height { 0.0 }; + double preferred_b_height { 0.0 }; + bool local_z_mode { false }; + size_t wall_loops { 1 }; +}; + +struct MixedFilamentDisplayContext +{ + size_t num_physical { 0 }; + std::vector physical_colors; + std::vector nozzle_diameters; + MixedFilamentPreviewSettings preview_settings; + bool component_bias_enabled { false }; +}; + +int mixed_filament_effective_local_z_preview_mix_b_percent(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings); +bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled); +std::pair mixed_filament_apparent_pair_percentages(const MixedFilament &mf, + const MixedFilamentPreviewSettings &preview_settings, + const std::vector &nozzle_diameters, + bool bias_mode_enabled); +std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context); + // --------------------------------------------------------------------------- // MixedFilamentManager // @@ -253,6 +284,7 @@ public: // Return the display colours of all enabled mixed filaments (in order). std::vector display_colors() const; + void set_display_context(const MixedFilamentDisplayContext &context); private: // Convert a 1-based virtual ID to a 0-based index into m_mixed. @@ -271,6 +303,7 @@ private: float m_height_upper_bound = 0.16f; bool m_advanced_dithering = false; uint64_t m_next_stable_id = 1; + MixedFilamentDisplayContext m_display_context; }; } // namespace Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 999f16fd45..fed7687401 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -244,6 +244,14 @@ struct MixedColorMatchRecipeResult MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow *parent, const std::vector &physical_colors, const wxColour &initial_color); +double color_delta_e00(const wxColour &lhs, const wxColour &rhs); + +namespace { + +MixedFilamentDisplayContext build_mixed_filament_display_context(const std::vector &physical_colors); +wxColour compute_color_match_recipe_display_color(const MixedColorMatchRecipeResult &recipe, const MixedFilamentDisplayContext &context); + +} // namespace #define PRINTER_THUMBNAIL_SIZE (wxSize(FromDIP(48), FromDIP(48))) #define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32))) @@ -1561,7 +1569,9 @@ Sidebar::Sidebar(Plater *parent) return; } + const MixedFilamentDisplayContext display_context = build_mixed_filament_display_context(colors); auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + mgr.set_display_context(display_context); mgr.add_custom_filament(recipe.component_a, recipe.component_b, recipe.mix_b_percent, colors); auto &mfs = mgr.mixed_filaments(); if (!mfs.empty()) { @@ -1573,7 +1583,7 @@ Sidebar::Sidebar(Plater *parent) created.pointillism_all_filaments = false; created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); created.custom = true; - created.display_color = recipe.preview_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + created.display_color = compute_color_match_recipe_display_color(recipe, display_context).GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); } if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option("mixed_filament_definitions")) @@ -3785,6 +3795,7 @@ public: { m_recipe_timer.SetOwner(this); m_loading_timer.SetOwner(this); + m_display_context = build_mixed_filament_display_context(m_physical_colors); m_palette.reserve(m_physical_colors.size()); for (const std::string &hex : m_physical_colors) @@ -3983,6 +3994,16 @@ public: } private: + void sync_recipe_preview(MixedColorMatchRecipeResult &recipe, const wxColour *requested_target = nullptr) + { + if (!recipe.valid) + return; + + recipe.preview_color = compute_color_match_recipe_display_color(recipe, m_display_context); + if (requested_target != nullptr && requested_target->IsOk()) + recipe.delta_e = color_delta_e00(*requested_target, recipe.preview_color); + } + void update_range_label() { if (m_range_value) @@ -3995,6 +4016,8 @@ private: return; m_presets = build_color_match_presets(m_physical_colors, m_min_component_percent); + for (MixedColorMatchRecipeResult &preset : m_presets) + sync_recipe_preview(preset); m_presets_host->Freeze(); while (m_presets_sizer->GetItemCount() > 0) { @@ -4128,6 +4151,7 @@ private: m_has_recipe_result = true; m_selected_recipe = std::move(recipe); + sync_recipe_preview(m_selected_recipe, &requested_target); set_recipe_loading(false, wxEmptyString); if (m_selected_recipe.valid) { @@ -4145,6 +4169,7 @@ private: void apply_preset(MixedColorMatchRecipeResult preset) { preset.delta_e = 0.0; + sync_recipe_preview(preset); ++m_recipe_request_token; m_requested_target = preset.preview_color; m_selected_target = preset.preview_color; @@ -4228,6 +4253,7 @@ private: private: std::vector m_physical_colors; + MixedFilamentDisplayContext m_display_context; std::vector m_palette; std::vector m_presets; MixedFilamentColorMapPanel *m_color_map = nullptr; @@ -4558,17 +4584,6 @@ private: // Forward declaration for MixedMixPreview (defined below) class MixedMixPreview; -struct MixedFilamentPreviewSettings -{ - double nominal_layer_height { 0.2 }; - double mixed_lower_bound { 0.04 }; - double mixed_upper_bound { 0.16 }; - double preferred_a_height { 0.0 }; - double preferred_b_height { 0.0 }; - bool local_z_mode { false }; - size_t wall_loops { 1 }; -}; - // Inline editor panel for configuring a single mixed filament class MixedFilamentConfigPanel : public wxPanel { @@ -5532,88 +5547,7 @@ std::vector MixedFilamentConfigPanel::build_local_z_preview_pass_heights int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf, const MixedFilamentPreviewSettings &preview_settings) { - if (!preview_settings.local_z_mode) - return std::clamp(mf.mix_b_percent, 0, 100); - - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); - if (!normalized_pattern.empty() || mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) - return std::clamp(mf.mix_b_percent, 0, 100); - - const std::vector gradient_ids = decode_gradient_ids(mf.gradient_component_ids); - if (gradient_ids.size() >= 3) - return std::clamp(mf.mix_b_percent, 0, 100); - - const std::vector pass_heights = build_local_z_preview_pass_heights(preview_settings.nominal_layer_height, - preview_settings.mixed_lower_bound, - preview_settings.mixed_upper_bound, - preview_settings.preferred_a_height, - preview_settings.preferred_b_height, - mf.mix_b_percent, - 0); - if (pass_heights.empty()) - return std::clamp(mf.mix_b_percent, 0, 100); - - double expected_h_a = preview_settings.preferred_a_height; - double expected_h_b = preview_settings.preferred_b_height; - if (expected_h_a <= EPSILON && expected_h_b <= EPSILON) { - const int mix_b = std::clamp(mf.mix_b_percent, 0, 100); - const double pct_b = double(mix_b) / 100.0; - const double pct_a = 1.0 - pct_b; - const double lo = std::max(0.01, preview_settings.mixed_lower_bound); - const double hi = std::max(lo, preview_settings.mixed_upper_bound); - expected_h_a = lo + pct_a * (hi - lo); - expected_h_b = lo + pct_b * (hi - lo); - } - - auto choose_start_with_component_a = [](const std::vector &passes, double local_expected_h_a, double local_expected_h_b) { - double err_ab = 0.0; - double err_ba = 0.0; - for (size_t pass_i = 0; pass_i < passes.size(); ++pass_i) { - const double expected_ab = (pass_i % 2) == 0 ? local_expected_h_a : local_expected_h_b; - const double expected_ba = (pass_i % 2) == 0 ? local_expected_h_b : local_expected_h_a; - err_ab += std::abs(passes[pass_i] - expected_ab); - err_ba += std::abs(passes[pass_i] - expected_ba); - } - if (err_ab + 1e-6 < err_ba) - return true; - if (err_ba + 1e-6 < err_ab) - return false; - return local_expected_h_a >= local_expected_h_b; - }; - - const bool start_with_a = choose_start_with_component_a(pass_heights, expected_h_a, expected_h_b); - double total_a = 0.0; - double total_b = 0.0; - for (size_t pass_i = 0; pass_i < pass_heights.size(); ++pass_i) { - const bool even_pass = (pass_i % 2) == 0; - const bool pass_is_a = even_pass ? start_with_a : !start_with_a; - if (pass_is_a) - total_a += pass_heights[pass_i]; - else - total_b += pass_heights[pass_i]; - } - - const double total = total_a + total_b; - if (total <= EPSILON) - return std::clamp(mf.mix_b_percent, 0, 100); - return std::clamp(int(std::lround(100.0 * total_b / total)), 0, 100); -} - -static bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings, - bool bias_mode_enabled) -{ - if (!bias_mode_enabled) - return false; - if (preview_settings.local_z_mode) - return false; - if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) - return false; - if (!MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern).empty()) - return false; - if (mf.gradient_component_ids.size() >= 3) - return false; - return mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b; + return Slic3r::mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings); } static double mixed_filament_reference_nozzle_mm(unsigned int component_a, @@ -5660,35 +5594,18 @@ static std::pair mixed_filament_single_surface_offset_pair(const M return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); } -static std::pair mixed_filament_apparent_pair_percentages(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings, - const std::vector &nozzle_diameters, - bool bias_mode_enabled) -{ - const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); - if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) - return { 100 - base_b, base_b }; - - const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); - const int apparent_b = MixedFilamentManager::apparent_mix_b_percent(base_b, - mf.component_a_surface_offset, - mf.component_b_surface_offset, - float(reference_nozzle_mm)); - return { 100 - apparent_b, apparent_b }; -} - static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf, const MixedFilamentPreviewSettings &preview_settings, const std::vector &nozzle_diameters, bool bias_mode_enabled) { - if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) + if (!Slic3r::mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) return {}; const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); const int base_a = 100 - base_b; const auto [apparent_a, apparent_b] = - mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); + Slic3r::mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f && (apparent_a != base_a || apparent_b != base_b)) { @@ -5702,6 +5619,202 @@ static std::string mixed_filament_apparent_pair_summary(const MixedFilament return ss.str(); } +MixedFilamentDisplayContext build_mixed_filament_display_context(const std::vector &physical_colors) +{ + MixedFilamentDisplayContext context; + context.num_physical = physical_colors.size(); + context.physical_colors = physical_colors; + context.nozzle_diameters.assign(context.num_physical, 0.4); + + auto *preset_bundle = wxGetApp().preset_bundle; + if (preset_bundle == nullptr) + return context; + + DynamicPrintConfig *print_cfg = &preset_bundle->prints.get_edited_preset().config; + if (const ConfigOptionFloats *opt = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")) { + const size_t opt_count = opt->values.size(); + if (opt_count > 0) { + for (size_t i = 0; i < context.num_physical; ++i) + context.nozzle_diameters[i] = std::max(0.05, opt->get_at(unsigned(std::min(i, opt_count - 1)))); + } + } + + auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { + if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + return opt->value != 0; + if (print_cfg != nullptr) { + if (const ConfigOptionBool *opt = print_cfg->option(key)) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option(key)) + return opt->value != 0; + } + return fallback; + }; + auto get_mixed_float = [preset_bundle, print_cfg](const std::string &key, float fallback) { + if (preset_bundle->project_config.has(key)) + return float(preset_bundle->project_config.opt_float(key)); + if (print_cfg != nullptr && print_cfg->has(key)) + return float(print_cfg->opt_float(key)); + return fallback; + }; + + context.preview_settings.mixed_lower_bound = std::max(0.01, double(get_mixed_float("mixed_filament_height_lower_bound", 0.04f))); + context.preview_settings.mixed_upper_bound = std::max(context.preview_settings.mixed_lower_bound, + double(get_mixed_float("mixed_filament_height_upper_bound", 0.16f))); + context.preview_settings.preferred_a_height = std::max(0.0, double(get_mixed_float("mixed_color_layer_height_a", 0.f))); + context.preview_settings.preferred_b_height = std::max(0.0, double(get_mixed_float("mixed_color_layer_height_b", 0.f))); + context.preview_settings.nominal_layer_height = 0.2; + if (print_cfg != nullptr && print_cfg->has("layer_height")) + context.preview_settings.nominal_layer_height = std::max(0.01, print_cfg->opt_float("layer_height")); + if (print_cfg != nullptr && print_cfg->has("wall_loops")) + context.preview_settings.wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); + context.preview_settings.local_z_mode = get_mixed_bool("dithering_local_z_mode", false); + context.component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); + + return context; +} + +static std::vector build_display_weighted_multi_sequence(const std::vector &ids, + const std::vector &weights, + size_t max_cycle_limit = 0) +{ + if (ids.empty()) + return {}; + + std::vector filtered_ids; + std::vector counts; + filtered_ids.reserve(ids.size()); + counts.reserve(ids.size()); + + const std::vector normalized = normalize_color_match_weights(weights, ids.size()); + for (size_t idx = 0; idx < ids.size(); ++idx) { + const int weight = idx < normalized.size() ? std::max(0, normalized[idx]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(ids[idx]); + counts.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = ids; + counts.assign(ids.size(), 1); + } + + int g = 0; + for (const int count : counts) + g = std::gcd(g, std::max(1, count)); + if (g > 1) { + for (int &count : counts) + count = std::max(1, count / g); + } + + constexpr size_t k_max_cycle = 48; + const size_t effective_cycle_limit = + max_cycle_limit > 0 ? std::min(k_max_cycle, std::max(1, max_cycle_limit)) : k_max_cycle; + reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit); + + std::vector reduced_ids; + std::vector reduced_counts; + reduced_ids.reserve(filtered_ids.size()); + reduced_counts.reserve(counts.size()); + for (size_t idx = 0; idx < counts.size(); ++idx) { + if (counts[idx] <= 0) + continue; + reduced_ids.emplace_back(filtered_ids[idx]); + reduced_counts.emplace_back(counts[idx]); + } + if (reduced_ids.empty()) + return {}; + filtered_ids = std::move(reduced_ids); + counts = std::move(reduced_counts); + + const int total = std::accumulate(counts.begin(), counts.end(), 0); + if (total <= 0) + return std::vector(filtered_ids.begin(), filtered_ids.end()); + + const size_t cycle = size_t(total); + + std::vector sequence; + sequence.reserve(cycle); + std::vector emitted(counts.size(), 0); + for (size_t pos = 0; pos < cycle; ++pos) { + size_t best_idx = 0; + double best_score = -1e9; + for (size_t idx = 0; idx < counts.size(); ++idx) { + const double target = double(pos + 1) * double(counts[idx]) / double(total); + const double score = target - double(emitted[idx]); + if (score > best_score) { + best_score = score; + best_idx = idx; + } + } + ++emitted[best_idx]; + sequence.emplace_back(filtered_ids[best_idx]); + } + if (sequence.empty()) + sequence = filtered_ids; + return sequence; +} + +static std::string blend_display_color_from_sequence(const std::vector &colors, + size_t num_physical, + const std::vector &sequence, + const std::string &fallback) +{ + if (colors.empty() || sequence.empty() || num_physical == 0) + return fallback; + + std::vector counts(num_physical + 1, size_t(0)); + size_t total = 0; + for (const unsigned int id : sequence) { + if (id == 0 || id > num_physical) + continue; + ++counts[id]; + ++total; + } + if (total == 0) + return fallback; + + unsigned int first_id = 0; + for (size_t id = 1; id <= num_physical; ++id) { + if (counts[id] > 0) { + first_id = unsigned(id); + break; + } + } + if (first_id == 0 || first_id > colors.size()) + return fallback; + + std::string blended = colors[first_id - 1]; + int accumulated = int(counts[first_id]); + for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { + if (counts[id] == 0 || id > colors.size()) + continue; + blended = MixedFilamentManager::blend_color(blended, colors[id - 1], accumulated, int(counts[id])); + accumulated += int(counts[id]); + } + + return blended; +} + +wxColour compute_color_match_recipe_display_color(const MixedColorMatchRecipeResult &recipe, const MixedFilamentDisplayContext &context) +{ + if (!recipe.valid) + return recipe.preview_color.IsOk() ? recipe.preview_color : wxColour("#26A69A"); + + MixedFilament entry; + entry.component_a = recipe.component_a; + entry.component_b = recipe.component_b; + entry.mix_b_percent = recipe.mix_b_percent; + entry.manual_pattern = recipe.manual_pattern; + entry.gradient_component_ids = recipe.gradient_component_ids; + entry.gradient_component_weights = recipe.gradient_component_weights; + entry.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); + + return parse_mixed_color(compute_mixed_filament_display_color(entry, context)); +} + std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector &seq) { if (seq.empty()) return ""; @@ -6367,11 +6480,11 @@ void MixedFilamentConfigPanel::build_ui() m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); } - if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && + if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && m_mf.component_a >= 1 && m_mf.component_b >= 1 && m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { const auto [apparent_pct_a, apparent_pct_b] = - mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); m_mf.display_color = MixedFilamentManager::blend_color( m_physical_colors[size_t(m_mf.component_a - 1)], m_physical_colors[size_t(m_mf.component_b - 1)], @@ -6680,11 +6793,11 @@ void MixedFilamentConfigPanel::update_preview() } if (m_mix_preview) { - if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && + if (Slic3r::mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && m_mf.component_a >= 1 && m_mf.component_b >= 1 && m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { const auto [apparent_pct_a, apparent_pct_b] = - mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); + Slic3r::mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); m_mf.display_color = MixedFilamentManager::blend_color( m_physical_colors[size_t(m_mf.component_a - 1)], m_physical_colors[size_t(m_mf.component_b - 1)], @@ -7187,6 +7300,13 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) local_z_mode, wall_loops }; + const MixedFilamentDisplayContext display_context { + num_physical, + physical_colors, + nozzle_diameters, + preview_settings, + component_bias_enabled + }; auto summarize_sequence = [num_physical](const std::vector &sequence) { if (sequence.empty() || num_physical == 0) return std::string(); @@ -7213,97 +7333,12 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) } return ss.str(); }; - auto blend_from_sequence = [num_physical](const std::vector &colors, const std::vector &sequence, const std::string &fallback) { - if (colors.empty() || sequence.empty() || num_physical == 0) - return fallback; - std::vector counts(num_physical + 1, size_t(0)); - size_t total = 0; - for (const unsigned int id : sequence) { - if (id == 0 || id > num_physical) - continue; - ++counts[id]; - ++total; - } - if (total == 0) - return fallback; - - unsigned int first_id = 0; - for (size_t id = 1; id <= num_physical; ++id) { - if (counts[id] > 0) { - first_id = unsigned(id); - break; - } - } - if (first_id == 0 || first_id > colors.size()) - return fallback; - - std::string blended = colors[first_id - 1]; - int acc = int(counts[first_id]); - for (size_t id = size_t(first_id + 1); id <= num_physical; ++id) { - if (counts[id] == 0 || id > colors.size()) - continue; - blended = MixedFilamentManager::blend_color(blended, colors[id - 1], acc, int(counts[id])); - acc += int(counts[id]); - } - return blended; - }; - auto build_entry_preview_sequence = [decode_manual_pattern_ids, decode_gradient_ids, decode_gradient_weights, - build_weighted_multi_sequence, preview_settings](const MixedFilament &entry) { - const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); - if (!normalized_pattern.empty()) - return decode_manual_pattern_ids(normalized_pattern, - entry.component_a, - entry.component_b, - preview_settings.wall_loops); - - const bool simple_mode = entry.distribution_mode == int(MixedFilament::Simple); - if (!simple_mode) { - const std::vector gradient_ids = decode_gradient_ids(entry.gradient_component_ids); - if (gradient_ids.size() >= 3) { - const std::vector gradient_weights = - decode_gradient_weights(entry.gradient_component_weights, gradient_ids.size()); - return build_weighted_multi_sequence(gradient_ids, gradient_weights, 0); - } - } - - const int effective_mix_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(entry, preview_settings); - const bool same_layer_mode = entry.distribution_mode == int(MixedFilament::SameLayerPointillisme); - return build_effective_pair_preview_sequence(entry.component_a, entry.component_b, effective_mix_b, same_layer_mode); - }; - auto compute_entry_display_color = [num_physical, &physical_colors, &nozzle_diameters, blend_from_sequence, build_entry_preview_sequence, - preview_settings, component_bias_enabled](const MixedFilament &entry) { - if (mixed_filament_supports_bias_apparent_color(entry, preview_settings, component_bias_enabled) && - entry.component_a >= 1 && entry.component_b >= 1 && - entry.component_a <= num_physical && entry.component_b <= num_physical && - entry.component_a <= physical_colors.size() && entry.component_b <= physical_colors.size()) { - const auto [apparent_pct_a, apparent_pct_b] = - mixed_filament_apparent_pair_percentages(entry, preview_settings, nozzle_diameters, component_bias_enabled); - return MixedFilamentManager::blend_color( - physical_colors[entry.component_a - 1], - physical_colors[entry.component_b - 1], - apparent_pct_a, - apparent_pct_b); - } - - const std::vector sequence = build_entry_preview_sequence(entry); - if (!sequence.empty()) - return blend_from_sequence(physical_colors, sequence, "#26A69A"); - - if (entry.component_a == 0 || entry.component_b == 0 || - entry.component_a > num_physical || entry.component_b > num_physical || - entry.component_a > physical_colors.size() || entry.component_b > physical_colors.size()) { - return std::string("#26A69A"); - } - - const int mix_b = std::clamp(entry.mix_b_percent, 0, 100); - return MixedFilamentManager::blend_color( - physical_colors[entry.component_a - 1], - physical_colors[entry.component_b - 1], - 100 - mix_b, - mix_b); + auto compute_entry_display_color = [display_context](const MixedFilament &entry) { + return compute_mixed_filament_display_color(entry, display_context); }; auto &mixed_mgr = preset_bundle->mixed_filaments; + mixed_mgr.set_display_context(display_context); if (sync_manager) { mixed_mgr.auto_generate(physical_colors); mixed_mgr.clear_custom_entries();