Files
OrcaSlicer/src/libslic3r/MixedFilament.cpp
T
Rad f758028de0 Add release notes for v0.92 and refactor mixed filament gradient handling
- Introduced a new `RELEASE_NOTES_v0.92.md` file detailing highlights such as gradual gradient behavior, mixed filament indexing fixes, and editable automatic mixed filaments.
- Refactored `compute_gradient_ratios` to remove the cycle layers parameter, implementing a new gradual integer cadence for gradient transitions.
- Updated `MixedFilament` structure to include an `origin_auto` flag for better management of auto-generated entries.
- Adjusted related parsing, serialization, and UI handling to reflect these changes, ensuring improved user experience and color fidelity in mixed filament rendering.
2026-02-23 21:47:26 +01:00

1268 lines
44 KiB
C++

#include "MixedFilament.hpp"
#include "filament_mixer.h"
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <iomanip>
#include <numeric>
#include <set>
namespace Slic3r {
// ---------------------------------------------------------------------------
// Colour helpers (internal)
// ---------------------------------------------------------------------------
struct RGB {
int r = 0, g = 0, b = 0;
};
struct RGBf {
float r = 0.f, g = 0.f, b = 0.f;
};
[[maybe_unused]] static float clamp01(float v)
{
return std::max(0.f, std::min(1.f, v));
}
[[maybe_unused]] static RGBf to_rgbf(const RGB &c)
{
return {
clamp01(static_cast<float>(c.r) / 255.f),
clamp01(static_cast<float>(c.g) / 255.f),
clamp01(static_cast<float>(c.b) / 255.f)
};
}
[[maybe_unused]] static RGB to_rgb8(const RGBf &c)
{
auto to_u8 = [](float v) -> int {
return std::clamp(static_cast<int>(std::round(clamp01(v) * 255.f)), 0, 255);
};
return { to_u8(c.r), to_u8(c.g), to_u8(c.b) };
}
// Convert RGB to an artist-pigment style RYB space.
// This is an approximation, but it gives expected pair mixes:
// Red + Blue -> Purple, Blue + Yellow -> Green, Red + Yellow -> Orange.
// Legacy RYB conversion helpers kept for reference.
// Active code paths use FilamentMixer.
[[maybe_unused]] static RGBf rgb_to_ryb(RGBf in)
{
float r = clamp01(in.r);
float g = clamp01(in.g);
float b = clamp01(in.b);
const float white = std::min({ r, g, b });
r -= white;
g -= white;
b -= white;
const float max_g = std::max({ r, g, b });
float y = std::min(r, g);
r -= y;
g -= y;
if (b > 0.f && g > 0.f) {
b *= 0.5f;
g *= 0.5f;
}
y += g;
b += g;
const float max_y = std::max({ r, y, b });
if (max_y > 1e-6f) {
const float n = max_g / max_y;
r *= n;
y *= n;
b *= n;
}
r += white;
y += white;
b += white;
return { clamp01(r), clamp01(y), clamp01(b) };
}
[[maybe_unused]] static RGBf ryb_to_rgb(RGBf in)
{
float r = clamp01(in.r);
float y = clamp01(in.g);
float b = clamp01(in.b);
const float white = std::min({ r, y, b });
r -= white;
y -= white;
b -= white;
const float max_y = std::max({ r, y, b });
float g = std::min(y, b);
y -= g;
b -= g;
if (b > 0.f && g > 0.f) {
b *= 2.f;
g *= 2.f;
}
r += y;
g += y;
const float max_g = std::max({ r, g, b });
if (max_g > 1e-6f) {
const float n = max_y / max_g;
r *= n;
g *= n;
b *= n;
}
r += white;
g += white;
b += white;
return { clamp01(r), clamp01(g), clamp01(b) };
}
// Parse "#RRGGBB" to RGB. Returns black on failure.
static RGB parse_hex_color(const std::string &hex)
{
RGB c;
if (hex.size() >= 7 && hex[0] == '#') {
try {
c.r = std::stoi(hex.substr(1, 2), nullptr, 16);
c.g = std::stoi(hex.substr(3, 2), nullptr, 16);
c.b = std::stoi(hex.substr(5, 2), nullptr, 16);
} catch (...) {
c = {};
}
}
return c;
}
static std::string rgb_to_hex(const RGB &c)
{
char buf[8];
std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", c.r, c.g, c.b);
return std::string(buf);
}
[[maybe_unused]] static std::string blend_color_ryb_legacy(const RGB &rgb_a,
const RGB &rgb_b,
int ratio_a,
int ratio_b)
{
const int safe_a = std::max(0, ratio_a);
const int safe_b = std::max(0, ratio_b);
const float total = static_cast<float>(safe_a + safe_b);
const float wa = (total > 0.f) ? static_cast<float>(safe_a) / total : 0.5f;
const float wb = 1.f - wa;
const RGBf color_a = to_rgbf(rgb_a);
const RGBf color_b = to_rgbf(rgb_b);
const RGBf ryb_a = rgb_to_ryb(color_a);
const RGBf ryb_b = rgb_to_ryb(color_b);
RGBf ryb_out;
ryb_out.r = wa * ryb_a.r + wb * ryb_b.r;
ryb_out.g = wa * ryb_a.g + wb * ryb_b.g;
ryb_out.b = wa * ryb_a.b + wb * ryb_b.b;
RGBf rgb_out = ryb_to_rgb(ryb_out);
const float v_out = std::max({ rgb_out.r, rgb_out.g, rgb_out.b });
const float v_tgt = wa * std::max({ color_a.r, color_a.g, color_a.b }) +
wb * std::max({ color_b.r, color_b.g, color_b.b });
if (v_out > 1e-6f && v_tgt > 0.f) {
const float scale = v_tgt / v_out;
rgb_out.r = clamp01(rgb_out.r * scale);
rgb_out.g = clamp01(rgb_out.g * scale);
rgb_out.b = clamp01(rgb_out.b * scale);
}
return rgb_to_hex(to_rgb8(rgb_out));
}
static int clamp_int(int v, int lo, int hi)
{
return std::max(lo, std::min(hi, v));
}
static int safe_ratio_from_height(float h, float unit)
{
if (unit <= 1e-6f)
return 1;
return std::max(0, int(std::lround(h / unit)));
}
static void compute_gradient_heights(const MixedFilament &mf, float lower_bound, float upper_bound, float &h_a, float &h_b)
{
const int mix_b = clamp_int(mf.mix_b_percent, 0, 100);
const float pct_b = float(mix_b) / 100.f;
const float pct_a = 1.f - pct_b;
const float lo = std::max(0.01f, lower_bound);
const float hi = std::max(lo, upper_bound);
h_a = lo + pct_a * (hi - lo);
h_b = lo + pct_b * (hi - lo);
}
static void normalize_ratio_pair(int &a, int &b)
{
a = std::max(0, a);
b = std::max(0, b);
if (a == 0 && b == 0) {
a = 1;
return;
}
if (a > 0 && b > 0) {
const int g = std::gcd(a, b);
if (g > 1) {
a /= g;
b /= g;
}
}
}
static void compute_gradient_ratios(MixedFilament &mf, int gradient_mode, float lower_bound, float upper_bound)
{
if (gradient_mode == 1) {
// Height-weighted mode:
// map blend to [lower, upper], then convert relative heights to an integer cadence.
float h_a = 0.f;
float h_b = 0.f;
compute_gradient_heights(mf, lower_bound, upper_bound, h_a, h_b);
// Use lower-bound as quantization unit so this mode differs clearly from layer-cycle mode.
const float unit = std::max(0.01f, std::min(h_a, h_b));
mf.ratio_a = std::max(1, safe_ratio_from_height(h_a, unit));
mf.ratio_b = std::max(1, safe_ratio_from_height(h_b, unit));
} else {
// Layer-cycle mode:
// derive a gradual integer cadence directly from the blend ratio
// by fixing the minority side to one layer and scaling the majority.
const int mix_b = clamp_int(mf.mix_b_percent, 0, 100);
if (mix_b <= 0) {
mf.ratio_a = 1;
mf.ratio_b = 0;
} else if (mix_b >= 100) {
mf.ratio_a = 0;
mf.ratio_b = 1;
} else {
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)))));
mf.ratio_a = b_is_major ? 1 : major_layers;
mf.ratio_b = b_is_major ? major_layers : 1;
}
}
normalize_ratio_pair(mf.ratio_a, mf.ratio_b);
}
static int safe_mod(int x, int m)
{
if (m <= 0)
return 0;
int r = x % m;
return (r < 0) ? (r + m) : r;
}
static int dithering_phase_step(int cycle)
{
if (cycle <= 1)
return 0;
int step = cycle / 2 + 1;
while (std::gcd(step, cycle) != 1)
++step;
return step % cycle;
}
static bool use_component_b_advanced_dither(int layer_index, int ratio_a, int ratio_b)
{
ratio_a = std::max(0, ratio_a);
ratio_b = std::max(0, ratio_b);
const int cycle = ratio_a + ratio_b;
if (cycle <= 0 || ratio_b <= 0)
return false;
if (ratio_a <= 0)
return true;
// Base ordered pattern: as evenly distributed as possible for ratio_b/cycle.
const int pos = safe_mod(layer_index, cycle);
const int cycle_idx = (layer_index - pos) / cycle;
// Rotate each cycle to avoid visible long-period vertical striping.
const int phase = safe_mod(cycle_idx * dithering_phase_step(cycle), cycle);
const int p = safe_mod(pos + phase, cycle);
const int b_before = (p * ratio_b) / cycle;
const int b_after = ((p + 1) * ratio_b) / cycle;
return b_after > b_before;
}
static bool parse_row_definition(const std::string &row,
unsigned int &a,
unsigned int &b,
bool &enabled,
bool &custom,
bool &origin_auto,
int &mix_b_percent,
bool &pointillism_all_filaments,
std::string &gradient_component_ids,
std::string &gradient_component_weights,
std::string &manual_pattern,
int &distribution_mode,
bool &deleted)
{
auto trim_copy = [](const std::string &s) {
size_t lo = 0;
size_t hi = s.size();
while (lo < hi && std::isspace(static_cast<unsigned char>(s[lo])))
++lo;
while (hi > lo && std::isspace(static_cast<unsigned char>(s[hi - 1])))
--hi;
return s.substr(lo, hi - lo);
};
auto parse_int_token = [&trim_copy](const std::string &tok, int &out) {
const std::string t = trim_copy(tok);
if (t.empty())
return false;
try {
size_t consumed = 0;
int v = std::stoi(t, &consumed);
if (consumed != t.size())
return false;
out = v;
return true;
} catch (...) {
return false;
}
};
std::vector<std::string> tokens;
std::stringstream ss(row);
std::string token;
while (std::getline(ss, token, ','))
tokens.emplace_back(trim_copy(token));
if (tokens.size() < 4 || tokens.size() > 12)
return false;
int values[5] = { 0, 0, 1, 1, 50 };
if (tokens.size() == 4) {
// Legacy: a,b,enabled,mix
if (!parse_int_token(tokens[0], values[0]) ||
!parse_int_token(tokens[1], values[1]) ||
!parse_int_token(tokens[2], values[2]) ||
!parse_int_token(tokens[3], values[4]))
return false;
} else {
// Current: a,b,enabled,custom,mix[,pointillism_all[,pattern]]
for (size_t i = 0; i < 5; ++i)
if (!parse_int_token(tokens[i], values[i]))
return false;
}
if (values[0] <= 0 || values[1] <= 0)
return false;
a = unsigned(values[0]);
b = unsigned(values[1]);
enabled = (values[2] != 0);
custom = (tokens.size() == 4) ? true : (values[3] != 0);
origin_auto = !custom;
mix_b_percent = clamp_int(values[4], 0, 100);
pointillism_all_filaments = false;
gradient_component_ids.clear();
gradient_component_weights.clear();
manual_pattern.clear();
distribution_mode = int(MixedFilament::Simple);
deleted = false;
size_t token_idx = 5;
if (tokens.size() >= 6) {
// Backward compatibility:
// - old: token[5] is pointillism flag ("0"/"1")
// - old: token[5] is pattern ("12", "1212", ...)
// - new: token[5] may be metadata token ("g..." / "m...")
const std::string &legacy = tokens[5];
if (legacy == "0" || legacy == "1") {
pointillism_all_filaments = (legacy == "1");
token_idx = 6;
} else if (legacy.empty() || legacy[0] == 'g' || legacy[0] == 'G' || legacy[0] == 'm' || legacy[0] == 'M') {
token_idx = 5;
} else {
manual_pattern = legacy;
token_idx = 6;
}
}
for (size_t i = token_idx; i < tokens.size(); ++i) {
const std::string &tok = tokens[i];
if (tok.empty())
continue;
if (tok[0] == 'g' || tok[0] == 'G') {
gradient_component_ids = tok.substr(1);
continue;
}
if (tok[0] == 'w' || tok[0] == 'W') {
gradient_component_weights = tok.substr(1);
continue;
}
if (tok[0] == 'm' || tok[0] == 'M') {
int parsed_mode = distribution_mode;
if (parse_int_token(tok.substr(1), parsed_mode))
distribution_mode = clamp_int(parsed_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
continue;
}
if (tok[0] == 'd' || tok[0] == 'D') {
int parsed_deleted = deleted ? 1 : 0;
if (parse_int_token(tok.substr(1), parsed_deleted))
deleted = parsed_deleted != 0;
continue;
}
if (tok[0] == 'o' || tok[0] == 'O') {
int parsed_origin_auto = origin_auto ? 1 : 0;
if (parse_int_token(tok.substr(1), parsed_origin_auto))
origin_auto = parsed_origin_auto != 0;
continue;
}
manual_pattern = tok;
}
// Compatibility for early same-layer prototype rows.
if (distribution_mode == int(MixedFilament::LayerCycle) && pointillism_all_filaments)
distribution_mode = int(MixedFilament::SameLayerPointillisme);
return true;
}
static bool is_pattern_separator(char c)
{
return std::isspace(static_cast<unsigned char>(c)) || c == '/' || c == '-' || c == '_' || c == '|' || c == ':' || c == ';' || c == ',';
}
static bool decode_pattern_step(char c, char &out)
{
if (c >= '1' && c <= '9') {
out = c;
return true;
}
switch (std::tolower(static_cast<unsigned char>(c))) {
case 'a':
out = '1';
return true;
case 'b':
out = '2';
return true;
default:
return false;
}
}
static int mix_percent_from_normalized_pattern(const std::string &pattern)
{
if (pattern.empty())
return 50;
// Legacy blend ratio for UI preview: count component-B aliases only.
// Tokens '3'..'9' are direct physical filament IDs and are ignored here.
const int count_b = int(std::count(pattern.begin(), pattern.end(), '2'));
return clamp_int(int(std::lround(100.0 * double(count_b) / double(pattern.size()))), 0, 100);
}
static std::string normalize_gradient_component_ids(const std::string &components)
{
std::string normalized;
normalized.reserve(components.size());
bool seen[10] = { false };
for (const char c : components) {
if (c < '1' || c > '9')
continue;
const int idx = c - '0';
if (seen[idx])
continue;
seen[idx] = true;
normalized.push_back(c);
}
return normalized;
}
static std::vector<unsigned int> decode_gradient_component_ids(const std::string &components, size_t num_physical)
{
std::vector<unsigned int> ids;
if (components.empty() || num_physical == 0)
return ids;
bool seen[10] = { false };
ids.reserve(components.size());
for (const char c : components) {
if (c < '1' || c > '9')
continue;
const unsigned int id = unsigned(c - '0');
if (id == 0 || id > num_physical || seen[id])
continue;
seen[id] = true;
ids.emplace_back(id);
}
return ids;
}
static std::vector<int> parse_gradient_weight_tokens(const std::string &weights)
{
std::vector<int> out;
std::string token;
for (const char c : weights) {
if (c >= '0' && c <= '9') {
token.push_back(c);
continue;
}
if (!token.empty()) {
out.emplace_back(std::max(0, std::atoi(token.c_str())));
token.clear();
}
}
if (!token.empty())
out.emplace_back(std::max(0, std::atoi(token.c_str())));
return out;
}
static std::vector<int> normalize_weight_vector_to_percent(const std::vector<int> &weights)
{
std::vector<int> out(weights.size(), 0);
if (weights.empty())
return out;
int sum = 0;
for (const int w : weights)
sum += std::max(0, w);
if (sum <= 0)
return out;
std::vector<double> remainders(weights.size(), 0.);
int assigned = 0;
for (size_t i = 0; i < weights.size(); ++i) {
const double exact = 100.0 * double(std::max(0, weights[i])) / double(sum);
out[i] = int(std::floor(exact));
remainders[i] = exact - double(out[i]);
assigned += out[i];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_rem = -1.0;
for (size_t i = 0; i < remainders.size(); ++i) {
if (weights[i] <= 0)
continue;
if (remainders[i] > best_rem) {
best_rem = remainders[i];
best_idx = i;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
static std::string normalize_gradient_component_weights(const std::string &weights, size_t expected_components)
{
if (expected_components == 0)
return std::string();
std::vector<int> parsed = parse_gradient_weight_tokens(weights);
if (parsed.size() != expected_components)
return std::string();
std::vector<int> normalized = normalize_weight_vector_to_percent(parsed);
int sum = 0;
for (const int v : normalized)
sum += v;
if (sum <= 0)
return std::string();
std::ostringstream ss;
for (size_t i = 0; i < normalized.size(); ++i) {
if (i > 0)
ss << '/';
ss << normalized[i];
}
return ss.str();
}
static std::vector<int> decode_gradient_component_weights(const std::string &weights, size_t expected_components)
{
if (expected_components == 0)
return {};
std::vector<int> parsed = parse_gradient_weight_tokens(weights);
if (parsed.size() != expected_components)
return {};
std::vector<int> normalized = normalize_weight_vector_to_percent(parsed);
int sum = 0;
for (const int v : normalized)
sum += v;
return (sum > 0) ? normalized : std::vector<int>();
}
static std::vector<unsigned int> build_weighted_gradient_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights)
{
if (ids.empty())
return {};
std::vector<unsigned int> filtered_ids;
std::vector<int> counts;
filtered_ids.reserve(ids.size());
counts.reserve(ids.size());
for (size_t i = 0; i < ids.size(); ++i) {
const int w = (i < weights.size()) ? std::max(0, weights[i]) : 0;
if (w <= 0)
continue;
filtered_ids.emplace_back(ids[i]);
counts.emplace_back(w);
}
if (filtered_ids.empty()) {
filtered_ids = ids;
counts.assign(ids.size(), 1);
}
int g = 0;
for (const int c : counts)
g = std::gcd(g, std::max(1, c));
if (g > 1) {
for (int &c : counts)
c = std::max(1, c / g);
}
int cycle = std::accumulate(counts.begin(), counts.end(), 0);
constexpr int k_max_cycle = 48;
if (cycle > k_max_cycle) {
const double scale = double(k_max_cycle) / double(cycle);
for (int &c : counts)
c = std::max(1, int(std::round(double(c) * scale)));
cycle = std::accumulate(counts.begin(), counts.end(), 0);
while (cycle > k_max_cycle) {
auto it = std::max_element(counts.begin(), counts.end());
if (it == counts.end() || *it <= 1)
break;
--(*it);
--cycle;
}
}
if (cycle <= 0)
return {};
std::vector<unsigned int> sequence;
sequence.reserve(size_t(cycle));
std::vector<int> emitted(counts.size(), 0);
for (int pos = 0; pos < cycle; ++pos) {
size_t best_idx = 0;
double best_score = -1e9;
for (size_t i = 0; i < counts.size(); ++i) {
const double target = double((pos + 1) * counts[i]) / double(cycle);
const double score = target - double(emitted[i]);
if (score > best_score) {
best_score = score;
best_idx = i;
}
}
++emitted[best_idx];
sequence.emplace_back(filtered_ids[best_idx]);
}
return sequence;
}
// ---------------------------------------------------------------------------
// MixedFilamentManager
// ---------------------------------------------------------------------------
void MixedFilamentManager::auto_generate(const std::vector<std::string> &filament_colours)
{
// Keep a copy of the old list so we can preserve user-modified ratios and
// enabled flags and custom rows.
std::vector<MixedFilament> old = std::move(m_mixed);
m_mixed.clear();
const size_t n = filament_colours.size();
if (n < 2)
return;
std::vector<MixedFilament> custom_rows;
custom_rows.reserve(old.size());
for (const MixedFilament &prev : old) {
if (!prev.custom)
continue;
if (prev.component_a == 0 || prev.component_b == 0 || prev.component_a > n || prev.component_b > n || prev.component_a == prev.component_b)
continue;
custom_rows.push_back(prev);
}
// Generate all C(N,2) pairwise combinations.
for (size_t i = 0; i < n; ++i) {
for (size_t j = i + 1; j < n; ++j) {
MixedFilament mf;
mf.component_a = static_cast<unsigned int>(i + 1); // 1-based
mf.component_b = static_cast<unsigned int>(j + 1);
mf.ratio_a = 1;
mf.ratio_b = 1;
mf.mix_b_percent = 50;
mf.enabled = true;
mf.deleted = false;
mf.custom = false;
mf.origin_auto = true;
// Try to preserve previous settings.
for (const auto &prev : old) {
if (!prev.custom &&
prev.component_a == mf.component_a &&
prev.component_b == mf.component_b) {
mf.enabled = prev.enabled;
mf.deleted = prev.deleted;
if (mf.deleted)
mf.enabled = false;
break;
}
}
m_mixed.push_back(mf);
}
}
for (MixedFilament &mf : custom_rows)
m_mixed.push_back(std::move(mf));
refresh_display_colors(filament_colours);
}
void MixedFilamentManager::remove_physical_filament(unsigned int deleted_filament_id)
{
if (deleted_filament_id == 0 || m_mixed.empty())
return;
std::vector<MixedFilament> filtered;
filtered.reserve(m_mixed.size());
for (MixedFilament mf : m_mixed) {
if (mf.component_a == deleted_filament_id || mf.component_b == deleted_filament_id)
continue;
if (mf.component_a > deleted_filament_id)
--mf.component_a;
if (mf.component_b > deleted_filament_id)
--mf.component_b;
filtered.emplace_back(std::move(mf));
}
m_mixed = std::move(filtered);
}
void MixedFilamentManager::add_custom_filament(unsigned int component_a,
unsigned int component_b,
int mix_b_percent,
const std::vector<std::string> &filament_colours)
{
const size_t n = filament_colours.size();
if (n < 2)
return;
component_a = std::max<unsigned int>(1, std::min<unsigned int>(component_a, unsigned(n)));
component_b = std::max<unsigned int>(1, std::min<unsigned int>(component_b, unsigned(n)));
if (component_a == component_b) {
component_b = (component_a == 1) ? 2 : 1;
}
MixedFilament mf;
mf.component_a = component_a;
mf.component_b = component_b;
mf.mix_b_percent = clamp_int(mix_b_percent, 0, 100);
mf.ratio_a = 1;
mf.ratio_b = 1;
mf.manual_pattern.clear();
mf.gradient_component_ids.clear();
mf.gradient_component_weights.clear();
mf.pointillism_all_filaments = false;
mf.distribution_mode = int(MixedFilament::Simple);
mf.enabled = true;
mf.deleted = false;
mf.custom = true;
mf.origin_auto = false;
m_mixed.push_back(std::move(mf));
refresh_display_colors(filament_colours);
}
void MixedFilamentManager::clear_custom_entries()
{
m_mixed.erase(std::remove_if(m_mixed.begin(), m_mixed.end(), [](const MixedFilament &mf) { return mf.custom; }), m_mixed.end());
}
std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pattern)
{
std::string normalized;
normalized.reserve(pattern.size());
for (char c : pattern) {
char step = '\0';
if (decode_pattern_step(c, step)) {
normalized.push_back(step);
continue;
}
if (is_pattern_separator(c))
continue;
// Unknown token => invalid pattern.
return std::string();
}
return normalized;
}
void MixedFilamentManager::apply_gradient_settings(int gradient_mode,
float lower_bound,
float upper_bound,
bool advanced_dithering)
{
m_gradient_mode = (gradient_mode != 0) ? 1 : 0;
m_height_lower_bound = std::max(0.01f, lower_bound);
m_height_upper_bound = std::max(m_height_lower_bound, upper_bound);
m_advanced_dithering = advanced_dithering;
for (MixedFilament &mf : m_mixed) {
if (!mf.custom) {
mf.ratio_a = 1;
mf.ratio_b = 1;
continue;
}
compute_gradient_ratios(mf, m_gradient_mode, m_height_lower_bound, m_height_upper_bound);
}
}
std::string MixedFilamentManager::serialize_custom_entries() const
{
std::ostringstream ss;
bool first = true;
for (const MixedFilament &mf : m_mixed) {
if (!first)
ss << ';';
first = false;
const std::string normalized_ids = normalize_gradient_component_ids(mf.gradient_component_ids);
const std::string normalized_weights = normalize_gradient_component_weights(mf.gradient_component_weights, normalized_ids.size());
ss << mf.component_a << ','
<< mf.component_b << ','
<< (mf.enabled ? 1 : 0) << ','
<< (mf.custom ? 1 : 0) << ','
<< clamp_int(mf.mix_b_percent, 0, 100) << ','
<< (mf.pointillism_all_filaments ? 1 : 0) << ','
<< 'g' << normalized_ids << ','
<< 'w' << normalized_weights << ','
<< 'm' << clamp_int(mf.distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)) << ','
<< 'd' << (mf.deleted ? 1 : 0) << ','
<< 'o' << (mf.origin_auto ? 1 : 0);
const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (!normalized_pattern.empty())
ss << ',' << normalized_pattern;
}
return ss.str();
}
void MixedFilamentManager::load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours)
{
const size_t n = filament_colours.size();
if (serialized.empty() || n < 2) {
BOOST_LOG_TRIVIAL(debug) << "MixedFilamentManager::load_custom_entries skipped"
<< ", serialized_empty=" << (serialized.empty() ? 1 : 0)
<< ", physical_count=" << n;
return;
}
size_t parsed_rows = 0;
size_t loaded_rows = 0;
size_t updated_auto = 0;
size_t appended_auto = 0;
size_t skipped_rows = 0;
auto canonical_pair = [](unsigned int a, unsigned int b) {
return std::make_pair(std::min(a, b), std::max(a, b));
};
std::vector<MixedFilament> auto_rows;
auto_rows.reserve(m_mixed.size());
for (const MixedFilament &mf : m_mixed) {
if (!mf.custom)
auto_rows.push_back(mf);
}
std::vector<MixedFilament> rebuilt;
rebuilt.reserve(m_mixed.size() + 8);
std::set<std::pair<unsigned int, unsigned int>> consumed_auto_pairs;
std::stringstream all(serialized);
std::string row;
while (std::getline(all, row, ';')) {
if (row.empty())
continue;
++parsed_rows;
unsigned int a = 0;
unsigned int b = 0;
bool enabled = true;
bool custom = true;
bool origin_auto = false;
int mix = 50;
bool pointillism_all_filaments = false;
std::string gradient_component_ids;
std::string gradient_component_weights;
std::string manual_pattern;
int distribution_mode = int(MixedFilament::Simple);
bool deleted = false;
if (!parse_row_definition(row, a, b, enabled, custom, origin_auto, mix, pointillism_all_filaments,
gradient_component_ids, gradient_component_weights, manual_pattern, distribution_mode, deleted)) {
++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row;
continue;
}
if (a == 0 || b == 0 || a > n || b > n || a == b) {
++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries row rejected"
<< ", row=" << row
<< ", a=" << a
<< ", b=" << b
<< ", physical_count=" << n;
continue;
}
if (!custom) {
const auto key = canonical_pair(a, b);
if (consumed_auto_pairs.count(key) != 0) {
++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries duplicate auto row"
<< ", row=" << row
<< ", a=" << key.first
<< ", b=" << key.second;
continue;
}
auto it_auto = std::find_if(auto_rows.begin(), auto_rows.end(), [key, canonical_pair](const MixedFilament &mf) {
return canonical_pair(mf.component_a, mf.component_b) == key;
});
if (it_auto == auto_rows.end()) {
++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries auto row missing after regenerate"
<< ", row=" << row
<< ", a=" << key.first
<< ", b=" << key.second;
continue;
}
MixedFilament mf = *it_auto;
mf.component_a = key.first;
mf.component_b = key.second;
mf.enabled = enabled;
mf.pointillism_all_filaments = pointillism_all_filaments;
mf.gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids);
mf.gradient_component_weights =
normalize_gradient_component_weights(gradient_component_weights, mf.gradient_component_ids.size());
mf.manual_pattern = normalize_manual_pattern(manual_pattern);
mf.distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
mf.mix_b_percent = mf.manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(mf.manual_pattern);
mf.deleted = deleted;
if (mf.deleted)
mf.enabled = false;
mf.custom = false;
mf.origin_auto = true;
rebuilt.push_back(std::move(mf));
consumed_auto_pairs.insert(key);
++updated_auto;
continue;
}
MixedFilament mf;
mf.component_a = a;
mf.component_b = b;
mf.mix_b_percent = mix;
mf.ratio_a = 1;
mf.ratio_b = 1;
mf.pointillism_all_filaments = pointillism_all_filaments;
mf.gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids);
mf.gradient_component_weights =
normalize_gradient_component_weights(gradient_component_weights, mf.gradient_component_ids.size());
mf.manual_pattern = normalize_manual_pattern(manual_pattern);
mf.distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
if (!mf.manual_pattern.empty())
mf.mix_b_percent = mix_percent_from_normalized_pattern(mf.manual_pattern);
mf.enabled = enabled;
mf.deleted = deleted;
if (mf.deleted)
mf.enabled = false;
mf.custom = custom;
mf.origin_auto = origin_auto;
rebuilt.push_back(std::move(mf));
++loaded_rows;
}
// Keep any newly generated auto rows that were not present in serialized
// definitions and append them at the end to preserve existing virtual IDs.
for (const MixedFilament &auto_mf : auto_rows) {
const auto key = canonical_pair(auto_mf.component_a, auto_mf.component_b);
if (consumed_auto_pairs.count(key) != 0)
continue;
MixedFilament mf = auto_mf;
mf.component_a = key.first;
mf.component_b = key.second;
mf.custom = false;
mf.origin_auto = true;
rebuilt.push_back(std::move(mf));
++appended_auto;
}
m_mixed = std::move(rebuilt);
refresh_display_colors(filament_colours);
BOOST_LOG_TRIVIAL(info) << "MixedFilamentManager::load_custom_entries"
<< ", physical_count=" << n
<< ", parsed_rows=" << parsed_rows
<< ", loaded_rows=" << loaded_rows
<< ", updated_auto_rows=" << updated_auto
<< ", appended_auto_rows=" << appended_auto
<< ", skipped_rows=" << skipped_rows
<< ", mixed_total=" << m_mixed.size();
}
unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z,
float layer_height,
bool force_height_weighted) const
{
const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical);
if (mixed_idx < 0)
return filament_id;
const MixedFilament &mf = m_mixed[size_t(mixed_idx)];
// Manual pattern takes precedence when provided. Pattern uses repeating
// steps: '1' => component_a, '2' => component_b, '3'..'9' => direct
// physical filament IDs.
if (!mf.manual_pattern.empty()) {
const int pos = safe_mod(layer_index, int(mf.manual_pattern.size()));
const char token = mf.manual_pattern[size_t(pos)];
if (token == '2')
return mf.component_b;
if (token == '1')
return mf.component_a;
if (token >= '3' && token <= '9') {
const unsigned int direct = unsigned(token - '0');
if (direct >= 1 && direct <= num_physical)
return direct;
}
return mf.component_a;
}
const bool use_simple_mode = mf.distribution_mode == int(MixedFilament::Simple);
const std::vector<unsigned int> gradient_ids = decode_gradient_component_ids(mf.gradient_component_ids, num_physical);
if (!use_simple_mode && gradient_ids.size() >= 3) {
const std::vector<int> gradient_weights =
decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size());
const std::vector<unsigned int> gradient_sequence = build_weighted_gradient_sequence(
gradient_ids, gradient_weights.empty() ? std::vector<int>(gradient_ids.size(), 1) : gradient_weights);
if (!gradient_sequence.empty()) {
const size_t pos = size_t(safe_mod(layer_index, int(gradient_sequence.size())));
return gradient_sequence[pos];
}
}
// Height-weighted cadence can be forced by the local-Z planner. The
// regular gradient height mode keeps historical behavior (custom rows).
const bool use_height_weighted = force_height_weighted || (m_gradient_mode == 1 && mf.custom);
if (use_height_weighted) {
float h_a = 0.f;
float h_b = 0.f;
compute_gradient_heights(mf, m_height_lower_bound, m_height_upper_bound, h_a, h_b);
const float cycle_h = std::max(0.01f, h_a + h_b);
const float z_anchor = (layer_height > 1e-6f)
? std::max(0.f, layer_print_z - 0.5f * layer_height)
: std::max(0.f, layer_print_z);
float phase = std::fmod(z_anchor, cycle_h);
if (phase < 0.f)
phase += cycle_h;
return (phase < h_a) ? mf.component_a : mf.component_b;
}
const int cycle = mf.ratio_a + mf.ratio_b;
if (cycle <= 0)
return mf.component_a;
if (m_gradient_mode == 0 && m_advanced_dithering && mf.custom)
return use_component_b_advanced_dither(layer_index, mf.ratio_a, mf.ratio_b) ? mf.component_b : mf.component_a;
const int pos = ((layer_index % cycle) + cycle) % cycle; // safe modulo for negatives
return (pos < mf.ratio_a) ? mf.component_a : mf.component_b;
}
int MixedFilamentManager::mixed_index_from_filament_id(unsigned int filament_id, size_t num_physical) const
{
if (filament_id <= num_physical)
return -1;
const size_t enabled_virtual_idx = size_t(filament_id - num_physical - 1);
size_t enabled_seen = 0;
for (size_t i = 0; i < m_mixed.size(); ++i) {
if (!m_mixed[i].enabled || m_mixed[i].deleted)
continue;
if (enabled_seen == enabled_virtual_idx)
return int(i);
++enabled_seen;
}
return -1;
}
const MixedFilament *MixedFilamentManager::mixed_filament_from_id(unsigned int filament_id, size_t num_physical) const
{
const int idx = mixed_index_from_filament_id(filament_id, num_physical);
return idx >= 0 ? &m_mixed[size_t(idx)] : nullptr;
}
// Blend N colours using weighted pairwise FilamentMixer blending.
std::string MixedFilamentManager::blend_color_multi(
const std::vector<std::pair<std::string, int>> &color_percents)
{
if (color_percents.empty())
return "#000000";
if (color_percents.size() == 1)
return color_percents.front().first;
struct WeightedColor {
RGB color;
int pct;
};
std::vector<WeightedColor> colors;
colors.reserve(color_percents.size());
int total_pct = 0;
for (const auto &[hex, pct] : color_percents) {
if (pct <= 0)
continue;
colors.push_back({parse_hex_color(hex), pct});
total_pct += pct;
}
if (colors.empty() || total_pct <= 0)
return "#000000";
unsigned char r = static_cast<unsigned char>(colors.front().color.r);
unsigned char g = static_cast<unsigned char>(colors.front().color.g);
unsigned char b = static_cast<unsigned char>(colors.front().color.b);
int accumulated_pct = colors.front().pct;
for (size_t i = 1; i < colors.size(); ++i) {
const auto &next = colors[i];
const int new_total = accumulated_pct + next.pct;
if (new_total <= 0)
continue;
const float t = static_cast<float>(next.pct) / static_cast<float>(new_total);
filament_mixer_lerp(
r, g, b,
static_cast<unsigned char>(next.color.r),
static_cast<unsigned char>(next.color.g),
static_cast<unsigned char>(next.color.b),
t, &r, &g, &b);
accumulated_pct = new_total;
}
return rgb_to_hex({int(r), int(g), int(b)});
}
std::string MixedFilamentManager::blend_color(const std::string &color_a,
const std::string &color_b,
int ratio_a, int ratio_b)
{
const int safe_a = std::max(0, ratio_a);
const int safe_b = std::max(0, ratio_b);
const int total = safe_a + safe_b;
const float t = (total > 0) ? (static_cast<float>(safe_b) / static_cast<float>(total)) : 0.5f;
const RGB rgb_a = parse_hex_color(color_a);
const RGB rgb_b = parse_hex_color(color_b);
unsigned char out_r = static_cast<unsigned char>(rgb_a.r);
unsigned char out_g = static_cast<unsigned char>(rgb_a.g);
unsigned char out_b = static_cast<unsigned char>(rgb_a.b);
filament_mixer_lerp(static_cast<unsigned char>(rgb_a.r),
static_cast<unsigned char>(rgb_a.g),
static_cast<unsigned char>(rgb_a.b),
static_cast<unsigned char>(rgb_b.r),
static_cast<unsigned char>(rgb_b.g),
static_cast<unsigned char>(rgb_b.b),
t, &out_r, &out_g, &out_b);
return rgb_to_hex({int(out_r), int(out_g), int(out_b)});
}
void MixedFilamentManager::refresh_display_colors(const std::vector<std::string> &filament_colours)
{
for (MixedFilament &mf : m_mixed) {
const std::vector<unsigned int> 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<int> gradient_weights =
decode_gradient_component_weights(mf.gradient_component_weights, gradient_ids.size());
const std::vector<unsigned int> gradient_sequence =
build_weighted_gradient_sequence(gradient_ids,
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 (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<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 int ratio_a = std::max(0, 100 - clamp_int(mf.mix_b_percent, 0, 100));
const int ratio_b = clamp_int(mf.mix_b_percent, 0, 100);
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 count = 0;
for (const auto &mf : m_mixed)
if (mf.enabled && !mf.deleted)
++count;
return count;
}
std::vector<std::string> MixedFilamentManager::display_colors() const
{
std::vector<std::string> colors;
for (const auto &mf : m_mixed)
if (mf.enabled && !mf.deleted)
colors.push_back(mf.display_color);
return colors;
}
} // namespace Slic3r