Forced pattern , separed to > 9 extruder

This commit is contained in:
Ian Bassi
2026-06-01 11:40:31 -03:00
parent 84c7186674
commit 42b506ad85
7 changed files with 219 additions and 303 deletions
+33 -38
View File
@@ -31,6 +31,7 @@
#include <chrono> #include <chrono>
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
#include <limits>
#include <numeric> #include <numeric>
#include <math.h> #include <math.h>
#include <stdlib.h> #include <stdlib.h>
@@ -3796,25 +3797,35 @@ size_t GCode::get_extruder_id(unsigned int filament_id) const
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
namespace { namespace {
// Decode a manual pattern string (e.g. "121212") into a sequence of // Decode a manual pattern string (e.g. "1,2,12,11") into a sequence of
// 1-based physical extruder IDs, honouring component_a / component_b aliases. // 1-based physical extruder IDs.
static std::vector<unsigned int> decode_manual_pattern_sequence_for_gcode( static std::vector<unsigned int> decode_manual_pattern_sequence_for_gcode(
const MixedFilament& mf, size_t num_physical) const MixedFilament& mf, size_t num_physical)
{ {
std::vector<unsigned int> sequence; std::vector<unsigned int> sequence;
if (mf.manual_pattern.empty()) if (mf.manual_pattern.empty() || num_physical == 0)
return sequence; return sequence;
sequence.reserve(mf.manual_pattern.size());
for (const char token : mf.manual_pattern) { const std::string normalized = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern);
unsigned int extruder_id = 0; if (normalized.empty())
if (token == '1') return sequence;
extruder_id = mf.component_a;
else if (token == '2') std::stringstream ss(normalized);
extruder_id = mf.component_b; std::string token;
else if (token >= '3' && token <= '9') while (std::getline(ss, token, ',')) {
extruder_id = unsigned(token - '0'); if (token.empty())
if (extruder_id >= 1 && extruder_id <= num_physical) continue;
sequence.emplace_back(extruder_id); try {
size_t consumed = 0;
const unsigned long value = std::stoul(token, &consumed);
if (consumed != token.size() || value == 0 || value > std::numeric_limits<unsigned int>::max())
continue;
const unsigned int extruder_id = static_cast<unsigned int>(value);
if (extruder_id >= 1 && extruder_id <= num_physical)
sequence.emplace_back(extruder_id);
} catch (...) {
continue;
}
} }
return sequence; return sequence;
} }
@@ -5823,32 +5834,16 @@ LayerResult GCode::process_layer(
return inserted.first->second.empty() ? nullptr : &inserted.first->second; return inserted.first->second.empty() ? nullptr : &inserted.first->second;
}; };
// Lambda: if `entity_type == PERIMETERS` and the configured filament has a grouped // Legacy grouped-perimeter pattern split is disabled for numeric comma-delimited
// (comma-containing) manual pattern, return that filament's virtual 1-based ID so // manual patterns; commas now delimit physical filament IDs.
// that split_extrusion_collection_for_multi_perimeter_pattern() can be used.
// Returns 0 when no grouped pattern applies.
auto grouped_manual_pattern_mixed_filament_id = auto grouped_manual_pattern_mixed_filament_id =
[&](const GCode::ObjectByExtruder::Island::Region::Type entity_type, [&](const GCode::ObjectByExtruder::Island::Region::Type entity_type,
const ExtrusionEntityCollection& entities, const ExtrusionEntityCollection& entities,
const PrintRegion& region) -> unsigned int { const PrintRegion& region) -> unsigned int {
(void)entity_type;
(void)entities; (void)entities;
if (layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0) (void)region;
return 0; return 0;
if (entity_type != ObjectByExtruder::Island::Region::PERIMETERS)
return 0;
unsigned int filament_id_1based =
layer_tools.extruder_override != 0 ?
layer_tools.extruder_override :
unsigned(region.config().wall_filament.value);
if (!layer_tools.mixed_mgr->is_mixed(filament_id_1based, layer_tools.num_physical))
return 0;
const MixedFilament* mixed_row = layer_tools.mixed_mgr->mixed_filament_from_id(
filament_id_1based, layer_tools.num_physical);
if (mixed_row == nullptr)
return 0;
const std::string normalized_pattern =
MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern);
return normalized_pattern.find(',') != std::string::npos ? filament_id_1based : 0;
}; };
// Storage for dynamically-created split collections that must outlive the // Storage for dynamically-created split collections that must outlive the
@@ -6117,7 +6112,7 @@ LayerResult GCode::process_layer(
correct_extruder_id >= 0) { correct_extruder_id >= 0) {
// 1. Uniform-segment pointillism (SameLayerPointillisme with a // 1. Uniform-segment pointillism (SameLayerPointillisme with a
// simple ratio or a non-comma manual pattern). // simple ratio or a numeric manual pattern sequence).
const unsigned int cfg_filament_id_1based = const unsigned int cfg_filament_id_1based =
layer_tools.extruder_override != 0 ? layer_tools.extruder_override != 0 ?
layer_tools.extruder_override : layer_tools.extruder_override :
@@ -6185,8 +6180,8 @@ LayerResult GCode::process_layer(
++pointillism_path_split_fallbacks; ++pointillism_path_split_fallbacks;
} }
// 2. Grouped per-perimeter-index pattern (comma-separated manual // 2. Legacy grouped per-perimeter-index pattern path.
// pattern, e.g. "12,21"). Uses resolve_perimeter() via inset_idx. // Kept for compatibility plumbing; currently disabled.
if (entity_type == ObjectByExtruder::Island::Region::PERIMETERS) { if (entity_type == ObjectByExtruder::Island::Region::PERIMETERS) {
const unsigned int grouped_id = const unsigned int grouped_id =
grouped_manual_pattern_mixed_filament_id( grouped_manual_pattern_mixed_filament_id(
+4 -7
View File
@@ -81,13 +81,10 @@ bool has_grouped_manual_pattern(const MixedFilamentManager *mixed_mgr,
size_t num_physical, size_t num_physical,
unsigned int filament_id_1based) unsigned int filament_id_1based)
{ {
if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical))) (void)mixed_mgr;
return false; (void)num_physical;
const MixedFilament *mixed_row = mixed_mgr->mixed_filament_from_id(filament_id_1based, num_physical); (void)filament_id_1based;
if (mixed_row == nullptr) return false;
return false;
const std::string normalized = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern);
return normalized.find(',') != std::string::npos;
} }
void append_unique_preserve_order(std::vector<unsigned int> &dst, unsigned int value) void append_unique_preserve_order(std::vector<unsigned int> &dst, unsigned int value)
+124 -164
View File
@@ -8,6 +8,7 @@
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <limits>
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <numeric> #include <numeric>
@@ -604,69 +605,108 @@ static bool decode_pattern_step(char c, char &out)
} }
} }
static std::vector<std::string> split_manual_pattern_groups(const std::string &pattern) static std::string trim_manual_pattern_token(const std::string &token)
{ {
std::vector<std::string> groups; size_t lo = 0;
size_t hi = token.size();
while (lo < hi && std::isspace(static_cast<unsigned char>(token[lo])))
++lo;
while (hi > lo && std::isspace(static_cast<unsigned char>(token[hi - 1])))
--hi;
return token.substr(lo, hi - lo);
}
static bool parse_manual_pattern_id_token(const std::string &token, unsigned int &out)
{
const std::string trimmed = trim_manual_pattern_token(token);
if (trimmed.empty())
return false;
for (const char c : trimmed)
if (!std::isdigit(static_cast<unsigned char>(c)))
return false;
try {
size_t consumed = 0;
const unsigned long value = std::stoul(trimmed, &consumed);
if (consumed != trimmed.size() || value == 0 || value > std::numeric_limits<unsigned int>::max())
return false;
out = static_cast<unsigned int>(value);
return true;
} catch (...) {
return false;
}
}
static bool parse_manual_pattern_numeric_ids(const std::string &pattern, std::vector<unsigned int> &out)
{
out.clear();
if (pattern.empty()) if (pattern.empty())
return groups; return false;
std::string current; std::stringstream ss(pattern);
for (const char c : pattern) { std::string token;
if (c == ',') { while (std::getline(ss, token, ',')) {
if (!current.empty()) { unsigned int id = 0;
groups.emplace_back(std::move(current)); if (!parse_manual_pattern_id_token(token, id))
current.clear(); return false;
} out.emplace_back(id);
}
return !out.empty();
}
static std::string encode_manual_pattern_ids(const std::vector<unsigned int> &ids)
{
if (ids.empty())
return {};
std::ostringstream out;
bool first = true;
for (const unsigned int id : ids) {
if (id == 0)
continue; continue;
} if (!first)
current.push_back(c); out << ',';
first = false;
out << id;
} }
if (!current.empty()) return out.str();
groups.emplace_back(std::move(current));
return groups;
} }
static std::string flatten_manual_pattern_groups(const std::string &pattern) static std::vector<unsigned int> decode_manual_pattern_ids_for_physical(const std::string &pattern, size_t num_physical)
{ {
std::string flattened; std::vector<unsigned int> parsed;
flattened.reserve(pattern.size()); if (!parse_manual_pattern_numeric_ids(pattern, parsed) || num_physical == 0)
for (const char c : pattern) return {};
if (c != ',')
flattened.push_back(c);
return flattened;
}
static unsigned int physical_filament_from_pattern_step(char token, const MixedFilament &mf, size_t num_physical) std::vector<unsigned int> out;
{ out.reserve(parsed.size());
if (token == '1') for (const unsigned int id : parsed)
return mf.component_a; if (id >= 1 && id <= num_physical)
if (token == '2') out.emplace_back(id);
return mf.component_b; return out;
if (token >= '3' && token <= '9') {
const unsigned int direct = unsigned(token - '0');
if (direct >= 1 && direct <= num_physical)
return direct;
}
return 0;
} }
static int mix_percent_from_normalized_pattern(const std::string &pattern) static int mix_percent_from_normalized_pattern(const std::string &pattern)
{ {
const std::vector<std::string> groups = split_manual_pattern_groups(pattern); std::vector<unsigned int> ids;
if (groups.empty()) if (!parse_manual_pattern_numeric_ids(pattern, ids) || ids.empty())
return 50; return 50;
// For grouped patterns, blend preview is the average of each perimeter const unsigned int first = ids.front();
// group's own cadence. This keeps simple outer/inner patterns like unsigned int second = 0;
// "12,21" at 50/50 and "11111112,11121111" at 12.5%. for (const unsigned int id : ids) {
double blend_b = 0.0; if (id != first) {
for (const std::string &group : groups) { second = id;
if (group.empty()) break;
continue; }
const int count_b = int(std::count(group.begin(), group.end(), '2'));
blend_b += double(count_b) / double(group.size());
} }
return clamp_int(int(std::lround(100.0 * blend_b / double(groups.size()))), 0, 100);
if (second == 0)
return 0;
const int count_second = int(std::count(ids.begin(), ids.end(), second));
return clamp_int(int(std::lround(100.0 * double(count_second) / double(ids.size()))), 0, 100);
} }
static std::string normalize_gradient_component_ids(const std::string &components) static std::string normalize_gradient_component_ids(const std::string &components)
@@ -884,25 +924,16 @@ 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, static std::vector<unsigned int> build_grouped_manual_pattern_preview_sequence(const std::string &pattern,
unsigned int component_a, unsigned int component_a,
unsigned int component_b, unsigned int component_b,
size_t num_physical, size_t num_physical,
size_t wall_loops) size_t wall_loops)
{ {
(void)component_a;
(void)component_b;
(void)wall_loops;
std::vector<unsigned int> sequence; std::vector<unsigned int> sequence;
if (num_physical == 0) if (num_physical == 0)
return sequence; return sequence;
@@ -911,49 +942,7 @@ static std::vector<unsigned int> build_grouped_manual_pattern_preview_sequence(c
if (normalized.empty()) if (normalized.empty())
return sequence; return sequence;
const std::vector<std::string> groups = split_manual_pattern_groups(normalized); return decode_manual_pattern_ids_for_physical(normalized, num_physical);
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) static std::pair<int, int> effective_pair_preview_ratios(int percent_b)
@@ -1482,10 +1471,8 @@ std::string compute_mixed_filament_display_color(const MixedFilament &entry, con
std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical) std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical)
{ {
const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern); const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(entry.manual_pattern);
if (!normalized_pattern.empty()) { if (!normalized_pattern.empty())
const std::string flattened = flatten_manual_pattern_groups(normalized_pattern); return std::string("Pattern ") + normalized_pattern;
return std::string("Pattern ") + (flattened.empty() ? normalized_pattern : flattened);
}
std::vector<std::pair<unsigned int, int>> parts; std::vector<std::pair<unsigned int, int>> parts;
parts.reserve(4); parts.reserve(4);
@@ -1695,31 +1682,31 @@ void MixedFilamentManager::clear_custom_entries()
std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pattern) std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pattern)
{ {
std::string normalized; if (pattern.empty())
normalized.reserve(pattern.size()); return std::string();
bool current_group_has_steps = false;
std::vector<unsigned int> ids;
if (pattern.find(',') != std::string::npos) {
if (!parse_manual_pattern_numeric_ids(pattern, ids))
return std::string();
return encode_manual_pattern_ids(ids);
}
// Backward compatibility for legacy compact patterns like "1212" and "A/B".
for (char c : pattern) { for (char c : pattern) {
char step = '\0'; char step = '\0';
if (decode_pattern_step(c, step)) { if (decode_pattern_step(c, step)) {
normalized.push_back(step); ids.emplace_back(unsigned(step - '0'));
current_group_has_steps = true;
continue;
}
if (c == ',') {
if (!current_group_has_steps)
return std::string();
normalized.push_back(',');
current_group_has_steps = false;
continue; continue;
} }
if (is_pattern_separator(c)) if (is_pattern_separator(c))
continue; continue;
// Unknown token => invalid pattern.
return std::string(); return std::string();
} }
if (!normalized.empty() && normalized.back() == ',')
if (ids.empty())
return std::string(); return std::string();
return normalized; return encode_manual_pattern_ids(ids);
} }
int MixedFilamentManager::mix_percent_from_manual_pattern(const std::string &pattern) int MixedFilamentManager::mix_percent_from_manual_pattern(const std::string &pattern)
@@ -1986,16 +1973,14 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id,
const MixedFilament &mf = m_mixed[size_t(mixed_idx)]; const MixedFilament &mf = m_mixed[size_t(mixed_idx)];
// Manual pattern takes precedence when provided. Pattern uses repeating // Manual pattern takes precedence when provided.
// steps: '1' => component_a, '2' => component_b, '3'..'9' => direct // Pattern stores comma-separated 1-based physical filament IDs.
// physical filament IDs.
if (!mf.manual_pattern.empty()) { if (!mf.manual_pattern.empty()) {
const std::string flattened_pattern = flatten_manual_pattern_groups(mf.manual_pattern); const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (!flattened_pattern.empty()) { const std::vector<unsigned int> pattern_ids = decode_manual_pattern_ids_for_physical(normalized_pattern, num_physical);
const int pos = safe_mod(layer_index, int(flattened_pattern.size())); if (!pattern_ids.empty()) {
const unsigned int resolved = physical_filament_from_pattern_step(flattened_pattern[size_t(pos)], mf, num_physical); const int pos = safe_mod(layer_index, int(pattern_ids.size()));
if (resolved >= 1 && resolved <= num_physical) return pattern_ids[size_t(pos)];
return resolved;
} }
return mf.component_a; return mf.component_a;
} }
@@ -2049,25 +2034,12 @@ unsigned int MixedFilamentManager::resolve_perimeter(unsigned int filament_id,
float layer_height, float layer_height,
bool force_height_weighted) const bool force_height_weighted) const
{ {
(void)perimeter_index;
const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical); const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical);
if (mixed_idx < 0) if (mixed_idx < 0)
return filament_id; return filament_id;
const MixedFilament &mf = m_mixed[size_t(mixed_idx)];
if (!mf.manual_pattern.empty()) {
const std::vector<std::string> pattern_groups = split_manual_pattern_groups(mf.manual_pattern);
if (!pattern_groups.empty()) {
const size_t group_idx = size_t(std::max(0, perimeter_index));
const std::string &group = pattern_groups[std::min(group_idx, pattern_groups.size() - 1)];
if (!group.empty()) {
const int pos = safe_mod(layer_index, int(group.size()));
const unsigned int resolved = physical_filament_from_pattern_step(group[size_t(pos)], mf, num_physical);
if (resolved >= 1 && resolved <= num_physical)
return resolved;
}
}
}
return resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height, force_height_weighted); return resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height, force_height_weighted);
} }
@@ -2088,10 +2060,6 @@ unsigned int MixedFilamentManager::effective_painted_region_filament_id(unsigned
if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme))
return filament_id; return filament_id;
const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (normalized_pattern.find(',') != std::string::npos)
return filament_id;
const bool is_custom_mixed = mf.custom; const bool is_custom_mixed = mf.custom;
if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) { if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) {
const float safe_base = std::max<float>(0.01f, base_layer_height); const float safe_base = std::max<float>(0.01f, base_layer_height);
@@ -2123,7 +2091,7 @@ float MixedFilamentManager::component_surface_offset(unsigned int filament_id,
return 0.f; return 0.f;
const std::string normalized_pattern = normalize_manual_pattern(mixed_row->manual_pattern); const std::string normalized_pattern = normalize_manual_pattern(mixed_row->manual_pattern);
if (normalized_pattern.find(',') != std::string::npos) if (!normalized_pattern.empty())
return 0.f; return 0.f;
const unsigned int resolved = resolve(filament_id, const unsigned int resolved = resolve(filament_id,
@@ -2157,19 +2125,11 @@ std::vector<unsigned int> MixedFilamentManager::ordered_perimeter_extruders(unsi
const MixedFilament &mf = m_mixed[size_t(mixed_idx)]; const MixedFilament &mf = m_mixed[size_t(mixed_idx)];
if (!mf.manual_pattern.empty()) { if (!mf.manual_pattern.empty()) {
const std::vector<std::string> pattern_groups = split_manual_pattern_groups(mf.manual_pattern); const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (!pattern_groups.empty()) { const std::vector<unsigned int> pattern_ids = decode_manual_pattern_ids_for_physical(normalized_pattern, num_physical);
ordered.reserve(pattern_groups.size()); if (!pattern_ids.empty()) {
for (size_t group_idx = 0; group_idx < pattern_groups.size(); ++group_idx) { ordered.reserve(pattern_ids.size());
const unsigned int resolved = resolve_perimeter(filament_id, for (const unsigned int resolved : pattern_ids) {
num_physical,
layer_index,
int(group_idx),
layer_print_z,
layer_height,
force_height_weighted);
if (resolved < 1 || resolved > num_physical)
continue;
if (std::find(ordered.begin(), ordered.end(), resolved) == ordered.end()) if (std::find(ordered.begin(), ordered.end(), resolved) == ordered.end())
ordered.emplace_back(resolved); ordered.emplace_back(resolved);
} }
+7 -6
View File
@@ -40,9 +40,9 @@ struct MixedFilament
// Blend percentage of component B in [0..100]. // Blend percentage of component B in [0..100].
int mix_b_percent = 50; int mix_b_percent = 50;
// Optional manual pattern for this mixed filament. Tokens: // Optional manual pattern for this mixed filament.
// '1' => component_a, '2' => component_b, '3'..'9' => direct physical // Canonical format: comma-separated 1-based physical filament IDs
// filament IDs (1-based). Example: "11112222" => AAAABBBB repeating. // (for example "1,2,12,11").
std::string manual_pattern; std::string manual_pattern;
// Optional explicit gradient multi-color component list, encoded as // Optional explicit gradient multi-color component list, encoded as
@@ -145,7 +145,7 @@ std::pair<int, int> mixed_filament_apparent_pair_percentages(const MixedFilament
std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context); std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context);
// Build a standardized user-facing mixed filament name. // Build a standardized user-facing mixed filament name.
// - Pattern rows: "Pattern <flattened pattern>" (for example "Pattern 1212354"). // - Pattern rows: "Pattern <normalized pattern>" (for example "Pattern 1,2,12,11").
// - Mix rows: "<id>:<pct>% + <id>:<pct>% ..." (for example "1:30% + 2:20% + 3:50%"). // - Mix rows: "<id>:<pct>% + <id>:<pct>% ..." (for example "1:30% + 2:20% + 3:50%").
std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical); std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical);
@@ -197,8 +197,9 @@ public:
std::string serialize_custom_entries(); std::string serialize_custom_entries();
void load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours); void load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours);
// Normalize a manual mixed-pattern string into compact token form. // Normalize a manual mixed-pattern string into comma-separated numeric IDs.
// Accepts separators and A/B aliases. Returns empty string if invalid. // Legacy compact patterns are still accepted for backward compatibility.
// Returns empty string if invalid.
static std::string normalize_manual_pattern(const std::string &pattern); static std::string normalize_manual_pattern(const std::string &pattern);
static int mix_percent_from_manual_pattern(const std::string &pattern); static int mix_percent_from_manual_pattern(const std::string &pattern);
+19 -10
View File
@@ -4,6 +4,8 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <cfloat> #include <cfloat>
#include <limits>
#include <sstream>
namespace Slic3r { namespace Slic3r {
@@ -1140,16 +1142,23 @@ static void append_mixed_component_extruders(const MixedFilamentManager &mixed_m
append_unique_painted_extruder(painting_extruders, unsigned(token - '0'), num_physical_extruders); append_unique_painted_extruder(painting_extruders, unsigned(token - '0'), num_physical_extruders);
} }
for (char token : mixed_row->manual_pattern) { const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern);
unsigned int extruder_id = 0; if (!normalized_pattern.empty()) {
if (token == '1') std::stringstream ss(normalized_pattern);
extruder_id = mixed_row->component_a; std::string token;
else if (token == '2') while (std::getline(ss, token, ',')) {
extruder_id = mixed_row->component_b; if (token.empty())
else if (token >= '3' && token <= '9') continue;
extruder_id = unsigned(token - '0'); try {
size_t consumed = 0;
append_unique_painted_extruder(painting_extruders, extruder_id, num_physical_extruders); const unsigned long value = std::stoul(token, &consumed);
if (consumed != token.size() || value == 0 || value > std::numeric_limits<unsigned int>::max())
continue;
append_unique_painted_extruder(painting_extruders, static_cast<unsigned int>(value), num_physical_extruders);
} catch (...) {
continue;
}
}
} }
} }
+31 -77
View File
@@ -33,6 +33,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits>
#include <numeric> #include <numeric>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -50,39 +51,21 @@ namespace Slic3r { namespace GUI {
namespace { namespace {
// -- Plater.cpp:4849 -------------------------------------------------------- // -- Plater.cpp:4849 --------------------------------------------------------
static std::vector<std::string> split_manual_pattern_preview_groups(const std::string &pattern) static bool parse_manual_pattern_preview_id_token(const std::string &token, unsigned int &out)
{ {
std::vector<std::string> groups; if (token.empty())
if (pattern.empty()) return false;
return groups;
std::string current; try {
for (const char c : pattern) { size_t consumed = 0;
if (c == ',') { const unsigned long value = std::stoul(token, &consumed);
if (!current.empty()) { if (consumed != token.size() || value == 0 || value > std::numeric_limits<unsigned int>::max())
groups.emplace_back(std::move(current)); return false;
current.clear(); out = static_cast<unsigned int>(value);
} return true;
continue; } catch (...) {
} return false;
current.push_back(c);
} }
if (!current.empty())
groups.emplace_back(std::move(current));
return groups;
}
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, static std::vector<unsigned int> build_grouped_manual_pattern_preview_sequence(const std::string &pattern,
@@ -91,6 +74,10 @@ static std::vector<unsigned int> build_grouped_manual_pattern_preview_sequence(c
size_t num_physical, size_t num_physical,
size_t wall_loops) size_t wall_loops)
{ {
(void)component_a;
(void)component_b;
(void)wall_loops;
std::vector<unsigned int> sequence; std::vector<unsigned int> sequence;
if (num_physical == 0) if (num_physical == 0)
return sequence; return sequence;
@@ -99,46 +86,14 @@ static std::vector<unsigned int> build_grouped_manual_pattern_preview_sequence(c
if (normalized.empty()) if (normalized.empty())
return sequence; return sequence;
const std::vector<std::string> groups = split_manual_pattern_preview_groups(normalized); std::stringstream ss(normalized);
if (groups.empty()) std::string token;
return sequence; while (std::getline(ss, token, ',')) {
unsigned int id = 0;
if (groups.size() == 1) { if (!parse_manual_pattern_preview_id_token(token, id))
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; continue;
cycle = std::lcm(cycle, group.size()); if (id >= 1 && id <= num_physical)
if (cycle >= k_max_preview_cycle) { sequence.emplace_back(id);
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; return sequence;
@@ -1216,10 +1171,9 @@ void MixedFilamentConfigPanel::build_ui()
pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); pattern_row->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, m_pattern_ctrl = new wxTextCtrl(this, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition,
wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER); wxSize(FromDIP(200), -1), wxTE_PROCESS_ENTER);
m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B for component A/B, " m_pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Enter physical filament IDs as comma-separated numbers. "
"and 3..9 for direct physical filament IDs. " "Values greater than 9 are supported. "
"Use commas to define deeper perimeter patterns, for example 12,21. " "Example: 1,2,12,11."));
"Example: 1/1/1/1/2/2/2/2, 12,21, or 1/2/3/4."));
pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); pattern_row->Add(m_pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL);
root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); root->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap);
@@ -1283,7 +1237,7 @@ void MixedFilamentConfigPanel::build_ui()
const wxString bias_tooltip = const wxString bias_tooltip =
_L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n"
"The color chip shows which filament the current value affects.\n\n" "The color chip shows which filament the current value affects.\n\n"
"Grouped wall patterns and Local-Z dithering ignore it."); "Manual patterns and Local-Z dithering ignore it.");
auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias"));
surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20));
@@ -1425,7 +1379,7 @@ void MixedFilamentConfigPanel::build_ui()
if (m_pattern_ctrl) { if (m_pattern_ctrl) {
m_mf.distribution_mode = int(MixedFilament::Simple); m_mf.distribution_mode = int(MixedFilament::Simple);
std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue())); std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(m_pattern_ctrl->GetValue()));
if (normalized.empty()) normalized = "12"; if (normalized.empty()) normalized = "1,2";
if (into_u8(m_pattern_ctrl->GetValue()) != normalized) if (into_u8(m_pattern_ctrl->GetValue()) != normalized)
m_pattern_ctrl->ChangeValue(from_u8(normalized)); m_pattern_ctrl->ChangeValue(from_u8(normalized));
m_mf.manual_pattern = normalized; m_mf.manual_pattern = normalized;
@@ -1685,8 +1639,8 @@ void MixedFilamentConfigPanel::build_ui()
std::string pattern = into_u8(m_pattern_ctrl->GetValue()); std::string pattern = into_u8(m_pattern_ctrl->GetValue());
if (!pattern.empty()) { if (!pattern.empty()) {
const char last = pattern.back(); const char last = pattern.back();
const bool has_sep = last == '/' || last == '-' || last == '_' || last == '|' || last == ':' || last == ';' || last == ',' || last == ' '; if (last != ',')
if (!has_sep) pattern.push_back('/'); pattern.push_back(',');
} }
pattern += std::to_string(filament_id); pattern += std::to_string(filament_id);
m_pattern_ctrl->ChangeValue(from_u8(pattern)); m_pattern_ctrl->ChangeValue(from_u8(pattern));
+1 -1
View File
@@ -2471,7 +2471,7 @@ Sidebar::Sidebar(Plater *parent)
mgr.add_custom_filament(1, 2, 50, colors); mgr.add_custom_filament(1, 2, 50, colors);
auto &mfs = mgr.mixed_filaments(); auto &mfs = mgr.mixed_filaments();
if (!mfs.empty()) { if (!mfs.empty()) {
mfs.back().manual_pattern = "12"; mfs.back().manual_pattern = "1,2";
mfs.back().custom = true; mfs.back().custom = true;
} }
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"))