added a unified color helper so that every created colors and cached colors and preview are the same

This commit is contained in:
Rad
2026-04-18 00:14:04 +02:00
parent 40f6e0239f
commit 6ddce3cfd8
4 changed files with 893 additions and 255 deletions

View File

@@ -77,10 +77,10 @@ jobs:
notes_content="" notes_content=""
for notes_file in \ for notes_file in \
"doc/changelogs/CHANGELOG_v${version}.md" \
"doc/changelogs/RELEASE_NOTES_v${version}.md" \ "doc/changelogs/RELEASE_NOTES_v${version}.md" \
"CHANGELOG_v${version}.md" \ "doc/changelogs/CHANGELOG_v${version}.md" \
"RELEASE_NOTES_v${version}.md"; do "RELEASE_NOTES_v${version}.md" \
"CHANGELOG_v${version}.md"; do
if [ -f "$notes_file" ]; then if [ -f "$notes_file" ]; then
notes_content=$(cat "$notes_file") notes_content=$(cat "$notes_file")
break break

View File

@@ -861,6 +861,601 @@ static std::vector<unsigned int> build_weighted_gradient_sequence(const std::vec
return sequence; 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<unsigned int> 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<unsigned int> sequence;
if (num_physical == 0)
return sequence;
const std::string normalized = MixedFilamentManager::normalize_manual_pattern(pattern);
if (normalized.empty())
return sequence;
const std::vector<std::string> 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<size_t>(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<int, int> 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<unsigned int> build_effective_pair_preview_sequence(unsigned int component_a,
unsigned int component_b,
int percent_b,
bool limit_cycle)
{
std::vector<unsigned int> 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<std::string> &colors,
size_t num_physical,
const std::vector<unsigned int> &sequence,
const std::string &fallback)
{
if (colors.empty() || sequence.empty() || num_physical == 0)
return fallback;
std::vector<size_t> 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<std::pair<std::string, int>> 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<double> 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<double>(0.01, lower_bound);
const double hi = std::max<double>(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<double> &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<double> out;
size_t min_passes = size_t(std::max<double>(1.0, std::ceil((base_height - EPSILON) / hi)));
size_t max_passes = size_t(std::max<double>(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<double>(1.0, std::llround(base_height / std::max<double>(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<double>(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<double>(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<double>{ base_height };
const double cycle_h = std::max<double>(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<double>(2.0, std::ceil((base_height - EPSILON) / hi)));
if ((min_passes % 2) != 0)
++min_passes;
size_t max_passes = size_t(std::max<double>(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<size_t>(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<double>(2.0, std::llround(base_height / std::max<double>(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<double> 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<double> 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<double> 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<double> 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<double> &nozzle_diameters)
{
std::vector<double> 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<unsigned int> 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<double> 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<double>(0.01, preview_settings.mixed_lower_bound);
const double hi = std::max<double>(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<double> &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<int, int> mixed_filament_apparent_pair_percentages(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings,
const std::vector<double> &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<unsigned int> 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<unsigned int> gradient_ids = decode_gradient_component_ids(entry.gradient_component_ids, context.num_physical);
if (gradient_ids.size() >= 3) {
const std::vector<int> gradient_weights =
decode_gradient_component_weights(entry.gradient_component_weights, gradient_ids.size());
const std::vector<unsigned int> sequence = build_weighted_gradient_sequence(
gradient_ids, gradient_weights.empty() ? std::vector<int>(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<unsigned int> 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 // MixedFilamentManager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1651,54 +2246,16 @@ int MixedFilamentManager::apparent_mix_b_percent(int mix_b_percent,
void MixedFilamentManager::refresh_display_colors(const std::vector<std::string> &filament_colours) void MixedFilamentManager::refresh_display_colors(const std::vector<std::string> &filament_colours)
{ {
for (MixedFilament &mf : m_mixed) { MixedFilamentDisplayContext context = m_display_context;
const std::vector<unsigned int> gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, filament_colours.size()); context.num_physical = filament_colours.size();
if (mf.distribution_mode != int(MixedFilament::Simple) && gradient_ids.size() >= 3) { context.physical_colors = filament_colours;
const std::vector<int> gradient_weights = if (context.preview_settings.wall_loops == 0)
decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size()); context.preview_settings.wall_loops = 1;
const std::vector<unsigned int> gradient_sequence = if (context.nozzle_diameters.size() < context.num_physical)
build_weighted_gradient_sequence(gradient_ids, context.nozzle_diameters.resize(context.num_physical, 0.4);
gradient_weights.empty() ? std::vector<int>(gradient_ids.size(), 1) : gradient_weights);
if (gradient_sequence.empty()) {
mf.display_color = "#26A69A";
continue;
}
std::vector<int> counts(gradient_ids.size(), 0); for (MixedFilament &mf : m_mixed)
for (const unsigned int id : gradient_sequence) { mf.display_color = compute_mixed_filament_display_color(mf, context);
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<std::pair<std::string, int>> 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);
}
} }
size_t MixedFilamentManager::enabled_count() const size_t MixedFilamentManager::enabled_count() const
@@ -1719,4 +2276,17 @@ std::vector<std::string> MixedFilamentManager::display_colors() const
return colors; 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 } // namespace Slic3r

View File

@@ -112,6 +112,37 @@ struct MixedFilament
bool operator!=(const MixedFilament &rhs) const { return !(*this == rhs); } 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<std::string> physical_colors;
std::vector<double> 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<int, int> mixed_filament_apparent_pair_percentages(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings,
const std::vector<double> &nozzle_diameters,
bool bias_mode_enabled);
std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// MixedFilamentManager // MixedFilamentManager
// //
@@ -253,6 +284,7 @@ public:
// Return the display colours of all enabled mixed filaments (in order). // Return the display colours of all enabled mixed filaments (in order).
std::vector<std::string> display_colors() const; std::vector<std::string> display_colors() const;
void set_display_context(const MixedFilamentDisplayContext &context);
private: private:
// Convert a 1-based virtual ID to a 0-based index into m_mixed. // 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; float m_height_upper_bound = 0.16f;
bool m_advanced_dithering = false; bool m_advanced_dithering = false;
uint64_t m_next_stable_id = 1; uint64_t m_next_stable_id = 1;
MixedFilamentDisplayContext m_display_context;
}; };
} // namespace Slic3r } // namespace Slic3r

View File

@@ -244,6 +244,14 @@ struct MixedColorMatchRecipeResult
MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow *parent, MixedColorMatchRecipeResult prompt_best_color_match_recipe(wxWindow *parent,
const std::vector<std::string> &physical_colors, const std::vector<std::string> &physical_colors,
const wxColour &initial_color); const wxColour &initial_color);
double color_delta_e00(const wxColour &lhs, const wxColour &rhs);
namespace {
MixedFilamentDisplayContext build_mixed_filament_display_context(const std::vector<std::string> &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 (wxSize(FromDIP(48), FromDIP(48)))
#define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32))) #define PRINTER_THUMBNAIL_SIZE_SMALL (wxSize(FromDIP(32), FromDIP(32)))
@@ -1561,7 +1569,9 @@ Sidebar::Sidebar(Plater *parent)
return; return;
} }
const MixedFilamentDisplayContext display_context = build_mixed_filament_display_context(colors);
auto &mgr = wxGetApp().preset_bundle->mixed_filaments; 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); mgr.add_custom_filament(recipe.component_a, recipe.component_b, recipe.mix_b_percent, colors);
auto &mfs = mgr.mixed_filaments(); auto &mfs = mgr.mixed_filaments();
if (!mfs.empty()) { if (!mfs.empty()) {
@@ -1573,7 +1583,7 @@ Sidebar::Sidebar(Plater *parent)
created.pointillism_all_filaments = false; created.pointillism_all_filaments = false;
created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle); created.distribution_mode = recipe.gradient_component_ids.empty() ? int(MixedFilament::Simple) : int(MixedFilament::LayerCycle);
created.custom = true; 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<ConfigOptionString>("mixed_filament_definitions")) if (ConfigOptionString *opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionString>("mixed_filament_definitions"))
@@ -3785,6 +3795,7 @@ public:
{ {
m_recipe_timer.SetOwner(this); m_recipe_timer.SetOwner(this);
m_loading_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()); m_palette.reserve(m_physical_colors.size());
for (const std::string &hex : m_physical_colors) for (const std::string &hex : m_physical_colors)
@@ -3983,6 +3994,16 @@ public:
} }
private: 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() void update_range_label()
{ {
if (m_range_value) if (m_range_value)
@@ -3995,6 +4016,8 @@ private:
return; return;
m_presets = build_color_match_presets(m_physical_colors, m_min_component_percent); 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(); m_presets_host->Freeze();
while (m_presets_sizer->GetItemCount() > 0) { while (m_presets_sizer->GetItemCount() > 0) {
@@ -4128,6 +4151,7 @@ private:
m_has_recipe_result = true; m_has_recipe_result = true;
m_selected_recipe = std::move(recipe); m_selected_recipe = std::move(recipe);
sync_recipe_preview(m_selected_recipe, &requested_target);
set_recipe_loading(false, wxEmptyString); set_recipe_loading(false, wxEmptyString);
if (m_selected_recipe.valid) { if (m_selected_recipe.valid) {
@@ -4145,6 +4169,7 @@ private:
void apply_preset(MixedColorMatchRecipeResult preset) void apply_preset(MixedColorMatchRecipeResult preset)
{ {
preset.delta_e = 0.0; preset.delta_e = 0.0;
sync_recipe_preview(preset);
++m_recipe_request_token; ++m_recipe_request_token;
m_requested_target = preset.preview_color; m_requested_target = preset.preview_color;
m_selected_target = preset.preview_color; m_selected_target = preset.preview_color;
@@ -4228,6 +4253,7 @@ private:
private: private:
std::vector<std::string> m_physical_colors; std::vector<std::string> m_physical_colors;
MixedFilamentDisplayContext m_display_context;
std::vector<wxColour> m_palette; std::vector<wxColour> m_palette;
std::vector<MixedColorMatchRecipeResult> m_presets; std::vector<MixedColorMatchRecipeResult> m_presets;
MixedFilamentColorMapPanel *m_color_map = nullptr; MixedFilamentColorMapPanel *m_color_map = nullptr;
@@ -4558,17 +4584,6 @@ private:
// Forward declaration for MixedMixPreview (defined below) // Forward declaration for MixedMixPreview (defined below)
class MixedMixPreview; 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 // Inline editor panel for configuring a single mixed filament
class MixedFilamentConfigPanel : public wxPanel class MixedFilamentConfigPanel : public wxPanel
{ {
@@ -5532,88 +5547,7 @@ std::vector<double> MixedFilamentConfigPanel::build_local_z_preview_pass_heights
int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf, int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings) const MixedFilamentPreviewSettings &preview_settings)
{ {
if (!preview_settings.local_z_mode) return Slic3r::mixed_filament_effective_local_z_preview_mix_b_percent(mf, preview_settings);
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<unsigned int> 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<double> 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<double>(0.01, preview_settings.mixed_lower_bound);
const double hi = std::max<double>(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<double> &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;
} }
static double mixed_filament_reference_nozzle_mm(unsigned int component_a, static double mixed_filament_reference_nozzle_mm(unsigned int component_a,
@@ -5660,35 +5594,18 @@ static std::pair<float, float> mixed_filament_single_surface_offset_pair(const M
return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm));
} }
static std::pair<int, int> mixed_filament_apparent_pair_percentages(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings,
const std::vector<double> &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, static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings, const MixedFilamentPreviewSettings &preview_settings,
const std::vector<double> &nozzle_diameters, const std::vector<double> &nozzle_diameters,
bool bias_mode_enabled) 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 {}; return {};
const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings);
const int base_a = 100 - base_b; const int base_a = 100 - base_b;
const auto [apparent_a, apparent_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 && if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f &&
(apparent_a != base_a || apparent_b != base_b)) { (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(); return ss.str();
} }
MixedFilamentDisplayContext build_mixed_filament_display_context(const std::vector<std::string> &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<ConfigOptionFloats>("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<ConfigOptionBool>(key))
return opt->value;
if (const ConfigOptionInt *opt = preset_bundle->project_config.option<ConfigOptionInt>(key))
return opt->value != 0;
if (print_cfg != nullptr) {
if (const ConfigOptionBool *opt = print_cfg->option<ConfigOptionBool>(key))
return opt->value;
if (const ConfigOptionInt *opt = print_cfg->option<ConfigOptionInt>(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<size_t>(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<unsigned int> build_display_weighted_multi_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights,
size_t max_cycle_limit = 0)
{
if (ids.empty())
return {};
std::vector<unsigned int> filtered_ids;
std::vector<int> counts;
filtered_ids.reserve(ids.size());
counts.reserve(ids.size());
const std::vector<int> 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<size_t>(1, max_cycle_limit)) : k_max_cycle;
reduce_weight_counts_to_cycle_limit(counts, effective_cycle_limit);
std::vector<unsigned int> reduced_ids;
std::vector<int> 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<unsigned int>(filtered_ids.begin(), filtered_ids.end());
const size_t cycle = size_t(total);
std::vector<unsigned int> sequence;
sequence.reserve(cycle);
std::vector<int> 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<std::string> &colors,
size_t num_physical,
const std::vector<unsigned int> &sequence,
const std::string &fallback)
{
if (colors.empty() || sequence.empty() || num_physical == 0)
return fallback;
std::vector<size_t> 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<unsigned int> &seq) std::string MixedFilamentConfigPanel::summarize_sequence(const std::vector<unsigned int> &seq)
{ {
if (seq.empty()) return ""; if (seq.empty()) return "";
@@ -6367,11 +6480,11 @@ void MixedFilamentConfigPanel::build_ui()
m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); 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 >= 1 && m_mf.component_b >= 1 &&
m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) {
const auto [apparent_pct_a, apparent_pct_b] = 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_mf.display_color = MixedFilamentManager::blend_color(
m_physical_colors[size_t(m_mf.component_a - 1)], m_physical_colors[size_t(m_mf.component_a - 1)],
m_physical_colors[size_t(m_mf.component_b - 1)], m_physical_colors[size_t(m_mf.component_b - 1)],
@@ -6680,11 +6793,11 @@ void MixedFilamentConfigPanel::update_preview()
} }
if (m_mix_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 >= 1 && m_mf.component_b >= 1 &&
m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) {
const auto [apparent_pct_a, apparent_pct_b] = 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_mf.display_color = MixedFilamentManager::blend_color(
m_physical_colors[size_t(m_mf.component_a - 1)], m_physical_colors[size_t(m_mf.component_a - 1)],
m_physical_colors[size_t(m_mf.component_b - 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, local_z_mode,
wall_loops 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<unsigned int> &sequence) { auto summarize_sequence = [num_physical](const std::vector<unsigned int> &sequence) {
if (sequence.empty() || num_physical == 0) if (sequence.empty() || num_physical == 0)
return std::string(); return std::string();
@@ -7213,97 +7333,12 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
} }
return ss.str(); return ss.str();
}; };
auto blend_from_sequence = [num_physical](const std::vector<std::string> &colors, const std::vector<unsigned int> &sequence, const std::string &fallback) { auto compute_entry_display_color = [display_context](const MixedFilament &entry) {
if (colors.empty() || sequence.empty() || num_physical == 0) return compute_mixed_filament_display_color(entry, display_context);
return fallback;
std::vector<size_t> 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<unsigned int> gradient_ids = decode_gradient_ids(entry.gradient_component_ids);
if (gradient_ids.size() >= 3) {
const std::vector<int> 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<unsigned int> 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 &mixed_mgr = preset_bundle->mixed_filaments; auto &mixed_mgr = preset_bundle->mixed_filaments;
mixed_mgr.set_display_context(display_context);
if (sync_manager) { if (sync_manager) {
mixed_mgr.auto_generate(physical_colors); mixed_mgr.auto_generate(physical_colors);
mixed_mgr.clear_custom_entries(); mixed_mgr.clear_custom_entries();