Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 138 additions & 25 deletions src/libslic3r/MixedFilament.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cerrno>
#include <cstdlib>
#include <sstream>
#include <iomanip>
Expand Down Expand Up @@ -693,14 +694,19 @@ std::vector<std::string> MixedFilamentManager::split_pattern_group_to_tokens(con

unsigned int MixedFilamentManager::physical_filament_from_token(const std::string &token, const MixedFilament &mf, size_t num_physical)
{
// Cycle-mode invariant: component_a≡1, component_b≡2 always
// (enforced by UI and MixedFilamentDialog::MODE_CYCLE).
// Under this invariant the symbolic tokens "1"/"2" are identity
// mappings — no ambiguity with direct physical IDs 1 and 2.
if (token == "1")
return mf.component_a;
return (mf.component_a >= 1 && mf.component_a <= num_physical) ? mf.component_a : 0;
if (token == "2")
return mf.component_b;
return (mf.component_b >= 1 && mf.component_b <= num_physical) ? mf.component_b : 0;

char *end = nullptr;
errno = 0;
unsigned long id = std::strtoul(token.c_str(), &end, 10);
if (end && *end == '\0' && id >= 1 && id <= num_physical)
if (errno != ERANGE && *end == '\0' && id >= 1 && id <= num_physical)
return unsigned(id);

return 0;
Expand Down Expand Up @@ -1005,8 +1011,9 @@ static unsigned int decode_manual_pattern_preview_token(const std::string &token
return (component_b >= 1 && component_b <= num_physical) ? component_b : 0;

char *end = nullptr;
errno = 0;
unsigned long id = std::strtoul(token.c_str(), &end, 10);
if (end && *end == '\0' && id >= 1 && id <= num_physical)
if (errno != ERANGE && *end == '\0' && id >= 1 && id <= num_physical)
return unsigned(id);

return 0;
Expand Down Expand Up @@ -1702,33 +1709,118 @@ void MixedFilamentManager::remove_physical_filament(unsigned int deleted_filamen
if (deleted_filament_id == 0 || m_mixed.empty())
return;

// Check and adjust filaments following resolve() order:
// 1. manual_pattern (cycle mode tokens)
// 2. gradient_component_ids
// 3. component_a / component_b (pair)

std::vector<MixedFilament> filtered;
filtered.reserve(m_mixed.size());
for (MixedFilament mf : m_mixed) {
// Check pair components
if (mf.component_a == deleted_filament_id || mf.component_b == deleted_filament_id)

// ---- 1. manual_pattern ----
bool uses_deleted_in_pattern = false;
const std::string norm = normalize_manual_pattern(mf.manual_pattern);
if (!norm.empty()) {
const auto groups = split_pattern_groups(norm);
for (const std::string &group : groups) {
const auto tokens = tokenize_pattern_group(group);
for (const std::string &token : tokens) {
if (physical_filament_from_token(token, mf, kMaxPhysicalFilaments) == deleted_filament_id) {
uses_deleted_in_pattern = true;
break;
}
}
if (uses_deleted_in_pattern) break;
}
}
if (uses_deleted_in_pattern)
continue;

// Check gradient components
bool uses_deleted_in_gradient = false;
for (unsigned int comp_id : decode_gradient_component_ids(mf.gradient_component_ids, 0)) {
if (comp_id == deleted_filament_id) {
uses_deleted_in_gradient = true;
break;
// ---- 2. gradient components ----
// Only check when there is no manual_pattern; a pattern already resolves
// every token, so the gradient check would be a false positive at worst.
if (norm.empty()) {
bool uses_deleted_in_gradient = false;
for (unsigned int comp_id : decode_gradient_component_ids(mf.gradient_component_ids, 0)) {
if (comp_id == deleted_filament_id) {
uses_deleted_in_gradient = true;
break;
}
}
if (uses_deleted_in_gradient)
continue;
}
if (uses_deleted_in_gradient)

// ---- 3. pair components ----
// Only check when there is no manual_pattern; a pattern already resolves
// every token through physical_filament_from_token (symbolic "1"/"2" or
// literal numeric), so the pair check would be redundant at best and a
// false positive at worst (component_a/b may hold unrelated default values).
if (norm.empty() && (mf.component_a == deleted_filament_id || mf.component_b == deleted_filament_id))
continue;

// Adjust component IDs for remaining filaments
if (mf.component_a > deleted_filament_id)
--mf.component_a;
if (mf.component_b > deleted_filament_id)
--mf.component_b;
// ---- Adjust IDs for the surviving mixed filament ----

// Adjust manual_pattern
if (!norm.empty()) {
const auto groups = split_pattern_groups(norm);
std::string adjusted;
for (size_t gi = 0; gi < groups.size(); ++gi) {
if (gi > 0) adjusted += ',';
const auto tokens = tokenize_pattern_group(groups[gi]);
for (const std::string &token : tokens) {
// All tokens are treated as literal physical-filament IDs
// during adjustment. In cycle mode (component_a≡1, component_b≡2)
// the "1"/"2" identity mapping means decrementing them produces
// the correct result; for non-cycle patterns without "1"/"2",
// component_a/b are irrelevant (pair adjustment is guarded by
// norm.empty()).
char *end = nullptr;
errno = 0;
unsigned long id = std::strtoul(token.c_str(), &end, 10);
if (errno != ERANGE && *end == '\0' && id > deleted_filament_id) {
--id;
if (id >= 10) {
adjusted += '[';
adjusted += std::to_string(id);
adjusted += ']';
} else {
adjusted += std::to_string(id);
}
} else {
if (token.size() > 1) {
adjusted += '[';
adjusted += token;
adjusted += ']';
} else {
adjusted += token;
}
}
}
}
mf.manual_pattern = adjusted;
}

// Adjust pair components (only when no pattern — same rationale as Step 3)
if (norm.empty()) {
if (mf.component_a > deleted_filament_id)
--mf.component_a;
if (mf.component_b > deleted_filament_id)
--mf.component_b;
}

// Adjust gradient component IDs
{
auto decoded = decode_gradient_component_ids(mf.gradient_component_ids, 0);
if (!norm.empty()) {
// When manual_pattern is the active resolution source the
// gradient deletion check was skipped — remove stale IDs
// that reference the now-deleted physical filament.
decoded.erase(
std::remove(decoded.begin(), decoded.end(), deleted_filament_id),
decoded.end());
}
for (unsigned int &id : decoded)
if (id > deleted_filament_id)
--id;
Expand Down Expand Up @@ -1825,6 +1917,9 @@ std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pa
if (num_str == "0")
return {};

// Compressing [1]→1 and [2]→2 is safe under the cycle-mode
// invariant (component_a≡1, component_b≡2) — the symbolic
// tokens are identity mappings, so no information is lost.
if (num_str.size() == 1) {
normalized.push_back(num_str[0]);
} else {
Expand Down Expand Up @@ -2381,21 +2476,39 @@ std::vector<size_t> MixedFilamentManager::mixed_filaments_using_physical(unsigne
if (mf.deleted || !mf.enabled) continue;

bool depends_on_physical = false;

// Check pair components
if (mf.component_a == physical_filament_1based || mf.component_b == physical_filament_1based) {
depends_on_physical = true;

// Check manual_pattern (cycle mode tokens — resolve order #1)
const std::string norm = normalize_manual_pattern(mf.manual_pattern);
if (!norm.empty()) {
const auto groups = split_pattern_groups(norm);
for (const std::string &group : groups) {
const auto tokens = tokenize_pattern_group(group);
for (const std::string &token : tokens) {
if (physical_filament_from_token(token, mf, kMaxPhysicalFilaments) == physical_filament_1based) {
depends_on_physical = true;
break;
}
}
if (depends_on_physical) break;
}
}
// Check gradient components
if (!depends_on_physical) {

// Check gradient components (resolve order #2)
if (!depends_on_physical && norm.empty()) {
for (unsigned int comp_id : decode_gradient_component_ids(mf.gradient_component_ids, 0)) {
if (comp_id == physical_filament_1based) {
depends_on_physical = true;
break;
}
}
}

// Check pair components (resolve order #3)
if (!depends_on_physical && norm.empty()) {
if (mf.component_a == physical_filament_1based || mf.component_b == physical_filament_1based) {
depends_on_physical = true;
}
}

if (depends_on_physical) {
result.push_back(j);
Expand Down
69 changes: 61 additions & 8 deletions src/libslic3r/PresetBundle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3611,16 +3611,50 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam

void PresetBundle::update_mixed_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments)
size_t new_num_filaments,
size_t deleted_mixed_idx)
{
build_filament_id_remap(old_mixed, old_num_filaments, new_num_filaments, false, 0u);
build_filament_id_remap(old_mixed, old_num_filaments, new_num_filaments, false, 0u, deleted_mixed_idx);
}

// Checks manual_pattern and gradient dependency.
// When norm is non-empty, the pair check is skipped (pattern tokens already
// cover dependency detection via physical_filament_from_token).
// When norm is empty, the caller checks pair first, then calls this as supplement.
static bool mixed_filament_depends_on_physical(const MixedFilament& mf, unsigned int physical_1based)
{
// ---- 1. manual_pattern ----
const std::string norm = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern);
if (!norm.empty()) {
const auto groups = MixedFilamentManager::split_pattern_groups(norm);
for (const std::string& group : groups) {
const auto tokens = MixedFilamentManager::split_pattern_group_to_tokens(group, 0);
for (const std::string& token : tokens) {
if (MixedFilamentManager::physical_filament_from_token(token, mf, MixedFilamentManager::kMaxPhysicalFilaments) == physical_1based)
return true;
}
}
}

// ---- 2. gradient components ----
// Only check when there is no manual_pattern; a pattern already resolves
// every token, so gradient IDs would be a false positive at worst.
if (norm.empty()) {
for (unsigned int comp_id : MixedFilamentManager::decode_gradient_component_ids(mf.gradient_component_ids, 0)) {
if (comp_id == physical_1based)
return true;
}
}

return false;
}

void PresetBundle::build_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments,
bool deleting_filament,
unsigned int deleted_1based)
unsigned int deleted_1based,
size_t deleted_mixed_idx)
{
size_t old_enabled_mixed = 0;
for (const auto &mf : old_mixed)
Expand All @@ -3634,10 +3668,10 @@ void PresetBundle::build_filament_id_remap(const std::vector<MixedFilament> &old
unsigned int mapped = 0;
if (deleting_filament && old_id == deleted_1based) {
mapped = 0;
} else if (deleting_filament && old_id > deleted_1based) {
mapped = old_id - 1;
} else if (old_id <= unsigned(new_num_filaments)) {
mapped = old_id;
if (deleting_filament && old_id > deleted_1based)
--mapped;
}
m_last_filament_id_remap[old_id] = mapped;
}
Expand All @@ -3661,14 +3695,32 @@ void PresetBundle::build_filament_id_remap(const std::vector<MixedFilament> &old
size_t stable_id_hits = 0;
size_t fallback_pair_hits = 0;
size_t missing_hits = 0;
size_t deleted_mixed_skips = 0;
unsigned int old_virtual_id = unsigned(old_num_filaments + 1);
for (const auto &mf : old_mixed) {
for (size_t midx = 0; midx < old_mixed.size(); ++midx) {
const auto &mf = old_mixed[midx];
if (!mf.enabled)
continue;

// When a mixed filament is explicitly deleted, leave its old virtual ID
// mapped to 0 (NONE) so paint on the deleted filament is removed, rather
// than being reassigned to a surviving mixed via pair-based fallback.
if (midx == deleted_mixed_idx) {
++old_virtual_id;
++deleted_mixed_skips;
continue;
}

const std::string norm = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern);
unsigned int a = mf.component_a;
unsigned int b = mf.component_b;
if (a == deleted_1based || b == deleted_1based) {
if (norm.empty() && (a == deleted_1based || b == deleted_1based)) {
m_last_filament_id_remap[old_virtual_id] = 0;
++missing_hits;
} else if (deleting_filament && mixed_filament_depends_on_physical(mf, deleted_1based)) {
// The mixed filament was removed by remove_physical_filament because
// its manual_pattern or gradient references the deleted physical,
// even though component_a/component_b do not.
m_last_filament_id_remap[old_virtual_id] = 0;
++missing_hits;
} else {
Expand All @@ -3682,7 +3734,7 @@ void PresetBundle::build_filament_id_remap(const std::vector<MixedFilament> &old
}
}
if (!mapped_by_stable_id) {
if (deleting_filament) {
if (deleting_filament && norm.empty()) {
if (a > deleted_1based)
--a;
if (b > deleted_1based)
Expand Down Expand Up @@ -3727,6 +3779,7 @@ void PresetBundle::build_filament_id_remap(const std::vector<MixedFilament> &old
<< " new_physical=" << new_num_filaments
<< " deleting=" << (deleting_filament ? 1 : 0)
<< " deleted_id=" << deleted_1based
<< " deleted_mixed_skips=" << deleted_mixed_skips
<< " old_mixed_enabled=" << old_enabled_mixed
<< " new_mixed_enabled=" << this->mixed_filaments.enabled_count()
<< " stable_id_hits=" << stable_id_hits
Expand Down
6 changes: 4 additions & 2 deletions src/libslic3r/PresetBundle.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ class PresetBundle
// changes when the physical filament count itself did not change.
void update_mixed_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments);
size_t new_num_filaments,
size_t deleted_mixed_idx = size_t(-1));
// Mapping generated during the latest filament count change.
// Index is old 1-based filament ID, value is new 1-based filament ID (0 = removed).
const std::vector<unsigned int>& last_filament_id_remap() const { return m_last_filament_id_remap; }
Expand Down Expand Up @@ -428,7 +429,8 @@ class PresetBundle
size_t old_num_filaments,
size_t new_num_filaments,
bool deleting_filament,
unsigned int deleted_1based);
unsigned int deleted_1based,
size_t deleted_mixed_idx = size_t(-1));
// Update renamed_from and alias maps of system profiles.
void update_system_maps();

Expand Down
Loading