Enhance mixed filament functionality: Introduce support for gradient component IDs and weights in mixed filaments, allowing for more complex color mixing configurations. Update parsing logic to accommodate new gradient definitions and ensure backward compatibility. Implement pointillism distribution mode for same-layer mixing, enhancing user control over filament blending. Improve GUI elements to facilitate gradient weight adjustments and multi-color previews, enriching the user experience in mixed filament management.

This commit is contained in:
Rad
2026-02-12 02:29:34 +01:00
parent 0178ad32ad
commit c414e377a0
10 changed files with 2889 additions and 72 deletions
+372 -17
View File
@@ -5,6 +5,7 @@
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <iomanip>
#include <numeric>
@@ -263,7 +264,11 @@ static bool parse_row_definition(const std::string &row,
bool &enabled,
bool &custom,
int &mix_b_percent,
std::string &manual_pattern)
bool &pointillism_all_filaments,
std::string &gradient_component_ids,
std::string &gradient_component_weights,
std::string &manual_pattern,
int &distribution_mode)
{
auto trim_copy = [](const std::string &s) {
size_t lo = 0;
@@ -297,7 +302,7 @@ static bool parse_row_definition(const std::string &row,
while (std::getline(ss, token, ','))
tokens.emplace_back(trim_copy(token));
if (tokens.size() < 4 || tokens.size() > 6)
if (tokens.size() < 4 || tokens.size() > 12)
return false;
int values[5] = { 0, 0, 1, 1, 50 };
@@ -309,7 +314,7 @@ static bool parse_row_definition(const std::string &row,
!parse_int_token(tokens[3], values[4]))
return false;
} else {
// Current: a,b,enabled,custom,mix[,pattern]
// 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;
@@ -323,23 +328,72 @@ static bool parse_row_definition(const std::string &row,
enabled = (values[2] != 0);
custom = (tokens.size() == 4) ? true : (values[3] != 0);
mix_b_percent = clamp_int(values[4], 0, 100);
manual_pattern = (tokens.size() == 6) ? tokens[5] : std::string();
pointillism_all_filaments = false;
gradient_component_ids.clear();
gradient_component_weights.clear();
manual_pattern.clear();
distribution_mode = int(MixedFilament::Simple);
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;
}
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 == ';';
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 '1':
case 'a':
out = '1';
return true;
case '2':
case 'b':
out = '2';
return true;
@@ -352,10 +406,211 @@ 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
// ---------------------------------------------------------------------------
@@ -455,6 +710,10 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a,
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.custom = true;
m_mixed.push_back(std::move(mf));
@@ -514,11 +773,17 @@ std::string MixedFilamentManager::serialize_custom_entries() const
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);
<< 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));
const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (!normalized_pattern.empty())
ss << ',' << normalized_pattern;
@@ -552,8 +817,13 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
bool enabled = true;
bool custom = true;
int mix = 50;
bool pointillism_all_filaments = false;
std::string gradient_component_ids;
std::string gradient_component_weights;
std::string manual_pattern;
if (!parse_row_definition(row, a, b, enabled, custom, mix, manual_pattern)) {
int distribution_mode = int(MixedFilament::Simple);
if (!parse_row_definition(row, a, b, enabled, custom, mix, pointillism_all_filaments,
gradient_component_ids, gradient_component_weights, manual_pattern, distribution_mode)) {
++skipped_rows;
BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row;
continue;
@@ -574,7 +844,12 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
});
if (it_auto != m_mixed.end()) {
it_auto->enabled = enabled;
it_auto->pointillism_all_filaments = pointillism_all_filaments;
it_auto->gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids);
it_auto->gradient_component_weights =
normalize_gradient_component_weights(gradient_component_weights, it_auto->gradient_component_ids.size());
it_auto->manual_pattern = normalize_manual_pattern(manual_pattern);
it_auto->distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple));
it_auto->mix_b_percent = it_auto->manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(it_auto->manual_pattern);
++updated_auto;
continue;
@@ -587,7 +862,12 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co
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;
@@ -612,20 +892,41 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
float layer_height,
bool force_height_weighted) const
{
if (!is_mixed(filament_id, num_physical))
const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical);
if (mixed_idx < 0)
return filament_id;
const size_t idx = index_of(filament_id, num_physical);
if (idx >= m_mixed.size())
return 1; // fallback to first extruder
const MixedFilament &mf = m_mixed[idx];
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.
// 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()));
return mf.manual_pattern[size_t(pos)] == '2' ? mf.component_b : mf.component_a;
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
@@ -656,6 +957,29 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
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)
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;
}
std::string MixedFilamentManager::blend_color(const std::string &color_a,
const std::string &color_b,
int ratio_a, int ratio_b)
@@ -693,6 +1017,37 @@ std::string MixedFilamentManager::blend_color(const std::string &color_a,
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::string blended = filament_colours[gradient_ids.front() - 1];
int accum = std::max(1, counts.front());
for (size_t i = 1; i < gradient_ids.size(); ++i) {
const int wi = std::max(0, counts[i]);
if (wi == 0)
continue;
blended = blend_color(blended, filament_colours[gradient_ids[i] - 1], accum, wi);
accum += wi;
}
mf.display_color = blended;
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";