diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index c81e6c391c..8ff9dcea32 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -800,6 +800,10 @@ std::string AppConfig::load() preset_info.nozzle_volume_type = NozzleVolumeType(cali_it.value()["nozzle_volume_type"].get()); if (cali_it.value().contains("bed_type")) preset_info.bed_type = BedType(cali_it.value()["bed_type"].get()); + if (cali_it.value().contains("nozzle_pos_id")) + preset_info.nozzle_pos_id = cali_it.value()["nozzle_pos_id"].get(); + if (cali_it.value().contains("nozzle_sn")) + preset_info.nozzle_sn = cali_it.value()["nozzle_sn"].get(); cali_info.selected_presets.push_back(preset_info); } } @@ -957,6 +961,8 @@ void AppConfig::save() preset_json["extruder_id"] = filament_preset.extruder_id; preset_json["nozzle_volume_type"] = int(filament_preset.nozzle_volume_type); preset_json["bed_type"] = int(filament_preset.bed_type); + preset_json["nozzle_pos_id"] = filament_preset.nozzle_pos_id; + preset_json["nozzle_sn"] = filament_preset.nozzle_sn; preset_json["nozzle_diameter"] = filament_preset.nozzle_diameter; preset_json["filament_id"] = filament_preset.filament_id; preset_json["setting_id"] = filament_preset.setting_id; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 01c1a0636b..a7ebab0803 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -471,6 +471,8 @@ set(lisbslic3r_sources FilamentGroup.cpp FilamentGroupUtils.hpp FilamentGroupUtils.cpp + MultiNozzleUtils.hpp + MultiNozzleUtils.cpp GCode/ToolOrderUtils.hpp GCode/ToolOrderUtils.cpp FlushVolPredictor.hpp diff --git a/src/libslic3r/Extruder.hpp b/src/libslic3r/Extruder.hpp index 0d76ede8c9..a0b679add6 100644 --- a/src/libslic3r/Extruder.hpp +++ b/src/libslic3r/Extruder.hpp @@ -51,6 +51,10 @@ public: double retracted() const { return m_retracted; } // Get extra retraction planned after double restart_extra() const { return m_restart_extra; } + // Share-aware retracted-length readers (for extruders shared between filaments), consumed by GCodeWriter::get_extruder_retracted_length. + bool is_share_extruder() const { return m_share_extruder; } + double get_single_retracted_length() const { return m_retracted; } + double get_share_retracted_length() const { return m_share_retracted[extruder_id()]; } // Setters for the PlaceholderParser. // Set current extruder position. Only applicable with absolute extruder addressing. void set_position(double e) { m_E = e; } diff --git a/src/libslic3r/FilamentGroup.cpp b/src/libslic3r/FilamentGroup.cpp index 2a9b5b6037..f5b5564538 100644 --- a/src/libslic3r/FilamentGroup.cpp +++ b/src/libslic3r/FilamentGroup.cpp @@ -5,10 +5,15 @@ #include #include #include +#include namespace Slic3r { using namespace FilamentGroupUtils; + static constexpr long long ENUM_THRESHOLD = 10000; + static constexpr long long ENUM_EARLY_EXIT = 10000000; + constexpr uint32_t GOLDEN_RATIO_32 = 0x9e3779b9; + // clear the array and heap,save the groups in heap to the array static void change_memoryed_heaps_to_arrays(MemoryedGroupHeap& heap,const int total_filament_num,const std::vector& used_filaments, std::vector>& arrs) { @@ -36,88 +41,350 @@ namespace Slic3r return filament_merge_map; } - - std::vector calc_filament_group_for_tpu(const std::set& tpu_filaments, const int filament_nums, const int master_extruder_id) + static uint64_t fnv_hash_nozzle(int volume_type, int is_right_extruder, int loaded_filament = -1) { - std::vector ret(filament_nums); - for (size_t fidx = 0; fidx < filament_nums; ++fidx) { - if (tpu_filaments.count(fidx)) - ret[fidx] = master_extruder_id; - else - ret[fidx] = 1 - master_extruder_id; + constexpr uint64_t FNV_OFFSET_BASIS = 14695981039346656037ULL; + constexpr uint64_t FNV_PRIME = 1099511628211ULL; + constexpr uint64_t SALT_A = 0xA5A5A5A5A5A5A5A5ULL; + constexpr uint64_t SALT_B = 0x5A5A5A5A5A5A5A5AULL; + constexpr uint64_t SALT_C = 0x3C3C3C3C3C3C3C3CULL; + + uint64_t h = FNV_OFFSET_BASIS; + h ^= static_cast(volume_type) + SALT_A; + h *= FNV_PRIME; + h ^= static_cast(is_right_extruder) + SALT_B; + h *= FNV_PRIME; + if (loaded_filament >= 0) { + h ^= static_cast(loaded_filament) + SALT_C; + h *= FNV_PRIME; } - return ret; + + return h; } - bool can_swap_groups(const int extruder_id_0, const std::set& group_0, const int extruder_id_1, const std::set& group_1, const FilamentGroupContext& ctx) + static double evaluate_score(const double flush, const double time, const bool with_time = false) { + if (!with_time) return flush; + + double approx_density = 1.26; // g/cm^3 + double approx_flush_speed = 180; // s/g + double correction_factor = 2; + double flush_score = flush * approx_density * approx_flush_speed * correction_factor / 1000; + return flush_score + time; + } + + static double calc_change_time_for_group( + const std::vector& filament_change_seq, + const std::vector& nozzle_change_seq, + const std::vector& logical_filaments, + const std::vector& nozzle_list, + const MultiNozzleUtils::FilamentChangeTimeParams& time_params, + const std::vector& ams_preload_enabled, + const std::vector& group_of_filament) { - std::vector>extruder_unprintables(2); - { - std::vector> unprintable_filaments = ctx.model_info.unprintable_filaments; - if (unprintable_filaments.size() > 1) - remove_intersection(unprintable_filaments[0], unprintable_filaments[1]); + auto r = MultiNozzleUtils::simulate_filament_change_time( + logical_filaments, nozzle_list, filament_change_seq, + nozzle_change_seq, group_of_filament, time_params, + ams_preload_enabled); + return r.actual_time; + } - std::map>unplaceable_limts; - for (auto& group_id : { extruder_id_0,extruder_id_1 }) - for (auto f : unprintable_filaments[group_id]) - unplaceable_limts[f].emplace_back(group_id); + static double full_evaluate( + const std::vector& used_filaments, + const std::vector& filament_nozzle_map, + const FilamentGroupContext& ctx, + std::optional&)>> get_custom_seq = std::nullopt, + int* out_flush = nullptr) + { + auto group_res = MultiNozzleUtils::LayeredNozzleGroupResult::create(filament_nozzle_map, ctx.nozzle_info.nozzle_list, used_filaments); + if (!group_res) { + if (out_flush) *out_flush = 0; + return 0.0; + } - for (auto& elem : unplaceable_limts) - sort_remove_duplicates(elem.second); - - for (auto& elem : unplaceable_limts) { - for (auto& eid : elem.second) { - if (eid == extruder_id_0) { - extruder_unprintables[0].insert(elem.first); - } - if (eid == extruder_id_1) { - extruder_unprintables[1].insert(elem.first); + MultiNozzleUtils::NozzleStatusRecorder initial_status; + for (auto& [nozzle_id, filament_id] : ctx.nozzle_info.nozzle_status) { + if (filament_id >= 0) { + int extruder_id = 0; + for (const auto& nozzle : ctx.nozzle_info.nozzle_list) { + if (nozzle.group_id == nozzle_id) { + extruder_id = nozzle.extruder_id; + break; } } + initial_status.set_nozzle_status(nozzle_id, filament_id, extruder_id); } } - // check printable limits - for (auto fid : group_0) { - if (extruder_unprintables[1].count(fid) > 0) - return false; + std::vector> filament_sequences; + int flush = reorder_filaments_for_multi_nozzle_extruder( + used_filaments, + *group_res, + ctx.model_info.layer_filaments, + ctx.model_info.flush_matrix, + get_custom_seq ? *get_custom_seq : std::function&)>{}, + &filament_sequences, + initial_status + ); + + if (out_flush) *out_flush = flush; + + double change_time = 0.0; + if (!filament_sequences.empty()) { + std::vector filament_change_seq; + std::vector nozzle_change_seq; + int prev_fil = -1, prev_nozzle = -1; + for (const auto& layer_seq : filament_sequences) { + for (unsigned int fil : layer_seq) { + auto nozzle_info = group_res->get_first_nozzle_for_filament(fil); + if (!nozzle_info) continue; + int nid = nozzle_info->group_id; + if ((int)fil == prev_fil && nid == prev_nozzle) continue; + filament_change_seq.push_back(static_cast(fil)); + nozzle_change_seq.push_back(nid); + prev_fil = (int)fil; + prev_nozzle = nid; + } + } + + std::vector logical_filaments(used_filaments.begin(), used_filaments.end()); + std::vector group_of_filament(used_filaments.size(), 0); + for (size_t fi = 0; fi < used_filaments.size(); ++fi) { + int nozzle_id = filament_nozzle_map[used_filaments[fi]]; + if (nozzle_id >= 0 && nozzle_id < (int)ctx.nozzle_info.nozzle_list.size()) + group_of_filament[fi] = ctx.nozzle_info.nozzle_list[nozzle_id].extruder_id; + } + change_time = calc_change_time_for_group( + filament_change_seq, + nozzle_change_seq, + logical_filaments, + ctx.nozzle_info.nozzle_list, + ctx.speed_info.change_time_params, + ctx.speed_info.ams_preload_enabled, + group_of_filament + ); } - for (auto fid : group_1) { - if (extruder_unprintables[0].count(fid) > 0) - return false; + double print_time = 0.0; + if (ctx.speed_info.group_with_time) { + TimeEvaluator time_evaluator(ctx.speed_info); + print_time = time_evaluator.get_estimated_time(filament_nozzle_map); } - // check extruder capacity ,if result before exchange meets the constraints and the result after exchange does not meet the constraints, return false - if (ctx.machine_info.max_group_size[extruder_id_0] >= group_0.size() && ctx.machine_info.max_group_size[extruder_id_1] >= group_1.size() && (ctx.machine_info.max_group_size[extruder_id_0] < group_1.size() || ctx.machine_info.max_group_size[extruder_id_1] < group_0.size())) - return false; - - return true; + return evaluate_score(flush, change_time + print_time, true); } - - // only support extruder nums with 2, try to swap the master extruder id with the other extruder id - std::vector optimize_group_for_master_extruder(const std::vector& used_filaments,const FilamentGroupContext& ctx, std::vector& filament_map) + static long long estimate_dedup_enum_count(int k, int n, const FilamentGroupContext& ctx) { - std::vector ret = filament_map; - std::unordered_map> groups; - for (size_t idx = 0; idx < used_filaments.size(); ++idx) { - int filament_id = used_filaments[idx]; - int group_id = ret[filament_id]; - groups[group_id].insert(filament_id); + if (n <= 0 || k <= 0) return 0; + + long long total = 1; + for (int i = 0; i < n; ++i) { + total *= k; + if (total > ENUM_EARLY_EXIT) return total; } - int none_master_extruder_id = 1 - ctx.machine_info.master_extruder_id; - assert(0 <= none_master_extruder_id && none_master_extruder_id <= 1); + if (k <= 1) return total; - if (can_swap_groups(none_master_extruder_id, groups[none_master_extruder_id], ctx.machine_info.master_extruder_id, groups[ctx.machine_info.master_extruder_id], ctx) - && groups[none_master_extruder_id].size()>groups[ctx.machine_info.master_extruder_id].size()) { - for (auto fid : groups[none_master_extruder_id]) - ret[fid] = ctx.machine_info.master_extruder_id; - for (auto fid : groups[ctx.machine_info.master_extruder_id]) - ret[fid] = none_master_extruder_id; + int dedup_factor = 1; + std::map nozzle_type_count; + for (const auto& nozzle : ctx.nozzle_info.nozzle_list) { + auto it = ctx.nozzle_info.nozzle_status.find(nozzle.group_id); + int loaded_filament = (it != ctx.nozzle_info.nozzle_status.end()) ? it->second : -1; + uint64_t hash = fnv_hash_nozzle(nozzle.volume_type, nozzle.group_id > 0, loaded_filament); + nozzle_type_count[hash]++; } - return ret; + + for (auto& [hash, count] : nozzle_type_count) { + int factorial = 1; + for (int i = 2; i <= count; ++i) factorial *= i; + dedup_factor *= factorial; + } + + return total / std::max(dedup_factor, 1); + } + + std::vector FilamentGroup::calc_group_by_enum( + int k, + const std::vector& used_filaments, + const std::unordered_map>& unplaceable_limits, + int* cost) + { + static constexpr int UNPLACEABLE_LIMIT_REWARD = 10000; + static constexpr int MAX_SIZE_LIMIT_REWARD = 5000; + static constexpr int SUPPORT_PREFER_REWARD = 100; + static constexpr int BEST_FIT_LIMIT_REWARD = 10; + + int n = (int)used_filaments.size(); + + std::vector> candidates(n); + for (int i = 0; i < n; i++) { + std::unordered_set group_set; + if (auto it = unplaceable_limits.find(i); it != unplaceable_limits.end()) { + for (int g = 0; g < k; g++) group_set.insert(g); + for (int g : it->second) group_set.erase(g); + } else { + for (int g = 0; g < k; g++) group_set.insert(g); + } + candidates[i].assign(group_set.begin(), group_set.end()); + } + + auto vector_equal = [](const std::vector& a, const std::vector& b) -> bool { + if (a.size() != b.size()) return false; + for (size_t i = 0; i < a.size(); i++) { + if (a[i] != b[i]) return false; + } + return true; + }; + auto vector_hash = [](const std::vector& v) -> size_t { + size_t h = 0; + for (auto val : v) { h ^= val + GOLDEN_RATIO_32 + (h << 6) + (h >> 2); } + return h; + }; + std::unordered_set, decltype(vector_hash), decltype(vector_equal)> group_set(0, vector_hash, vector_equal); + std::vector group_hashs; + + std::vector nozzles_hash(k); + for (const auto& nozzle : ctx.nozzle_info.nozzle_list) { + if (nozzle.group_id < k) { + auto it = ctx.nozzle_info.nozzle_status.find(nozzle.group_id); + int loaded_filament = (it != ctx.nozzle_info.nozzle_status.end()) ? it->second : -1; + nozzles_hash[nozzle.group_id] = fnv_hash_nozzle(nozzle.volume_type, nozzle.group_id > 0, loaded_filament); + } + } + + std::vector best_full_map(ctx.group_info.total_filament_num, ctx.machine_info.master_extruder_id); + double best_score = std::numeric_limits::max(); + int best_prefer_level = 0; + int best_flush = 0; + + const long long total = (long long)std::pow(k, n); + for (long long mask = 0; mask < total; mask++) { + long long num = mask; + std::unordered_map> nozzles_filaments; + std::vector groups_count(k, 0); + std::vector used_labels(n, 0); + + for (int i = 0; i < n; i++) { + int g_id = num % k; + num /= k; + used_labels[i] = g_id; + nozzles_filaments[g_id].emplace_back(i); + groups_count[g_id]++; + } + + // Hash dedup + group_hashs.clear(); + for (auto& nf : nozzles_filaments) { + uint64_t filament_mask = 0; + for (int filament : nf.second) filament_mask |= (1ULL << filament); + size_t gh = nozzles_hash[nf.first]; + gh ^= (std::hash{}(filament_mask) + GOLDEN_RATIO_32 + (gh << 6) + (gh >> 2)); + group_hashs.emplace_back(gh); + } + std::sort(group_hashs.begin(), group_hashs.end()); + if (group_set.find(group_hashs) != group_set.end()) continue; + group_set.insert(group_hashs); + + // Prefer level + int prefer_level = 0; + int placeable_count = 0; + for (int i = 0; i < n; i++) { + if (std::find(candidates[i].begin(), candidates[i].end(), used_labels[i]) != candidates[i].end()) + placeable_count++; + } + prefer_level += placeable_count * UNPLACEABLE_LIMIT_REWARD; + + bool size_ok = true; + for (int g = 0; g < k; g++) { + if (g < (int)ctx.machine_info.max_group_size.size() && groups_count[g] > ctx.machine_info.max_group_size[g]) + size_ok = false; + } + if (size_ok) + prefer_level += MAX_SIZE_LIMIT_REWARD; + + if (ctx.group_info.strategy == FGStrategy::BestFit) { + bool all_full = true; + for (int g = 0; g < k; g++) { + if (g < (int)ctx.machine_info.max_group_size.size() && groups_count[g] < ctx.machine_info.max_group_size[g]) + all_full = false; + } + if (all_full) + prefer_level += BEST_FIT_LIMIT_REWARD; + } + + for (int g = 0; g < k; g++) { + if (g < (int)ctx.machine_info.prefer_non_model_filament.size() && ctx.machine_info.prefer_non_model_filament[g]) { + for (int fidx : nozzles_filaments[g]) { + if (ctx.model_info.filament_info[used_filaments[fidx]].usage_type == SupportOnly) + prefer_level += SUPPORT_PREFER_REWARD; + } + } + } + + // Build full map and evaluate + std::vector full_map(ctx.group_info.total_filament_num, ctx.machine_info.master_extruder_id); + for (int i = 0; i < n; ++i) + full_map[used_filaments[i]] = used_labels[i]; + + int flush_vol = 0; + double score = full_evaluate(used_filaments, full_map, ctx, get_custom_seq, &flush_vol); + + int master_ex_id = ctx.machine_info.master_extruder_id; + if (master_ex_id < k && groups_count[master_ex_id] < (int)(used_filaments.size() + 1) / 2) + score += ABSOLUTE_FLUSH_GAP_TOLERANCE; + + if (prefer_level > best_prefer_level || (prefer_level == best_prefer_level && score < best_score)) { + best_score = score; + best_prefer_level = prefer_level; + best_full_map = full_map; + best_flush = flush_vol; + } + + MemoryedGroup mg(used_labels, score, prefer_level); + update_memoryed_groups(mg, ctx.group_info.max_gap_threshold, m_memoryed_heap); + } + + if (cost) *cost = best_flush; + return best_full_map; + } + + std::vector FilamentGroup::calc_group_by_kmedoids( + int k, + const std::vector& used_filaments, + const std::unordered_map>& unplaceable_limits, + int* cost, + int timeout_ms) + { + auto distance_evaluator = std::make_shared(ctx.model_info.flush_matrix, used_filaments, ctx.model_info.layer_filaments); + KMediods PAM(k, (int)used_filaments.size(), distance_evaluator, ctx.machine_info.master_extruder_id); + PAM.set_unplacable_limits(unplaceable_limits); + PAM.set_memory_threshold(ctx.group_info.max_gap_threshold); + + std::vector, int>> cluster_size_limit; + for (auto& [extruder_id, nozzles] : ctx.nozzle_info.extruder_nozzle_list) { + std::pair, int> clusters; + clusters.first = std::set(nozzles.begin(), nozzles.end()); + clusters.second = ctx.machine_info.max_group_size.at(extruder_id); + cluster_size_limit.emplace_back(clusters); + } + PAM.set_cluster_group_size(cluster_size_limit); + + PAM.do_clustering(ctx, timeout_ms, 30); + + m_memoryed_heap = PAM.get_memoryed_groups(); + + auto labels = PAM.get_cluster_labels(); + std::vector full_map(ctx.group_info.total_filament_num, ctx.machine_info.master_extruder_id); + for (int i = 0; i < (int)labels.size(); ++i) + full_map[used_filaments[i]] = labels[i]; + + if (cost) { + int flush_vol = 0; + full_evaluate(used_filaments, full_map, ctx, get_custom_seq, &flush_vol); + *cost = flush_vol; + } + + return full_map; } /** @@ -128,20 +395,26 @@ namespace Slic3r * considered valid. * * @param map_lists Group list with similar flush count + * @param nozzle_lists nozzle_id -> extruder_id * @param used_filaments Idx of used filaments * @param used_filament_info Information of filaments used * @param machine_filament_info Information of filaments loaded in printer * @param color_threshold Threshold for considering colors to be similar * @return The group that best fits the filament distribution in AMS */ - std::vector select_best_group_for_ams(const std::vector>& map_lists, + std::vector select_best_group_for_ams(const std::vector>& filament_to_nozzles, + const std::vector& nozzle_list, const std::vector& used_filaments, - const std::vector& used_filament_info, + const std::vector& used_filament_info, const std::vector>& machine_filament_info_, + const bool has_filament_switcher, const double color_threshold) { using namespace FlushPredict; + if (has_filament_switcher) + return filament_to_nozzles.size() ? filament_to_nozzles.front() : std::vector(); + const int fail_cost = 9999; // these code is to make we machine filament info size is 2 @@ -151,12 +424,13 @@ namespace Slic3r int best_cost = std::numeric_limits::max(); std::vectorbest_map; - for (auto& map : map_lists) { + for (auto &filament_to_nozzle : filament_to_nozzles) { std::vector> group_filaments(2); std::vector>group_colors(2); for (size_t i = 0; i < used_filaments.size(); ++i) { - int target_group = map[used_filaments[i]] == 0 ? 0 : 1; + auto &nozzle = nozzle_list[filament_to_nozzle[used_filaments[i]]]; + int target_group = nozzle.extruder_id == 0 ? 0 : 1; group_colors[target_group].emplace_back(used_filament_info[i].color); group_filaments[target_group].emplace_back(i); } @@ -211,7 +485,7 @@ namespace Slic3r if (best_map.empty() || group_cost < best_cost) { best_cost = group_cost; - best_map = map; + best_map = filament_to_nozzle; } } @@ -228,7 +502,7 @@ namespace Slic3r return; } double gap_rate = (double)std::abs(elem.cost - best.cost) / (double)best.cost; - if (gap_rate < gap_threshold) + if (gap_rate <= gap_threshold) heap.push(elem); }; @@ -271,11 +545,11 @@ namespace Slic3r for (const auto& f : lf) used_filaments_set.insert(f); std::vectorused_filaments(used_filaments_set.begin(), used_filaments_set.end()); - std::sort(used_filaments.begin(), used_filaments.end()); + sort_remove_duplicates(used_filaments); return used_filaments; } - FlushDistanceEvaluator::FlushDistanceEvaluator(const FlushMatrix& flush_matrix, const std::vector& used_filaments, const std::vector>& layer_filaments, double p) + FlushDistanceEvaluator::FlushDistanceEvaluator(const std::vector& flush_matrix, const std::vector& used_filaments, const std::vector>& layer_filaments, double p) { //calc pair counts std::vector>count_matrix(used_filaments.size(), std::vector(used_filaments.size())); @@ -296,200 +570,358 @@ namespace Slic3r } } - m_distance_matrix.resize(used_filaments.size(), std::vector(used_filaments.size())); + m_distance_matrix.resize(flush_matrix.size(), std::vector>(used_filaments.size(), std::vector(used_filaments.size()))); for (size_t i = 0; i < used_filaments.size(); ++i) { for (size_t j = 0; j < used_filaments.size(); ++j) { - if (i == j) - m_distance_matrix[i][j] = 0; - else { + for (size_t k = 0; k < flush_matrix.size(); k++) { + if (i == j) + m_distance_matrix[k][i][j] = 0; + else { //TODO: check m_flush_matrix - float max_val = std::max(flush_matrix[used_filaments[i]][used_filaments[j]], flush_matrix[used_filaments[j]][used_filaments[i]]); - float min_val = std::min(flush_matrix[used_filaments[i]][used_filaments[j]], flush_matrix[used_filaments[j]][used_filaments[i]]); - m_distance_matrix[i][j] = (max_val * p + min_val * (1 - p)) * count_matrix[i][j]; - } - } - } - } - - double FlushDistanceEvaluator::get_distance(int idx_a, int idx_b) const - { - assert(0 <= idx_a && idx_a < m_distance_matrix.size()); - assert(0 <= idx_b && idx_b < m_distance_matrix.size()); - - return m_distance_matrix[idx_a][idx_b]; - } - - std::vector KMediods2::cluster_small_data(const std::map& unplaceable_limits, const std::vector& group_size) - { - std::vectorlabels(m_elem_count, -1); - std::vectornew_group_size = group_size; - - for (auto& [elem, center] : unplaceable_limits) { - if (labels[elem] == -1) { - int gid = 1 - center; - labels[elem] = gid; - new_group_size[gid] -= 1; - } - } - - for (auto& label : labels) { - if (label == -1) { - int gid = -1; - for (size_t idx = 0; idx < new_group_size.size(); ++idx) { - if (new_group_size[idx] > 0) { - gid = idx; - break; + float max_val = std::max(flush_matrix[k][used_filaments[i]][used_filaments[j]], flush_matrix[k][used_filaments[j]][used_filaments[i]]); + float min_val = std::min(flush_matrix[k][used_filaments[i]][used_filaments[j]], flush_matrix[k][used_filaments[j]][used_filaments[i]]); + m_distance_matrix[k][i][j] = (max_val * p + min_val * (1 - p)) * (std::max(count_matrix[i][j], 1)); } } - if (gid != -1) { - label = gid; - new_group_size[gid] -= 1; - } - else { - label = m_default_group_id; - } } } - - return labels; } - std::vector KMediods2::assign_cluster_label(const std::vector& center, const std::map& unplaceable_limtis, const std::vector& group_size, const FGStrategy& strategy) + double FlushDistanceEvaluator::get_distance(int idx_a, int idx_b, int extruder_id) const { - struct Comp { - bool operator()(const std::pair& a, const std::pair& b) { - return a.second > b.second; - } - }; + assert(0 <= idx_a && idx_a < m_distance_matrix[extruder_id].size()); + assert(0 <= idx_b && idx_b < m_distance_matrix[extruder_id].size()); - std::vector>groups(2); - std::vectornew_max_group_size = group_size; - // store filament idx and distance gap between center 0 and center 1 - std::priority_queue, std::vector>, Comp>min_heap; - - for (int i = 0; i < m_elem_count; ++i) { - if (auto it = unplaceable_limtis.find(i); it != unplaceable_limtis.end()) { - int gid = it->second; - assert(gid == 0 || gid == 1); - groups[1 - gid].insert(i); // insert to group - new_max_group_size[1 - gid] = std::max(new_max_group_size[1 - gid] - 1, 0); // decrease group_size - continue; - } - int distance_to_0 = m_evaluator->get_distance(i, center[0]); - int distance_to_1 = m_evaluator->get_distance(i, center[1]); - min_heap.push({ i,distance_to_0 - distance_to_1 }); - } - - bool have_enough_size = (min_heap.size() <= (new_max_group_size[0] + new_max_group_size[1])); - - if (have_enough_size || strategy == FGStrategy::BestFit) { - while (!min_heap.empty()) { - auto top = min_heap.top(); - min_heap.pop(); - if (groups[0].size() < new_max_group_size[0] && (top.second <= 0 || groups[1].size() >= new_max_group_size[1])) - groups[0].insert(top.first); - else if (groups[1].size() < new_max_group_size[1] && (top.second > 0 || groups[0].size() >= new_max_group_size[0])) - groups[1].insert(top.first); - else { - if (top.second <= 0) - groups[0].insert(top.first); - else - groups[1].insert(top.first); - } - } - } - else { - while (!min_heap.empty()) { - auto top = min_heap.top(); - min_heap.pop(); - if (top.second <= 0) - groups[0].insert(top.first); - else - groups[1].insert(top.first); - } - } - - std::vectorlabels(m_elem_count); - for (auto& f : groups[0]) - labels[f] = 0; - for (auto& f : groups[1]) - labels[f] = 1; - - return labels; + return m_distance_matrix[extruder_id][idx_a][idx_b]; } - int KMediods2::calc_cost(const std::vector& labels, const std::vector& medoids) + double TimeEvaluator::get_estimated_time(const std::vector& filament_map) const { + double time = 0; + for(auto &elem : m_speed_info.filament_print_time){ + int filament_idx = elem.first; + auto extruder_time = elem.second; + int filament_extruder_id = filament_map[filament_idx]; + time += extruder_time[filament_extruder_id]; + } + return time; + } + + + + void KMediods::set_cluster_group_size(const std::vector, int>> &cluster_group_size) + { + m_cluster_group_size = cluster_group_size; + m_nozzle_to_extruder.resize(m_k, 0); + for (int i = 0; i < m_cluster_group_size.size(); i++) { + for (auto nozzle_id : m_cluster_group_size[i].first) m_nozzle_to_extruder[nozzle_id] = i; + } + } + + int KMediods::calc_cost(const std::vector& cluster_labels, const std::vector& cluster_centers,int cluster_id) + { + assert(m_evaluator); int total_cost = 0; - for (int i = 0; i < m_elem_count; ++i) - total_cost += m_evaluator->get_distance(i, medoids[labels[i]]); + + std::vector> nozzle_cost(m_k,{0,0.0}); + std::vector nozzle_filaments(m_k, 0); + for (int i = 0; i < m_elem_count; ++i) { + if (cluster_id != -1 && cluster_labels[i] != cluster_id) + continue; + if (cluster_centers[cluster_labels[i]] == -1) + continue; + + nozzle_filaments[cluster_labels[i]]++; + for (int j = i + 1; j < m_elem_count; ++j) { + int nozzle_i = cluster_labels[i]; + int nozzle_j = cluster_labels[j]; + if (nozzle_i == nozzle_j) { + nozzle_cost[nozzle_i].first++; + nozzle_cost[nozzle_j].second += m_evaluator->get_distance(i, j, m_nozzle_to_extruder[nozzle_i]); + } + } + } + for (size_t i = 0; i < nozzle_cost.size(); ++i) { + if (nozzle_filaments[i] > 0 && nozzle_cost[i].second > 0) + total_cost += nozzle_cost[i].second / nozzle_cost[i].first * (nozzle_filaments[i] - 1); + } + return total_cost; } - void KMediods2::do_clustering(const FGStrategy& g_strategy, int timeout_ms) + bool KMediods::have_enough_size(const std::vector& cluster_size, const std::vector, int>>& cluster_group_size,int elem_count) + { + bool have_enough_size = true; + std::optionalcluster_sum; + std::optionalcluster_group_sum; + + if (!cluster_size.empty()) + cluster_sum = std::accumulate(cluster_size.begin(), cluster_size.end(), 0); + if (!cluster_group_size.empty()) + cluster_group_sum = std::accumulate(cluster_group_size.begin(), cluster_group_size.end(), 0, [](int a, const std::pair, int>& p) {return a + p.second; }); + if (cluster_sum.has_value()) + have_enough_size &= (cluster_sum >= elem_count); + if (cluster_group_sum.has_value()) + have_enough_size &= (cluster_group_sum >= elem_count); + return have_enough_size; + } + + + // make sure each cluster has at least one element + std::vector KMediods::init_cluster_center(const std::unordered_map>& placeable_limits, const std::unordered_map>& unplaceable_limits,const std::vector& cluster_size,const std::vector,int>>& cluster_group_size, int seed) + { + // max flow network + std::vector l_nodes(m_elem_count); // represent the filament idx, to be shuffled + std::vector r_nodes(m_k); // represent the group idx + std::iota(l_nodes.begin(), l_nodes.end(), 0); + std::iota(r_nodes.begin(), r_nodes.end(), 0); + + std::unordered_map> shuffled_placeable_limits; + std::unordered_map> shuffled_unplaceable_limits; + // shuffle the filaments and transfer placeable,unplaceable limits + { + std::mt19937 rng(seed); + std::shuffle(l_nodes.begin(), l_nodes.end(), rng); + + std::unordered_mapidx_transfer; + for (size_t idx = 0; idx < l_nodes.size(); ++idx){ + int new_idx = std::find(l_nodes.begin(),l_nodes.end(), idx) - l_nodes.begin(); + idx_transfer[idx] = new_idx; + } + for (auto& elem : placeable_limits) + shuffled_placeable_limits[idx_transfer[elem.first]] = elem.second; + for (auto& elem : unplaceable_limits) + shuffled_unplaceable_limits[idx_transfer[elem.first]] = elem.second; + } + + + MaxFlowSolver M(l_nodes, r_nodes, shuffled_placeable_limits, shuffled_unplaceable_limits); + auto ret = M.solve(); + + // A remaining -1 means some filaments cannot be placed under the limit. We ignore the -1 here since we + // are deciding the cluster center; the -1 can be handled in later steps. + std::vector cluster_center(m_k, -1); + for (size_t idx = 0; idx < ret.size(); ++idx) { + if (ret[idx] != -1) { + cluster_center[ret[idx]] = l_nodes[idx]; + } + } + + return cluster_center; + } + + std::vector KMediods::assign_cluster_label(const std::vector& center, const std::unordered_map>& placeable_limits, const std::unordered_map>& unplaceable_limits, const std::vector& cluster_size, const std::vector, int>>& cluster_group_size) + { + std::vector labels(m_elem_count, -1); + std::vector l_nodes(m_elem_count); + std::vector r_nodes(m_k); + std::iota(l_nodes.begin(), l_nodes.end(), 0); + std::iota(r_nodes.begin(), r_nodes.end(), 0); + + std::vector> distance_matrix(m_elem_count, std::vector(m_k)); + for (int i = 0; i < m_elem_count; ++i) { + for (int j = 0; j < m_k; ++j) { + if (center[j] == -1) + distance_matrix[i][j] = static_cast(MaxFlowGraph::MCMF_MAX_EDGE_COST); + else + distance_matrix[i][j] = m_evaluator->get_distance(i, center[j], m_nozzle_to_extruder[j]); + } + } + + // only consider the size limit if the group can contain all of the filaments + std::vector r_nodes_capacity = {}; + std::vector, int>> r_nodes_group_capacity = {}; + if (have_enough_size(cluster_size, cluster_group_size, m_elem_count)) { + r_nodes_capacity = cluster_size; + r_nodes_group_capacity = cluster_group_size; + } + else { + // TODO: throw exception here? + // adjust group size to elem count if the group cannot contain all of the filaments + r_nodes_capacity = std::vector(m_k, m_elem_count); + } + std::vector l_nodes_capacity(l_nodes.size(),1); + //for (size_t idx = 0; idx < center.size(); ++idx) + // if (center[idx] != -1) + // l_nodes_capacity[center[idx]] = 0; + + + // Each group can receive up to m_elem_count materials at most, so the flow from r_nodes to sink is adjusted to m_elem_count. + MinFlushFlowSolver M(distance_matrix, l_nodes, r_nodes, placeable_limits, unplaceable_limits, l_nodes_capacity, r_nodes_capacity, r_nodes_group_capacity); + auto ret = M.solve(); + + for (size_t idx = 0; idx < ret.size(); ++idx) { + if (ret[idx] != MaxFlowGraph::INVALID_ID) { + labels[l_nodes[idx]] = r_nodes[ret[idx]]; + } + } + + for (size_t idx = 0; idx < center.size(); ++idx) + if (center[idx] != -1) + assert(labels[center[idx]] == idx); + + //for (size_t idx = 0; idx < center.size(); ++idx) { + // if (center[idx] != -1) { + // labels[center[idx]] = idx; + // } + //} + + // If there are materials that have not been grouped in the last step, assign them to a valid group. + for (size_t idx = 0; idx < labels.size(); ++idx) { + if (labels[idx] == -1) { + int fallback = m_default_group_id; + auto it = unplaceable_limits.find(static_cast(idx)); + if (it != unplaceable_limits.end()) { + for (int nid = 0; nid < m_k; ++nid) { + if (std::find(it->second.begin(), it->second.end(), nid) == it->second.end()) { + fallback = nid; + break; + } + } + } + labels[idx] = fallback; + } + } + + return labels; + } + + /* + 1.Select initial medoids randomly + 2.Iterate while the cost decreases: + 2.1 In each cluster, make the point that minimizes the sum of distances within the cluster the medoid + 2.2 Reassign each point to the cluster defined by the closest medoid determined in the previous step + */ + void KMediods::do_clustering(const FilamentGroupContext &context, int timeout_ms, int retry) { FlushTimeMachine T; T.time_machine_start(); - if (m_elem_count < m_k) { - m_cluster_labels = cluster_small_data(m_unplaceable_limits, m_max_cluster_size); - { - std::vectorcluster_center(m_k, -1); - for (size_t idx = 0; idx < m_cluster_labels.size(); ++idx) { - if (cluster_center[m_cluster_labels[idx]] == -1) - cluster_center[m_cluster_labels[idx]] = idx; - } - MemoryedGroup g(m_cluster_labels, calc_cost(m_cluster_labels, cluster_center), 1); - update_memoryed_groups(g, memory_threshold, memoryed_groups); - } - return; - } + const std::vector used_filaments = collect_sorted_used_filaments(context.model_info.layer_filaments); - std::vectorbest_labels; - int best_cost = std::numeric_limits::max(); + auto build_full_map = [&](const std::vector& labels) -> std::vector { + std::vector full_map(context.group_info.total_filament_num, m_default_group_id); + for (int i = 0; i < (int)labels.size(); ++i) + full_map[used_filaments[i]] = labels[i]; + return full_map; + }; - for (int center_0 = 0; center_0 < m_elem_count; ++center_0) { - if (auto iter = m_unplaceable_limits.find(center_0); iter != m_unplaceable_limits.end() && iter->second == 0) - continue; - for (int center_1 = 0; center_1 < m_elem_count; ++center_1) { - if (center_0 == center_1) - continue; - if (auto iter = m_unplaceable_limits.find(center_1); iter != m_unplaceable_limits.end() && iter->second == 1) - continue; + auto evaluate_labels = [&](const std::vector& labels) -> double { + auto full_map = build_full_map(labels); + return full_evaluate(used_filaments, full_map, context); + }; - std::vectornew_centers = { center_0,center_1 }; - std::vectornew_labels = assign_cluster_label(new_centers, m_unplaceable_limits, m_max_cluster_size, g_strategy); + std::vector best_cluster_centers = std::vector(m_k, 0); + std::vector best_cluster_labels = std::vector(m_elem_count, m_default_group_id); + double best_cluster_cost = std::numeric_limits::max(); + int retry_count = 0; - int new_cost = calc_cost(new_labels, new_centers); - if (new_cost < best_cost) { - best_cost = new_cost; - best_labels = new_labels; + while (retry_count < retry && T.time_machine_end() < timeout_ms) { + std::vector curr_cluster_centers = init_cluster_center(m_placeable_limits, m_unplaceable_limits, m_max_cluster_size, m_cluster_group_size, retry_count); + std::vector curr_cluster_labels = assign_cluster_label(curr_cluster_centers, m_placeable_limits, m_unplaceable_limits, m_max_cluster_size, m_cluster_group_size); + double curr_cluster_cost = evaluate_labels(curr_cluster_labels); + + MemoryedGroup g(curr_cluster_labels, curr_cluster_cost, 1); + update_memoryed_groups(g, memory_threshold, memoryed_groups); + + bool mediods_changed = true; + while (mediods_changed && T.time_machine_end() < timeout_ms) { + mediods_changed = false; + double best_swap_cost = curr_cluster_cost; + int best_swap_cluster = -1; + int best_swap_elem = -1; + + for (size_t cluster_id = 0; cluster_id < m_k; ++cluster_id) { + if (curr_cluster_centers[cluster_id] == -1) continue; + for (int elem = 0; elem < m_elem_count; ++elem) { + if (std::find(curr_cluster_centers.begin(), curr_cluster_centers.end(), elem) != curr_cluster_centers.end() || + std::find(m_unplaceable_limits[cluster_id].begin(), m_unplaceable_limits[cluster_id].end(), elem) != m_unplaceable_limits[cluster_id].end()) + continue; + std::vector tmp_centers = curr_cluster_centers; + tmp_centers[cluster_id] = elem; + std::vector tmp_labels = assign_cluster_label(tmp_centers, m_placeable_limits, m_unplaceable_limits, m_max_cluster_size, m_cluster_group_size); + double tmp_cost = evaluate_labels(tmp_labels); + + if (tmp_cost < best_swap_cost) { + best_swap_cost = tmp_cost; + best_swap_cluster = cluster_id; + best_swap_elem = elem; + mediods_changed = true; + } + } } - { - MemoryedGroup g(new_labels,new_cost,1); + if (mediods_changed) { + curr_cluster_centers[best_swap_cluster] = best_swap_elem; + curr_cluster_labels = assign_cluster_label(curr_cluster_centers, m_placeable_limits, m_unplaceable_limits, m_max_cluster_size, m_cluster_group_size); + curr_cluster_cost = evaluate_labels(curr_cluster_labels); + + MemoryedGroup g(curr_cluster_labels, curr_cluster_cost, 1); update_memoryed_groups(g, memory_threshold, memoryed_groups); } - - if (T.time_machine_end() > timeout_ms) - break; } - if (T.time_machine_end() > timeout_ms) - break; + + if (curr_cluster_cost < best_cluster_cost) { + best_cluster_centers = curr_cluster_centers; + best_cluster_cost = curr_cluster_cost; + best_cluster_labels = curr_cluster_labels; + } + retry_count += 1; } - this->m_cluster_labels = best_labels; + m_cluster_labels = best_cluster_labels; } std::vector FilamentGroup::calc_min_flush_group(int* cost) { auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments); - int used_filament_num = used_filaments.size(); + int n = (int)used_filaments.size(); + int k = (int)ctx.nozzle_info.nozzle_list.size(); - if (used_filament_num < 10) - return calc_min_flush_group_by_enum(used_filaments, cost); + std::unordered_map> unplaceable_limits; + extract_unprintable_limit_indices(ctx.model_info.unprintable_filaments, used_filaments, unplaceable_limits); + unplaceable_limits = rebuild_nozzle_unprintables(used_filaments, unplaceable_limits, ctx.group_info.filament_volume_map); + + m_memoryed_heap = MemoryedGroupHeap(); + std::vector result; + + long long estimated = estimate_dedup_enum_count(k, n, ctx); + if (estimated < ENUM_THRESHOLD) + result = calc_group_by_enum(k, used_filaments, unplaceable_limits, cost); else - return calc_min_flush_group_by_pam2(used_filaments, cost, 500); + result = calc_group_by_kmedoids(k, used_filaments, unplaceable_limits, cost, 3000); + + change_memoryed_heaps_to_arrays(m_memoryed_heap, ctx.group_info.total_filament_num, used_filaments, m_memoryed_groups); + + return result; + } + + std::map FilamentGroup::rebuild_unprintables(const std::vector& used_filaments, const std::map& extruder_unprintables) + { + std::map ret; + for (int f_idx = 0; f_idx < used_filaments.size(); f_idx++) { + int unprintable_ext = -1; + if (extruder_unprintables.find(f_idx) != extruder_unprintables.end()) { + unprintable_ext = extruder_unprintables.at(f_idx); + } + + bool multi_unprintable = false; + auto unprintable_volumes = ctx.model_info.unprintable_volumes[used_filaments[f_idx]]; + for (int nozzle_idx = 0; nozzle_idx != ctx.nozzle_info.nozzle_list.size(); nozzle_idx++) { + auto nozzle_info = ctx.nozzle_info.nozzle_list[nozzle_idx]; + + if (unprintable_volumes.count(nozzle_info.volume_type)) { + if (unprintable_ext == -1) + unprintable_ext = nozzle_info.extruder_id; + else if (unprintable_ext != nozzle_info.extruder_id) + multi_unprintable = true; + } + } + + if (!multi_unprintable && unprintable_ext != -1) ret[f_idx] = unprintable_ext; + + } + return ret; } std::unordered_map> FilamentGroup::try_merge_filaments() @@ -577,6 +1009,11 @@ namespace Slic3r std::vector FilamentGroup::calc_filament_group(int* cost) { + /*auto extruder_variant_list = ctx.nozzle_info.extruder_nozzle_list; + for (auto nozzle : ctx.nozzle_info.nozzle_list) + if (nozzle.volume_type == NozzleVolumeType::nvtTPUHighFlow) + return calc_filament_group_for_tpu(cost);*/ + try { if (FGMode::MatchMode == ctx.group_info.mode) return calc_filament_group_for_match(cost); @@ -584,18 +1021,16 @@ namespace Slic3r catch (const FilamentGroupException& e) { } - auto merged_map = try_merge_filaments(); - rebuild_context(merged_map); - auto filamnet_map = calc_filament_group_for_flush(cost); - return seperate_merged_filaments(filamnet_map, merged_map); + return calc_filament_group_for_flush(cost); } std::vector FilamentGroup::calc_filament_group_for_match(int* cost) { using namespace FlushPredict; + constexpr int SupportPreferScore = 3; auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments); - std::vector used_filament_list; + std::vector used_filament_list; for (auto f : used_filaments) used_filament_list.emplace_back(ctx.model_info.filament_info[f]); @@ -613,6 +1048,7 @@ namespace Slic3r std::map unprintable_limit_indices; // key stores filament idx in used_filament, value stores unprintable extruder extract_unprintable_limit_indices(ctx.model_info.unprintable_filaments, used_filaments, unprintable_limit_indices); + unprintable_limit_indices = rebuild_unprintables(used_filaments, unprintable_limit_indices); std::vector> color_dist_matrix(used_filament_list.size(), std::vector(machine_filament_list.size())); for (size_t i = 0; i < used_filament_list.size(); ++i) { @@ -652,19 +1088,17 @@ namespace Slic3r return unlink_limits; }; - auto optimize_map_to_machine_filament = [&](const std::vector& map_to_machine_filament, const std::vector& l_nodes, const std::vector& r_nodes, std::vector& filament_map, bool consider_capacity) { + auto optimize_map_to_machine_filament = [&](const std::vector& map_to_machine_filament, const std::vector& l_nodes, const std::vector& r_nodes, std::vector& filament_map) { std::vector ungrouped_filaments; std::vector filaments_to_optimize; auto map_filament_to_machine_filament = [&](int filament_idx, int machine_filament_idx) { auto& machine_filament = machine_filament_list[machine_filament_idx]; - machine_filament_capacity[machine_filament_idx] = std::max(0, machine_filament_capacity[machine_filament_idx] - 1); // decrease machine filament capacity filament_map[used_filaments[filament_idx]] = machine_filament.extruder_id; // set extruder id to filament map extruder_filament_count[machine_filament.extruder_id] += 1; // increase filament count in extruder }; auto unmap_filament_to_machine_filament = [&](int filament_idx, int machine_filament_idx) { auto& machine_filament = machine_filament_list[machine_filament_idx]; - machine_filament_capacity[machine_filament_idx] += 1; // increase machine filament capacity extruder_filament_count[machine_filament.extruder_id] -= 1; // increase filament count in extruder }; @@ -684,25 +1118,63 @@ namespace Slic3r // try to optimize the result for (auto idx : filaments_to_optimize) { int filament_idx = l_nodes[idx]; + bool is_support_filament = used_filament_list[filament_idx].usage_type == FilamentUsageType::SupportOnly; int old_machine_filament_idx = r_nodes[map_to_machine_filament[idx]]; auto& old_machine_filament = machine_filament_list[old_machine_filament_idx]; - int curr_gap = std::abs(extruder_filament_count[0] - extruder_filament_count[1]); unmap_filament_to_machine_filament(filament_idx, old_machine_filament_idx); auto optional_filaments = machine_filament_set[old_machine_filament]; - auto iter = optional_filaments.begin(); - for (; iter != optional_filaments.end(); ++iter) { - int new_extruder_id = machine_filament_list[*iter].extruder_id; - int new_gap = std::abs(extruder_filament_count[new_extruder_id] + 1 - extruder_filament_count[1 - new_extruder_id]); - if (new_gap < curr_gap && (!consider_capacity || machine_filament_capacity[*iter] > 0)) { - map_filament_to_machine_filament(filament_idx, *iter); - break; + + // Phase 1: collect all candidates and compute their preference scores + std::vector> valid_candidates; // available machine-filament idx and its score + for (auto machine_filament : optional_filaments) { + int new_extruder_id = machine_filament_list[machine_filament].extruder_id; + + // preference score for this assignment + int preference_score = 0; + bool new_extruder_prefer_support = ctx.machine_info.prefer_non_model_filament[new_extruder_id]; + + // reward a support filament assigned to a support-preferring nozzle + if (is_support_filament && new_extruder_prefer_support) { + preference_score += SupportPreferScore; + } + + valid_candidates.emplace_back(machine_filament, preference_score); + } + // Phase 2: determine the best preference score + int best_preference_score = 0; + for (const auto& candidate : valid_candidates) { + if (candidate.second >= best_preference_score) { + best_preference_score = candidate.second; } } - if (iter == optional_filaments.end()) + // Phase 3: among candidates with the best preference score, pick the most load-balanced one + int best_candidate = -1; + int best_gap = std::numeric_limits::max(); + + for (const auto& candidate : valid_candidates) { + // only consider candidates with the best preference score + int machine_filament = candidate.first; + int score = candidate.second; + if (score == best_preference_score) { + int new_extruder_id = machine_filament_list[machine_filament].extruder_id; + int new_gap = std::abs(extruder_filament_count[new_extruder_id] + 1 - extruder_filament_count[1 - new_extruder_id]); + + // among equal-preference candidates, pick the one giving the most balanced load + if (new_gap < best_gap) { + best_gap = new_gap; + best_candidate = machine_filament; + } + } + } + // apply the best choice + if (best_candidate != -1) { + map_filament_to_machine_filament(filament_idx, best_candidate); + } else { map_filament_to_machine_filament(filament_idx, old_machine_filament_idx); + } } return ungrouped_filaments; }; @@ -718,7 +1190,7 @@ namespace Slic3r { MatchModeGroupSolver s(color_dist_matrix, l_nodes, r_nodes, machine_filament_capacity, unlink_limits_full); - ungrouped_filaments = optimize_map_to_machine_filament(s.solve(), l_nodes, r_nodes,group,false); + ungrouped_filaments = optimize_map_to_machine_filament(s.solve(), l_nodes, r_nodes,group); if (ungrouped_filaments.empty()) return group; } @@ -731,7 +1203,7 @@ namespace Slic3r }); MatchModeGroupSolver s(color_dist_matrix, l_nodes, r_nodes, machine_filament_capacity, unlink_limits); - ungrouped_filaments = optimize_map_to_machine_filament(s.solve(), l_nodes, r_nodes, group,false); + ungrouped_filaments = optimize_map_to_machine_filament(s.solve(), l_nodes, r_nodes, group); if (ungrouped_filaments.empty()) return group; } @@ -740,7 +1212,7 @@ namespace Slic3r { l_nodes = ungrouped_filaments; MatchModeGroupSolver s(color_dist_matrix, l_nodes, r_nodes, machine_filament_capacity, {}); - auto ret = optimize_map_to_machine_filament(s.solve(), l_nodes, r_nodes, group,false); + auto ret = optimize_map_to_machine_filament(s.solve(), l_nodes, r_nodes, group); for (size_t idx = 0; idx < ret.size(); ++idx) { if (ret[idx] == MaxFlowGraph::INVALID_ID) assert(false); @@ -760,140 +1232,253 @@ namespace Slic3r std::vector> memoryed_maps = this->m_memoryed_groups; memoryed_maps.insert(memoryed_maps.begin(), ret); - std::vector optimized_ret = optimize_group_for_master_extruder(used_filaments, ctx, ret); - if (optimized_ret != ret) - memoryed_maps.insert(memoryed_maps.begin(), optimized_ret); - std::vector used_filament_info; for (auto f : used_filaments) { used_filament_info.emplace_back(ctx.model_info.filament_info[f]); } - ret = select_best_group_for_ams(memoryed_maps, used_filaments, used_filament_info, ctx.machine_info.machine_filament_info); + ret = select_best_group_for_ams(memoryed_maps, ctx.nozzle_info.nozzle_list, used_filaments, used_filament_info, ctx.machine_info.machine_filament_info, ctx.group_info.has_filament_switcher); return ret; } + std::vector FilamentGroup::calc_filament_group_for_tpu(int *cost) { + + auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments); + std::vector used_filament_list; + for (auto f : used_filaments) + used_filament_list.emplace_back(ctx.model_info.filament_info[f]); + + std::vector> print_time_matrix(used_filaments.size(), std::vector(ctx.nozzle_info.extruder_nozzle_list.size())); + for (int i = 0; i < used_filaments.size(); ++i){ + for (int j = 0; j < ctx.nozzle_info.extruder_nozzle_list.size(); ++j){ + print_time_matrix[i][j] = ctx.speed_info.filament_print_time[used_filaments[i]][j]; + if (ctx.nozzle_info.nozzle_list[j].volume_type == nvtTPUHighFlow) // when both TPU High Flow and other nozzle types exist, prefer assigning filament to the TPU High Flow nozzle + print_time_matrix[i][j] *= 0.9; + } + } + + std::vector l_nodes(used_filaments.size()); + std::iota(l_nodes.begin(), l_nodes.end(), 0); + std::vector r_nodes(ctx.nozzle_info.extruder_nozzle_list.size()); + std::iota(r_nodes.begin(), r_nodes.end(), 0); + std::vector machine_filament_capacity({int(used_filaments.size()), int(used_filaments.size())}); + + std::map unprintable_limit_indices; // key stores filament idx in used_filament, value stores unprintable extruder + extract_unprintable_limit_indices(ctx.model_info.unprintable_filaments, used_filaments, unprintable_limit_indices); + unprintable_limit_indices = rebuild_unprintables(used_filaments, unprintable_limit_indices); + + std::unordered_map> unlink_limits(used_filaments.size()); + for (int i = 0; i < used_filaments.size(); i++) { + auto iter = unprintable_limit_indices.find(i); + if (iter == unprintable_limit_indices.end() || iter->second < 0 || iter->second >= 2) continue; + unlink_limits[i].emplace_back(iter->second); + } + + MatchModeGroupSolver s(print_time_matrix, l_nodes, r_nodes, machine_filament_capacity, unlink_limits); + auto ret = s.solve(); + for (size_t idx = 0; idx < ret.size(); ++idx) { + if (ret[idx] == MaxFlowGraph::INVALID_ID) { + assert(false); + ret[idx] = 1; + } + } + std::vector group(ctx.group_info.total_filament_num, ctx.machine_info.master_extruder_id); + for (int i = 0; i < ret.size(); ++i) group[used_filaments[i]] = ret[i]; + return group; + } // sorted used_filaments - std::vector FilamentGroup::calc_min_flush_group_by_enum(const std::vector& used_filaments, int* cost) + + std::unordered_map> FilamentGroup::rebuild_nozzle_unprintables(const std::vector& used_filaments, const std::unordered_map>& extruder_unprintables, const std::vector& filament_volume_map) { - static constexpr int UNPLACEABLE_LIMIT_REWARD = 100; // reward value if the group result follows the unprintable limit - static constexpr int MAX_SIZE_LIMIT_REWARD = 10; // reward value if the group result follows the max size per extruder - static constexpr int BEST_FIT_LIMIT_REWARD = 1; // reward value if the group result try to fill the max size per extruder + std::unordered_map> nozzle_unprintables; - MemoryedGroupHeap memoryed_groups; + for(size_t fidx = 0 ;fidx unexpected_extruders; + if(extruder_unprintables.find(fidx) != extruder_unprintables.end()){ + unexpected_extruders = extruder_unprintables.at(fidx); + } - auto bit_count_one = [](uint64_t n) - { - int count = 0; - while (n != 0) - { - n &= n - 1; - count++; + auto unprintable_volumes = ctx.model_info.unprintable_volumes[used_filaments[fidx]]; + + std::vector unprintable_nozzles; + for(size_t nozzle_idx =0 ;nozzle_idx < ctx.nozzle_info.nozzle_list.size(); ++nozzle_idx){ + auto nozzle_info = ctx.nozzle_info.nozzle_list[nozzle_idx]; + + if(std::find(unexpected_extruders.begin(), unexpected_extruders.end(), nozzle_info.extruder_id)!= unexpected_extruders.end() || (expected_volume!=nvtHybrid && expected_volume != nozzle_info.volume_type) || + (unprintable_volumes.count(nozzle_info.volume_type) != 0)) + unprintable_nozzles.push_back(nozzle_idx); + } + if(unprintable_nozzles.empty()) + continue; + + sort_remove_duplicates(unprintable_nozzles); + nozzle_unprintables[fidx] = unprintable_nozzles; + } + + return nozzle_unprintables; + } + + + std::vector calc_filament_group_for_match_multi_nozzle(const FilamentGroupContext& ctx) + { + FilamentGroup fg1(ctx); + auto filament_extruder_map = fg1.calc_filament_group_for_match(); + + FilamentGroupContext new_ctx = ctx; + auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments); + for(size_t idx = 0; idx < used_filaments.size(); ++idx) + new_ctx.model_info.unprintable_filaments[1 - filament_extruder_map[used_filaments[idx]]].insert(used_filaments[idx]); + new_ctx.machine_info.max_group_size.assign(new_ctx.machine_info.max_group_size.size(), std::numeric_limits::max()); + FilamentGroup fg(new_ctx); + return fg.calc_filament_group_for_flush(); + } + + std::vector plan_filament_nozzle_mapping_and_order(const FilamentGroupContext &ctx) + { + std::vector res; + + // right nodes: nozzles + std::vector r_nodes(ctx.nozzle_info.nozzle_list.size(), -1); + auto initial_nozzle = ctx.nozzle_info.nozzle_status; + for (int r_id = 0; r_id < r_nodes.size(); r_id++) { if (initial_nozzle.count(r_id)) r_nodes[r_id] = initial_nozzle[r_id]; } + + std::vector r_nodes_group(ctx.nozzle_info.nozzle_list.size(), -1); + for (auto &nozzle_info : ctx.nozzle_info.nozzle_list) { r_nodes_group[nozzle_info.group_id] = nozzle_info.extruder_id; } + + int layer_nums = ctx.model_info.layer_filaments.size(); + auto &flush_matrix = ctx.model_info.flush_matrix; + + int prev_layer_last_nozzle_id = -1; + bool used_prev_layer_last_nozzle = false; + + // per-layer filament->nozzle matching + std::vector layer_fil_nozzle_match(ctx.model_info.filament_info.size(), 0); + for (int i = 0; i < layer_nums; i++) { + // left nodes: filaments, deduplicated + const auto &layer_filaments = ctx.model_info.layer_filaments[i]; + std::vector l_nodes(layer_filaments.begin(), layer_filaments.end()); + std::sort(l_nodes.begin(), l_nodes.end()); + l_nodes.erase(std::unique(l_nodes.begin(), l_nodes.end()), l_nodes.end()); + + if (l_nodes.empty()) { + res.emplace_back(FilamentPlanRes{{}, {}}); + continue; + } + + // per-layer queue of filaments used within each nozzle + std::vector> nozzle_fil_deq(r_nodes.size()); + + // build a reverse map from nozzle-loaded filament to nozzle index to speed lookups from O(n) to O(1) + std::unordered_map filament_to_nozzle; + filament_to_nozzle.reserve(r_nodes.size()); + for (size_t noz_id = 0; noz_id < r_nodes.size(); ++noz_id) { + if (r_nodes[noz_id] >= 0) { filament_to_nozzle[r_nodes[noz_id]] = noz_id; } + } + + const int epochs = std::ceil(double(l_nodes.size()) / r_nodes.size()); + for (int j = 0; j < epochs; j++) { + // 1. filter out filaments already matching the state loaded in a nozzle + std::vector remaining_l_nodes; + std::vector remaining_r_nodes; + std::vector remaining_r_nodes_to_origin; + std::vector used_r_nodes(r_nodes.size(), false); + + remaining_l_nodes.reserve(l_nodes.size()); + remaining_r_nodes.reserve(r_nodes.size()); + remaining_r_nodes_to_origin.reserve(r_nodes.size()); + + for (int f_id : l_nodes) { + auto it = filament_to_nozzle.find(f_id); + if (it != filament_to_nozzle.end()) { + size_t noz_id = it->second; + layer_fil_nozzle_match[f_id] = noz_id; + nozzle_fil_deq[noz_id].push_back(f_id); + used_prev_layer_last_nozzle = (noz_id == prev_layer_last_nozzle_id); + used_r_nodes[noz_id] = true; + } else { + remaining_l_nodes.emplace_back(f_id); + } } - return count; - }; + l_nodes = std::move(remaining_l_nodes); - std::mapunplaceable_limit_indices; - extract_unprintable_limit_indices(ctx.model_info.unprintable_filaments, used_filaments, unplaceable_limit_indices); + for (int r_id = 0; r_id < r_nodes.size(); r_id++) { + if (!used_r_nodes[r_id]) { + remaining_r_nodes.emplace_back(r_nodes[r_id]); + remaining_r_nodes_to_origin.emplace_back(r_id); + } + } - int used_filament_num = used_filaments.size(); - uint64_t max_group_num = (static_cast(1) << used_filament_num); - - int best_cost = std::numeric_limits::max(); - std::vectorbest_label; - int best_prefer_level = 0; - - for (uint64_t i = 0; i < max_group_num; ++i) { - std::vector>groups(2); - for (int j = 0; j < used_filament_num; ++j) { - if (i & (static_cast(1) << j)) - groups[1].insert(j); - else - groups[0].insert(j); + // 2. run min-cost flow on the remaining nodes + GroupMinCostFlowSolver s(flush_matrix, l_nodes, remaining_r_nodes, r_nodes_group); + auto match = s.solve(); + int write = 0; + for (int l_id = 0; l_id < l_nodes.size(); l_id++) { + if (match[l_id] >= 0 && match[l_id] < remaining_r_nodes.size()) { + int noz_id = remaining_r_nodes_to_origin[match[l_id]]; + int filament_id = l_nodes[l_id]; + layer_fil_nozzle_match[filament_id] = noz_id; + r_nodes[noz_id] = filament_id; + nozzle_fil_deq[noz_id].push_back(filament_id); + // update the reverse map + filament_to_nozzle[filament_id] = noz_id; + } else { + l_nodes[write++] = l_nodes[l_id]; + } + } + l_nodes.resize(write); } - int prefer_level = 0; - - if (check_printable(groups, unplaceable_limit_indices)) - prefer_level += UNPLACEABLE_LIMIT_REWARD; - if (groups[0].size() <= ctx.machine_info.max_group_size[0] && groups[1].size() <= ctx.machine_info.max_group_size[1]) - prefer_level += MAX_SIZE_LIMIT_REWARD; - if (FGStrategy::BestFit == ctx.group_info.strategy && groups[0].size() >= ctx.machine_info.max_group_size[0] && groups[1].size() >= ctx.machine_info.max_group_size[1]) - prefer_level += BEST_FIT_LIMIT_REWARD; - - std::vectorfilament_maps(used_filament_num); - for (int i = 0; i < used_filament_num; ++i) { - if (groups[0].find(i) != groups[0].end()) - filament_maps[i] = 0; - if (groups[1].find(i) != groups[1].end()) - filament_maps[i] = 1; + // order the filaments within the layer + int start_extruder = 0; + int start_nozzle = 0; + if (used_prev_layer_last_nozzle) { + start_extruder = ctx.nozzle_info.nozzle_list[prev_layer_last_nozzle_id].extruder_id; + start_nozzle = prev_layer_last_nozzle_id; } - int total_cost = reorder_filaments_for_minimum_flush_volume( - used_filaments, - filament_maps, - ctx.model_info.layer_filaments, - ctx.model_info.flush_matrix, - get_custom_seq, - nullptr - ); - - if (prefer_level > best_prefer_level || (prefer_level == best_prefer_level && total_cost < best_cost)) { - best_prefer_level = prefer_level; - best_cost = total_cost; - best_label = filament_maps; + std::deque used_nozzle_deq; + for (int m = 0; m < ctx.nozzle_info.extruder_nozzle_list.size(); m++) { + int cur_extruder = (m + start_extruder) % ctx.nozzle_info.extruder_nozzle_list.size(); + for (auto noz_id : ctx.nozzle_info.extruder_nozzle_list.at(cur_extruder)) { + if (nozzle_fil_deq[noz_id].empty()) continue; + if (noz_id == start_nozzle) + used_nozzle_deq.push_front(noz_id); + else + used_nozzle_deq.push_back(noz_id); + } } - { - MemoryedGroup mg(filament_maps, total_cost, prefer_level); - update_memoryed_groups(mg, ctx.group_info.max_gap_threshold, memoryed_groups); + std::vector layer_fil_order; + for (auto noz_id : used_nozzle_deq) { + auto deq = nozzle_fil_deq[noz_id]; + layer_fil_order.reserve(layer_fil_order.size() + deq.size()); + layer_fil_order.insert(layer_fil_order.end(), deq.begin(), deq.end()); + + prev_layer_last_nozzle_id = noz_id; } + + FilamentPlanRes layer_pan{layer_fil_order, layer_fil_nozzle_match}; + res.emplace_back(layer_pan); } - if (cost) - *cost = best_cost; - - std::vector filament_labels(ctx.group_info.total_filament_num, 0); - for (size_t i = 0; i < best_label.size(); ++i) - filament_labels[used_filaments[i]] = best_label[i]; - - - change_memoryed_heaps_to_arrays(memoryed_groups, ctx.group_info.total_filament_num, used_filaments, m_memoryed_groups); - - return filament_labels; + return res; } - // sorted used_filaments - std::vector FilamentGroup::calc_min_flush_group_by_pam2(const std::vector& used_filaments, int* cost, int timeout_ms) + std::vector calc_filament_group_for_manual_multi_nozzle(const std::vector& filament_map_manual, const FilamentGroupContext& ctx) { - std::vectorfilament_labels_ret(ctx.group_info.total_filament_num, ctx.machine_info.master_extruder_id); + FilamentGroupContext new_ctx = ctx; + auto used_filaments = collect_sorted_used_filaments(ctx.model_info.layer_filaments); + for(size_t idx = 0; idx < used_filaments.size(); ++idx) + new_ctx.model_info.unprintable_filaments[1 - filament_map_manual[used_filaments[idx]]].insert(used_filaments[idx]); - std::mapunplaceable_limits; - extract_unprintable_limit_indices(ctx.model_info.unprintable_filaments, used_filaments, unplaceable_limits); - - auto distance_evaluator = std::make_shared(ctx.model_info.flush_matrix[0], used_filaments, ctx.model_info.layer_filaments); - KMediods2 PAM((int)used_filaments.size(), distance_evaluator, ctx.machine_info.master_extruder_id); - PAM.set_max_cluster_size(ctx.machine_info.max_group_size); - PAM.set_unplaceable_limits(unplaceable_limits); - PAM.set_memory_threshold(ctx.group_info.max_gap_threshold); - PAM.do_clustering(ctx.group_info.strategy, timeout_ms); - - std::vectorfilament_labels = PAM.get_cluster_labels(); - - { - auto memoryed_groups = PAM.get_memoryed_groups(); - change_memoryed_heaps_to_arrays(memoryed_groups, ctx.group_info.total_filament_num, used_filaments, m_memoryed_groups); - } - - if (cost) - *cost = reorder_filaments_for_minimum_flush_volume(used_filaments, filament_labels, ctx.model_info.layer_filaments, ctx.model_info.flush_matrix, std::nullopt, nullptr); - - for (int i = 0; i < filament_labels.size(); ++i) - filament_labels_ret[used_filaments[i]] = filament_labels[i]; - return filament_labels_ret; + new_ctx.machine_info.max_group_size.assign(new_ctx.machine_info.max_group_size.size(), std::numeric_limits::max()); + FilamentGroup fg(new_ctx); + return fg.calc_filament_group_for_flush(); } + } diff --git a/src/libslic3r/FilamentGroup.hpp b/src/libslic3r/FilamentGroup.hpp index ce1aedfbb4..828b673b38 100644 --- a/src/libslic3r/FilamentGroup.hpp +++ b/src/libslic3r/FilamentGroup.hpp @@ -13,7 +13,8 @@ const static int DEFAULT_CLUSTER_SIZE = 16; -const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 5; +const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 10; + namespace Slic3r { @@ -52,12 +53,12 @@ namespace Slic3r struct MemoryedGroup { MemoryedGroup() = default; - MemoryedGroup(const std::vector& group_, const int cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {} + MemoryedGroup(const std::vector& group_, const double cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {} bool operator>(const MemoryedGroup& other) const { return prefer_level < other.prefer_level || (prefer_level == other.prefer_level && cost > other.cost); } - int cost{ 0 }; + double cost{ 0 }; int prefer_level{ 0 }; std::vectorgroup; }; @@ -75,6 +76,7 @@ namespace Slic3r std::vector filament_info; std::vector filament_ids; std::vector> unprintable_filaments; + std::map> unprintable_volumes; } model_info; struct GroupInfo { @@ -82,40 +84,64 @@ namespace Slic3r double max_gap_threshold; FGMode mode; FGStrategy strategy; - bool ignore_ext_filament; //wai gua filament + bool ignore_ext_filament; + bool has_filament_switcher = false; + std::vector filament_volume_map; } group_info; struct MachineInfo { std::vector max_group_size; std::vector> machine_filament_info; - std::vector, int>> extruder_group_size; + std::vector prefer_non_model_filament; int master_extruder_id; } machine_info; + + struct SpeedInfo{ + std::unordered_map> filament_print_time; + double extruder_change_time; + double filament_change_time; + bool group_with_time; + MultiNozzleUtils::FilamentChangeTimeParams change_time_params; + std::vector ams_preload_enabled; + } speed_info; + + struct NozzleInfo { + std::map> extruder_nozzle_list; + std::vector nozzle_list; + std::unordered_map nozzle_status; + } nozzle_info; }; - std::vector select_best_group_for_ams(const std::vector>& map_lists, + std::vector select_best_group_for_ams(const std::vector> &filament_to_nozzles, + const std::vector& nozzle_list, const std::vector& used_filaments, const std::vector& used_filament_info, const std::vector>& machine_filament_info, + const bool has_filament_switcher = false, const double color_delta_threshold = 20); - std::vector optimize_group_for_master_extruder(const std::vector& used_filaments, const FilamentGroupContext& ctx, const std::vector& filament_map); - - bool can_swap_groups(const int extruder_id_0, const std::set& group_0, const int extruder_id_1, const std::set& group_1, const FilamentGroupContext& ctx); - - std::vector calc_filament_group_for_tpu(const std::set& tpu_filaments, const int filament_nums, const int master_extruder_id); class FlushDistanceEvaluator { public: - FlushDistanceEvaluator(const FlushMatrix& flush_matrix,const std::vector&used_filaments,const std::vector>& layer_filaments, double p = 0.65); + FlushDistanceEvaluator(const std::vector& flush_matrix,const std::vector&used_filaments,const std::vector>& layer_filaments, double p = 0.65); ~FlushDistanceEvaluator() = default; - double get_distance(int idx_a, int idx_b) const; + double get_distance(int idx_a, int idx_b, int extruder_id) const; private: - std::vector>m_distance_matrix; + std::vector>>m_distance_matrix; }; + + class TimeEvaluator + { + public: + TimeEvaluator(const FilamentGroupContext::SpeedInfo& speed_info) : m_speed_info(speed_info) {} + double get_estimated_time(const std::vector& filament_map) const; + private: + FilamentGroupContext::SpeedInfo m_speed_info; + }; + class FilamentGroup { using MemoryedGroup = FilamentGroupUtils::MemoryedGroup; @@ -129,11 +155,17 @@ namespace Slic3r public: std::vector calc_filament_group_for_match(int* cost = nullptr); std::vector calc_filament_group_for_flush(int* cost = nullptr); - + std::vector calc_filament_group_for_tpu(int* cost = nullptr); private: std::vector calc_min_flush_group(int* cost = nullptr); - std::vector calc_min_flush_group_by_enum(const std::vector& used_filaments, int* cost = nullptr); - std::vector calc_min_flush_group_by_pam2(const std::vector& used_filaments, int* cost = nullptr, int timeout_ms = 300); + + std::vector calc_group_by_enum(int k, const std::vector& used_filaments, + const std::unordered_map>& unplaceable_limits, int* cost = nullptr); + std::vector calc_group_by_kmedoids(int k, const std::vector& used_filaments, + const std::unordered_map>& unplaceable_limits, int* cost = nullptr, int timeout_ms = 500); + + std::map rebuild_unprintables(const std::vector& used_filaments, const std::map& extruder_unprintables); + std::unordered_map> rebuild_nozzle_unprintables(const std::vector& used_filaments, const std::unordered_map>& extruder_unprintables, const std::vector& filament_volume_map); std::unordered_map> try_merge_filaments(); void rebuild_context(const std::unordered_map>& merged_filaments); @@ -141,57 +173,78 @@ namespace Slic3r private: FilamentGroupContext ctx; + MemoryedGroupHeap m_memoryed_heap; std::vector> m_memoryed_groups; - public: std::optional&)>> get_custom_seq; }; - class KMediods2 + std::vector calc_filament_group_for_manual_multi_nozzle(const std::vector& filament_map_manual,const FilamentGroupContext& ctx); + + std::vector calc_filament_group_for_match_multi_nozzle(const FilamentGroupContext& ctx); + + struct FilamentPlanRes { + std::vector fil_order; + std::vector fil_nozzle_match; + }; + + std::vector plan_filament_nozzle_mapping_and_order(const FilamentGroupContext& ctx); + + + class KMediods + { + protected: using MemoryedGroupHeap = FilamentGroupUtils::MemoryedGroupHeap; using MemoryedGroup = FilamentGroupUtils::MemoryedGroup; - - enum INIT_TYPE - { - Random = 0, - Farthest - }; public: - KMediods2(const int elem_count, const std::shared_ptr& evaluator, int default_group_id = 0) : - m_evaluator{ evaluator }, - m_elem_count{ elem_count }, - m_default_group_id{ default_group_id } - { - m_max_cluster_size = std::vector(m_k, DEFAULT_CLUSTER_SIZE); + KMediods(const int k, const int elem_count, const std::shared_ptr& evaluator, int default_group_id = 0) { + m_k = k; + m_evaluator = evaluator; + m_max_cluster_size = std::vector(k, DEFAULT_CLUSTER_SIZE); + m_elem_count = elem_count; + m_default_group_id = default_group_id; } - // set max group size void set_max_cluster_size(const std::vector& group_size) { m_max_cluster_size = group_size; } - // key stores elem idx, value stores the cluster id that elem cnanot be placed - void set_unplaceable_limits(const std::map& placeable_limits) { m_unplaceable_limits = placeable_limits; } + void set_cluster_group_size(const std::vector,int>>& cluster_group_size); - void do_clustering(const FGStrategy& g_strategy,int timeout_ms = 100); + // key stores elem, value stores the cluster id that the elem must be placed + void set_placable_limits(const std::unordered_map>& placable_limits) { m_placeable_limits = placable_limits; } + + // key stores elem, value stores the cluster id that the elem cannot be placed + void set_unplacable_limits(const std::unordered_map>& unplacable_limits) { m_unplaceable_limits = unplacable_limits; } void set_memory_threshold(double threshold) { memory_threshold = threshold; } MemoryedGroupHeap get_memoryed_groups()const { return memoryed_groups; } - std::vectorget_cluster_labels()const { return m_cluster_labels; } + void do_clustering(const FilamentGroupContext& context, int timeout_ms = 100, int retry = 10); + std::vector get_cluster_labels()const { return m_cluster_labels; } - private: - std::vectorcluster_small_data(const std::map& unplaceable_limits, const std::vector& group_size); - std::vectorassign_cluster_label(const std::vector& center, const std::map& unplaceable_limits, const std::vector& group_size, const FGStrategy& strategy); - int calc_cost(const std::vector& labels, const std::vector& medoids); protected: - FilamentGroupUtils::MemoryedGroupHeap memoryed_groups; - std::shared_ptr m_evaluator; - std::mapm_unplaceable_limits; - std::vectorm_cluster_labels; - std::vectorm_max_cluster_size; + bool have_enough_size(const std::vector& cluster_size, const std::vector, int>>& cluster_group_size,int elem_count); + // calculate cluster distance + int calc_cost(const std::vector& clusters, const std::vector& cluster_centers, int cluster_id = -1); - const int m_k = 2; + // get initial cluster center + std::vectorinit_cluster_center(const std::unordered_map>& placeable_limits, const std::unordered_map>& unplaceable_limits, const std::vector& cluster_size, const std::vector, int>>& cluster_group_size, int seed); + // assign each elem to the cluster + std::vector assign_cluster_label(const std::vector& center, const std::unordered_map>& placeable_limits, const std::unordered_map>& unplaceable_limits, const std::vector& group_size, const std::vector, int>>& cluster_group_size); + + protected: + MemoryedGroupHeap memoryed_groups; + std::shared_ptrm_evaluator; + std::unordered_map> m_unplaceable_limits; // key: filament, value: nozzle ids it cannot be assigned to + std::unordered_map> m_placeable_limits; // key: filament, value: nozzle ids it must be assigned to + std::vectorm_max_cluster_size; // max number of filaments each nozzle can hold + std::vectorm_cluster_labels; // assignment result, resolved down to nozzle id + std::vector,int>> m_cluster_group_size; + std::vector m_nozzle_to_extruder; + + + int m_k; int m_elem_count; int m_default_group_id{ 0 }; double memory_threshold{ 0 }; diff --git a/src/libslic3r/FilamentGroupUtils.cpp b/src/libslic3r/FilamentGroupUtils.cpp index 1b16a1337f..13c9603e0a 100644 --- a/src/libslic3r/FilamentGroupUtils.cpp +++ b/src/libslic3r/FilamentGroupUtils.cpp @@ -274,5 +274,66 @@ namespace FilamentGroupUtils } return true; } + + int get_estimate_extruder_change_count(const std::vector> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info) + { + int ret = 0; + for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) { + int extruder_count = extruder_nozzle_info.get_used_extruders(layer_id).size(); + ret += (extruder_count - 1); + } + return ret; + } + + int get_estimate_nozzle_change_count(const std::vector> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info) + { + int ret = 0; + for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) { + auto extruder_list = extruder_nozzle_info.get_used_extruders(layer_id); + for (auto extruder_id : extruder_list) { + int nozzle_count = extruder_nozzle_info.get_used_nozzles_in_extruder(extruder_id, layer_id).size(); + if (nozzle_count > 1) ret += (nozzle_count - 1); + } + } + return ret; + } + + std::pair get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info) + { + std::pair ret{0,0}; + int layer_nums = extruder_nozzle_info.get_layer_filament_sequences().size(); + for (int layer_id = 0; layer_id < layer_nums; layer_id++) { + std::vector extruders = extruder_nozzle_info.get_used_extruders(layer_id); + ret.first = extruders.size() - 1; + + for (auto ext_id : extruders) { + int nozzles = extruder_nozzle_info.get_used_nozzles_in_extruder(ext_id, layer_id).size(); + ret.second += nozzles; + } + ret.second = std::max(0, ret.second - ret.first); + } + return ret; + } + + std::map> build_extruder_nozzle_list(const std::vector& nozzle_list) + { + std::map> ret; + for (auto& nozzle : nozzle_list) { + ret[nozzle.extruder_id].emplace_back(nozzle.group_id); + } + + for (auto& elem : ret) + std::sort(elem.second.begin(), elem.second.end()); + return ret; + } + + std::vector update_used_filament_values(const std::vector& old_values, const std::vector& new_values, const std::vector& used_filaments) + { + std::vector res = old_values; + for (size_t i = 0; i < used_filaments.size(); ++i) { + res[used_filaments[i]] = new_values[used_filaments[i]]; + } + return res; + } } } \ No newline at end of file diff --git a/src/libslic3r/FilamentGroupUtils.hpp b/src/libslic3r/FilamentGroupUtils.hpp index 71839c2be6..f0865a93c6 100644 --- a/src/libslic3r/FilamentGroupUtils.hpp +++ b/src/libslic3r/FilamentGroupUtils.hpp @@ -7,6 +7,7 @@ #include #include "PrintConfig.hpp" +#include "MultiNozzleUtils.hpp" namespace Slic3r { @@ -31,6 +32,10 @@ namespace Slic3r Color color; std::string type; bool is_support; + // How this filament is used across the model. Orca's shipping grouping + // algorithm does not read it yet; defaulted so a default-built FilamentInfo + // is deterministic. The nozzle-centric engine consumes it later. + FilamentUsageType usage_type = FilamentUsageType::ModelOnly; }; struct MachineFilamentInfo: public FilamentInfo { @@ -80,6 +85,20 @@ namespace Slic3r void extract_unprintable_limit_indices(const std::vector>& unprintable_elems, const std::vector& used_filaments, std::unordered_map>& unplaceable_limits); bool check_printable(const std::vector>& groups, const std::map& unprintable); + + // Nozzle-centric grouping helpers. The estimate helpers read a LayeredNozzleGroupResult's + // per-layer extruder/nozzle usage; the two builders support building the grouping context + // (extruder->nozzle inventory) and writing back a resolved map onto only the used-filament + // slots. + int get_estimate_extruder_change_count(const std::vector>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info); + + int get_estimate_nozzle_change_count(const std::vector>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info); + + std::pair get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info); + + std::map> build_extruder_nozzle_list(const std::vector& nozzle_list); + + std::vector update_used_filament_values(const std::vector& old_values, const std::vector& new_values, const std::vector& used_filaments); } diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index fd395135ae..e7fda3ac4a 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -347,6 +347,7 @@ static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_laye static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; +static constexpr const char* FILAMENT_VOL_MAP_ATTR = "filament_volume_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; static constexpr const char* GCODE_FILE_ATTR = "gcode_file"; static constexpr const char* THUMBNAIL_FILE_ATTR = "thumbnail_file"; @@ -699,6 +700,43 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) info.id = it->first; info.used_g = used_filament_g; info.used_m = used_filament_m; + + // Stamp each filament's logical-nozzle assignment onto the saved 3mf so the + // device/monitor can reconstruct it. NOTE: this block runs for the whole fleet, not just + // multi-nozzle prints: reorder_extruders_for_minimum_flush_volume runs unconditionally and + // set_nozzle_group_result stores a (non-null) 1-nozzle result even for a single-extruder print, + // so result->nozzle_group_result is non-null here for X1/P1/A1/H2S too. Saved-3mf byte-identity + // vs the older filament_maps path therefore holds by VALUE-COINCIDENCE, not by skipping: for a + // standard 0.4/0.2/0.6/0.8 nozzle the stamped group_id/nozzle_diameter/volume_type render the same + // bytes as the filament_maps-derived fallback below (empirically confirmed on an X1C 0.4 saved 3mf: + // slice_info.config emits group_id="0" nozzle_diameter="0.40" volume_type="Standard" and + // == the else-path). + // The one divergence is a non-standard nozzle (e.g. 0.3/0.5): format_diameter_to_str snaps the + // stamped diameter to the nearest of {0.2,0.4,0.6,0.8} (0.5 -> "0.4"), whereas the older path wrote + // the raw config value — a metadata-only carry. No g-code effect; the raw nozzle_diameters plate + // metadata is unchanged. + if (result && result->nozzle_group_result) { + auto nozzles_for_filament = result->nozzle_group_result->get_nozzles_for_filament(it->first); + if (!nozzles_for_filament.empty()) { + info.group_id.reserve(nozzles_for_filament.size()); + std::set diameters; + std::set volume_types; + for (const auto& nozzle : nozzles_for_filament) { + info.group_id.emplace_back(nozzle.group_id); + diameters.insert(string_to_double_decimal_point(nozzle.diameter)); + volume_types.insert(nozzle.volume_type); + } + std::sort(info.group_id.begin(), info.group_id.end()); + info.group_id.erase(std::unique(info.group_id.begin(), info.group_id.end()), info.group_id.end()); + if (!diameters.empty()) + info.nozzle_diameter = *diameters.begin(); + if (volume_types.size() > 1) + info.nozzle_volume_type = get_nozzle_volume_type_string(nvtHybrid); + else if (!volume_types.empty()) + info.nozzle_volume_type = get_nozzle_volume_type_string(*volume_types.begin()); + } + } + auto model_volume_it = ps.model_volumes_per_extruder.find(it->first); auto support_volume_it = ps.support_volumes_per_extruder.find(it->first); info.used_for_object = model_volume_it != ps.model_volumes_per_extruder.end() && model_volume_it->second > EPSILON; @@ -706,6 +744,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) slice_filaments_info.push_back(info); } + // Carry the layer-aware grouping result into the plate so the 3mf writer can emit the tags + // and the enable_filament_dynamic_map flag. Only a LayeredNozzleGroupResult (the slicer output) is + // stored; a device-side StaticNozzleGroupResult loaded from a 3mf is not re-serialized here. + auto layered_group_result = std::dynamic_pointer_cast(result->nozzle_group_result); + if (layered_group_result) + nozzle_group_result = *layered_group_result; + /* only for test GCodeProcessorResult::SliceWarning sw; sw.msg = BED_TEMP_TOO_HIGH_THAN_FILAMENT; @@ -1283,6 +1328,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes); bool _handle_end_config_warning(); + bool _handle_start_config_nozzle(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_nozzle(); + //BBS: add plater config parse functions bool _handle_start_config_plater(const char** attributes, unsigned int num_attributes); bool _handle_end_config_plater(); @@ -1618,8 +1666,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) plate->is_label_object_enabled = it->second->is_label_object_enabled; plate->skipped_objects = it->second->skipped_objects; plate->slice_filaments_info = it->second->slice_filaments_info; + plate->nozzles_info = it->second->nozzles_info; plate->printer_model_id = it->second->printer_model_id; plate->nozzle_diameters = it->second->nozzle_diameters; + plate->nozzle_volume_types = it->second->nozzle_volume_types; plate->filament_maps = it->second->filament_maps; plate->filament_change_sequence = it->second->filament_change_sequence; plate->nozzle_change_sequence = it->second->nozzle_change_sequence; @@ -2289,9 +2339,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) plate_data_list[it->first-1]->is_support_used = it->second->is_support_used; plate_data_list[it->first-1]->is_label_object_enabled = it->second->is_label_object_enabled; plate_data_list[it->first-1]->slice_filaments_info = it->second->slice_filaments_info; + plate_data_list[it->first-1]->nozzles_info = it->second->nozzles_info; plate_data_list[it->first-1]->skipped_objects = it->second->skipped_objects; plate_data_list[it->first-1]->printer_model_id = it->second->printer_model_id; plate_data_list[it->first-1]->nozzle_diameters = it->second->nozzle_diameters; + plate_data_list[it->first-1]->nozzle_volume_types = it->second->nozzle_volume_types; plate_data_list[it->first-1]->filament_maps = it->second->filament_maps; plate_data_list[it->first-1]->filament_change_sequence = it->second->filament_change_sequence; plate_data_list[it->first-1]->nozzle_change_sequence = it->second->nozzle_change_sequence; @@ -3469,6 +3521,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_start_config_filament(attributes, num_attributes); else if (::strcmp(SLICE_WARNING_TAG, name) == 0) res = _handle_start_config_warning(attributes, num_attributes); + else if (::strcmp(NOZZLE_TAG, name) == 0) + res = _handle_start_config_nozzle(attributes, num_attributes); else if (::strcmp(ASSEMBLE_TAG, name) == 0) res = _handle_start_assemble(attributes, num_attributes); else if (::strcmp(ASSEMBLE_ITEM_TAG, name) == 0) @@ -3503,6 +3557,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) res = _handle_end_config_plater(); else if (::strcmp(FILAMENT_TAG, name) == 0) res = _handle_end_config_filament(); + else if (::strcmp(NOZZLE_TAG, name) == 0) + res = _handle_end_config_nozzle(); else if (::strcmp(INSTANCE_TAG, name) == 0) res = _handle_end_config_plater_instance(); else if (::strcmp(ASSEMBLE_TAG, name) == 0) @@ -4460,6 +4516,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_curr_plater->config.set_key_value("filament_map", new ConfigOptionInts(filament_map)); } } + else if (key == FILAMENT_VOL_MAP_ATTR) { + if (m_curr_plater){ + auto filament_volume_map = get_vector_from_string(value); + for (size_t idx = 0; idx < filament_volume_map.size(); ++idx) { + // Only Standard(0)/High Flow(1) are currently supported; clamp any higher + // volume-type (TPU High Flow/Hybrid) back to Standard until they are wired in. + if (filament_volume_map[idx] > 1) { + filament_volume_map[idx] = 0; + } + } + m_curr_plater->config.set_key_value("filament_volume_map", new ConfigOptionInts(filament_volume_map)); + } + } else if (key == GCODE_FILE_ATTR) { m_curr_plater->gcode_file = value; @@ -4569,6 +4638,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (m_curr_plater) m_curr_plater->printer_model_id = value; } + else if (key == NOZZLE_VOLUME_TYPE_ATTR) + { + if (m_curr_plater) + m_curr_plater->nozzle_volume_types = value; + } else if (key == NOZZLE_DIAMETERS_ATTR) { if (m_curr_plater) @@ -4622,6 +4696,41 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) return true; } + bool _BBS_3MF_Importer::_handle_start_config_nozzle(const char** attributes, unsigned int num_attributes) + { + // Read the per-plate tags. Older 3mf without tags leave nozzles_info + // empty; load_nozzle_infos_with_compatibility then rebuilds the list from the per-filament + // group_id / filament_map on the device side. + if (m_curr_plater) { + // id="0" extruder_id="1" nozzle_diameter="0.4" volume_type="Standard" + std::string id = bbs_get_attribute_value_string(attributes, num_attributes, "id"); + std::string extruder_id = bbs_get_attribute_value_string(attributes, num_attributes, "extruder_id"); + std::string nozzle_diameter= bbs_get_attribute_value_string(attributes, num_attributes, "nozzle_diameter"); + std::string volume_type = bbs_get_attribute_value_string(attributes, num_attributes, "volume_type"); + + auto volume_type_str_to_enum = ConfigOptionEnum::get_enum_values(); + + MultiNozzleUtils::NozzleInfo nozzle_info; + nozzle_info.group_id = atoi(id.c_str()); + nozzle_info.extruder_id = atoi(extruder_id.c_str()) - 1; + nozzle_info.diameter = nozzle_diameter; + + if (volume_type_str_to_enum.count(volume_type)) + nozzle_info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(volume_type)); + else + nozzle_info.volume_type = NozzleVolumeType::nvtStandard; + + m_curr_plater->nozzles_info.push_back(nozzle_info); + } + return true; + } + + bool _BBS_3MF_Importer::_handle_end_config_nozzle() + { + // do nothing + return true; + } + bool _BBS_3MF_Importer::_handle_start_config_warning(const char** attributes, unsigned int num_attributes) { if (m_curr_plater) { @@ -7980,6 +8089,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << "\"/>\n"; } + ConfigOptionInts* filament_volume_maps_opt = plate_data->config.option("filament_volume_map"); + if (filament_map_mode_opt != nullptr && filament_volume_maps_opt != nullptr) { + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << FILAMENT_VOL_MAP_ATTR << "\" " << VALUE_ATTR << "=\""; + const std::vector& volume_values = filament_volume_maps_opt->values; + for (int i = 0; i < volume_values.size(); ++i) { + stream << volume_values[i]; + if (i != (volume_values.size() - 1)) + stream << " "; + } + stream << "\"/>\n"; + } + if (save_gcode) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << GCODE_FILE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << xml_escape(plate_data->gcode_file) << "\"/>\n"; if (!plate_data->gcode_file.empty()) { @@ -8119,7 +8240,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) [](unsigned int filament_id) { return filament_id + 1; }); const std::string plate_key = "plate_" + std::to_string(idx + 1); - sequence_json[plate_key]["sequence"] = filament_sequence; + // Dynamic-map plates write the sequence under "filament_sequence"; every other plate (the + // whole shipping fleet + H2C static mode) keeps the "sequence" key, so the saved 3mf is + // byte-identical to the older format. The reader accepts both. + const bool enable_dynamic_map = plate_data->nozzle_group_result && plate_data->nozzle_group_result->is_support_dynamic_nozzle_map(); + const std::string seq_key = enable_dynamic_map ? "filament_sequence" : "sequence"; + sequence_json[plate_key][seq_key] = filament_sequence; sequence_json[plate_key]["nozzle_sequence"] = plate_data->nozzle_change_sequence; sequence_json[plate_key]["optimal_assignment"] = plate_data->optimal_assignment; } @@ -8214,7 +8340,15 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << OUTSIDE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->toolpath_outside << "\"/>\n"; stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SUPPORT_USED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_support_used << "\"/>\n"; stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << LABEL_OBJECT_ENABLED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_label_object_enabled << "\"/>\n"; - stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n"; + // Report the plate's dynamic-map state from the grouping result. The result is present + // for the whole fleet (a static single-nozzle result too), so this if-branch is normally + // taken; is_support_dynamic_nozzle_map() is false for any non-dynamic (static / + // single-extruder) result ⇒ byte-identical to the previously hard-coded value. The else + // is a defensive fallback for a missing result. + if (plate_data && plate_data->nozzle_group_result) + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << plate_data->nozzle_group_result->is_support_dynamic_nozzle_map() << "\"/>\n"; + else + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n"; { bool has_filament_switcher = config.has("has_filament_switcher") ? config.opt_bool("has_filament_switcher") : false; stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << HAS_FILAMENT_SWITCHER_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << has_filament_switcher << "\"/>\n"; @@ -8349,12 +8483,27 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n"; } - for (int nozzle_group_id : used_nozzle_groups) { - stream << " <" << NOZZLE_TAG << " " - << "id=\"" << nozzle_group_id << "\" " - << "extruder_id=\"" << nozzle_group_id + 1 << "\" " - << "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" " - << "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n"; + // Emit the tags from the grouping result when present. The result is present for + // the whole fleet (see the value-coincidence note in parse_filament_info), so this if-branch + // is normally taken; for single-nozzle-per-extruder prints get_used_nozzles_in_extruder() + // yields one nozzle per used extruder whose serialize() output is byte-identical to the + // filament_maps-derived else-path below for a standard nozzle diameter (a non-standard + // diameter snaps via format_diameter_to_str — the metadata-only carry). Only genuinely + // multi-nozzle prints emit per-nozzle tags the extruder map cannot express. The else + // is a defensive fallback for a missing result. + if (plate_data->nozzle_group_result) { + auto used_nozzle_list = plate_data->nozzle_group_result->get_used_nozzles_in_extruder(); + for (auto& used_nozzle : used_nozzle_list) { + stream << " <" << NOZZLE_TAG << " " << used_nozzle.serialize() << "/>\n"; + } + } else { + for (int nozzle_group_id : used_nozzle_groups) { + stream << " <" << NOZZLE_TAG << " " + << "id=\"" << nozzle_group_id << "\" " + << "extruder_id=\"" << nozzle_group_id + 1 << "\" " + << "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" " + << "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n"; + } } if (!plate_data->layer_filaments.empty()) { diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index c16c423f66..9c697a14fc 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -73,6 +73,7 @@ struct PlateData std::map> obj_inst_map; std::string printer_model_id; std::string nozzle_diameters; + std::string nozzle_volume_types; std::string gcode_file; std::string gcode_file_md5; std::string thumbnail_file; @@ -102,6 +103,13 @@ struct PlateData std::vector nozzle_change_sequence; std::vector optimal_assignment; + // Multi-nozzle grouping surface. nozzles_info accumulates the tags read from a + // gcode.3mf; nozzle_group_result is the slicer's per-filament→nozzle assignment carried into the + // saved 3mf metadata (write) and reconstructed on load. Both are empty/nullopt for single-nozzle + // prints, so the saved-3mf output for single-nozzle printers is byte-identical. + std::vector nozzles_info; + std::optional nozzle_group_result; + // Hexadecimal number, // the 0th digit corresponds to extruder 1 // the 1th digit corresponds to extruder 2 diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 0b62fd0d6f..8ad1126096 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -99,11 +99,131 @@ static bool is_bambu_x2d_printer(const FullPrintConfig &config) return config.printer_model.value == "Bambu Lab X2D"; } +// Multi-nozzle printer predicate: an extruder carries a nozzle cluster (extruder_max_nozzle_count +// entry > 1). Today only H2C profiles trip it, so every existing single- and dual-extruder printer +// is excluded and keeps its historic placeholder values. +static bool is_multi_nozzle_printer(const FullPrintConfig &config) +{ + return std::any_of(config.extruder_max_nozzle_count.values.begin(), + config.extruder_max_nozzle_count.values.end(), + [](int v) { return v > 1; }); +} + static int hotend_id_for_gcode_placeholder(const FullPrintConfig &config, int hotend_id) { return is_bambu_x2d_printer(config) ? -1 : hotend_id; } +// current_hotend / next_hotend value. For multi-nozzle printers a dynamic nozzle map yields the real +// nozzle id, a static map yields -1: +// - multi-nozzle (H2C): dynamic nozzle map -> real nozzle id; static -> -1. +// The dynamic branch is dormant today: the selector create() overload that sets the flag has no +// callers yet (deferred with the nozzle-assignment pipeline), so H2C currently resolves to -1. +// - X2D: keeps its historic -1 (single-nozzle -> falls through to the fallback helper). +// - every other (existing single-nozzle) printer: keeps its historic extruder-id value, so +// existing g-code stays byte-identical. +// group_result may be null on slicing paths that don't populate it -> the dynamic branch is simply +// skipped, so we never dereference null. +static int hotend_id_for_gcode_placeholder(const FullPrintConfig &config, + const std::shared_ptr &group_result, + int filament_id, + int extruder_id, + int layer_id = -1) +{ + if (is_multi_nozzle_printer(config)) { + if (group_result && group_result->is_support_dynamic_nozzle_map() && filament_id >= 0) + return group_result->get_nozzle_id(filament_id, layer_id); + return -1; + } + return hotend_id_for_gcode_placeholder(config, extruder_id); +} + +// Logical nozzle id for the *_nozzle_id placeholders. Null-safe: falls back to the +// extruder id (single-nozzle equivalent) so existing printers are unaffected and we never crash. +static int nozzle_id_for_gcode_placeholder(const std::shared_ptr &group_result, + int filament_id, int extruder_id, int layer_id = -1) +{ + if (group_result && filament_id >= 0) + return group_result->get_nozzle_id(filament_id, layer_id); + return extruder_id; +} + +// Init variants: the start-gcode init sites (first_non_support_hotend / initial_no_support_hotend / +// current_hotend / initial_nozzle_id / filament_start current_nozzle_id) use get_first_nozzle_for_filament +// (the nozzle a filament FIRST uses) rather than the layer-based get_nozzle_id. Same hotend-value semantics +// as hotend_id_for_gcode_placeholder above (multi-nozzle static -> -1; dynamic branch dormant; +// existing printers -> extruder id; X2D -> -1); they differ from the layer-based helper only on the dormant +// dynamic path for a filament first used after layer 0. +static int first_hotend_id_for_gcode_placeholder(const FullPrintConfig &config, + const std::shared_ptr &group_result, + int filament_id, + int extruder_id) +{ + if (is_multi_nozzle_printer(config)) { + if (group_result && group_result->is_support_dynamic_nozzle_map() && filament_id >= 0) { + auto nozzle = group_result->get_first_nozzle_for_filament(filament_id); + if (nozzle) + return nozzle->group_id; + } + return -1; + } + return hotend_id_for_gcode_placeholder(config, extruder_id); +} + +static int first_nozzle_id_for_gcode_placeholder(const std::shared_ptr &group_result, + int filament_id, int extruder_id) +{ + if (group_result && filament_id >= 0) { + auto nozzle = group_result->get_first_nozzle_for_filament(filament_id); + if (nozzle) + return nozzle->group_id; + } + return extruder_id; +} + +// Nozzle diameters indexed by logical nozzle id, for the nozzle_diameter_at_nozzle_id[] +// placeholder. Empty when there is no group result. +static std::vector get_nozzle_diameters_by_nozzle_id(const MultiNozzleUtils::NozzleGroupResultBase *group_result) +{ + std::vector diameters; + if (!group_result) + return diameters; + for (int id = 0;; ++id) { + auto nozzle = group_result->get_nozzle_from_id(id); + if (!nozzle) + break; + diameters.push_back(std::stod(nozzle->diameter)); + } + return diameters; +} + +// Nozzle volume-type strings indexed by logical nozzle id, for the nozzle_volume_types[] +// placeholder. Empty when there is no group result. +static std::vector get_nozzle_volume_types_by_nozzle_id(const MultiNozzleUtils::NozzleGroupResultBase *group_result) +{ + std::vector volume_types; + if (!group_result) + return volume_types; + + int max_nozzle_id = -1; + for (unsigned int filament_id : group_result->get_used_filaments()) { + for (const auto &nozzle : group_result->get_nozzles_for_filament(filament_id)) { + if (nozzle.group_id > max_nozzle_id) + max_nozzle_id = nozzle.group_id; + } + } + if (max_nozzle_id < 0) + max_nozzle_id = 0; + + volume_types.resize(max_nozzle_id + 1, get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard)); + for (int id = 0; id <= max_nozzle_id; ++id) { + auto nozzle = group_result->get_nozzle_from_id(id); + if (nozzle) + volume_types[id] = get_nozzle_volume_type_string(nozzle->volume_type); + } + return volume_types; +} + Vec2d travel_point_1; Vec2d travel_point_2; Vec2d travel_point_3; @@ -716,6 +836,9 @@ static std::vector get_path_of_change_filament(const Print& print) int new_extruder_id = get_extruder_index(*m_print_config, new_filament_id); + // Logical nozzle grouping for this print (null on paths that don't populate it). + auto group_result = gcodegen.m_print->get_layered_nozzle_group_result(); + bool is_nozzle_change = !tcr.nozzle_change_result.gcode.empty() && (gcodegen.config().nozzle_diameter.size() > 1); std::string gcode; @@ -780,6 +903,10 @@ static std::vector get_path_of_change_filament(const Print& print) const std::string& filament_end_gcode = gcodegen.config().filament_end_gcode.get_at(old_filament_id); if (gcodegen.writer().filament() != nullptr && !filament_end_gcode.empty()) { DynamicConfig config; + config.set_key_value("current_filament_id", new ConfigOptionInt((int) old_filament_id)); + config.set_key_value("current_nozzle_id", new ConfigOptionInt(nozzle_id_for_gcode_placeholder(group_result, (int) old_filament_id, (int) gcodegen.writer().filament()->extruder_id(), m_layer_idx))); + config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z)); if (!gcodegen.m_filament_instances_code.empty()) { @@ -843,13 +970,24 @@ static std::vector get_path_of_change_filament(const Print& print) DynamicConfig config; int old_filament_id = gcodegen.writer().filament() ? (int)gcodegen.writer().filament()->id() : -1; int old_extruder_id = gcodegen.writer().filament() ? (int)gcodegen.writer().filament()->extruder_id() : -1; + // Logical nozzle ids for old/new filament (null-safe -> extruder id). + int old_nozzle_id = nozzle_id_for_gcode_placeholder(group_result, old_filament_id, old_extruder_id, m_layer_idx); + int next_nozzle_id = nozzle_id_for_gcode_placeholder(group_result, new_filament_id, new_extruder_id, m_layer_idx); config.set_key_value("previous_extruder", new ConfigOptionInt(old_filament_id)); config.set_key_value("next_extruder", new ConfigOptionInt(new_filament_id)); - config.set_key_value("current_hotend", new ConfigOptionInt(old_extruder_id >= 0 ? - hotend_id_for_gcode_placeholder(gcodegen.m_config, old_extruder_id) : -1)); - config.set_key_value("next_hotend", - new ConfigOptionInt(hotend_id_for_gcode_placeholder(gcodegen.m_config, (int) gcodegen.get_extruder_id(new_filament_id)))); + // current_hotend/next_hotend (see hotend_id_for_gcode_placeholder): multi-nozzle H2C -> -1 + // (static; dynamic branch dormant), X2D -> -1, existing printers -> extruder id. + config.set_key_value("current_hotend", new ConfigOptionInt( + hotend_id_for_gcode_placeholder(gcodegen.m_config, group_result, old_filament_id, old_extruder_id, m_layer_idx))); + config.set_key_value("next_hotend", new ConfigOptionInt( + hotend_id_for_gcode_placeholder(gcodegen.m_config, group_result, new_filament_id, (int) gcodegen.get_extruder_id(new_filament_id), m_layer_idx))); + config.set_key_value("current_nozzle_id", new ConfigOptionInt(old_nozzle_id)); + config.set_key_value("next_nozzle_id", new ConfigOptionInt(next_nozzle_id)); + config.set_key_value("current_filament_id", new ConfigOptionInt(old_filament_id)); + config.set_key_value("next_filament_id", new ConfigOptionInt(new_filament_id)); + config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); config.set_key_value("layer_num", new ConfigOptionInt(gcodegen.m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(tcr.print_z)); config.set_key_value("toolchange_z", new ConfigOptionFloat(z)); @@ -887,6 +1025,22 @@ static std::vector get_path_of_change_filament(const Print& print) wipe_avoid_pos_x = get_wipe_avoid_pos_x(box_min, box_max, 3.f); } + // Nozzle-heating center just outside the wipe tower. Clamp X to the + // region every extruder can reach (shared printable polygon), which equals the full + // printable_area for all current single/dual printers, so existing output is unchanged. + Vec2f stop_pos = tool_change_start_pos; + { + BoundingBoxf bbx = m_wipe_tower_bbx; + bbx.translate((m_wipe_tower_pos + m_rib_offset).cast()); + stop_pos.x() += (stop_pos.x() < bbx.center().x()) ? -2.f : 2.f; + auto printer_bbx = unscaled(get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon())); + if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = float(printer_bbx.min[0]); + if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = float(printer_bbx.max[0]); + } + config.set_key_value("wipe_tower_center_pos_x", new ConfigOptionFloat(stop_pos.x())); + config.set_key_value("wipe_tower_center_pos_y", new ConfigOptionFloat(stop_pos.y())); + config.set_key_value("wipe_tower_center_pos_valid", new ConfigOptionBool(true)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(gcodegen.m_max_layer_z)); config.set_key_value("relative_e_axis", new ConfigOptionBool(full_config.use_relative_e_distances)); config.set_key_value("toolchange_count", new ConfigOptionInt((int) gcodegen.m_toolchange_count)); @@ -895,8 +1049,14 @@ static std::vector get_path_of_change_filament(const Print& print) config.set_key_value("fan_speed", new ConfigOptionInt((int) 0)); config.set_key_value("old_retract_length", new ConfigOptionFloat(old_retract_length)); config.set_key_value("new_retract_length", new ConfigOptionFloat(new_retract_length)); + // Expose the old filament's nozzle-change retract length (filament_retract_length_nc; nil/-1 -> 0). + config.set_key_value("filament_retract_length_nc", new ConfigOptionFloat( + (old_filament_id != -1) ? (float) full_config.filament_retract_length_nc.get_at(old_filament_id) : 0.f)); config.set_key_value("old_retract_length_toolchange", new ConfigOptionFloat(old_retract_length_toolchange)); config.set_key_value("new_retract_length_toolchange", new ConfigOptionFloat(new_retract_length_toolchange)); + // Current parked-retract length of the incoming filament's extruder. + config.set_key_value("new_extruder_retracted_length", + new ConfigOptionFloat(gcode_writer.get_extruder_retracted_length((int) new_filament_id))); config.set_key_value("old_filament_temp", new ConfigOptionInt(old_filament_temp)); int interface_temp = full_config.filament_tower_interface_print_temp.get_at(new_filament_id); if (interface_temp == -1) @@ -930,7 +1090,8 @@ static std::vector get_path_of_change_filament(const Print& print) config.set_key_value("travel_point_3_y", new ConfigOptionFloat(float(travel_point_3.y()))); auto flush_v_speed = m_print_config->filament_flush_volumetric_speed.values; - auto flush_temps = m_print_config->filament_flush_temp.values; + // Fast purge mode uses filament_flush_temp_fast; Default is inert. + auto flush_temps = (m_print_config->prime_volume_mode == PrimeVolumeMode::pvmFast ? m_print_config->filament_flush_temp_fast : m_print_config->filament_flush_temp).values; auto filament_cooling_before_tower = m_print_config->filament_cooling_before_tower.values; for (size_t idx = 0; idx < flush_v_speed.size(); ++idx) { if (flush_v_speed[idx] == 0) @@ -1015,10 +1176,20 @@ static std::vector get_path_of_change_filament(const Print& print) BoundingBox avoid_bbx, printer_bbx; { // set printer_bbx - Pointfs bed_pointsf = gcodegen.m_config.printable_area.values; - Points bed_points; - for (auto p : bed_pointsf) { bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); } - printer_bbx = BoundingBox(bed_points); + // Multi-nozzle: clamp the avoid-perimeter travel bounds to the region every + // extruder can reach (get_extruder_shared_printable_polygon) instead of the full + // bed. Gated on the multi-nozzle predicate so H2D and every existing single/dual + // printer keep the historic full-printable_area routing byte-identical. + if (is_multi_nozzle_printer(gcodegen.m_config)) { + printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon()); + printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.min) + plate_origin_2d); + printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled(printer_bbx.max) + plate_origin_2d); + } else { + Pointfs bed_pointsf = gcodegen.m_config.printable_area.values; + Points bed_points; + for (auto p : bed_pointsf) { bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast() + plate_origin_2d)); } + printer_bbx = BoundingBox(bed_points); + } } { // set avoid_bbx @@ -1049,7 +1220,21 @@ static std::vector get_path_of_change_filament(const Print& print) } // do unretract after setting current extruder_id - std::string toolchange_unretract_str = gcodegen.unretract(); + // PETG filaments on a device with a filament switcher get a small (2 mm) pre-extrusion + // before the tool change. has_filament_switcher is a develop-only key read defensively from the + // full config (Orca does not carry it as a static PrintConfig member — same convention as + // enable_filament_dynamic_map); no shipping profile sets it (grep resources/profiles = 0), so + // is_petg_pre_extrusion is always false -> extra_unretract stays 0 -> byte-identical to the plain + // unretract() fleet-wide. The tower-interface contact pre-extrusion length (the + // is_contact_pre_extrusion branch) is NOT applied here; it is only computed as the guard used to + // give the contact path priority over PETG. + const ConfigOptionBool* has_filament_switcher_opt = gcodegen.m_print->full_print_config().option("has_filament_switcher"); + bool is_contact_pre_extrusion = tcr.is_contact && gcodegen.m_config.enable_tower_interface_features; + bool is_petg_pre_extrusion = !is_contact_pre_extrusion + && gcodegen.config().filament_type.get_at(tcr.new_tool) == "PETG" + && has_filament_switcher_opt && has_filament_switcher_opt->value; + float extra_unretract = is_petg_pre_extrusion ? 2.f : 0.f; + std::string toolchange_unretract_str = (extra_unretract > 0.f) ? gcodegen.unretract(extra_unretract) : gcodegen.unretract(); check_add_eol(toolchange_unretract_str); gcodegen.placeholder_parser().set("current_extruder", new_filament_id); @@ -1065,6 +1250,10 @@ static std::vector get_path_of_change_filament(const Print& print) // Process the filament_start_gcode for the active filament only. DynamicConfig config; config.set_key_value("filament_extruder_id", new ConfigOptionInt(new_filament_id)); + config.set_key_value("current_filament_id", new ConfigOptionInt(new_filament_id)); + config.set_key_value("current_nozzle_id", new ConfigOptionInt(nozzle_id_for_gcode_placeholder(group_result, new_filament_id, new_extruder_id, m_layer_idx))); + config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); start_filament_gcode_str = gcodegen.placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config); if (add_change_filament_624) { start_filament_gcode_str += "M625\n"; @@ -2172,9 +2361,31 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu extruder_unprintable_polys, m_print->get_extruder_printable_height(), m_print->get_filament_maps(), m_print->get_physical_unprintable_filaments(m_print->get_slice_used_filaments(false))); + // Hand the per-filament nozzle grouping to the processor BEFORE finalize, so the + // pre-heat injector's second pass can resolve filament->nozzle->extruder (get_nozzle_from_id / + // is_support_dynamic_nozzle_map). Print::_do_export re-assigns it onto the extracted result afterwards + // (Print.cpp) for the device GUI, but that is too late for the in-finalize injector. Null for + // single-nozzle prints, where the injector is gated off anyway (enable_pre_heating false). + m_processor.result().nozzle_group_result = m_print->get_layered_nozzle_group_result(); + m_processor.finalize(true); // DoExport::update_print_estimated_times_stats(m_processor, print->m_print_statistics); DoExport::update_print_estimated_stats(m_processor, m_writer.extruders(), print->m_print_statistics, print->config()); + // Printed-mass safety check. Flushed filament leaves the bed, so subtract it + // from the total to get the mass actually resting on the plate. Gated on machine_max_printed_mass + // (>0 only for A2L machines), so no existing printer's gcode_check_result changes. + if (m_print->config().machine_max_printed_mass.value > EPSILON) { + double mass_on_bed_total = print->m_print_statistics.total_weight; + for (auto volume : m_processor.get_result().print_statistics.flush_per_filament) { + size_t extruder_id = volume.first; + auto extruder = std::find_if(m_writer.extruders().begin(), m_writer.extruders().end(), [extruder_id](const Extruder &extr) { return extr.id() == extruder_id; }); + if (extruder == m_writer.extruders().end()) continue; + mass_on_bed_total -= (volume.second * extruder->filament_density() * 0.001); + } // flushed weight will not be keeped on the hot bed, exclude it + if (mass_on_bed_total > m_print->config().machine_max_printed_mass.value) { + m_processor.result().gcode_check_result.error_code |= (1 << 11); // printed weight over limit + } + } if (result != nullptr) { *result = std::move(m_processor.extract_result()); // set the filename to the correct value @@ -2476,6 +2687,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_last_layer_z = 0.f; m_max_layer_z = 0.f; m_last_width = 0.f; + m_last_layer_accumulated_mass = 0.0; m_is_role_based_fan_on.fill(false); m_role_based_fan_marker_layer.fill(-1); @@ -2848,14 +3060,32 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato //BBS match_physical_extruder_for_each_filament(first_non_support_filaments, m_config); + // Logical nozzle grouping for this print (null on paths that don't populate it). + auto group_result = m_print->get_layered_nozzle_group_result(); + std::vector first_non_support_hotends; + first_non_support_hotends.reserve(first_non_support_filaments.size()); + for (int filament_id : first_non_support_filaments) + first_non_support_hotends.push_back(filament_id < 0 ? -1 : + first_hotend_id_for_gcode_placeholder(m_config, group_result, filament_id, (int) get_extruder_id(filament_id))); + this->placeholder_parser().set("first_non_support_tools", new ConfigOptionInts(first_non_support_filaments)); this->placeholder_parser().set("first_non_support_filaments", new ConfigOptionInts(first_non_support_filaments)); + this->placeholder_parser().set("first_non_support_hotend", new ConfigOptionInts(first_non_support_hotends)); this->placeholder_parser().set("initial_no_support_tool", initial_non_support_extruder_id); this->placeholder_parser().set("initial_no_support_extruder", initial_non_support_extruder_id); + // initial_no_support_hotend/current_hotend (see first_hotend_id_for_gcode_placeholder): multi-nozzle + // H2C -> -1 (static; dynamic branch dormant), X2D -> -1, existing printers -> extruder id. this->placeholder_parser().set("initial_no_support_hotend", - hotend_id_for_gcode_placeholder(m_config, (int) get_extruder_id(initial_non_support_extruder_id))); + first_hotend_id_for_gcode_placeholder(m_config, group_result, (int) initial_non_support_extruder_id, (int) get_extruder_id(initial_non_support_extruder_id))); this->placeholder_parser().set("current_extruder", initial_extruder_id); - this->placeholder_parser().set("current_hotend", hotend_id_for_gcode_placeholder(m_config, extruder_id)); + this->placeholder_parser().set("current_hotend", + first_hotend_id_for_gcode_placeholder(m_config, group_result, (int) initial_extruder_id, extruder_id)); + // Initial filament/nozzle vocabulary + this->placeholder_parser().set("initial_filament_id", (int) initial_extruder_id); + this->placeholder_parser().set("initial_no_support_filament_id", (int) initial_non_support_extruder_id); + this->placeholder_parser().set("initial_nozzle_id", first_nozzle_id_for_gcode_placeholder(group_result, (int) initial_extruder_id, extruder_id)); + this->placeholder_parser().set("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + this->placeholder_parser().set("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); //Orca: set the key for compatibilty this->placeholder_parser().set("retraction_distance_when_cut", m_config.retraction_distances_when_cut.get_at(initial_extruder_id)); this->placeholder_parser().set("long_retraction_when_cut", m_config.long_retractions_when_cut.get_at(initial_extruder_id)); @@ -2875,7 +3105,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("additional_fan_full_speed_layer", new ConfigOptionInts(m_config.additional_fan_full_speed_layer)); auto flush_v_speed = m_config.filament_flush_volumetric_speed.values; - auto flush_temps = m_config.filament_flush_temp.values; + // Fast purge mode uses filament_flush_temp_fast; Default is inert. + auto flush_temps = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast ? m_config.filament_flush_temp_fast : m_config.filament_flush_temp).values; for (size_t idx = 0; idx < flush_v_speed.size(); ++idx) { if (flush_v_speed[idx] == 0) flush_v_speed[idx] = m_config.filament_max_volumetric_speed.get_at(idx); @@ -2893,6 +3124,28 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("current_object_idx", 0); // For the start / end G-code to do the priming and final filament pull in case there is no wipe tower provided. this->placeholder_parser().set("has_wipe_tower", has_wipe_tower); + + // Nozzle-heating center just outside the wipe tower. The tower side is chosen + // from the full-bed midpoint, then X is clamped to the region every extruder can reach (shared printable + // polygon) = the full printable_area for all current single/dual printers, so existing output is unchanged. + Vec2f wipe_tower_center = Vec2f::Zero(); + bool wipe_tower_center_valid = false; + if (has_wipe_tower) { + BoundingBoxf bbx = print.wipe_tower_data().bbx; + bbx.translate(print.get_fake_wipe_tower().pos.cast()); + BoundingBoxf printer_bed_bbx(m_config.printable_area.values); + if (bbx.center().x() < printer_bed_bbx.center().x()) + wipe_tower_center = Vec2f(float(bbx.max.x() + 2.f), float(bbx.center().y())); + else + wipe_tower_center = Vec2f(float(bbx.min.x() - 2.f), float(bbx.center().y())); + auto printer_bbx = unscaled(get_extents(print.get_extruder_shared_printable_polygon())); + if (wipe_tower_center.x() < printer_bbx.min[0]) wipe_tower_center.x() = float(printer_bbx.min[0]); + if (wipe_tower_center.x() > printer_bbx.max[0]) wipe_tower_center.x() = float(printer_bbx.max[0]); + wipe_tower_center_valid = true; + } + this->placeholder_parser().set("wipe_tower_center_pos_x", new ConfigOptionFloat(wipe_tower_center.x())); + this->placeholder_parser().set("wipe_tower_center_pos_y", new ConfigOptionFloat(wipe_tower_center.y())); + this->placeholder_parser().set("wipe_tower_center_pos_valid", new ConfigOptionBool(wipe_tower_center_valid)); this->placeholder_parser().set("has_single_extruder_multi_material_priming", wipe_tower_type == WipeTowerType::Type2 && has_wipe_tower && print.config().single_extruder_multi_material_priming); this->placeholder_parser().set("total_toolchanges", DoExport::resolve_total_toolchanges(print.wipe_tower_data(), print.tool_ordering())); this->placeholder_parser().set("num_extruders", int(print.config().nozzle_diameter.values.size())); @@ -3136,6 +3389,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Write the custom start G-code file.writeln(machine_start_gcode); + // Mark the end of the machine start g-code so the GCodeProcessor usage-block builder knows where user + // g-code ends and can start attributing filament/extruder usage. Gated on enable_pre_heating: only the + // injector fleet (H2D/X2D/H2D-Pro/H2C) emits it; the byte-frozen fleet (X1/P1/A1/H2S, flag false) never + // does, so their g-code is byte-identical. Without this marker handle_filament_change early-returns and + // no blocks are built, so this line is what actually activates the pre-heat injector. + if (m_config.enable_pre_heating.value) + file.write_format(";%s\n", GCodeProcessor::Machine_Start_GCode_End_Tag.c_str()); //BBS: gcode writer doesn't know where the real position of extruder is after inserting custom gcode m_writer.set_current_position_clear(false); @@ -3147,11 +3407,27 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato { DynamicConfig config; config.set_key_value("filament_extruder_id", new ConfigOptionInt((int)(initial_non_support_extruder_id))); + config.set_key_value("current_filament_id", new ConfigOptionInt((int)(initial_non_support_extruder_id))); + config.set_key_value("current_extruder_id", new ConfigOptionInt((int) get_extruder_id(initial_non_support_extruder_id))); + config.set_key_value("current_nozzle_id", new ConfigOptionInt(first_nozzle_id_for_gcode_placeholder(group_result, (int) initial_non_support_extruder_id, (int) get_extruder_id(initial_non_support_extruder_id)))); + config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.values.at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config); file.writeln(filament_start_gcode); - // mark the first filament used in print - file.write_format(";VT%d\n", initial_extruder_id); + // Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where + // H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their + // g-code stays byte-identical. (The dynamic branch is dormant, so H2C currently emits H-1.) + if (is_multi_nozzle_printer(m_config)) { + int initial_nozzle_id = -1; + if (group_result && group_result->is_support_dynamic_nozzle_map()) { + auto initial_nozzle = group_result->get_first_nozzle_for_filament(initial_extruder_id); + initial_nozzle_id = initial_nozzle ? initial_nozzle->group_id : -1; + } + file.write_format(";VT%d H%d\n", initial_extruder_id, initial_nozzle_id); + } else { + file.write_format(";VT%d\n", initial_extruder_id); + } } // Orca: add missing PA settings for initial filament if (m_config.enable_pressure_advance.get_at(initial_non_support_extruder_id)) { @@ -3471,6 +3747,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // adds tag for processor file.write_format(";%s%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role).c_str(), ExtrusionEntity::role_to_string(erCustom).c_str()); + // Mark the start of the machine end g-code so the usage-block builder closes its open blocks here and + // ignores filament changes inside the end g-code. Same enable_pre_heating gate as the start marker → + // byte-frozen fleet unaffected. + if (m_config.enable_pre_heating.value) + file.write_format(";%s\n", GCodeProcessor::Machine_End_GCode_Start_Tag.c_str()); + // Process filament-specific gcode in extruder order. { DynamicConfig config; @@ -3478,16 +3760,24 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato //BBS config.set_key_value("layer_z", new ConfigOptionFloat(m_writer.get_position()(2) - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); if (print.config().single_extruder_multi_material) { // Process the filament_end_gcode for the active filament only. int extruder_id = m_writer.filament()->id(); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); + config.set_key_value("current_filament_id", new ConfigOptionInt(extruder_id)); + config.set_key_value("current_extruder_id", new ConfigOptionInt((int) get_extruder_id(extruder_id))); + config.set_key_value("current_nozzle_id", new ConfigOptionInt(nozzle_id_for_gcode_placeholder(group_result, extruder_id, (int) get_extruder_id(extruder_id), m_layer_index))); file.writeln(this->placeholder_parser_process("filament_end_gcode", print.config().filament_end_gcode.get_at(extruder_id), extruder_id, &config)); } else { for (const std::string &end_gcode : print.config().filament_end_gcode.values) { int extruder_id = (unsigned int)(&end_gcode - &print.config().filament_end_gcode.values.front()); config.set_key_value("filament_extruder_id", new ConfigOptionInt(extruder_id)); + config.set_key_value("current_filament_id", new ConfigOptionInt(extruder_id)); + config.set_key_value("current_extruder_id", new ConfigOptionInt((int) get_extruder_id(extruder_id))); + config.set_key_value("current_nozzle_id", new ConfigOptionInt(nozzle_id_for_gcode_placeholder(group_result, extruder_id, (int) get_extruder_id(extruder_id), m_layer_index))); file.writeln(this->placeholder_parser_process("filament_end_gcode", end_gcode, extruder_id, &config)); } } @@ -3581,7 +3871,18 @@ void GCode::export_layer_filaments(GCodeProcessorResult* result) std::vectorprev_filament(m_config.nozzle_diameter.size(), -1); for (size_t idx = 0; idx < m_sorted_layer_filaments.size(); ++idx) { for (auto f : m_sorted_layer_filaments[idx]) { + // Mirror the guard in the sibling sequence loop below (the `filament_id < filament_map.size() && + // filament_map[filament_id] > 0` check): the H2C dynamic engine can yield a + // per-layer filament set whose ids fall outside filament_map, or a filament_map value beyond the + // physical-extruder count (prev_filament is sized nozzle_diameter.size()). Either an unguarded read + // (filament_map[f], prev_filament[extruder_idx]) or the write below would be out of bounds and + // corrupt the heap. Skipping such filaments keeps every access in range; on any valid slice the + // guard never fires, so the shipping/static path is byte-identical. + if (f >= filament_map.size() || filament_map[f] <= 0) + continue; int extruder_idx = filament_map[f] - 1; + if (extruder_idx >= (int) prev_filament.size()) + continue; if (prev_filament[extruder_idx] != -1 && f != prev_filament[extruder_idx]) { std::pair from_to_pair = { prev_filament[extruder_idx],f }; auto iter = result->filament_change_count_map.find(from_to_pair); @@ -4564,6 +4865,289 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o return emit_brim(brim_it->second, { object.id() }); } +// Bedslinger model. The heavier the bed load, the lower the achievable Y acceleration for a given +// drive force (a = F / (bed_mass + printed_mass)). Reads machine_max_force_Y / machine_bed_mass_Y (both +// default 0, i.e. absent on every existing printer), in which case it just returns the min configured Y +// acceleration. +void GCode::mass_load_limited_machine_acceleration( + const PrintStatistics &curr_print_statistics, + const Print &print, // input + double &y_acceleration_limit_res, // output + double &accumulated_mass_res) +{ + double curr_acceleration_y_config = 1e10; + auto &machine_max_acceleration_y = print.config().machine_max_acceleration_y.values; + for (auto &temp : machine_max_acceleration_y) + if (curr_acceleration_y_config > temp) curr_acceleration_y_config = temp; + accumulated_mass_res = curr_print_statistics.total_weight; + // mass in g, acceleration in mm/s2 + double machine_max_force_Y = print.config().machine_max_force_Y.getFloat(), + machine_bed_mass_Y = print.config().machine_bed_mass_Y.getFloat(); + if (machine_max_force_Y > EPSILON && machine_bed_mass_Y > EPSILON) { + // This item is not applicable to this printer FIXME-other printers need acceleration limit? + double virtual_force_g_mms2 = machine_max_force_Y * 1e6, // x N =x * 1e6 g*mm/s2 + curr_acceleration_temp = curr_acceleration_y_config; // temps + if (accumulated_mass_res > EPSILON) { + curr_acceleration_temp = virtual_force_g_mms2 / (machine_bed_mass_Y + accumulated_mass_res); + y_acceleration_limit_res = std::min(curr_acceleration_temp, curr_acceleration_y_config); + } else { + y_acceleration_limit_res = curr_acceleration_y_config; + BOOST_LOG_TRIVIAL(info) << "mass_load_limited_machine_acceleration: Printed mass not detected"; + } + } else { + y_acceleration_limit_res = curr_acceleration_y_config; + } +} + +// Farthest-point timelapse. Scan every extrusion path on this layer and record the point +// farthest from the camera (bed origin 0,0), plus which extruder prints it and whether that extruder is +// the photo head (most_used_extruder). Called only when the subsystem is enabled, so it is a no-op for +// every printer that does not set farthest_point_timelapse. +// Orca: the PrintRegionConfig filament keys are named outer_wall_filament_id / sparse_infill_filament_id / +// internal_solid_filament_id (handle_legacy renames), so the region reads use those names. +void GCode::compute_farthest_point(const std::vector &layers, int most_used_extruder, + const std::map, unsigned int> &support_filaments) +{ + m_farthest_point_timelapse.farthest_point = Point(0, 0); + m_farthest_point_timelapse.farthest_gcode_pos = Vec2d(0, 0); + m_farthest_point_timelapse.farthest_extruder_id = 0; + m_farthest_point_timelapse.farthest_is_photo_head = false; + + // Track farthest point for external perimeters and fallback (infill/support) separately + int64_t max_dist_sq_ext = -1; + Point farthest_point_ext; + int farthest_extruder_ext = 0; + + int64_t max_dist_sq_fallback = -1; + Point farthest_point_fallback; + int farthest_extruder_fallback = 0; + + // Recursive visitor: call fn(const ExtrusionPath&) for every leaf path + auto for_each_path = [](const ExtrusionEntity *entity, const auto &fn, const auto &self) -> void { + if (entity->is_collection()) { + for (const auto *child : static_cast(entity)->entities) + self(child, fn, self); + } else if (entity->is_loop()) { + for (const ExtrusionPath &p : static_cast(entity)->paths) + fn(p); + } else if (const auto *mp = dynamic_cast(entity)) { + for (const ExtrusionPath &p : mp->paths) + fn(p); + } else { + fn(static_cast(*entity)); + } + }; + + auto is_ext_perimeter_role = [](ExtrusionRole role) -> bool { + return role == erExternalPerimeter; + }; + auto is_fallback_role = [](ExtrusionRole role) -> bool { + return role == erInternalInfill || role == erSolidInfill || role == erTopSolidInfill; + }; + auto is_candidate_support_role = [](ExtrusionRole role) -> bool { + return role == erSupportMaterial || role == erSupportMaterialInterface || role == erSupportTransition; + }; + + // Update a (max_dist_sq, point, extruder_id) triple + auto update_max = [](int64_t &max_dsq, Point &out_point, int &out_ext, + const Point &p, const Point &shift, int extruder_id) { + Point global = p + shift; + int64_t dsq = (int64_t)global.x() * global.x() + (int64_t)global.y() * global.y(); + if (dsq > max_dsq) { + max_dsq = dsq; + out_point = global; + out_ext = extruder_id; + } + }; + + // Collect candidate endpoints from one ExtrusionPath, handling arc fitting. + // Orca: ExtrusionPath::polyline is a Polyline3 (Point3 points) rather than a 2D Polyline, so each + // stored point is projected to 2D via to_point() before the distance test (Z is irrelevant here). + auto collect_from_path = [&update_max](int64_t &max_dsq, Point &out_point, int &out_ext, + const ExtrusionPath &path, const Point &shift, int extruder_id) { + const Polyline3 &poly = path.polyline; + if (poly.points.empty()) return; + + if (!poly.fitting_result.empty()) { + if (poly.fitting_result.front().start_point_index < poly.points.size()) + update_max(max_dsq, out_point, out_ext, + poly.points[poly.fitting_result.front().start_point_index].to_point(), shift, extruder_id); + + for (const PathFittingData &seg : poly.fitting_result) { + if (seg.path_type == EMovePathType::Linear_move) { + for (size_t i = seg.start_point_index; i <= seg.end_point_index && i < poly.points.size(); ++i) + update_max(max_dsq, out_point, out_ext, poly.points[i].to_point(), shift, extruder_id); + } else if (seg.path_type == EMovePathType::Arc_move_cw || seg.path_type == EMovePathType::Arc_move_ccw) { + update_max(max_dsq, out_point, out_ext, seg.arc_data.end_point, shift, extruder_id); + } + } + } else { + for (const Point3 &pt : poly.points) + update_max(max_dsq, out_point, out_ext, pt.to_point(), shift, extruder_id); + } + }; + + // Single pass: scan all paths, collecting ext-perimeter and fallback candidates. + // Use original_object (not ltp.object()) to get instance shifts, because + // ltp.object() may return a shared/merged PrintObject whose instances() only + // reflects one copy's shift. original_object preserves the per-ModelObject + // PrintObject with correct instance positions. + for (const LayerToPrint <p : layers) { + const PrintObject *print_obj = ltp.original_object; + if (!print_obj) continue; + + for (const PrintInstance &inst : print_obj->instances()) { + const Point &shift = inst.shift; + + if (ltp.object_layer) { + for (const LayerRegion *region : ltp.object_layer->regions()) { + const PrintRegionConfig &rcfg = region->region().config(); + + for (const ExtrusionEntity *entity : region->perimeters.entities) { + for_each_path(entity, [&](const ExtrusionPath &path) { + if (is_ext_perimeter_role(path.role())) + collect_from_path(max_dist_sq_ext, farthest_point_ext, farthest_extruder_ext, + path, shift, (int)get_extruder_id(rcfg.outer_wall_filament_id.value - 1)); + }, for_each_path); + } + + for (const ExtrusionEntity *entity : region->fills.entities) { + for_each_path(entity, [&](const ExtrusionPath &path) { + if (!is_fallback_role(path.role())) return; + int eid = (path.role() == erInternalInfill) + ? (int)get_extruder_id(rcfg.sparse_infill_filament_id.value - 1) + : (int)get_extruder_id(rcfg.internal_solid_filament_id.value - 1); + collect_from_path(max_dist_sq_fallback, farthest_point_fallback, farthest_extruder_fallback, + path, shift, eid); + }, for_each_path); + } + } + } + + if (ltp.support_layer) { + for (const ExtrusionEntity *entity : ltp.support_layer->support_fills.entities) { + for_each_path(entity, [&](const ExtrusionPath &path) { + if (!is_candidate_support_role(path.role())) return; + auto support_filament = support_filaments.find({ ltp.support_layer, path.role() }); + if (support_filament == support_filaments.end()) return; + int eid = (int)get_extruder_id(support_filament->second); + collect_from_path(max_dist_sq_fallback, farthest_point_fallback, farthest_extruder_fallback, + path, shift, eid); + }, for_each_path); + } + } + } + } + + // Prefer external perimeter result; fall back to infill/support + int64_t max_dist_sq; + if (max_dist_sq_ext > 0) { + max_dist_sq = max_dist_sq_ext; + m_farthest_point_timelapse.farthest_point = farthest_point_ext; + m_farthest_point_timelapse.farthest_extruder_id = farthest_extruder_ext; + } else { + max_dist_sq = max_dist_sq_fallback; + m_farthest_point_timelapse.farthest_point = farthest_point_fallback; + m_farthest_point_timelapse.farthest_extruder_id = farthest_extruder_fallback; + } + + if (max_dist_sq > 0) { + m_farthest_point_timelapse.farthest_gcode_pos = unscale(m_farthest_point_timelapse.farthest_point); + // Single nozzle with AMS: all virtual extruders share one physical nozzle, + // so the nozzle is always the photo head regardless of which filament is used. + bool single_nozzle = (m_config.nozzle_diameter.size() <= 1); + m_farthest_point_timelapse.farthest_is_photo_head = single_nozzle || (m_farthest_point_timelapse.farthest_extruder_id == most_used_extruder); + } +} + +// Build the per-layer timelapse snapshot g-code. Extracted from the former process_layer +// `insert_timelapse_gcode` lambda so the per-extrusion inline hook in _extrude() can call it too. +// Byte-identical to the old lambda whenever the farthest-point subsystem is off (skip_pos_pick=false + +// m_farthest_point_timelapse.enabled=false → farthest_point unset in the picker ctx and +// farthest_point_timelapse_enabled=false in the template). +// Orca: returns the g-code string directly (Orca's timelapse path never tracked a final_pos travel +// optimization). +std::string GCode::generate_timelapse_gcode(const Print &print, coordf_t print_z, int most_used_extruder, + const std::set *layer_object_label_ids, + const std::vector *printed_objects, + bool skip_pos_pick) +{ + if (!m_writer.filament()) + return {}; + + PosPickCtx ctx; + ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; + ctx.curr_layer = this->layer(); + ctx.curr_extruder_id = m_writer.filament()->extruder_id(); + ctx.picture_extruder_id = most_used_extruder; + if (m_farthest_point_timelapse.enabled) { + // farthest_point is stored in the global print frame (includes plate origin); the picker works in + // the plate-relative frame, so subtract the plate origin here. + Vec3d po = print.get_plate_origin(); + ctx.farthest_point = m_farthest_point_timelapse.farthest_point - Point(scale_(po.x()), scale_(po.y())); + } + if (m_config.print_sequence == PrintSequence::ByObject && printed_objects) + ctx.printed_objects = *printed_objects; + + auto timelapse_pos = skip_pos_pick ? DefaultTimelapsePos : m_timelapse_pos_picker.pick_pos(ctx); + + std::string timelapse_gcode; + if (!print.config().time_lapse_gcode.value.empty()) { + DynamicConfig config; + config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); + config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); + config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); + config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); + config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(ctx.curr_extruder_id))); + config.set_key_value("timelapse_pos_x", new ConfigOptionInt((int)timelapse_pos.x())); + config.set_key_value("timelapse_pos_y", new ConfigOptionInt((int)timelapse_pos.y())); + config.set_key_value("has_timelapse_safe_pos", new ConfigOptionBool(timelapse_pos != DefaultTimelapsePos)); + // Timelapse-context vars. + config.set_key_value("timelapse_inline_photo", new ConfigOptionBool(skip_pos_pick)); + // Drive the template ternary Z{layer_z + (farthest_point_timelapse_enabled ? 0.0 : 0.4)} from the + // effective (per-layer, config+traditional+non-i3-folded) enabled state. False for every printer + // that leaves the toggle off, so their template evaluates to the pre-existing +0.4 lift byte-for-byte. + config.set_key_value("farthest_point_timelapse_enabled", new ConfigOptionBool(m_farthest_point_timelapse.enabled)); + config.set_key_value("clear_to_x0", new ConfigOptionBool(m_timelapse_pos_picker.get_is_clear_to_x0(ctx))); + timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; + } + + if (!timelapse_gcode.empty()) { + m_writer.set_current_position_clear(false); + + double temp_z_after_tool_change; + if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { + Vec3d pos = m_writer.get_position(); + pos(2) = temp_z_after_tool_change; + m_writer.set_position(pos); + } + } + + // (layer_object_label_ids->size() < 64) this restriction comes from _encode_label_ids_to_base64() + if (layer_object_label_ids && + is_BBL_Printer() && + (print.num_object_instances() <= g_max_label_object) && // Don't support too many objects on one plate + (print.num_object_instances() > 1) && // Don't support skipping single object + (!layer_object_label_ids->empty()) && + (print.calib_params().mode == CalibMode::Calib_None)) { + std::ostringstream oss; + for (auto it = layer_object_label_ids->begin(); it != layer_object_label_ids->end(); ++it) { + if (it != layer_object_label_ids->begin()) oss << ","; + oss << *it; + } + + std::string start_str = std::string("; object ids of layer ") + std::to_string(m_layer_index + 1) + (" start: ") + oss.str() + "\n"; + start_str += "M624 " + _encode_label_ids_to_base64(std::vector(layer_object_label_ids->begin(), layer_object_label_ids->end())) + "\n"; + + std::string end_str = std::string("; object ids of this layer") + std::to_string(m_layer_index + 1) + (" end: ") + oss.str() + "\n"; + end_str += "M625\n"; + + timelapse_gcode = start_str + timelapse_gcode + end_str; + } + + return timelapse_gcode; +} + // In sequential mode, process_layer is called once per each object and its copy, // therefore layers will contain a single entry and single_object_instance_idx will point to the copy of the object. // In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated. @@ -4678,6 +5262,16 @@ LayerResult GCode::process_layer( bool is_i3_printer = printer_structure == PrinterStructure::psI3; bool is_multi_extruder = m_config.nozzle_diameter.size() > 1; + // Farthest-point timelapse enable-fold. Corexy-only: gated OFF for i3 (psI3 → A1/A2L), for + // smooth timelapse, and whenever the toggle is unset. When false every downstream hook (compute, + // _extrude inline photo, process_layer case gates, template ternary) is inert, so the whole shipping + // fleet that does not set farthest_point_timelapse is byte-identical to the pre-existing behavior. + m_farthest_point_timelapse.enabled = m_config.farthest_point_timelapse.value + && m_config.timelapse_type.value == TimelapseType::tlTraditional + && printer_structure != PrinterStructure::psI3; + m_farthest_point_timelapse.most_used_extruder = most_used_extruder; + m_farthest_point_timelapse.inserted_this_layer = false; + bool need_insert_timelapse_gcode_for_traditional = false; if ((!m_wipe_tower || !m_wipe_tower->enable_timelapse_print()) && (is_BBL_Printer() || !m_config.time_lapse_gcode.value.empty())) { need_insert_timelapse_gcode_for_traditional = ((is_i3_printer && !m_spiral_vase) || is_multi_extruder); @@ -4712,6 +5306,28 @@ LayerResult GCode::process_layer( config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); + + // Bedslinger mass model. Compute the running printed mass at this layer (same weight calc as + // DoExport::update_print_stats_and_format_filament_stats) and the Y acceleration limit it implies. + // These are new placeholder vars no existing template reads, and mass_load_limited_machine_acceleration + // returns the plain min Y accel unless the A2L force/bed-mass keys are set, so this is inert for every + // existing printer. + PrintStatistics curr_print_statistics; + for (const Extruder &extruder : m_writer.extruders()) { + double extruded_volume = extruder.extruded_volume() + (has_wipe_tower ? print.wipe_tower_data().used_filament[extruder.id()] * 2.4052f : 0.f); // assumes 1.75mm filament diameter + double filament_weight = extruded_volume * extruder.filament_density() * 0.001; + if (filament_weight > 0.) + curr_print_statistics.total_weight += filament_weight; + } + double curr_y_acceleration_limit = -1, curr_accumulated_mass = -1; + mass_load_limited_machine_acceleration(curr_print_statistics, print, curr_y_acceleration_limit, curr_accumulated_mass); + double curr_layer_mass = curr_print_statistics.total_weight - m_last_layer_accumulated_mass; + if (curr_layer_mass <= EPSILON) curr_layer_mass = 0.0; + m_last_layer_accumulated_mass = curr_print_statistics.total_weight; + config.set_key_value("curr_y_acceleration_limit", new ConfigOptionFloat(curr_y_acceleration_limit)); + config.set_key_value("curr_accumulated_mass", new ConfigOptionFloat(curr_accumulated_mass)); + config.set_key_value("curr_layer_mass", new ConfigOptionFloat(curr_layer_mass)); + gcode += this->placeholder_parser_process("layer_change_gcode", print.config().layer_change_gcode.value, m_writer.filament()->id(), &config) + "\n"; @@ -4896,6 +5512,10 @@ LayerResult GCode::process_layer( // Group extrusions by an extruder, then by an object, an island and a region. std::map> by_extruder; + // Farthest-point timelapse: per-support-layer/role → extruder map, consumed only by + // compute_farthest_point (below, gated on m_farthest_point_timelapse.enabled). Populated alongside the + // existing support-extruder assignment; unused (and thus output-neutral) when the subsystem is off. + std::map, unsigned int> support_filaments; std::vector> split_perimeter_storage; bool is_anything_overridden = const_cast(layer_tools).wiping_extrusions().is_anything_overridden(); for (const LayerToPrint &layer_to_print : layers) { @@ -4989,6 +5609,16 @@ LayerResult GCode::process_layer( // Both the support and the support interface are printed with the same extruder, therefore // the interface may be interleaved with the support base. bool single_extruder = ! has_support || support_extruder == interface_extruder; + // Farthest-point timelapse: record the extruder for each support role so + // compute_farthest_point can attribute farthest support points correctly. + if (has_support) { + support_filaments[{ &support_layer, erSupportMaterial }] = support_extruder; + support_filaments[{ &support_layer, erSupportTransition }] = support_extruder; + } + if (has_interface) { + support_filaments[{ &support_layer, erSupportMaterialInterface }] = + single_extruder ? (has_support ? support_extruder : interface_extruder) : interface_extruder; + } // Assign an extruder to the base. ObjectByExtruder &obj = object_by_extruder(by_extruder, has_support ? support_extruder : interface_extruder, &layer_to_print - layers.data(), layers.size()); obj.support = &support_layer.support_fills; @@ -5136,6 +5766,11 @@ LayerResult GCode::process_layer( } } // for objects + // Farthest-point timelapse: once all this layer's extrusions are grouped, find the point + // farthest from the camera. No-op when the subsystem is disabled. + if (m_farthest_point_timelapse.enabled) + compute_farthest_point(layers, most_used_extruder, support_filaments); + std::map> filament_to_print_instances; { for (unsigned int filament_id : layer_tools.extruders) { @@ -5175,67 +5810,20 @@ LayerResult GCode::process_layer( } } - auto insert_timelapse_gcode = [this, print_z, &print, &most_used_extruder, &layer_object_label_ids,&printed_objects = std::as_const(m_printed_objects)]() -> std::string { - PosPickCtx ctx; - ctx.curr_pos = { (coord_t)(scale_(m_writer.get_position().x())),(coord_t)(scale_(m_writer.get_position().y())) }; - ctx.curr_layer = this->layer(); - ctx.curr_extruder_id = m_writer.filament()->extruder_id(); - ctx.picture_extruder_id = most_used_extruder; - if (m_config.print_sequence == PrintSequence::ByObject) - ctx.printed_objects = printed_objects; + m_farthest_point_timelapse.layer_object_label_ids = layer_object_label_ids; - auto timelapse_pos=m_timelapse_pos_picker.pick_pos(ctx); - - std::string timelapse_gcode; - if (!print.config().time_lapse_gcode.value.empty()) { - DynamicConfig config; - config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); - config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); - config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); - config.set_key_value("most_used_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(most_used_extruder))); - config.set_key_value("curr_physical_extruder_id", new ConfigOptionInt(m_config.physical_extruder_map.get_at(ctx.curr_extruder_id))); - config.set_key_value("timelapse_pos_x", new ConfigOptionInt((int)timelapse_pos.x())); - config.set_key_value("timelapse_pos_y", new ConfigOptionInt((int)timelapse_pos.y())); - config.set_key_value("has_timelapse_safe_pos", new ConfigOptionBool(timelapse_pos != DefaultTimelapsePos)); - timelapse_gcode = this->placeholder_parser_process("timelapse_gcode", print.config().time_lapse_gcode.value, m_writer.filament()->id(), &config) + "\n"; - } - - if (!timelapse_gcode.empty()) { - m_writer.set_current_position_clear(false); - - double temp_z_after_tool_change; - if (GCodeProcessor::get_last_z_from_gcode(timelapse_gcode, temp_z_after_tool_change)) { - Vec3d pos = m_writer.get_position(); - pos(2) = temp_z_after_tool_change; - m_writer.set_position(pos); - } - } - - // (layer_object_label_ids.size() < 64) this restriction comes from _encode_label_ids_to_base64() - if (is_BBL_Printer() && - (print.num_object_instances() <= g_max_label_object) && // Don't support too many objects on one plate - (print.num_object_instances() > 1) && // Don't support skipping single object - (layer_object_label_ids.size() > 0) && - (print.calib_params().mode == CalibMode::Calib_None)) { - std::ostringstream oss; - for (auto it = layer_object_label_ids.begin(); it != layer_object_label_ids.end(); ++it) { - if (it != layer_object_label_ids.begin()) oss << ","; - oss << *it; - } - - std::string start_str = std::string("; object ids of layer ") + std::to_string(m_layer_index + 1) + (" start: ") + oss.str() + "\n"; - start_str += "M624 " + _encode_label_ids_to_base64(std::vector(layer_object_label_ids.begin(), layer_object_label_ids.end())) + "\n"; - - std::string end_str = std::string("; object ids of this layer") + std::to_string(m_layer_index + 1) + (" end: ") + oss.str() + "\n"; - end_str += "M625\n"; - - timelapse_gcode = start_str + timelapse_gcode + end_str; - } - - return timelapse_gcode; + // skip_pos_pick: when true the head takes an inline photo at its current spot instead of moving to a + // picked safe position. Delegates to the extracted member so the per-extrusion farthest-point hook in + // _extrude() shares the exact same generation path. Every existing call site uses the default (false). + auto insert_timelapse_gcode = [this, print_z, &print, &most_used_extruder, &layer_object_label_ids,&printed_objects = std::as_const(m_printed_objects)](bool skip_pos_pick = false) -> std::string { + return generate_timelapse_gcode(print, print_z, most_used_extruder, &layer_object_label_ids, &printed_objects, skip_pos_pick); }; - if (!need_insert_timelapse_gcode_for_traditional && is_BBL_Printer()) { // Equivalent to the timelapse gcode placed in layer_change_gcode + // With farthest-point-is-photo-head active, the snapshot is deferred to the inline _extrude + // hook / layer-end fallback, so skip the layer-start placement here. The extra clause is inert (true) + // whenever the subsystem is off → pre-existing behavior byte-for-byte. + if (!need_insert_timelapse_gcode_for_traditional && is_BBL_Printer() + && !(m_farthest_point_timelapse.enabled && m_farthest_point_timelapse.farthest_is_photo_head)) { // Equivalent to the timelapse gcode placed in layer_change_gcode if (FILAMENT_CONFIG(retract_when_changing_layer)) { gcode += this->retract(false, false, auto_lift_type, true); } @@ -5291,15 +5879,24 @@ LayerResult GCode::process_layer( m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id); } + // The inline _extrude hook may already have taken the snapshot mid-extrusion on a + // previous extruder; sync so we don't insert it a second time. + has_insert_timelapse_gcode |= m_farthest_point_timelapse.inserted_this_layer; + std::string gcode_toolchange; if (has_wipe_tower) { if (!m_wipe_tower->is_empty_wipe_tower_gcode(*this, extruder_id, extruder_id == layer_tools.extruders.back())) { - if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode) { + if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode + && !(m_farthest_point_timelapse.enabled && m_farthest_point_timelapse.farthest_is_photo_head)) { bool should_insert = true; if (m_config.nozzle_diameter.values.size() == 2){ - if (!writer().filament() || get_extruder_id(writer().filament()->id()) != most_used_extruder) { - should_insert = false; - } + bool curr_is_photo_head = writer().filament() && + get_extruder_id(writer().filament()->id()) == most_used_extruder; + // Case B (farthest point printed by a non-photo head): the firmware + // snapshots at the picked safe pos, so insert when leaving the photo head instead. + // is_case_b is false whenever the subsystem is off → should_insert == pre-existing value. + bool is_case_b = m_farthest_point_timelapse.enabled && !m_farthest_point_timelapse.farthest_is_photo_head; + should_insert = is_case_b ? !curr_is_photo_head : curr_is_photo_head; } if (should_insert) { @@ -5319,17 +5916,24 @@ LayerResult GCode::process_layer( gcode_toolchange = m_wipe_tower->tool_change(*this, extruder_id, extruder_id == layer_tools.extruders.back()); } } else { + // Same case-A/case-B split for the non-wipe-tower path. With the subsystem off, + // is_case_b is false and should_insert == (curr == most_used) = pre-existing behavior. if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode && + !(m_farthest_point_timelapse.enabled && m_farthest_point_timelapse.farthest_is_photo_head) && m_writer.need_toolchange(extruder_id) && m_config.nozzle_diameter.values.size() == 2 && - writer().filament() && - (get_extruder_id(writer().filament()->id()) == most_used_extruder)) { - gcode += this->retract(false, false, auto_lift_type, true); - m_writer.add_object_change_labels(gcode); + writer().filament()) { + bool curr_is_photo_head = get_extruder_id(writer().filament()->id()) == most_used_extruder; + bool is_case_b = m_farthest_point_timelapse.enabled && !m_farthest_point_timelapse.farthest_is_photo_head; + bool should_insert = is_case_b ? !curr_is_photo_head : curr_is_photo_head; + if (should_insert) { + gcode += this->retract(false, false, auto_lift_type, true); + m_writer.add_object_change_labels(gcode); - gcode += insert_timelapse_gcode(); - has_insert_timelapse_gcode = true; + gcode += insert_timelapse_gcode(); + has_insert_timelapse_gcode = true; + } } if (print.config().enable_wrapping_detection && !has_insert_wrapping_detection_gcode) { @@ -5557,6 +6161,22 @@ LayerResult GCode::process_layer( BOOST_LOG_TRIVIAL(trace) << "Exported layer " << layer.id() << " print_z " << print_z << log_memory_info(); + // The inline _extrude hook may have taken the snapshot; sync. + has_insert_timelapse_gcode |= m_farthest_point_timelapse.inserted_this_layer; + + // Farthest-point-is-photo-head layer-end fallback. If the head never passed within tolerance of + // the farthest point during extrusion (so the inline hook never fired) insert the snapshot here — + // single-extruder prints have no traditional multi-extruder fallback below. Guarded on enabled, so this + // block never runs for a printer with the subsystem off. + if (m_farthest_point_timelapse.enabled && m_farthest_point_timelapse.farthest_is_photo_head && !has_insert_timelapse_gcode) { + if (FILAMENT_CONFIG(retract_when_changing_layer)) { + gcode += this->retract(false, false, auto_lift_type, true); + } + m_writer.add_object_change_labels(gcode); + gcode += insert_timelapse_gcode(); + has_insert_timelapse_gcode = true; + } + if (need_insert_timelapse_gcode_for_traditional && !has_insert_timelapse_gcode) { // The traditional model of thin-walled object will have flaws for I3 if (m_support_traditional_timelapse @@ -5625,7 +6245,10 @@ void GCode::append_full_config(const Print &print, std::string &str) { DynamicPrintConfig cfg = print.full_print_config(); { // correct the flush_volumes_matrix with flush_multiplier values - std::vector temp_cfg_flush_multiplier = cfg.option("flush_multiplier")->values; + // Fast purge mode uses flush_multiplier_fast; Default is inert. + std::vector temp_cfg_flush_multiplier = (print.config().prime_volume_mode == PrimeVolumeMode::pvmFast) + ? cfg.option("flush_multiplier_fast")->values + : cfg.option("flush_multiplier")->values; std::vector temp_flush_volumes_matrix = cfg.option("flush_volumes_matrix")->values; auto temp_filament_color = cfg.option("filament_colour")->values; size_t heads_count_tmp = temp_cfg_flush_multiplier.size(), @@ -5648,6 +6271,29 @@ void GCode::append_full_config(const Print &print, std::string &str) } // Sorted list of config keys, which shall not be stored into the G-code. Initializer list. static const std::set banned_keys( { + // filament_extruder_compatibility is a device-side (blacklist) compatibility hint read from the + // loaded presets, never consumed by slicing or firmware. Keep it out of the G-code config block so + // the config key stays inert to g-code (byte-identical output). + "filament_extruder_compatibility"sv, + // The fast-purge / prime-volume-mode keys are new static-member registrations. Excluding them from + // the config block keeps registration byte-identical for the shipping fleet (default + // prime_volume_mode==Default leaves the slicing body unchanged; only the config-dump would otherwise + // gain lines). They only affect output on the pvmFast / pvmSaving branch. + "prime_volume_mode"sv, + "flush_multiplier_fast"sv, + "filament_flush_temp_fast"sv, + "support_fast_purge_mode"sv, + // deretract_speed_extruder_change is a device/firmware-facing machine-profile key with no slicer + // consumer. Banning it from the g-code config dump keeps the H2D/A2L/X2D/P2S leaves that carry it + // byte-identical, while still registering it as a known, validated config key. + "deretract_speed_extruder_change"sv, + // farthest_point_timelapse is a newly-registered printer key (corexy-only timelapse refinement). + // Excluding it from the config block keeps the config-dump byte-identical for the whole shipping + // fleet — otherwise every printer's dump would gain a `= 0` line vs the previous baseline, and + // H2C/H2D would gain a non-timelapse `= 1` line. The key is read at slice time from m_config, so + // banning it from the dump has no effect on the feature; the only H2C/H2D delta is the M9711/M971 + // snapshot reposition. + "farthest_point_timelapse"sv, "compatible_printers"sv, "compatible_prints"sv, "print_host"sv, @@ -6969,6 +7615,34 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, ironing_fan_speed >= 0 && path.role() == erIroning); }; + // Farthest-point timelapse inline hook. When the photo head reaches (within 0.5 mm of) the + // farthest-from-camera point while extruding, take the snapshot in place (skip_pos_pick → M971 inline + // photo, no travel). Early-returns unless the subsystem is enabled AND the farthest point is on the + // photo head, so it is a no-op for every printer that leaves the toggle off. + auto check_and_insert_timelapse = [this, &gcode](const Point &endpoint_scaled) { + if (!m_farthest_point_timelapse.enabled || !m_farthest_point_timelapse.farthest_is_photo_head || m_farthest_point_timelapse.inserted_this_layer) + return; + // Inline M971 only when current extruder is the photo head; other nozzles may pass through nearby + // coordinates but their physical position differs. + if (!m_writer.filament() || get_extruder_id(m_writer.filament()->id()) != m_farthest_point_timelapse.most_used_extruder) + return; + // Compare in the global print frame to match farthest_gcode_pos, which is unscale(farthest_point) + // without the per-extruder nozzle offset. Using point_to_gcode() here would subtract extruder_offset + // on this side only, leaving a constant mismatch that could prevent the inline photo from ever + // triggering on machines with a non-zero photo-head extruder_offset. + Vec2d endpoint_mm = unscale(endpoint_scaled) + m_origin; + if ((endpoint_mm - m_farthest_point_timelapse.farthest_gcode_pos).norm() >= 0.5) + return; + if (!m_print || !m_layer) + return; + std::string timelapse_gcode = generate_timelapse_gcode(*m_print, m_layer->print_z, m_farthest_point_timelapse.most_used_extruder, + &m_farthest_point_timelapse.layer_object_label_ids, &m_printed_objects, true); + if (!timelapse_gcode.empty()) { + gcode += timelapse_gcode; + m_farthest_point_timelapse.inserted_this_layer = true; + } + }; + if (!variable_speed) { // F is mm per minute. if( (std::abs(writer().get_current_speed() - F) > EPSILON) || (std::abs(_mm3_per_mm - m_last_mm3_mm) > EPSILON) ){ @@ -7092,6 +7766,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); } + check_and_insert_timelapse(line.b.to_point()); // Inline farthest-point snapshot } } else { // BBS: start to generate gcode from arc fitting data which includes line and arc @@ -7121,6 +7796,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, this->point_to_gcode(line.b), dE, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + check_and_insert_timelapse(line.b); // Inline farthest-point snapshot } break; } @@ -7146,6 +7822,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, dE, arc.direction == ArcDirection::Arc_Dir_CCW, GCodeWriter::full_gcode_comment ? tempDescription : "", path.is_force_no_extrusion()); + check_and_insert_timelapse(arc.end_point); // Inline farthest-point snapshot break; } default: @@ -7292,6 +7969,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, gcode += m_writer.extrude_to_xyz(dest3d, dE * e_ratio, GCodeWriter::full_gcode_comment ? tempDescription : ""); } + // Inline farthest-point snapshot on the variable-speed emission path. Inert unless the + // farthest-point subsystem is on, matching the other paths. + check_and_insert_timelapse(processed_point.p.to_point()); + prev = p; } @@ -7905,13 +8586,32 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // set volumetric speed of outer wall ,ignore per obejct,just use default setting float outer_wall_volumetric_speed = get_outer_wall_volumetric_speed(m_config, *m_print, new_filament_id, get_extruder_id(new_filament_id)); float wipe_avoid_pos_x = 110.f; + // Logical nozzle grouping (null on paths that don't populate it) + null-safe nozzle ids. + auto group_result = m_print->get_layered_nozzle_group_result(); + int old_nozzle_id = nozzle_id_for_gcode_placeholder(group_result, old_filament_id, old_extruder_id, m_layer_index); + int next_nozzle_id = nozzle_id_for_gcode_placeholder(group_result, (int) new_filament_id, new_extruder_id, m_layer_index); DynamicConfig dyn_config; dyn_config.set_key_value("outer_wall_volumetric_speed", new ConfigOptionFloat(outer_wall_volumetric_speed)); dyn_config.set_key_value("previous_extruder", new ConfigOptionInt(old_filament_id)); dyn_config.set_key_value("next_extruder", new ConfigOptionInt((int)new_filament_id)); - dyn_config.set_key_value("current_hotend", - new ConfigOptionInt(old_filament_id >= 0 ? hotend_id_for_gcode_placeholder(m_config, old_extruder_id) : -1)); - dyn_config.set_key_value("next_hotend", new ConfigOptionInt(hotend_id_for_gcode_placeholder(m_config, new_extruder_id))); + // current_hotend/next_hotend (see hotend_id_for_gcode_placeholder): multi-nozzle H2C -> -1 + // (static; dynamic branch dormant), X2D -> -1, existing printers -> extruder id. + dyn_config.set_key_value("current_hotend", new ConfigOptionInt( + hotend_id_for_gcode_placeholder(m_config, group_result, old_filament_id, old_extruder_id, m_layer_index))); + dyn_config.set_key_value("next_hotend", new ConfigOptionInt( + hotend_id_for_gcode_placeholder(m_config, group_result, (int) new_filament_id, new_extruder_id, m_layer_index))); + dyn_config.set_key_value("current_nozzle_id", new ConfigOptionInt(old_nozzle_id)); + dyn_config.set_key_value("next_nozzle_id", new ConfigOptionInt(next_nozzle_id)); + dyn_config.set_key_value("current_filament_id", new ConfigOptionInt(old_filament_id)); + dyn_config.set_key_value("next_filament_id", new ConfigOptionInt((int)new_filament_id)); + dyn_config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); + dyn_config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); + // Old filament's nozzle-change retract length (filament_retract_length_nc; nil/-1 -> 0). + dyn_config.set_key_value("filament_retract_length_nc", new ConfigOptionFloat( + (old_filament_id != -1) ? (float) m_config.filament_retract_length_nc.get_at(old_filament_id) : 0.f)); + // Current parked-retract length of the incoming filament's extruder. + dyn_config.set_key_value("new_extruder_retracted_length", + new ConfigOptionFloat(m_writer.get_extruder_retracted_length((int) new_filament_id))); dyn_config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); dyn_config.set_key_value("layer_z", new ConfigOptionFloat(print_z)); dyn_config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); @@ -7964,7 +8664,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo } auto flush_v_speed = m_print->config().filament_flush_volumetric_speed.values; - auto flush_temps = m_print->config().filament_flush_temp.values; + // Fast purge mode uses filament_flush_temp_fast; Default is inert. + auto flush_temps = (m_print->config().prime_volume_mode == PrimeVolumeMode::pvmFast ? m_print->config().filament_flush_temp_fast : m_print->config().filament_flush_temp).values; auto filament_cooling_before_tower = m_print->config().filament_cooling_before_tower.values; for (size_t idx = 0; idx < flush_v_speed.size(); ++idx) { if (flush_v_speed[idx] == 0) @@ -8052,7 +8753,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo } this->placeholder_parser().set("current_extruder", new_filament_id); - this->placeholder_parser().set("current_hotend", hotend_id_for_gcode_placeholder(m_config, new_extruder_id)); + this->placeholder_parser().set("current_hotend", + hotend_id_for_gcode_placeholder(m_config, group_result, (int) new_filament_id, new_extruder_id, m_layer_index)); this->placeholder_parser().set("retraction_distance_when_cut", m_config.retraction_distances_when_cut.get_at(new_filament_id)); this->placeholder_parser().set("long_retraction_when_cut", m_config.long_retractions_when_cut.get_at(new_filament_id)); this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 01c253a4fc..9acff7c82c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -252,7 +252,8 @@ public: std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX); bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type); std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } + // extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract. + std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); } std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); @@ -402,6 +403,11 @@ private: std::string preamble(); // BBS std::string change_layer(coordf_t print_z); + // Bedslinger model: derive the Y-axis acceleration limit from the machine force/bed-mass config + // and the mass already printed. Yields the min machine Y acceleration when the A2L config keys are + // unset (i.e. every existing printer), so it is inert for them. + void mass_load_limited_machine_acceleration(const PrintStatistics &curr_print_statistics, const Print &print, + double &y_acceleration_limit_res, double &accumulated_mass_res); // Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop // should be executed std::string extrude_entity(const ExtrusionEntity& entity, @@ -500,6 +506,18 @@ private: std::string extrude_infill(const Print& print, const std::vector& by_region, bool ironing); std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role); + // Farthest-point timelapse: find the extrusion point farthest from camera (0,0) + void compute_farthest_point(const std::vector &layers, int most_used_extruder, + const std::map, unsigned int> &support_filaments); + // Build the per-layer timelapse snapshot g-code (safe-position or, when skip_pos_pick, + // an inline photo at the current head position). Extracted from the former process_layer lambda so the + // per-extrusion farthest-point hook (_extrude) can call it too. Identical to the old lambda when the + // farthest-point subsystem is disabled (skip_pos_pick=false, m_farthest_point_timelapse.enabled=false). + std::string generate_timelapse_gcode(const Print &print, coordf_t print_z, int most_used_extruder, + const std::set *layer_object_label_ids, + const std::vector *printed_objects, + bool skip_pos_pick = false); + // BBS LiftType to_lift_type(ZHopType z_hop_types); @@ -556,6 +574,32 @@ private: AvoidCrossingPerimeters m_avoid_crossing_perimeters; RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters; TimelapsePosPicker m_timelapse_pos_picker; + + // Farthest-point timelapse context. Corexy-only refinement layered on top of the existing + // timelapse_type. All fields default to the inert state; `enabled` is (re)computed each layer in + // process_layer and is false whenever the farthest_point_timelapse config toggle is off, the printer + // is i3 (psI3), or timelapse_type is not traditional — so every shipping printer that does not set the + // toggle is identical to the previous path. + struct FarthestPointTimelapseContext { + // Whether farthest-point timelapse is active for this layer + bool enabled{false}; + // The farthest extrusion point from camera (0,0) in global scaled coordinates (includes plate origin + inst.shift) + Point farthest_point; + // farthest_point converted to mm (gcode coordinate space, includes plate origin) + Vec2d farthest_gcode_pos{0, 0}; + // Extruder index (0-based) that prints the farthest point + int farthest_extruder_id{0}; + // Whether the farthest point is printed by the photo head (most_used_extruder) + bool farthest_is_photo_head{false}; + // Whether inline timelapse gcode has already been inserted on this layer + bool inserted_this_layer{false}; + // The extruder used most on this layer, chosen as the photo head + int most_used_extruder{0}; + // Object labels for the current layer, used when inline timelapse is inserted from extrusion code. + std::set layer_object_label_ids; + }; + FarthestPointTimelapseContext m_farthest_point_timelapse; + bool m_enable_loop_clipping; //resonance avoidance bool m_resonance_avoidance; @@ -595,6 +639,9 @@ private: float m_last_layer_z{ 0.0f }; float m_max_layer_z{ 0.0f }; float m_last_width{ 0.0f }; + // Bedslinger mass model: cumulative printed mass at the previous layer, used to derive + // the current layer mass for the per-layer Y acceleration limit (curr_y_acceleration_limit). + double m_last_layer_accumulated_mass{ 0.0 }; // Always check gcode placeholders when building in debug mode. #if !defined(NDEBUG) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index c0a92780c4..22ed145ca5 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -110,6 +111,21 @@ const std::string GCodeProcessor::VFlush_End_Tag = " VFLUSH_END"; //Orca: External device purge tag const std::string GCodeProcessor::External_Purge_Tag = " EXTERNAL_PURGE"; +// SKIPPABLE region tags. SKIPTYPE carries a trailing "" payload so it is matched with +// starts_with; START/END are whole-line tags. +const std::string GCodeProcessor::Skippable_Start_Tag = " SKIPPABLE_START"; +const std::string GCodeProcessor::Skippable_End_Tag = " SKIPPABLE_END"; +const std::string GCodeProcessor::Skippable_Type_Tag = " SKIPTYPE: "; + +// Usage-block builder markers. START/END machine-gcode markers are whole-line tags; the +// NOZZLE_CHANGE_* and CP_TOOLCHANGE_WIPE tags carry an OF/NF/ON/NN or CT/FL payload and are +// matched with starts_with. +const std::string GCodeProcessor::Machine_Start_GCode_End_Tag = " MACHINE_START_GCODE_END"; +const std::string GCodeProcessor::Machine_End_GCode_Start_Tag = " MACHINE_END_GCODE_START"; +const std::string GCodeProcessor::Nozzle_Change_Start_Tag = " NOZZLE_CHANGE_START"; +const std::string GCodeProcessor::Nozzle_Change_End_Tag = " NOZZLE_CHANGE_END"; +const std::string GCodeProcessor::Toolchange_Wipe_Tag = " CP_TOOLCHANGE_WIPE"; + const float GCodeProcessor::Wipe_Width = 0.05f; const float GCodeProcessor::Wipe_Height = 0.05f; @@ -469,6 +485,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P } time += double(block_time); + // Orca: accumulate per-SkipType time spent inside SKIPPABLE regions, folded into this single + // calculate_time write site. Fires fleet-wide (the shipping time_lapse_gcode template stamps + // blocks stTimelapse), but writes only skippable_part_time, which has no g-code-emitting + // reader until the pre-heat injector consumes it, so output is byte-identical. + if (block.skippable_type != SkipType::stNone) + result.skippable_part_time[block.skippable_type] += block.time(); result.moves[block.move_id].time[static_cast(mode)] = block_time; gcode_time.cache += block_time; //BBS @@ -960,6 +982,22 @@ public: } } + // Map a single first-pass (input) line id to its final output line id, using the same + // original->final table synchronize_moves() applies to the moves. The pre-heat injector's + // usage/skippable blocks are keyed on the pre-M73 input line id; this rebases them into the + // output-line-id space the moves and the second pass both use. Sentinel (unsigned)-1 and any + // id with no exact table entry are returned unchanged, matching synchronize_moves' exact-match rule. + unsigned int remap_gcode_id(unsigned int gcode_id) const + { + if (gcode_id == static_cast(-1)) + return gcode_id; + auto it = std::lower_bound(m_gcode_lines_map.begin(), m_gcode_lines_map.end(), static_cast(gcode_id), + [](const std::pair& e, size_t v) { return e.first < v; }); + if (it != m_gcode_lines_map.end() && it->first == static_cast(gcode_id)) + return static_cast(it->second); + return gcode_id; + } + size_t get_size() const { return m_size; } private: @@ -1392,6 +1430,11 @@ void GCodeProcessor::run_post_process() m_result.lines_ends.clear(); // m_result.lines_ends.emplace_back(std::vector()); + // Orca: freshly collect SKIPPABLE ranges each post-process pass. The ranges are stored on the + // member (rather than a local) so the injection pass can consume them, hence the clear here to + // avoid stale ranges on re-invocation. + m_skippable_blocks.clear(); + unsigned int line_id = 0; // Backtrace data for Tx gcode lines const ExportLines::Backtrace backtrace_T = { m_preheat_time, m_preheat_steps }; @@ -1399,6 +1442,161 @@ void GCodeProcessor::run_post_process() // to flush the backtrace cache accordingly float max_backtrace_time = 120.0f; + // First-pass usage-block builder. Reconstructs, from the emitted g-code, which filament/extruder + // is active over each output-line span so the pre-heat injector can locate idle-hotend windows. + // Gated behind m_enable_pre_heating: the byte-frozen fleet (X1/P1/A1/H2S, which never set the + // flag) runs none of this. It is pure data construction — it only fills m_filament_blocks / + // m_extruder_blocks / m_machine_*_gcode_*_line_id and never touches the exported g-code, so even + // the enable_pre_heating fleet stays byte-identical (nothing reads the blocks until the injection + // pass). In practice it also stays empty/degenerate today because no template/code yet emits the + // MACHINE_*_GCODE_* / NOZZLE_CHANGE_* / CP_TOOLCHANGE_WIPE markers it keys off. + m_filament_blocks.clear(); + m_extruder_blocks.clear(); + m_machine_start_gcode_end_line_id = (unsigned int) (-1); + m_machine_end_gcode_start_line_id = (unsigned int) (-1); + // the first use of an extruder emits no nozzle-change tag, so seed a dummy leading block + if (m_enable_pre_heating) + m_extruder_blocks.push_back(ExtruderPreHeating::ExtruderUsageBlcok()); + // Resolve the concrete layer-aware grouping: get_nozzle_id/get_extruder_id are not on the base + // interface. May be null on slicing paths that never populated it, in which case the builder + // degrades to empty blocks (every dereference below is nil-guarded). + auto layered_ngr = std::dynamic_pointer_cast(m_result.nozzle_group_result); + ExtruderPreHeating::ExtruderUsageBlcok temp_construct_block; // constructed, then pushed after init + + // Append a per-filament usage block at a filament change. + auto handle_filament_change = [&](int filament_id, int cur_line_id, int nozzle_id) { + // skip filament changes emitted inside the machine start / end gcode + if (m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id || + m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id) + return; + if (!m_filament_blocks.empty()) + m_filament_blocks.back().upper_gcode_id = cur_line_id; + if (nozzle_id == -1 && layered_ngr) + nozzle_id = layered_ngr->get_nozzle_id(filament_id, current_layer_id); + int extruder_id = 0; + if (layered_ngr) { + // Orca: nil-guard the optional nozzle lookup. + if (auto nozzle_info = layered_ngr->get_nozzle_from_id(nozzle_id)) + extruder_id = nozzle_info->extruder_id; + } + m_filament_blocks.emplace_back(filament_id, extruder_id, nozzle_id, cur_line_id, (unsigned int) (-1)); + }; + + // Parse a "; NOZZLE_CHANGE_* OF NF ON NN" line. The get_nozzle_from_id optional + // is nil-guarded here. + auto handle_nozzle_change_line = [&](const std::string& line, int& old_filament, int& next_filament, int& extruder_id, int gcode_id, int& old_nozzle_id, int& new_nozzle_id) -> bool { + std::regex re(R"(OF(\d+)\s+NF(\d+)\s+ON(\d+)\s+NN(\d+))"); + std::smatch match; + if (!std::regex_search(line, match, re)) + return false; + old_filament = std::stoi(match[1]); + next_filament = std::stoi(match[2]); + old_nozzle_id = std::stoi(match[3]); + new_nozzle_id = std::stoi(match[4]); + std::optional nozzle_info; + if (layered_ngr) + nozzle_info = layered_ngr->get_nozzle_from_id(new_nozzle_id); + extruder_id = nozzle_info ? nozzle_info->extruder_id : -1; + return true; + }; + + // Parse a "; CP_TOOLCHANGE_WIPE CT [FL]" line. + auto handle_toolchange_wipe_line = [](const std::string& line, bool& is_contact, bool& is_first_layer) -> bool { + std::regex re(R"(CT(\d)(?:\s+FL(\d))?)"); + std::smatch match; + if (!std::regex_search(line, match, re)) + return false; + is_contact = std::stoi(match[1]) != 0; + is_first_layer = match[2].matched ? std::stoi(match[2]) != 0 : false; + return true; + }; + + // Inspect one output line and update the usage-block state. Called only when m_enable_pre_heating; + // never modifies gcode_line. + auto build_usage_blocks = [&](const std::string& gcode_line, int line_id) { + using GLine = GCodeReader::GCodeLine; + // leading-whitespace count (GCodeReader::skip_whitespaces is private; is_whitespace == space|tab) + size_t skips = 0; + while (skips < gcode_line.size() && (gcode_line[skips] == ' ' || gcode_line[skips] == '\t')) + ++skips; + + // whole-line machine start / end gcode markers + if (gcode_line.size() > 1 && gcode_line.front() == ';') { + std::string_view tag(gcode_line); + while (!tag.empty() && (tag.back() == '\n' || tag.back() == '\r')) + tag.remove_suffix(1); + tag.remove_prefix(1); // strip leading ';' + if (tag == Machine_Start_GCode_End_Tag) { m_machine_start_gcode_end_line_id = line_id; return; } + if (tag == Machine_End_GCode_Start_Tag) { m_machine_end_gcode_start_line_id = line_id; return; } + } + + // filament-change commands: T, ;VT, M1020 S, each optionally " H" + if (GLine::cmd_starts_with(gcode_line, "T")) { + std::istringstream str(gcode_line.substr(skips + 1)); // skip whitespace and 'T' + int fid; + str >> fid; + if (!str.fail() && 0 <= fid && fid < 255) { + int nozzle_id = -1; char param; + while (str >> param) { if (param == 'H') { if (!(str >> nozzle_id)) BOOST_LOG_TRIVIAL(warning) << "Invalid nozzle id format in T command: " << gcode_line; break; } } + handle_filament_change(fid, line_id, nozzle_id); + } + return; + } + if (GLine::cmd_starts_with(gcode_line, ";VT")) { + std::istringstream str(gcode_line.substr(skips + 3)); // skip whitespace and ";VT" + int fid; + str >> fid; + if (!str.fail() && 0 <= fid && fid < 255) { + int nozzle_id = -1; char param; + while (str >> param) { if (param == 'H') { if (!(str >> nozzle_id)) BOOST_LOG_TRIVIAL(warning) << "Invalid nozzle id format in VT command: " << gcode_line; break; } } + handle_filament_change(fid, line_id, nozzle_id); + } + return; + } + if (GLine::cmd_starts_with(gcode_line, "M1020")) { + size_t s_pos = gcode_line.find('S'); + if (s_pos != std::string::npos) { + std::istringstream str(gcode_line.substr(s_pos + 1)); + int fid; + str >> fid; + if (!str.fail() && 0 <= fid && fid < 255) { + int nozzle_id = -1; char param; + while (str >> param) { if (param == 'H') { if (!(str >> nozzle_id)) BOOST_LOG_TRIVIAL(warning) << "Invalid nozzle id format in M1020 command: " << gcode_line; break; } } + handle_filament_change(fid, line_id, nozzle_id); + } + } + return; + } + + // extruder usage blocks are delimited by the NOZZLE_CHANGE_START/END markers + if (GLine::cmd_starts_with(gcode_line, (std::string(";") + Nozzle_Change_Start_Tag).c_str())) { + int prev_filament{-1}, next_filament{-1}, extruder_id{-1}, prev_nozzle_id{-1}, next_nozzle_id{-1}; + handle_nozzle_change_line(gcode_line, prev_filament, next_filament, extruder_id, line_id, prev_nozzle_id, next_nozzle_id); + if (!m_extruder_blocks.empty()) + m_extruder_blocks.back().initialize_step_2(line_id); + return; + } + if (GLine::cmd_starts_with(gcode_line, (std::string(";") + Nozzle_Change_End_Tag).c_str())) { + int prev_filament{-1}, next_filament{-1}, extruder_id{-1}, prev_nozzle_id{-1}, next_nozzle_id{-1}; + handle_nozzle_change_line(gcode_line, prev_filament, next_filament, extruder_id, line_id, prev_nozzle_id, next_nozzle_id); + if (!m_extruder_blocks.empty()) + m_extruder_blocks.back().initialize_step_3(line_id, prev_filament, line_id, prev_nozzle_id); + temp_construct_block.initialize_step_1(extruder_id, line_id, next_filament, next_nozzle_id); + m_extruder_blocks.emplace_back(temp_construct_block); + temp_construct_block.reset(); + return; + } + // CP_TOOLCHANGE_WIPE records whether the upcoming tower wipe contacts the model / is on the + // first layer, which later suppresses pre-cooling before the tower. + if (GLine::cmd_starts_with(gcode_line, (std::string(";") + Toolchange_Wipe_Tag).c_str())) { + bool is_contact = false, is_first_layer = false; + handle_toolchange_wipe_line(gcode_line, is_contact, is_first_layer); + if (!m_extruder_blocks.empty()) + m_extruder_blocks.back().ignore_cooling_before_tower = is_contact || is_first_layer; + return; + } + }; + { // Read the input stream 64kB at a time, extract lines and process them. std::vector buffer(65536 * 10, 0); @@ -1444,7 +1642,22 @@ void GCodeProcessor::run_post_process() tag_line.remove_prefix(1); if (tag_line == reserved_tag(ETags::Layer_Change)) ++current_layer_id; + // Collect [start,end] output-line-id ranges of each SKIPPABLE region for the + // pre-heat injector. The `!empty()` guard on END avoids dereferencing .back() + // on an empty vector. The shipping time_lapse_gcode template emits SKIPPABLE_* + // fleet-wide, so this records ~100 ranges per timelapse-on slice; output stays + // byte-identical because it only records line-ids (comment lines pass through + // unmodified) and nothing reads the ranges until the injection pass. + else if (tag_line == Skippable_Start_Tag) + m_skippable_blocks.emplace_back(line_id, 0); + else if (tag_line == Skippable_End_Tag && !m_skippable_blocks.empty()) + m_skippable_blocks.back().second = line_id; } + // Build the first-pass usage blocks off the raw line (before any placeholder + // replacement/clear below). Gated on m_enable_pre_heating so the byte-frozen fleet + // executes nothing; pure data construction, so the output is untouched regardless. + if (m_enable_pre_heating) + build_usage_blocks(gcode_line, line_id); // replace placeholder lines bool processed = process_placeholders(gcode_line); if (processed) @@ -1488,6 +1701,39 @@ void GCodeProcessor::run_post_process() } } + // Close out the usage blocks after the stream. The trailing filament block runs to the end-gcode + // start; the seeded first extruder block gets its start filled from the first filament, and the + // open last extruder block is completed with the trailing filament/nozzle. Gated + pure data — + // no effect on the exported g-code. + if (m_enable_pre_heating) { + if (!m_filament_blocks.empty()) + m_filament_blocks.back().upper_gcode_id = m_machine_end_gcode_start_line_id; + + if (!m_extruder_blocks.empty()) { + int first_filament = 0; + int last_filament = 0; + if (!m_filament_blocks.empty()) { + first_filament = m_filament_blocks.front().filament_id; + last_filament = m_filament_blocks.back().filament_id; + } + { + int extruder_id = -1; + std::optional nozzle_info; + if (layered_ngr) + nozzle_info = layered_ngr->get_first_nozzle_for_filament(first_filament); + if (nozzle_info) + extruder_id = nozzle_info->extruder_id; + int start_nozzle_id = nozzle_info ? nozzle_info->group_id : -1; + m_extruder_blocks.front().initialize_step_1(extruder_id, m_machine_start_gcode_end_line_id, first_filament, start_nozzle_id); + } + m_extruder_blocks.back().initialize_step_2(m_machine_end_gcode_start_line_id); + int last_nozzle_id = -1; + if (!m_filament_blocks.empty()) + last_nozzle_id = m_filament_blocks.back().nozzle_id; + m_extruder_blocks.back().initialize_step_3(m_machine_end_gcode_start_line_id, last_filament, m_machine_end_gcode_start_line_id, last_nozzle_id); + } + } + export_line.flush(out, m_result, out_path); out.close(); @@ -1496,11 +1742,628 @@ void GCodeProcessor::run_post_process() const std::string result_filename = m_result.filename; export_line.synchronize_moves(m_result); + // Rebase the first-pass usage/skippable blocks + the machine start/end gcode line ids from the + // pre-M73 input-line-id space into the final output-line-id space, so they share one coordinate + // system with the moves (whose gcode_ids synchronize_moves() just rebased) and with the + // second-pass line_id. This is the block-side counterpart to synchronize_moves, which does the + // same rebasing for the moves. Gated on m_enable_pre_heating so the byte-frozen fleet skips it + // entirely; on the flag-true fleet it writes only the (inert) block members, so the emitted + // g-code is unaffected either way. + if (m_enable_pre_heating) { + auto remap = [&export_line](unsigned int& id) { id = export_line.remap_gcode_id(id); }; + for (auto& b : m_filament_blocks) { + remap(b.lower_gcode_id); + remap(b.upper_gcode_id); + } + for (auto& b : m_extruder_blocks) { + remap(b.start_id); + remap(b.end_id); + remap(b.post_extrusion_start_id); + remap(b.post_extrusion_end_id); + } + for (auto& b : m_skippable_blocks) { + remap(b.first); + remap(b.second); + } + remap(m_machine_start_gcode_end_line_id); + remap(m_machine_end_gcode_start_line_id); + } + if (rename_file(out_path, result_filename)) throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + result_filename + '\n' + "Is " + out_path + " locked?" + '\n'); } +// Additive second file-rewrite pass for the pre-heat/pre-cool injector. The single-pass +// run_post_process (M73 / filament stats / ActualSpeedMove / Backtrace / machine_tool_change_time / +// is_final EOF hardening) is left untouched; this pass runs AFTER it, reads the finished g-code back, +// and splices the injector's InsertedLinesMap (keyed on the final output line id) after the matching +// lines, then re-shifts every move's gcode_id by the inserted-line count before it (see +// handle_offsets_of_second_process). It runs only when m_enable_pre_heating — the byte-frozen fleet +// (X1/P1/A1/H2S) never sets the flag, so it never enters this pass. The PreCoolingInjector (below) +// fills the map; when the injector produces no lines (no idle windows / no usage blocks) the map is +// empty and the pass is a byte-for-byte identity rewrite of the finished file. +void GCodeProcessor::run_second_pass_injection() +{ + // The ordered map of M632/M400/M104/M633 lines to splice into the finished g-code, keyed on the + // final output line id. Populated by the PreCoolingInjector below. If the injector produces + // nothing (no idle windows / no usage blocks), the map stays empty and the rewrite below is a + // byte-for-byte identity pass. + TimeProcessor::InsertedLinesMap inserted_operation_lines; + + // The enable_pre_heating orchestration block. Build the injector from the reconciled first-pass + // usage/skippable blocks (now in the final output-line-id space, sharing one coordinate with the + // moves' synchronized gcode_ids) and the per-move time substrate, then have it populate + // inserted_operation_lines. Runs under the same m_enable_pre_heating gate finalize() already + // applied, so the byte-frozen fleet (X1/P1/A1/H2S) never reaches here. + { + // Pick the active time mode (Normal/Stealth) whose move.time[] the injector reads. + int valid_machine_id = 0; + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + if (m_time_processor.machines[i].enabled) { + valid_machine_id = (int) i; + break; + } + } + // Reach the concrete layer-aware grouping (get_nozzle_from_id / is_support_dynamic_nozzle_map + // are not on the base interface). Orca: nil-guard it — on a slicing path that never populated + // the grouping, skip injection → empty map → identity rewrite. The shared_ptr local keeps the + // object alive for the injector's const ref. + auto layered_ngr = std::dynamic_pointer_cast(m_result.nozzle_group_result); + if (layered_ngr) { + constexpr float inject_time_threshold = 0.f; // threshold hardcoded to 0 + // Orca: nozzle temps are stored as float (ExtruderTemps) but the injector signature takes + // std::vector, so convert to int locals that outlive the injector (they are + // int-valued floats — see apply_config). + std::vector filament_nozzle_temps(m_filament_nozzle_temp.begin(), m_filament_nozzle_temp.end()); + std::vector filament_nozzle_temps_initial(m_filament_nozzle_temp_first_layer.begin(), m_filament_nozzle_temp_first_layer.end()); + // filament_max_temperature_drop_when_ec is inert (default {0}, never read by the injector); + // pass the same inert default (the config is registered but unwired). + const std::vector filament_max_temperature_drop_when_ec{ 0.f }; + + // Orca: MoveVertex.time[mode] stores the per-move DURATION (calculate_time: `= block_time`), + // but the injector's algorithm reads move.time[] as CUMULATIVE print time (idle-window gap = + // upper.time - prev(lower).time; heating_start back-solve = upper.time - delta/rate). Build a + // cumulative view of the active mode IN PLACE (prefix-sum in gcode-id/move order — moves are + // sorted by gcode_id), run the injector, then RESTORE the per-move durations so every + // downstream consumer (and the byte output) is unperturbed. Only the single active-mode + // column is touched. + std::vector saved_time; + saved_time.reserve(m_result.moves.size()); + { + float acc = 0.f; + for (auto& mv : m_result.moves) { + saved_time.push_back(mv.time[valid_machine_id]); + acc += mv.time[valid_machine_id]; + mv.time[valid_machine_id] = acc; + } + } + + PreCoolingInjector injector( + m_result.moves, + m_filament_types, + *layered_ngr, + filament_nozzle_temps, + filament_nozzle_temps_initial, + m_physical_extruder_map, + valid_machine_id, + inject_time_threshold, + m_handle_hotend_as_extruder, + m_has_filament_switcher, + m_filament_pre_cooling_temp, + m_hotend_cooling_rate, + m_hotend_heating_rate, + m_skippable_blocks, + m_extruder_max_nozzle_count, + m_filament_preheat_temperature_delta, + filament_max_temperature_drop_when_ec, + m_machine_start_gcode_end_line_id, + m_machine_end_gcode_start_line_id, + m_result.extruder_types, + m_nozzle_diameter); + injector.build_extruder_free_blocks(m_filament_blocks, m_extruder_blocks); + injector.process_pre_cooling_and_heating(inserted_operation_lines); + + // Restore the per-move durations (leave the moves exactly as run_post_process produced them). + for (size_t i = 0; i < m_result.moves.size(); ++i) + m_result.moves[i].time[valid_machine_id] = saved_time[i]; + } + } + + FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") }; + if (in.f == nullptr) + throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nCannot open file for reading.\n")); + + const std::string out_path = m_result.filename + ".preheat"; + FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") }; + if (out.f == nullptr) + throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nCannot open file for writing.\n")); + + // The rewrite may shift byte positions (once the injector inserts lines), so rebuild lines_ends from scratch. + // With an empty map the scanned '\n' offsets reproduce the current lines_ends exactly. + m_result.lines_ends.clear(); + size_t out_file_pos = 0; + + auto write_out = [&out, &out_path, this, &out_file_pos](std::string& str) { + if (str.empty()) + return; + fwrite((const void*) str.c_str(), 1, str.length(), out.f); + if (ferror(out.f)) { + out.close(); + boost::nowide::remove(out_path.c_str()); + throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nIs the disk full?\n")); + } + for (size_t i = 0; i < str.size(); ++i) { + if (str[i] == '\n') + m_result.lines_ends.emplace_back(out_file_pos + i + 1); + } + out_file_pos += str.size(); + str.clear(); + }; + + // Orca: read/split lines with EOL-preserving semantics (keep the original \r and \n bytes, and + // synthesize no trailing newline). This is required for the empty-map identity: normalizing every + // line ending to "\n" would not be byte-identical if the finished file used \r\n or lacked a + // final newline. + std::string gcode_line; + std::string export_buffer; + unsigned int line_id = 0; + auto op_it = inserted_operation_lines.begin(); + std::vector buffer(65536 * 10, 0); + for (;;) { + size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f); + if (::ferror(in.f)) + throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nError while reading from file.\n")); + bool eof = cnt_read == 0; + auto it = buffer.begin(); + auto it_bufend = buffer.begin() + cnt_read; + while (it != it_bufend || (eof && !gcode_line.empty())) { + bool eol = false; + auto it_end = it; + for (; it_end != it_bufend && !(eol = *it_end == '\r' || *it_end == '\n'); ++it_end); + eol |= eof && it_end == it_bufend; + gcode_line.insert(gcode_line.end(), it, it_end); + it = it_end; + if (it != it_bufend && *it == '\r') + gcode_line += *it++; + if (it != it_bufend && *it == '\n') + gcode_line += *it++; + if (eol) { + ++line_id; + // Splice any injector lines registered at this output line id. They are appended to the + // end of the current line's text (which already carries its EOL), so they land AFTER + // this line. PlaceholderReplace / TimePredict / ExtruderChangePredict are handled in the + // first pass and skipped here. + if (op_it != inserted_operation_lines.end() && line_id == op_it->first) { + for (const auto& elem : op_it->second) { + switch (elem.second) { + case TimeProcessor::InsertLineType::PreCooling: + case TimeProcessor::InsertLineType::PreHeating: + case TimeProcessor::InsertLineType::FilamentChangePredict: + gcode_line += elem.first; + break; + default: + break; + } + } + ++op_it; + } + export_buffer += gcode_line; + gcode_line.clear(); + if (export_buffer.length() >= 65536) + write_out(export_buffer); + } + } + if (eof) + break; + } + write_out(export_buffer); + + out.close(); + in.close(); + + // Re-shift the moves by the inserted-line counts (no-op with an empty map). + handle_offsets_of_second_process(inserted_operation_lines); + + if (rename_file(out_path, m_result.filename)) + throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + m_result.filename + '\n' + + "Is " + out_path + " locked?" + '\n'); +} + +// Every move whose gcode_id sits at or after an inserted line is pushed down by the running count of +// injector lines inserted before it. +void GCodeProcessor::handle_offsets_of_second_process(const TimeProcessor::InsertedLinesMap& inserted_operation_lines) +{ + int total_offset = 0; + auto iter = inserted_operation_lines.begin(); + for (GCodeProcessorResult::MoveVertex& move : m_result.moves) { + while (iter != inserted_operation_lines.end() && iter->first < move.gcode_id) { + total_offset += static_cast(iter->second.size()); + ++iter; + } + move.gcode_id += total_offset; + } +} + +// The pre-cool / pre-heat injection engine. See the class comment in the header. The methods populate +// a TimeProcessor::InsertedLinesMap (keyed on the final output line id) that run_second_pass_injection +// splices into the finished g-code. + +// Group the free-blocks per extruder; for each, always pre-cool, and pre-heat all-but-the-last; derive +// curr/target temps from the filament nozzle temps +/- filament_preheat_temperature_delta; apply the +// X2D mixed-extruder-type workaround. +void GCodeProcessor::PreCoolingInjector::process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines) +{ + bool is_multiple_nozzle = std::any_of(extruder_max_nozzle_count.begin(), extruder_max_nozzle_count.end(), [](auto& elem) { return elem > 1; }); + (void) is_multiple_nozzle; // computed but currently unused here + auto get_nozzle_temp = [this](int filament_id, bool is_first_layer, bool from_or_to, bool consider_preheat_temperature_delta) { + if (filament_id == -1) + return from_or_to ? 140 : 0; // default temp + double temp = (is_first_layer ? filament_nozzle_temps_initial_layer[filament_id] : filament_nozzle_temps[filament_id]); + if (consider_preheat_temperature_delta) + return (int) (temp - filament_preheat_temperature_delta[filament_id]); + else + return (int) (temp); + }; + + // Temporary workaround for X2D: when extruder types are mixed (e.g. DirectDrive + Bowden), + // limit pre-heating target to avoid overshooting on the slower-responding extruder. + bool has_mixed_extruder_types = extruder_types.size() > 1 && + std::adjacent_find(extruder_types.begin(), extruder_types.end(), std::not_equal_to<>()) != extruder_types.end(); + // Temporary workaround for X2D: Use the first nozzle diameter to determine the temp offset: 40 for large nozzles (0.6/0.8), 20 for others + float first_nozzle_dia = nozzle_diameter.empty() ? 0.4 : nozzle_diameter.front(); + float switcher_temp_offset = (first_nozzle_dia >= 0.6 - EPSILON) ? 40.f : 20.f; + + std::map> per_extruder_free_blocks; + + for (auto& block : m_extruder_free_blocks) + per_extruder_free_blocks[block.extruder_id].emplace_back(block); + + for (auto& elem : per_extruder_free_blocks) { + auto& extruder_free_blcoks = elem.second; + for (auto iter = extruder_free_blcoks.begin(); iter != extruder_free_blcoks.end(); ++iter) { + bool is_end = std::next(iter) == extruder_free_blcoks.end(); + bool apply_pre_cooling = true; + bool apply_pre_heating = is_end ? false : true; + float curr_temp = get_nozzle_temp(iter->last_filament_id, false, true, false); + float target_temp = get_nozzle_temp(iter->next_filament_id, false, false, !iter->ignore_cooling_before_tower); + // X2D temporary workaround: only apply temp offset when extruder types are mixed + if (has_filament_switcher && has_mixed_extruder_types && apply_pre_heating) { + float print_temp = get_nozzle_temp(iter->next_filament_id, false, false, false); + target_temp = std::min(target_temp, print_temp - switcher_temp_offset); + } + inject_cooling_heating_command(inserted_operation_lines, *iter, curr_temp, target_temp, apply_pre_cooling, apply_pre_heating); + } + } +} + +// A single extruder-usage block (or none) means the print stays on one extruder, so the idle windows +// are between per-filament usages; otherwise the print switches extruders and the idle windows are +// between per-extruder usages. +void GCodeProcessor::PreCoolingInjector::build_extruder_free_blocks(const std::vector& filament_usage_blocks, const std::vector& extruder_usage_blocks) +{ + if (extruder_usage_blocks.size() <= 1) + build_by_filament_blocks(filament_usage_blocks); + else + build_by_extruder_blocks(extruder_usage_blocks); +} + +// The core injector. Measures the idle-window duration from move.time[valid_machine_id], bails if it is +// below the threshold, and emits pre-cool M104 at the window start and pre-heat M104 (relocated out of +// SKIPPABLE blocks) at the back-solved heating-start move. +void GCodeProcessor::PreCoolingInjector::inject_cooling_heating_command(TimeProcessor::InsertedLinesMap& inserted_operation_lines, const ExtruderFreeBlock& block, float curr_temp, float target_temp, bool pre_cooling, bool pre_heating) +{ + auto get_valid_extruder_id = [&](int last_nozzle_id) { + auto nozzle_opt = nozzle_group_result.get_nozzle_from_id(last_nozzle_id); + return nozzle_opt ? nozzle_opt->extruder_id : 0; + }; + + auto is_pre_cooling_valid = [&nozzle_temps = this->filament_nozzle_temps, &pre_cooling_temps = this->filament_pre_cooling_temps](int idx) -> bool { + if (idx < 0) + return false; + return pre_cooling_temps[idx] > 0 && pre_cooling_temps[idx] < nozzle_temps[idx]; + }; + + auto get_partial_free_cooling_thres = [&](int idx) -> float { + if (idx < 0) + return 30.f; + float temp_in_tower = filament_nozzle_temps[idx]; + return temp_in_tower - (float) (filament_pre_cooling_temps[idx]); + }; + + auto gcode_move_comp = [](const GCodeProcessorResult::MoveVertex& a, unsigned int gcode_id) { + return a.gcode_id < gcode_id; + }; + + auto find_skip_block_end = [&skippable_blocks = this->skippable_blocks](unsigned int gcode_id) -> unsigned int { + auto it = std::upper_bound( + skippable_blocks.begin(), skippable_blocks.end(), gcode_id, + [](unsigned int id, const std::pair& block) { return id < block.first; }); + if (it != skippable_blocks.begin()) { + auto candidate = std::prev(it); + if (gcode_id >= candidate->first && gcode_id <= candidate->second) + return candidate->second; + } + return 0; + }; + + auto find_skip_block_start = [&skippable_blocks = this->skippable_blocks](unsigned int gcode_id) -> unsigned int { + auto it = std::upper_bound( + skippable_blocks.begin(), skippable_blocks.end(), gcode_id, + [](unsigned int id, const std::pair& block) { return id < block.first; }); + if (it != skippable_blocks.begin()) { + auto candidate = std::prev(it); + if (gcode_id >= candidate->first && gcode_id <= candidate->second) + return candidate->first; + } + return 0; + }; + + auto adjust_iter = [&](std::vector::const_iterator iter, + const std::vector::const_iterator& begin, + const std::vector::const_iterator& end, + bool forward) -> std::vector::const_iterator { + if (forward) { + while (iter != end) { + unsigned current_id = iter->gcode_id; + unsigned skip_block_end = find_skip_block_end(current_id); + if (skip_block_end == 0) + break; + iter = std::lower_bound(iter, end, skip_block_end + 1, gcode_move_comp); + } + } else { + while (iter != begin) { + unsigned current_id = iter->gcode_id; + unsigned skip_block_start = find_skip_block_start(current_id); + if (skip_block_start == 0) + break; + auto new_iter = std::lower_bound(begin, iter, skip_block_start, gcode_move_comp); + if (new_iter == begin) + break; + iter = std::prev(new_iter); + } + } + return iter; + }; + + if (!pre_cooling && !pre_heating && block.free_upper_gcode_id <= block.free_lower_gcode_id) + return; + + auto move_iter_lower = std::lower_bound(moves.begin(), moves.end(), block.free_lower_gcode_id, gcode_move_comp); + auto move_iter_upper = std::lower_bound(moves.begin(), moves.end(), block.free_upper_gcode_id, gcode_move_comp); // closed iter + + if (move_iter_lower == moves.end() || move_iter_upper == moves.begin()) + return; + --move_iter_upper; + float complete_free_time_gap = 0; // time of complete free + if (move_iter_lower == moves.begin()) + complete_free_time_gap = move_iter_upper->time[valid_machine_id]; + else + complete_free_time_gap = move_iter_upper->time[valid_machine_id] - std::prev(move_iter_lower)->time[valid_machine_id]; + + auto partial_free_move_lower = std::lower_bound(moves.begin(), moves.end(), block.partial_free_lower_id, gcode_move_comp); + auto partial_free_move_upper = std::lower_bound(moves.begin(), moves.end(), block.partial_free_upper_id, gcode_move_comp); // closed iter + if (partial_free_move_lower == moves.end() || partial_free_move_upper == moves.begin()) + return; + --partial_free_move_upper; + float partial_free_time_gap = 0; // time of partial free + if (partial_free_move_lower == moves.begin()) + partial_free_time_gap = partial_free_move_upper->time[valid_machine_id]; + else + partial_free_time_gap = partial_free_move_upper->time[valid_machine_id] - std::prev(partial_free_move_lower)->time[valid_machine_id]; + + if (move_iter_lower >= move_iter_upper) + return; + + bool apply_cooling_when_partial_free = is_pre_cooling_valid(block.last_filament_id) && pre_cooling; + + if (apply_cooling_when_partial_free && partial_free_time_gap + complete_free_time_gap < inject_time_threshold) + return; + + if (!apply_cooling_when_partial_free && complete_free_time_gap < inject_time_threshold) + return; + + int extruder_id = get_valid_extruder_id(block.last_nozzle_id); + float ext_heating_rate = heating_rate[extruder_id]; + float ext_cooling_rate = cooling_rate[extruder_id]; + + auto add_M104_lines = [&](int gcode_id, int target_extruder, int target_temp, int target_filament, bool skippable, int next_filament_idx, int next_nozzle_id, TimeProcessor::InsertLineType type, const std::string& comment = std::string()) { + auto format_line_M104 = [&](int target_extruder, int target_temp, int target_filament, bool skippable, int next_filament_idx, int next_nozzle_id, const std::string& comment = std::string()) -> std::vector { + std::vector buffer; + if (skippable) { + const bool support_dynamic_nozzle_map = this->nozzle_group_result.is_support_dynamic_nozzle_map(); + std::string m632_line = "M632 S" + std::to_string(next_filament_idx); + if (support_dynamic_nozzle_map) + m632_line += " H" + std::to_string(next_nozzle_id); + if (extruder_max_nozzle_count[target_extruder] > 1) + m632_line += " N R"; + m632_line += " W\n"; + buffer.emplace_back(std::move(m632_line)); + } + buffer.emplace_back("M400\n"); + std::string M104_line = "M104"; + if (handle_hotend_as_extruder) { + M104_line += (" I" + std::to_string(target_filament == -1 ? next_filament_idx : target_filament)); + } else if (target_extruder != -1) { + M104_line += (" T" + std::to_string(physical_extruder_map[target_extruder])); + } + + M104_line += " S" + std::to_string(target_temp); + M104_line += " N0"; // N0 means the gcode is generated by slicer + + if (!comment.empty()) + M104_line += " ;" + comment; + M104_line += '\n'; + + buffer.emplace_back(M104_line); + + if (skippable) + buffer.emplace_back("M633\n"); + + return buffer; + }; + + std::vector line_buf = format_line_M104(target_extruder, target_temp, target_filament, skippable, next_filament_idx, next_nozzle_id, comment); + for (auto& line : line_buf) + inserted_operation_lines[gcode_id].emplace_back(line, type); + }; + + constexpr float room_temperature = 25.f; + + if (apply_cooling_when_partial_free) { + float max_cooling_temp = std::min(curr_temp, std::min(get_partial_free_cooling_thres(block.last_filament_id), partial_free_time_gap * ext_cooling_rate)); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": partial cooling for %1% %2%") % max_cooling_temp % curr_temp; + curr_temp = std::max(room_temperature, curr_temp - max_cooling_temp); // set the temperature after doing cooling when post-extruding + add_M104_lines(block.partial_free_lower_id, extruder_id, curr_temp, block.last_filament_id, false, block.next_filament_id, block.next_nozzle_id, TimeProcessor::InsertLineType::PreCooling, "Multi extruder pre cooling in post extrusion"); + } + + if (pre_cooling && !pre_heating) { + // only perform cooling + if (target_temp >= curr_temp) + return; + int clamped_target = std::max((int) room_temperature, (int) target_temp); + add_M104_lines(block.free_lower_gcode_id, extruder_id, clamped_target, block.last_filament_id, false, block.next_filament_id, block.next_nozzle_id, TimeProcessor::InsertLineType::PreCooling, "Multi extruder pre cooling"); + return; + } + if (!pre_cooling && pre_heating) { + // only perform heating + if (target_temp <= curr_temp) + return; + float heating_start_time = move_iter_upper->time[valid_machine_id] - (target_temp - curr_temp) / ext_heating_rate; + auto heating_move_iter = std::upper_bound(move_iter_lower, move_iter_upper + 1, heating_start_time, [valid_machine_id = this->valid_machine_id](float time, const GCodeProcessorResult::MoveVertex& a) { return time < a.time[valid_machine_id]; }); + if (heating_move_iter == move_iter_lower) { + add_M104_lines(block.free_lower_gcode_id, extruder_id, target_temp, block.next_filament_id, true, block.next_filament_id, block.next_nozzle_id, TimeProcessor::InsertLineType::PreHeating, "Multi extruder pre heating"); + } else { + --heating_move_iter; + heating_move_iter = adjust_iter(heating_move_iter, move_iter_lower, move_iter_upper, false); + add_M104_lines(heating_move_iter->gcode_id, extruder_id, target_temp, block.next_filament_id, true, block.next_filament_id, block.next_nozzle_id, TimeProcessor::InsertLineType::PreHeating, "Multi extruder pre heating"); + } + return; + } + // perform cooling first and then perform heating + float mid_temp = std::max(room_temperature, (curr_temp * ext_heating_rate + target_temp * ext_cooling_rate - complete_free_time_gap * ext_cooling_rate * ext_heating_rate) / (ext_cooling_rate + ext_heating_rate)); + float heating_temp = target_temp - mid_temp; + float heating_start_time = move_iter_upper->time[valid_machine_id] - heating_temp / ext_heating_rate; + auto heating_move_iter = std::upper_bound(move_iter_lower, move_iter_upper + 1, heating_start_time, [valid_machine_id = this->valid_machine_id](float time, const GCodeProcessorResult::MoveVertex& a) { return time < a.time[valid_machine_id]; }); + if (heating_move_iter == move_iter_lower) + return; + --heating_move_iter; + heating_move_iter = adjust_iter(heating_move_iter, move_iter_lower, move_iter_upper, false); + + // get the insert pos of heat cmd and recalculate time gap and delta temp + float real_cooling_time = heating_move_iter->time[valid_machine_id] - move_iter_lower->time[valid_machine_id]; + int real_delta_temp = std::min((int) (real_cooling_time * ext_cooling_rate), (int) curr_temp); + if (real_delta_temp == 0) + return; + int cooling_temp = std::max((int) room_temperature, (int) curr_temp - real_delta_temp); + add_M104_lines(block.free_lower_gcode_id, extruder_id, cooling_temp, block.last_filament_id, false, block.next_filament_id, block.next_nozzle_id, TimeProcessor::InsertLineType::PreCooling, "Multi extruder pre cooling"); + add_M104_lines(heating_move_iter->gcode_id, extruder_id, target_temp, block.next_filament_id, true, block.next_filament_id, block.next_nozzle_id, TimeProcessor::InsertLineType::PreHeating, "Multi extruder pre heating"); +} + +// Single-extruder / A2L path. Idle windows are the gaps between consecutive per-filament usages on the +// same extruder; ignore_cooling_before_tower is forced true (pre-cooling before the tower is handled by +// the wipe-tower template, not the injector). +void GCodeProcessor::PreCoolingInjector::build_by_filament_blocks(const std::vector& filament_usage_blocks_) +{ + m_extruder_free_blocks.clear(); + + std::map> per_extruder_usage_blocks; + for (auto& block : filament_usage_blocks_) { + per_extruder_usage_blocks[block.extruder_id].emplace_back(block); + } + ExtruderPreHeating::FilamentUsageBlock start_filament_block(-1, -1, -1, 0, machine_start_gcode_end_id); + ExtruderPreHeating::FilamentUsageBlock end_filament_block(-1, -1, -1, machine_end_gcode_start_id, std::numeric_limits::max()); + + for (auto& elem : per_extruder_usage_blocks) { + auto& blocks = elem.second; + blocks.insert(blocks.begin(), start_filament_block); + blocks.emplace_back(end_filament_block); + } + + for (auto& elem : per_extruder_usage_blocks) { + size_t extruder_id = elem.first; + const auto& filament_blocks = elem.second; + + for (auto iter = filament_blocks.begin(); iter < filament_blocks.end(); ++iter) { + auto niter = std::next(iter); + if (niter == filament_blocks.end()) + break; + ExtruderFreeBlock block; + block.free_lower_gcode_id = iter->upper_gcode_id; + block.last_filament_id = iter->filament_id; + block.last_nozzle_id = iter->nozzle_id; + block.free_upper_gcode_id = niter->lower_gcode_id; + block.next_filament_id = niter->filament_id; + block.next_nozzle_id = niter->nozzle_id; + if (block.last_nozzle_id == -1) + block.last_nozzle_id = block.next_nozzle_id; + block.extruder_id = extruder_id; + block.partial_free_lower_id = block.free_lower_gcode_id; + block.partial_free_upper_id = block.free_lower_gcode_id; + m_extruder_free_blocks.emplace_back(block); + } + } + std::for_each(m_extruder_free_blocks.begin(), m_extruder_free_blocks.end(), [](ExtruderFreeBlock& block) { block.ignore_cooling_before_tower = true; }); + sort(m_extruder_free_blocks.begin(), m_extruder_free_blocks.end(), [](const auto& a, const auto& b) { + return a.free_lower_gcode_id < b.free_lower_gcode_id || (a.free_lower_gcode_id == b.free_lower_gcode_id && a.free_upper_gcode_id < b.free_upper_gcode_id); + }); +} + +// Multi-extruder path. Idle windows are the gaps between consecutive per-extruder usages, with +// post-extrusion partial-free sub-ranges (the tower ramming region). +void GCodeProcessor::PreCoolingInjector::build_by_extruder_blocks(const std::vector& extruder_usage_blocks_) +{ + m_extruder_free_blocks.clear(); + std::map> per_extruder_usage_blocks; + for (auto& block : extruder_usage_blocks_) + per_extruder_usage_blocks[block.extruder_id].emplace_back(block); + + for (auto& elem : per_extruder_usage_blocks) { + size_t extruder_id = elem.first; + auto& blocks = elem.second; + ExtruderPreHeating::ExtruderUsageBlcok start_filament_block; + start_filament_block.initialize_step_1(extruder_id, 0, -1, -1); + start_filament_block.initialize_step_2(machine_start_gcode_end_id); + start_filament_block.initialize_step_3(machine_start_gcode_end_id, -1, machine_start_gcode_end_id, -1); + + ExtruderPreHeating::ExtruderUsageBlcok end_filament_block; + end_filament_block.initialize_step_1(extruder_id, machine_end_gcode_start_id, -1, -1); + end_filament_block.initialize_step_2(std::numeric_limits::max()); + end_filament_block.initialize_step_3(std::numeric_limits::max(), -1, std::numeric_limits::max(), -1); + + blocks.insert(blocks.begin(), start_filament_block); + blocks.emplace_back(end_filament_block); + } + + for (auto& elem : per_extruder_usage_blocks) { + size_t extruder_id = elem.first; + const auto& extruder_usage_blocks = elem.second; + for (auto iter = extruder_usage_blocks.begin(); iter != extruder_usage_blocks.end(); ++iter) { + auto niter = std::next(iter); + if (niter == extruder_usage_blocks.end()) + break; + ExtruderFreeBlock block; + block.free_lower_gcode_id = iter->end_id; + block.last_filament_id = iter->end_filament; + block.last_nozzle_id = iter->end_nozzle_id; + block.free_upper_gcode_id = niter->start_id; + block.next_filament_id = niter->start_filament; + block.next_nozzle_id = niter->start_nozzle_id; + if (block.last_nozzle_id == -1) + block.last_nozzle_id = block.next_nozzle_id; + block.extruder_id = extruder_id; + block.partial_free_lower_id = iter->post_extrusion_start_id; + block.partial_free_upper_id = iter->post_extrusion_end_id; + block.ignore_cooling_before_tower = niter->ignore_cooling_before_tower; + m_extruder_free_blocks.emplace_back(block); + } + } + + sort(m_extruder_free_blocks.begin(), m_extruder_free_blocks.end(), [](const auto& a, const auto& b) { + return a.free_lower_gcode_id < b.free_lower_gcode_id || (a.free_lower_gcode_id == b.free_lower_gcode_id && a.free_upper_gcode_id < b.free_upper_gcode_id); + }); +} + void GCodeProcessor::UsedFilaments::reset() { color_change_cache = 0.0f; @@ -1681,6 +2544,12 @@ void GCodeProcessorResult::reset() { optimal_assignment.clear(); filament_change_count_map.clear(); warnings.clear(); + // keep the grouping-result field default-empty across resets. + nozzle_group_result.reset(); + // per-extruder hotend types (pre-heat injector input); repopulated by apply_config. + extruder_types.clear(); + // SKIPPABLE per-type accumulated time. + skippable_part_time.clear(); //BBS: add mutex for protection of gcode result unlock(); @@ -2056,6 +2925,26 @@ void GCodeProcessor::apply_config(const PrintConfig& config) m_nozzle_volume[idx] = config.nozzle_volume.values[idx]; m_physical_extruder_map = config.physical_extruder_map.values; + // Multi-nozzle context state, consumed by the multi-nozzle time model and pre-heat injector. + m_extruder_max_nozzle_count = config.extruder_max_nozzle_count.values; + + // Pre-heat / pre-cool injector estimator inputs. Gated on enable_pre_heating; the injector + // side-pass that consumes them runs later in run_second_pass_injection. has_filament_switcher is + // read defensively because it is registered as a ConfigDef only, not a static PrintConfig member. + m_filament_types.resize(filament_count); + for (size_t idx = 0; idx < filament_count; ++idx) + m_filament_types[idx] = config.filament_type.get_at(idx); + m_nozzle_diameter = config.nozzle_diameter.values; + m_hotend_cooling_rate = config.hotend_cooling_rate.values; + m_hotend_heating_rate = config.hotend_heating_rate.values; + m_filament_pre_cooling_temp = config.filament_pre_cooling_temperature.values; + m_filament_preheat_temperature_delta = config.filament_preheat_temperature_delta.values; + m_enable_pre_heating = config.enable_pre_heating.value; + if (const ConfigOptionBool* has_switcher = config.option("has_filament_switcher")) + m_has_filament_switcher = has_switcher->value; + m_result.extruder_types.resize(config.extruder_type.values.size()); + for (size_t idx = 0; idx < config.extruder_type.values.size(); ++idx) + m_result.extruder_types[idx] = static_cast(config.extruder_type.values[idx]); m_extruder_offsets.resize(filament_count); m_extruder_colors.resize(filament_count); @@ -2184,6 +3073,61 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) m_physical_extruder_map = physical_extruder_map->values; } + // Multi-nozzle context state, consumed by the multi-nozzle time model and pre-heat injector. + const ConfigOptionIntsNullable* extruder_max_nozzle_count = config.option("extruder_max_nozzle_count"); + if (extruder_max_nozzle_count != nullptr) { + m_extruder_max_nozzle_count = extruder_max_nozzle_count->values; + } + + // Pre-heat / pre-cool injector estimator inputs. Gated on enable_pre_heating; consumed later by the + // injector side-pass. handle_hotend_as_extruder is read only in this overload; nozzle_diameter uses + // the non-nullable ConfigOptionFloats type. + const ConfigOptionFloatsNullable* hotend_cooling_rate = config.option("hotend_cooling_rate"); + if (hotend_cooling_rate != nullptr) + m_hotend_cooling_rate = hotend_cooling_rate->values; + + const ConfigOptionFloatsNullable* hotend_heating_rate = config.option("hotend_heating_rate"); + if (hotend_heating_rate != nullptr) + m_hotend_heating_rate = hotend_heating_rate->values; + + const ConfigOptionIntsNullable* filament_pre_cooling_temperature = config.option("filament_pre_cooling_temperature"); + if (filament_pre_cooling_temperature != nullptr) + m_filament_pre_cooling_temp = filament_pre_cooling_temperature->values; + + const ConfigOptionFloatsNullable* filament_preheat_temperature_delta = config.option("filament_preheat_temperature_delta"); + if (filament_preheat_temperature_delta != nullptr) + m_filament_preheat_temperature_delta = filament_preheat_temperature_delta->values; + + const ConfigOptionBool* enable_pre_heating = config.option("enable_pre_heating"); + if (enable_pre_heating != nullptr) + m_enable_pre_heating = enable_pre_heating->value; + + const ConfigOptionBool* handle_hotend_as_extruder = config.option("handle_hotend_as_extruder"); + if (handle_hotend_as_extruder != nullptr) + m_handle_hotend_as_extruder = handle_hotend_as_extruder->value; + + const ConfigOptionBool* has_filament_switcher = config.option("has_filament_switcher"); + if (has_filament_switcher != nullptr) + m_has_filament_switcher = has_filament_switcher->value; + + const ConfigOptionFloats* nozzle_diameter = config.option("nozzle_diameter"); + if (nozzle_diameter != nullptr) + m_nozzle_diameter = nozzle_diameter->values; + + const ConfigOptionStrings* filament_type = config.option("filament_type"); + if (filament_type != nullptr) { + m_filament_types.resize(filament_type->size()); + for (size_t idx = 0; idx < filament_type->size(); ++idx) + m_filament_types[idx] = filament_type->get_at(idx); + } + + const ConfigOptionEnumsGeneric* extruder_type = config.option("extruder_type"); + if (extruder_type != nullptr) { + m_result.extruder_types.resize(extruder_type->values.size()); + for (size_t idx = 0; idx < extruder_type->values.size(); ++idx) + m_result.extruder_types[idx] = static_cast(extruder_type->values[idx]); + } + const ConfigOptionEnumsGenericNullable* nozzle_type = config.option("nozzle_type"); if (nozzle_type != nullptr) { m_result.nozzle_type.resize(nozzle_type->size()); @@ -2520,6 +3464,15 @@ void GCodeProcessor::reset() m_flushing = false; m_virtual_flushing = false; m_wipe_tower = false; + // reset SKIPPABLE tracking + the collected ranges (stored on the member; see run_post_process). + m_skippable = false; + m_skippable_type = SkipType::stNone; + m_skippable_blocks.clear(); + // reset the first-pass usage-block state (rebuilt each run_post_process). + m_filament_blocks.clear(); + m_extruder_blocks.clear(); + m_machine_start_gcode_end_line_id = (unsigned int) (-1); + m_machine_end_gcode_start_line_id = (unsigned int) (-1); m_remaining_volume = std::vector(MAXIMUM_EXTRUDER_NUMBER, 0.f); m_line_id = 0; @@ -2539,6 +3492,9 @@ void GCodeProcessor::reset() m_filament_id = std::vector(MAXIMUM_EXTRUDER_NUMBER, static_cast(-1)); m_last_filament_id = std::vector(MAXIMUM_EXTRUDER_NUMBER, static_cast(-1)); m_extruder_id = static_cast(-1); + // clear the multi-nozzle occupancy tracker between slices (the richer hotend-change model's only + // mutable state). Inert for the single-nozzle fleet (never populated). + m_nozzle_status_recorder = MultiNozzleUtils::NozzleStatusRecorder{}; m_extruder_colors.resize(MIN_EXTRUDERS_COUNT); for (size_t i = 0; i < MIN_EXTRUDERS_COUNT; ++i) { m_extruder_colors[i] = static_cast(i); @@ -2549,6 +3505,18 @@ void GCodeProcessor::reset() } m_physical_extruder_map.clear(); + m_extruder_max_nozzle_count = { 1 }; + + // reset pre-heat / pre-cool injector estimator inputs to defaults. Repopulated by apply_config + // each slice. + m_filament_types.clear(); + m_nozzle_diameter.clear(); + m_hotend_cooling_rate = m_hotend_heating_rate = { 2.f }; + m_filament_pre_cooling_temp = { 0 }; + m_filament_preheat_temperature_delta.clear(); + m_enable_pre_heating = false; + m_handle_hotend_as_extruder = false; + m_has_filament_switcher = false; m_highest_bed_temp = 0; @@ -2726,6 +3694,12 @@ void GCodeProcessor::finalize(bool post_process) if (post_process){ run_post_process(); + // Additive pre-heat/pre-cool injection second pass. Gated on m_enable_pre_heating so the + // byte-frozen fleet (X1/P1/A1/H2S) never enters it. When the injector produces no lines the + // InsertedLinesMap is empty, so this is a byte-for-byte identity rewrite; it exercises the + // merge/offset machinery on the multi-nozzle fleet (H2D/X2D/H2D-Pro/H2C). + if (m_enable_pre_heating) + run_second_pass_injection(); } //BBS: update slice warning update_slice_warnings(); @@ -3178,6 +4152,29 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + // SKIPPABLE region tags. Mark the current section so its time blocks are stamped with the skip + // type. The shipping time_lapse_gcode template emits these tags fleet-wide, so this parse fires on + // essentially every slice (m_skippable → true, m_skippable_type → stTimelapse); it only sets + // internal state and early-returns, so the output is byte-identical until the injector reads the + // stamps. + if (boost::starts_with(comment, GCodeProcessor::Skippable_Start_Tag)) { + m_skippable = true; + return; + } + + if (boost::starts_with(comment, GCodeProcessor::Skippable_End_Tag)) { + m_skippable = false; + m_skippable_type = SkipType::stNone; + return; + } + + // skippable type + if (boost::starts_with(comment, GCodeProcessor::Skippable_Type_Tag)) { + std::string_view type = comment.substr(GCodeProcessor::Skippable_Type_Tag.length()); + set_skippable_type(type); + return; + } + //BBS: flush start tag if (boost::starts_with(comment, GCodeProcessor::Flush_Start_Tag)) { m_flushing = true; @@ -4009,6 +5006,10 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes TimeBlock block; block.move_type = type; + // stamp the current SKIPPABLE type onto the block. Stamped stTimelapse inside the fleet-wide + // time_lapse_gcode regions; byte-inert because nothing reads the stamp until the injector + // consumes it. + block.skippable_type = m_skippable_type; //BBS: don't calculate travel time into extrusion path, except travel inside start and end gcode. block.role = (type != EMoveType::Travel || m_extrusion_role == erCustom) ? m_extrusion_role : erNone; block.distance = distance; @@ -4369,6 +5370,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) TimeBlock block; block.move_type = type; + // stamp the current SKIPPABLE type onto the block. Stamped stTimelapse inside the fleet-wide + // time_lapse_gcode regions; byte-inert because nothing reads the stamp until the injector + // consumes it. + block.skippable_type = m_skippable_type; //BBS: don't calculate travel time into extrusion path, except travel inside start and end gcode. block.role = (type != EMoveType::Travel || m_extrusion_role == erCustom) ? m_extrusion_role : erNone; block.distance = distance; @@ -5450,7 +6455,14 @@ void GCodeProcessor::process_SYNC(const GCodeReader::GCodeLine& line) void GCodeProcessor::process_T(const GCodeReader::GCodeLine& line) { - process_T(line.cmd()); + // parse the H logical-nozzle id off the T line. H2C's change_filament template emits + // `T H`; the single-nozzle fleet emits a bare `T` (H absent → -1 → byte-inert + // via the self-gate). + int nozzle_id = -1; + float nozzle_val = 0.f; + if (line.has_value('H', nozzle_val)) + nozzle_id = static_cast(nozzle_val); + process_T(line.cmd(), nozzle_id); } void GCodeProcessor::process_M1020(const GCodeReader::GCodeLine &line) @@ -5474,12 +6486,25 @@ void GCodeProcessor::process_M1020(const GCodeReader::GCodeLine &line) BOOST_LOG_TRIVIAL(error) << "Invalid M1020 command (" << line.raw() << ")."; return; } - process_filament_change(eid); + // carry the H logical-nozzle id. process_filament_change(int,int) self-gates back + // to the single-arg model for the single-nozzle fleet, so this is byte-inert for them. + int nozzle_id = -1; + float nozzle_val = 0.f; + if (line.has_value('H', nozzle_val)) + nozzle_id = static_cast(nozzle_val); + process_filament_change(eid, nozzle_id); } } } void GCodeProcessor::process_T(const std::string_view command) +{ + // the no-nozzle-context callers (custom-gcode tool-change strings, color-print T parsing) route + // through the H-less variant; -1 keeps the richer model on its filament-first nozzle fallback. + process_T(command, -1); +} + +void GCodeProcessor::process_T(const std::string_view command, int nozzle_id) { unsigned int eid = 0; auto ret = std::from_chars(command.data() + 1, command.data()+command.size(), eid); @@ -5505,7 +6530,9 @@ void GCodeProcessor::process_T(const std::string_view command) BOOST_LOG_TRIVIAL(error) << "Invalid T command (" << command << ")."; return; } - process_filament_change(eid); + // process_filament_change(int,int) self-gates back to the single-arg model for the + // single-nozzle fleet, so this call is byte-inert for X1/P1/A1/H2S/A2L. + process_filament_change(eid, nozzle_id); } } } @@ -5521,6 +6548,148 @@ void GCodeProcessor::init_filament_maps_and_nozzle_type_when_import_only_gcode() } } +bool GCodeProcessor::use_multi_nozzle_change_time_model() const +{ + // Multi-nozzle context = a printer that can incur nozzle-change events the single-arg model + // cannot distinguish: either a multi-extruder machine (nozzle_diameter has >1 physical extruder, + // e.g. H2D/X2D/H2D-Pro) or an extruder that carries a nozzle cluster (extruder_max_nozzle_count>1, + // e.g. H2C's {1,6}). Every single-extruder single-nozzle printer (X1/P1/A1/H2S/A2L) fails both + // tests → false → they keep the byte-frozen single-arg time model. Both members are populated by + // apply_config (each overload) and cleared in reset(), so this is stream-time safe. + if (m_nozzle_diameter.size() > 1) + return true; + for (int count : m_extruder_max_nozzle_count) + if (count > 1) + return true; + return false; +} + +// The richer hotend-change time model. It resolves the target nozzle from the nozzle-grouping result +// (by the H id when present, else the filament's first nozzle) and splits the change into three +// independent costs — physical-extruder switch, nozzle-in-extruder change, and filament-in-nozzle +// change — so a multi-nozzle print's ETA reflects only the transitions that actually occur. +// Estimator-gated: for the single-nozzle fleet it delegates to the single-arg model, so their time +// estimate — hence exported g-code — is unchanged. +// +// Orca: +// - The nozzle-grouping result is stored on m_result. Both resolver methods are virtual on +// NozzleGroupResultBase, so no down-cast is needed. +// - The flush delay is attributed via a Tool_change move-type block with no extrusion role (mirroring +// the single-arg convention) rather than an erFlush-stamped block, to keep the delay out of the +// per-role feature-time distribution, so the multi-nozzle delta stays a pure ETA/time shift. +// - Beyond total_(flush_)filament_changes, the richer counters (total_extruder_changes / load / +// unload / tool_change time) are maintained in the matching branches so the multi-nozzle +// GCodeViewer stats do not regress to zero. These are UI-only (not written to g-code). +void GCodeProcessor::process_filament_change(int id, int nozzle_id) +{ + // Gate: outside the multi-nozzle context, or when the nozzle-grouping result is not available + // (e.g. re-importing a bare g-code file), run the existing single-arg model byte-for-byte. + if (!use_multi_nozzle_change_time_model() || !m_result.nozzle_group_result) { + process_filament_change(id); + return; + } + + assert(id < m_result.filaments_count); + const int prev_extruder_id = get_extruder_id(false); + const int prev_filament_id = get_filament_id(false); + float extra_time = 0.0f; + + // A same-filament re-select with no explicit nozzle target costs nothing. + if (prev_filament_id == id && nozzle_id == -1) + return; + + if (prev_extruder_id != -1) + m_last_filament_id[prev_extruder_id] = static_cast(prev_filament_id); + + // Resolve the destination nozzle: by explicit H id first, else the filament's first nozzle. + std::optional target_nozzle_info; + if (nozzle_id != -1) + target_nozzle_info = m_result.nozzle_group_result->get_nozzle_from_id(nozzle_id); + if (!target_nozzle_info) { + auto used_nozzles = m_result.nozzle_group_result->get_nozzles_for_filament(id); + if (used_nozzles.empty()) + return; + target_nozzle_info = used_nozzles.front(); + } + if (!target_nozzle_info) + return; + + const int new_extruder_id = target_nozzle_info->extruder_id; + const int old_extruder_id = prev_extruder_id; + const int new_nozzle_id_in_extruder = target_nozzle_info->group_id; + const int old_nozzle_id_in_extruder = m_nozzle_status_recorder.get_nozzle_in_extruder(new_extruder_id); + const int new_filament_id = id; + const int old_filament_in_nozzle = m_nozzle_status_recorder.get_filament_in_nozzle(new_nozzle_id_in_extruder); + const int old_filament_in_extruder = m_nozzle_status_recorder.get_filament_in_nozzle(old_nozzle_id_in_extruder); + + const bool extruder_change = new_extruder_id != old_extruder_id; + const bool nozzle_in_extruder_change = new_nozzle_id_in_extruder != old_nozzle_id_in_extruder; + const bool filament_in_nozzle_change = new_filament_id != old_filament_in_nozzle; + + // Orca: attribute the accumulated volume usage to the OLD extruder before switching state. Unlike + // an unconditional call, the single-arg model (and every other Orca estimator path) skips this on + // the very first tool-select (prev_extruder_id == -1) where there is no prior filament segment to + // close out. Matching that keeps H2C's initial `T H` byte-identical to its single-arg + // baseline; every subsequent change closes out identically. + if (prev_extruder_id != -1) + process_filaments(CustomGCode::ToolChange); + + m_result.lock(); + if (extruder_change && old_extruder_id != -1) { + const float t = get_extruder_change_time(new_extruder_id); + extra_time += t; + m_result.print_statistics.total_tool_change_time += t; // Orca-only stat + m_result.print_statistics.total_extruder_changes += 1; // Orca-only stat + } + if (nozzle_in_extruder_change || filament_in_nozzle_change) { + if (old_filament_in_extruder >= 0) { + const float t = get_filament_unload_time(static_cast(old_filament_in_extruder)); + extra_time += t; + m_result.print_statistics.total_filament_unload_time += t; // Orca-only stat + } + m_time_processor.extruder_unloaded = false; + const float t = get_filament_load_time(static_cast(new_filament_id)); + extra_time += t; + m_result.print_statistics.total_filament_load_time += t; // Orca-only stat + if (filament_in_nozzle_change && old_filament_in_nozzle != -1) + m_result.print_statistics.total_flush_filament_changes += 1; + } + if (prev_filament_id != -1) + m_result.print_statistics.total_filament_changes += 1; + m_result.unlock(); + + if (new_extruder_id != -1) + m_filament_id[new_extruder_id] = static_cast(new_filament_id); + m_extruder_id = static_cast(new_extruder_id); + + // Record the resulting nozzle/extruder occupancy so the next change can classify itself. + m_nozzle_status_recorder.set_nozzle_status(new_nozzle_id_in_extruder, new_filament_id, new_extruder_id); + + m_cp_color.current = m_extruder_colors[new_filament_id]; + + // Same tool-change move + zero-distance time block plumbing as the single-arg model, so the flush + // delay lands on a Tool_change block (kept out of the per-role feature-time distribution). + store_move_vertex(EMoveType::Tool_change); + + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + TimeMachine& machine = m_time_processor.machines[i]; + if (!machine.enabled) + continue; + TimeBlock block; + block.move_id = static_cast(m_result.moves.size()) - 1; + block.move_type = EMoveType::Tool_change; + block.skippable_type = m_skippable_type; + block.layer_id = std::max(1, m_layer_id); + block.g1_line_id = m_g1_line_id; + block.flags.prepare_stage = m_processing_start_custom_gcode; + block.distance = 0.0f; + block.calculate_trapezoid(); + machine.blocks.push_back(block); + } + + simulate_st_synchronize(extra_time, EMoveType::Tool_change); +} + void GCodeProcessor::process_filament_change(int id) { assert(id < m_result.filaments_count); @@ -5628,6 +6797,10 @@ void GCodeProcessor::process_filament_change(int id) TimeBlock block; block.move_id = static_cast(m_result.moves.size()) - 1; block.move_type = EMoveType::Tool_change; + // stamp the current SKIPPABLE type onto the tool-change block (the fleet-wide time_lapse_gcode + // regions stamp stTimelapse); byte-inert because nothing reads the stamp until the injector + // consumes it. + block.skippable_type = m_skippable_type; block.layer_id = std::max(1, m_layer_id); block.g1_line_id = m_g1_line_id; block.flags.prepare_stage = m_processing_start_custom_gcode; @@ -5735,6 +6908,21 @@ void GCodeProcessor::set_extrusion_role(ExtrusionRole role) m_extrusion_role = role; } +// Resolve a SKIPPABLE_TYPE payload to a SkipType. Only meaningful inside a SKIPPABLE region. +void GCodeProcessor::set_skippable_type(const std::string_view type) +{ + if (!m_skippable) { + m_skippable_type = SkipType::stNone; + return; + } + auto iter = skip_type_map.find(type); + if (iter != skip_type_map.end()) { + m_skippable_type = iter->second; + } else { + m_skippable_type = SkipType::stOther; + } +} + float GCodeProcessor::minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const { if (m_time_processor.machine_limits.machine_min_extruding_rate.empty()) diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index fcefd1a9a7..633695b03e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -6,6 +6,7 @@ #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/CustomGCode.hpp" +#include "libslic3r/MultiNozzleUtils.hpp" #include #include @@ -43,6 +44,23 @@ class Print; Count }; + // Classifies why a wipe-tower / change_filament / time-lapse region is safe to relocate a + // pre-heat M104 into, for the pre-heat/pre-cool injector. The shipping time_lapse_gcode + // template (timelapse-on by default) emits SKIPPABLE_* on essentially every slice, so the + // "timelapse" payload -> stTimelapse classification is exercised widely. + enum SkipType + { + stTimelapse, + stHeadWrapDetect, + stOther, + stNone + }; + + const std::unordered_map skip_type_map{ + {"timelapse", SkipType::stTimelapse}, + {"head_wrap_detect", SkipType::stHeadWrapDetect} + }; + struct PrintEstimatedStatistics { enum class ETimeMode : unsigned char @@ -77,6 +95,10 @@ class Print; std::array(ETimeMode::Count)> modes; unsigned int total_filament_changes; + // Number of filament changes that actually re-flush a nozzle (a filament-in-nozzle change + // onto a non-empty nozzle), tracked only by the richer multi-nozzle hotend-change time model. + // Stays 0 for single-nozzle printers (X1/P1/A1/H2S/A2L), which never enter the two-arg model. + unsigned int total_flush_filament_changes; unsigned int total_extruder_changes; float total_filament_load_time; float total_filament_unload_time; @@ -101,6 +123,7 @@ class Print; flush_per_filament.clear(); used_filaments_per_role.clear(); total_filament_changes = 0; + total_flush_filament_changes = 0; total_extruder_changes = 0; total_filament_load_time = 0.0f; total_filament_unload_time = 0.0f; @@ -166,6 +189,14 @@ class Print; ConflictResultOpt conflict_result; GCodeCheckResult gcode_check_result; FilamentPrintableResult filament_printable_reuslt; + // The per-filament -> logical-nozzle grouping the slicer computed for this + // result, surfaced onto the object the device GUI reads + // (plater->background_process().get_current_gcode_result()). Populated only from + // Print::get_layered_nozzle_group_result() (ToolOrdering's static L/R + rack subset); + // default-empty (null) and read by no g-code emitter, so it is invisible in the emitted + // g-code. Consumed by the print-dispatch nozzle mapping (DevNozzleMappingCtrl) via + // DevUtilBackend::GetNozzleGroupResult. + std::shared_ptr nozzle_group_result; float initial_layer_time; struct SettingsIds @@ -263,6 +294,10 @@ class Print; std::vector warnings; int nozzle_hrc; std::vector nozzle_type; + // Per-extruder physical hotend type. Fed to the pre-heat injector's TimeProcessContext + // (mixed-type X2D workaround). Populated in apply_config; unused until the injector side-pass + // consumes it. + std::vector extruder_types; // first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments std::unordered_map, std::vector>,FilamentSequenceHash> layer_filaments; std::vector nozzle_change_sequence; @@ -271,6 +306,11 @@ class Print; // first key stores `from` filament, second keys stores the `to` filament std::map, int > filament_change_count_map; + // Accumulated print time spent inside SKIPPABLE regions, per skip type. Populated by the time + // estimator; consumed only downstream. The shipping time_lapse_gcode template emits SKIPPABLE_* + // widely, so this is typically populated (stTimelapse) on most slices. + std::unordered_map skippable_part_time; + BedType bed_type = BedType::btCount; void reset(); @@ -304,11 +344,18 @@ class Print; gcode_check_result = other.gcode_check_result; limit_filament_maps = other.limit_filament_maps; filament_printable_reuslt = other.filament_printable_reuslt; + // Orca: copy the shared grouping result so a copied result keeps it (shared_ptr => + // memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way. + nozzle_group_result = other.nozzle_group_result; + // Keep the per-extruder hotend types on a copied result (injector input). + extruder_types = other.extruder_types; layer_filaments = other.layer_filaments; filament_change_sequence = other.filament_change_sequence; nozzle_change_sequence = other.nozzle_change_sequence; optimal_assignment = other.optimal_assignment; filament_change_count_map = other.filament_change_count_map; + // Keep the SKIPPABLE per-type time on a copied result. + skippable_part_time = other.skippable_part_time; initial_layer_time = other.initial_layer_time; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; @@ -319,6 +366,75 @@ class Print; void unlock() const { result_mutex.unlock(); } }; + // First-pass usage-block descriptors for the pre-heat/pre-cool injector. FilamentUsageBlock + // records the [lower,upper) output-line-id span a single filament occupies; ExtruderUsageBlcok + // (the "Blcok" typo is intentional) records the span an extruder is active in, with the start/end + // filament + logical-nozzle ids and the post-extrusion (pre-switch) partial-free sub-range. Built + // during run_post_process, consumed only by the injector side-pass under the enable_pre_heating gate. + namespace ExtruderPreHeating + { + struct FilamentUsageBlock + { + int filament_id; + int extruder_id; + int nozzle_id; + unsigned int lower_gcode_id; + unsigned int upper_gcode_id; // [lower_gcode_id,upper_gcode_id) uses current filament , upper gcode id will be set after finding next block + FilamentUsageBlock(int filament_id_, int extruder_id_, int nozzle_id_, unsigned int lower_gcode_id_, unsigned int upper_gcode_id_) :filament_id(filament_id_), extruder_id(extruder_id_), nozzle_id(nozzle_id_), lower_gcode_id(lower_gcode_id_), upper_gcode_id(upper_gcode_id_) {} + }; + + /** + * @brief Describle the usage of a exturder in a section + * + * The strucutre stores the start and end lines of the sections as well as + * the filament used at the beginning and end of the section. + * Post extrusion means the final extrusion before switching to the next extruder. + * + * Simplified GCode Flow: + * 1.Extruder Change Block (ext0 switch to ext1) + * 2.Extruder Usage Block (use ext1 to print) + * 3.Extruder Change Block (ext1 switch to ext0) + * 4.Extruder Usage Block (use ext0 to print) + * 5.Extruder Change Block (ext0 switch to ex1) + * ... + * + * So the construct of extruder usage block relys on two extruder change block + */ + struct ExtruderUsageBlcok + { + int extruder_id = -1; + unsigned int start_id = -1; + unsigned int end_id = -1; + int start_filament = -1; + int end_filament = -1; + int start_nozzle_id = -1; + int end_nozzle_id = -1; + unsigned int post_extrusion_start_id = -1; + unsigned int post_extrusion_end_id = -1; + bool ignore_cooling_before_tower = false; + + void initialize_step_1(int extruder_id_, int start_id_, int start_filament_, int start_nozzle_id_) { + extruder_id = extruder_id_; + start_id = start_id_; + start_filament = start_filament_; + start_nozzle_id = start_nozzle_id_; + }; + void initialize_step_2(int post_extrusion_start_id_) { + post_extrusion_start_id = post_extrusion_start_id_; + } + void initialize_step_3(int end_id_, int end_filament_, int post_extrusion_end_id_, int end_nozzle_id_) { + end_id = end_id_; + end_filament = end_filament_; + post_extrusion_end_id = post_extrusion_end_id_; + end_nozzle_id = end_nozzle_id_; + } + void reset() { + *this = ExtruderUsageBlcok(); + } + ExtruderUsageBlcok() = default; + }; + } + class CommandProcessor { public: @@ -347,6 +463,24 @@ class Print; static const std::string VFlush_Start_Tag; static const std::string VFlush_End_Tag; static const std::string External_Purge_Tag; + public: + // Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than + // a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament + // path) can reference them single-sourced. + static const std::string Skippable_Start_Tag; + static const std::string Skippable_End_Tag; + static const std::string Skippable_Type_Tag; + // Orca: usage-block builder markers (MACHINE_START_GCODE_END / MACHINE_END_GCODE_START / + // NOZZLE_CHANGE_START / NOZZLE_CHANGE_END / CP_TOOLCHANGE_WIPE), stored as static strings (the + // FLUSH/SKIPPABLE idiom above) rather than extending the Reserved_Tags arrays — these are + // multi-nozzle markers only ever emitted by BBL-printer paths. Public so the emission sites can + // reference them single-sourced. The MACHINE_*_GCODE_* emission (GCode.cpp, gated + // enable_pre_heating) activates the usage-block builder. + static const std::string Machine_Start_GCode_End_Tag; + static const std::string Machine_End_GCode_Start_Tag; + static const std::string Nozzle_Change_Start_Tag; + static const std::string Nozzle_Change_End_Tag; + static const std::string Toolchange_Wipe_Tag; public: enum class ETags : unsigned char { @@ -455,6 +589,9 @@ class Print; EMoveType move_type{ EMoveType::Noop }; ExtrusionRole role{ erNone }; + // SKIPPABLE tag classification stamped onto each time block. Feeds skippable_part_time + // and the injector's SKIPPABLE relocation. stNone unless inside a SKIPPABLE_* region. + SkipType skippable_type{ SkipType::stNone }; unsigned int move_id{ 0 }; unsigned int g1_line_id{ 0 }; unsigned int remaining_internal_g1_lines{ 0 }; @@ -624,6 +761,25 @@ class Print; struct TimeProcessor { + // Orca: the insert-line taxonomy + the ordered map of lines the pre-heat/pre-cool injector + // splices into the finished g-code, keyed by output-line id. Orca keeps its single-pass + // run_post_process (M73 / filament stats / ActualSpeedMove / Backtrace / + // machine_tool_change_time) intact and applies this map in a separate, gated ADDITIVE + // second file-rewrite pass (run_second_pass_injection); with an empty map that pass is a + // byte-for-byte identity rewrite. The map is populated by the PreCoolingInjector. + enum InsertLineType + { + PlaceholderReplace, + TimePredict, + FilamentChangePredict, + ExtruderChangePredict, + PreCooling, + PreHeating, + }; + + // first key is line id, second key is content + using InsertedLinesMap = std::map>>; + struct Planner { // Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks. @@ -651,6 +807,117 @@ class Print; void reset(); }; + + // The pre-cool / pre-heat injection engine. It consumes the already-computed per-move time + // substrate (moves[i].time[valid_machine_id] / .gcode_id) and the first-pass usage blocks to + // locate idle-hotend windows, then emits M632/M400/M104/M633 lines into a + // TimeProcessor::InsertedLinesMap that the additive second file-rewrite pass + // (run_second_pass_injection) splices into the finished g-code. It is constructed and run ONLY + // when m_enable_pre_heating — single-nozzle printers (X1/P1/A1/H2S, flag false) never reach it. + // Every input is a const reference bundled from GCodeProcessor members; the injector never + // mutates GCodeProcessor state. + class PreCoolingInjector { + public: + struct ExtruderFreeBlock { + unsigned int free_lower_gcode_id; + unsigned int free_upper_gcode_id; + unsigned int partial_free_lower_id; // range of extrusion in wipe tower; without a wipe tower + unsigned int partial_free_upper_id; // partial_free lower/upper equal free_lower_gcode_id + int last_filament_id; + int next_filament_id; + int last_nozzle_id; + int next_nozzle_id; + int extruder_id; // partition key for the pre-heat/pre-cool region (extruder or hotend), not + // necessarily a real extruder id + bool ignore_cooling_before_tower = false; + }; + + void process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines); + void build_extruder_free_blocks(const std::vector& filament_usage_blocks, const std::vector& extruder_usage_blocks); + + PreCoolingInjector( + const std::vector& moves_, + const std::vector& filament_types_, + const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result_, + const std::vector& filament_nozzle_temps_, + const std::vector& filament_nozzle_temps_initial_layer_, + const std::vector& physical_extruder_map_, + int valid_machine_id_, + float inject_time_threshold_, + bool handle_hotend_as_extruder_, + bool has_filament_switcher_, + const std::vector& pre_cooling_temp_, + const std::vector& cooling_rate_, + const std::vector& heating_rate_, + const std::vector>& skippable_blocks_, + const std::vector& extruder_max_nozzle_count_, + const std::vector& filament_preheat_temperature_delta_, + const std::vector& filament_max_temperature_drop_when_ec_, + unsigned int machine_start_gcode_end_id_, + unsigned int machine_end_gcode_start_id_, + const std::vector& extruder_types_, + const std::vector& nozzle_diameter_ + ) : + moves(moves_), + filament_types(filament_types_), + nozzle_group_result(nozzle_group_result_), + filament_nozzle_temps(filament_nozzle_temps_), + filament_nozzle_temps_initial_layer(filament_nozzle_temps_initial_layer_), + physical_extruder_map(physical_extruder_map_), + valid_machine_id(valid_machine_id_), + inject_time_threshold(inject_time_threshold_), + handle_hotend_as_extruder(handle_hotend_as_extruder_), + has_filament_switcher(has_filament_switcher_), + filament_pre_cooling_temps(pre_cooling_temp_), + cooling_rate(cooling_rate_), + heating_rate(heating_rate_), + skippable_blocks(skippable_blocks_), + extruder_max_nozzle_count(extruder_max_nozzle_count_), + filament_preheat_temperature_delta(filament_preheat_temperature_delta_), + filament_max_temperature_drop_when_ec(filament_max_temperature_drop_when_ec_), + machine_start_gcode_end_id(machine_start_gcode_end_id_), + machine_end_gcode_start_id(machine_end_gcode_start_id_), + extruder_types(extruder_types_), + nozzle_diameter(nozzle_diameter_) + { + } + + private: + std::vector m_extruder_free_blocks; + const std::vector& moves; + const std::vector& filament_types; + const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result; + const std::vector& filament_nozzle_temps; + const std::vector& filament_nozzle_temps_initial_layer; + const std::vector& physical_extruder_map; + const int valid_machine_id; + const float inject_time_threshold; + const bool handle_hotend_as_extruder; + const bool has_filament_switcher; + const std::vector& cooling_rate; + const std::vector& heating_rate; + const std::vector& filament_pre_cooling_temps; // target cooling temp during post extrusion + const std::vector>& skippable_blocks; + const std::vector& extruder_max_nozzle_count; + const std::vector& filament_preheat_temperature_delta; + const std::vector& filament_max_temperature_drop_when_ec; + const unsigned int machine_start_gcode_end_id; + const unsigned int machine_end_gcode_start_id; + const std::vector& extruder_types; + const std::vector& nozzle_diameter; + + void inject_cooling_heating_command( + TimeProcessor::InsertedLinesMap& inserted_operation_lines, + const ExtruderFreeBlock& free_block, + float curr_temp, + float target_temp, + bool pre_cooling, + bool pre_heating + ); + + void build_by_filament_blocks(const std::vector& filament_usage_blocks); + void build_by_extruder_blocks(const std::vector& extruder_usage_blocks); + }; public: class SeamsDetector { @@ -795,12 +1062,51 @@ class Print; bool m_flushing; // mark a section with real flush bool m_virtual_flushing; // mark a section with virtual flush, only for statistics bool m_wipe_tower; + // Current-section SKIPPABLE state. Set by process_tags when inside a SKIPPABLE_* region; + // stamped onto each TimeBlock. The shipping time_lapse_gcode template emits SKIPPABLE_* + // widely, so these commonly go active (true / stTimelapse) and stamp blocks on most slices. + bool m_skippable{false}; + SkipType m_skippable_type{SkipType::stNone}; int m_object_label_id{-1}; float m_print_z{0.0f}; std::vector m_remaining_volume; ExtruderTemps m_filament_nozzle_temp; ExtruderTemps m_filament_nozzle_temp_first_layer; std::vector m_physical_extruder_map; + // Multi-nozzle context state. Per-extruder max (sub-)nozzle count; >1 marks a multi-nozzle + // extruder. Input for the pre-heat/filament-change-time injection model; not yet consumed by + // Orca's time estimator, so it is inert for existing printers. + std::vector m_extruder_max_nozzle_count{1}; + // Pre-heat / pre-cool injector estimator inputs. Populated from the config in apply_config + // (both overloads) and cleared in reset(), so the PreCoolingInjector has its inputs in place. + // Consumed only by the injector two-pass side-pass, gated on m_enable_pre_heating. + std::vector m_filament_types; + std::vector m_nozzle_diameter; + std::vector m_hotend_cooling_rate{ 2.f }; + std::vector m_hotend_heating_rate{ 2.f }; + std::vector m_filament_pre_cooling_temp{ 0 }; + std::vector m_filament_preheat_temperature_delta; + bool m_enable_pre_heating{ false }; + bool m_handle_hotend_as_extruder{ false }; + bool m_has_filament_switcher{ false }; + // [start,end] output-line-id ranges of each SKIPPABLE region, collected during + // run_post_process. The injector relocates pre-heat M104s out of these ranges. The shipping + // time_lapse_gcode template emits SKIPPABLE_* widely, so on a timelapse-on slice this is + // populated with many timelapse ranges (not empty) — the consumer must expect the common + // timelapse case, not only H2C/A2L wipe-tower ranges. + std::vector> m_skippable_blocks; + // First-pass usage blocks, built in run_post_process and stored on the member so the + // injector side-pass can consume them. Filled only when m_enable_pre_heating — single-nozzle + // printers (X1/P1/A1/H2S) never build them. They depend on the MACHINE_*_GCODE_* / + // NOZZLE_CHANGE_* emission the builder keys off. + std::vector m_filament_blocks; + std::vector m_extruder_blocks; + unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) }; + unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) }; + // Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each + // extruder currently carries. Consumed ONLY by the richer two-arg process_filament_change + // model, which single-nozzle printers (X1/P1/A1/H2S/A2L) never enter. + MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status_recorder; bool m_manual_filament_change; //BBS: x, y offset for gcode generated @@ -1094,22 +1400,43 @@ class Print; // Processes T line (Select Tool) void process_T(const GCodeReader::GCodeLine& line); void process_T(const std::string_view command); + // T variant carrying the H logical-nozzle id parsed off the command line. -1 = absent. + void process_T(const std::string_view command, int nozzle_id); void process_M1020(const GCodeReader::GCodeLine &line); void process_M622(const GCodeReader::GCodeLine &line); void process_M623(const GCodeReader::GCodeLine &line); void process_filament_change(int id); + // Richer hotend-change time model distinguishing extruder-switch / nozzle-in-extruder change / + // filament-in-nozzle change. Self-gated: for single-nozzle printers it delegates to + // process_filament_change(int) so their time estimate — hence exported g-code — is unchanged. + void process_filament_change(int id, int nozzle_id); + // True only for multi-nozzle-capable printers (H2C cluster, or a dual/multi-extruder machine + // like H2D/X2D): the gate that admits the richer two-arg hotend-change time model. False for + // every single-extruder single-nozzle printer (X1/P1/A1/H2S/A2L). + bool use_multi_nozzle_change_time_model() const; // post process the file with the given filename to: // 1) add remaining time lines M73 and update moves' gcode ids accordingly // 2) update used filament data void run_post_process(); + // Additive second file-rewrite pass. Splices the pre-heat/pre-cool injector's InsertedLinesMap + // into the finished g-code and re-shifts every move's gcode_id by the number of inserted lines + // before it. Runs only when m_enable_pre_heating, AFTER run_post_process, so single-nozzle + // printers (X1/P1/A1/H2S) never enter it; with an empty map it is a byte-for-byte identity rewrite. + void run_second_pass_injection(); + // Shift each move's gcode_id by the count of injector lines inserted before it. No-op when the + // map is empty. + void handle_offsets_of_second_process(const TimeProcessor::InsertedLinesMap& inserted_operation_lines); + //BBS: different path_type is only used for arc move void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false); void set_extrusion_role(ExtrusionRole role); + // Resolve the SKIPPABLE_TYPE payload to a SkipType. + void set_skippable_type(const std::string_view type); float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const; float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const; diff --git a/src/libslic3r/GCode/TimelapsePosPicker.cpp b/src/libslic3r/GCode/TimelapsePosPicker.cpp index 0301d01122..364624a88c 100644 --- a/src/libslic3r/GCode/TimelapsePosPicker.cpp +++ b/src/libslic3r/GCode/TimelapsePosPicker.cpp @@ -361,7 +361,8 @@ namespace Slic3r { * @param safe_areas A collection of extended polygons defining the safe areas. * @return Point The nearest point within the safe areas or the default timelapse position if no safe areas exist. */ - Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision) + Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision, + const std::optional& farthest_point = std::nullopt) { struct CandidatePoint { @@ -381,7 +382,11 @@ namespace Slic3r { std::priority_queue max_heap; const double candidate_point_segment = scale_(5), weight_of_camera=1./3.; - auto penaltyFunc = [&weight_of_camera](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double { + auto penaltyFunc = [&weight_of_camera, &farthest_point](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double { + if (farthest_point.has_value()) { + // Farthest-point timelapse: prefer candidate closest to the farthest point (L1 norm) + return (farthest_point.value() - candidatet).cwiseAbs().sum(); + } // move distance + Camera occlusion penalty function double ret_pen = (curr_post - candidatet).cwiseAbs().sum() - weight_of_camera * (CameraPos - candidatet).cwiseAbs().sum(); return ret_pen; @@ -523,7 +528,7 @@ namespace Slic3r { path_collision_area = union_ex(layer_slices_without_curr, rod_limit_areas); } - return pick_pos_internal(center_p, safe_area,path_collision_area, by_object); + return pick_pos_internal(center_p, safe_area,path_collision_area, by_object, ctx.farthest_point); } /** @@ -610,4 +615,50 @@ namespace Slic3r { return *m_all_layer_pos; } + // Whether the head can travel to X0 without crossing any other instance that is + // taller than the current print position. + bool TimelapsePosPicker::get_is_clear_to_x0(const PosPickCtx &ctx) + { + bool by_object = m_print_seq == PrintSequence::ByObject; + std::vector object_list = get_object_list(ctx.printed_objects); + + auto range_intersect = [](int left1, int right1, int left2, int right2) { + if (left1 <= left2 && left2 <= right1) return true; + if (left2 <= left1 && left1 <= right2) return true; + return false; + }; + + ExPolygons unclear_area; + const Layer *layer = ctx.curr_layer; + float z_target = layer->print_z; + float z_low = layer->print_z - 0.5; + float z_high = layer->print_z + 0.5; + + for (auto &obj : object_list) { + for (auto &instance : obj->instances()) { + auto instance_bbox = get_real_instance_bbox(instance); + bool is_curr_obj = ( obj == object_list.back() ) || ( !by_object ), + higher_than_curr_pos = instance_bbox.max.z() > z_target; + if (!is_curr_obj && range_intersect(instance_bbox.min.z(), instance_bbox.max.z(), z_low, z_high)) { + ExPolygon expoly; + expoly.contour = {{scale_(instance_bbox.min.x()), scale_(instance_bbox.min.y())}, + {scale_(instance_bbox.max.x()), scale_(instance_bbox.min.y())}, + {scale_(instance_bbox.max.x()), scale_(instance_bbox.max.y())}, + {scale_(instance_bbox.min.x()), scale_(instance_bbox.max.y())}}; + expoly.contour = expand_object_projection(expoly.contour, by_object, higher_than_curr_pos); + unclear_area.emplace_back(std::move(expoly)); + } + } + } + + Point curr_pos_in_plate = {ctx.curr_pos.x() - scale_(m_plate_offset.x()), ctx.curr_pos.y() - scale_(m_plate_offset.y())}; + for (const ExPolygon &expoly : unclear_area) { + BoundingBox bbox = expoly.contour.bounding_box(); + if (curr_pos_in_plate.y() < bbox.min.y() || curr_pos_in_plate.y() > bbox.max.y()) continue; + if (bbox.min.x() <= curr_pos_in_plate.x()) return false; + } + + return true; + } + } \ No newline at end of file diff --git a/src/libslic3r/GCode/TimelapsePosPicker.hpp b/src/libslic3r/GCode/TimelapsePosPicker.hpp index c4930b3022..9ed6b14fc7 100644 --- a/src/libslic3r/GCode/TimelapsePosPicker.hpp +++ b/src/libslic3r/GCode/TimelapsePosPicker.hpp @@ -22,6 +22,9 @@ namespace Slic3r { int picture_extruder_id; // the extruder id to take picture int curr_extruder_id; std::optional> printed_objects; // printed objects, only have value in by object mode + // Farthest-point timelapse: plate-relative scaled point; when set, pick_pos_internal + // biases the picked snapshot position toward this point (nullopt → legacy camera-occlusion loss). + std::optional farthest_point; }; // data are stored without plate offset @@ -32,6 +35,9 @@ namespace Slic3r { ~TimelapsePosPicker() = default; Point pick_pos(const PosPickCtx& ctx); + // Is the path to X0 clear of other (taller) instances? Drives the + // `clear_to_x0` timelapse-gcode variable (g39 clamping detection). + bool get_is_clear_to_x0(const PosPickCtx& ctx); void init(const Print* print, const Point& plate_offset); void reset(); private: diff --git a/src/libslic3r/GCode/ToolOrderUtils.cpp b/src/libslic3r/GCode/ToolOrderUtils.cpp index 5a6e9059f2..c8b8ac3fdc 100644 --- a/src/libslic3r/GCode/ToolOrderUtils.cpp +++ b/src/libslic3r/GCode/ToolOrderUtils.cpp @@ -7,13 +7,100 @@ namespace Slic3r { +// ==================== MaxFlowWithLowerBounds ==================== + struct MaxFlowWithLowerBounds { + public: + + void add_edge(int from, int to, int capacity); + + bool bfs(); + int dfs(int u, int f); + int solve(std::vector& matching); + + public: + std::vector l_nodes; + std::vector r_nodes; + std::vector edges; + std::vector> adj; + std::vector level; + std::vector it; + + int total_nodes{ -1 }; + int source_id{ -1 }; + int sink_id{ -1 }; + }; + + void MaxFlowWithLowerBounds::add_edge(int from, int to, int capacity) + { + adj[from].emplace_back(edges.size()); + edges.emplace_back(from, to, capacity, 0); + // also add the reverse residual edge with zero capacity + adj[to].emplace_back(edges.size()); + edges.emplace_back(to, from, 0, 0); + } + + bool MaxFlowWithLowerBounds::bfs() { + level.assign(total_nodes, -1); + std::queue q; + q.push(source_id); + level[source_id] = 0; + + while (!q.empty()) { + int u = q.front(); q.pop(); + for (int eid : adj[u]) { + Edge &e = edges[eid]; + if (e.flow < e.capacity && level[e.to] == -1) { + level[e.to] = level[u] + 1; + q.push(e.to); + } + } + } + return level[sink_id] != -1; + } + + int MaxFlowWithLowerBounds::dfs(int u, int f) { + if (u == sink_id) return f; + for (int &i = it[u]; i < (int)adj[u].size(); ++i) { + int eid = adj[u][i]; + Edge &e = edges[eid]; + if (e.flow < e.capacity && level[e.to] == level[u] + 1) { + int pushed = dfs(e.to, std::min(f, e.capacity - e.flow)); + if (pushed > 0) { + e.flow += pushed; + edges[eid ^ 1].flow -= pushed; + return pushed; + } + } + } + return 0; + } + + int MaxFlowWithLowerBounds::solve(std::vector& matching) { + int flow = 0; + while (bfs()) { + it.assign(total_nodes, 0); + while (int pushed = dfs(source_id, MaxFlowGraph::INF)) + flow += pushed; + } + + int L = l_nodes.size(); + int R = r_nodes.size(); + // collect l-r matches + matching.resize(l_nodes.size(), MaxFlowGraph::INVALID_ID); + for (int u = 0; u < L; ++u) { + for (int eid : adj[u]) { + Edge &e = edges[eid]; + if (e.flow > 0 && e.to >= L && e.to < L + R) { + matching[e.from] = e.to - L; + } + } + } + return flow; + } + +// ==================== MinCostMaxFlow ==================== struct MinCostMaxFlow { public: - struct Edge { - int from, to, capacity, cost, flow; - Edge(int u, int v, int cap, int cst) : from(u), to(v), capacity(cap), cost(cst), flow(0) {} - }; - std::vector solve(); void add_edge(int from, int to, int capacity, int cost); bool spfa(int source, int sink); @@ -107,15 +194,10 @@ namespace Slic3r { if (l_nodes[idx_in_left] == -1) { return 0; - //TODO: test more here - int sum = 0; - for (int i = 0; i < matrix.size(); ++i) - sum += matrix[i][idx_in_right]; - sum /= matrix.size(); - return -sum; } - return matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]]; + float val = matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]]; + return std::min(static_cast(val), MaxFlowGraph::MCMF_MAX_EDGE_COST); } @@ -123,27 +205,40 @@ namespace Slic3r const std::unordered_map>& uv_link_limits, const std::unordered_map>& uv_unlink_limits, const std::vector& u_capacity, - const std::vector& v_capacity) + const std::vector& v_capacity, + const std::vector,int>>& v_group_capacity) { assert(u_capacity.empty() || u_capacity.size() == u_nodes.size()); assert(v_capacity.empty() || v_capacity.size() == v_nodes.size()); l_nodes = u_nodes; r_nodes = v_nodes; - total_nodes = u_nodes.size() + v_nodes.size() + 2; + total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2; source_id = total_nodes - 2; sink_id = total_nodes - 1; adj.resize(total_nodes); + std::vectorv_node_to(v_nodes.size(), sink_id); + for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) { + for (auto vid : v_group_capacity[gid].first) + v_node_to[vid] = l_nodes.size() + r_nodes.size() + gid; + } + // add edge from source to left nodes for (int idx = 0; idx < l_nodes.size(); ++idx) { int capacity = u_capacity.empty() ? 1 : u_capacity[idx]; add_edge(source_id, idx, capacity); } - // add edge from right nodes to sink node + // add edge from right nodes to v_node_to(sink node or temp group node) for (int idx = 0; idx < r_nodes.size(); ++idx) { int capacity = v_capacity.empty() ? 1 : v_capacity[idx]; - add_edge(l_nodes.size() + idx, sink_id, capacity); + add_edge(l_nodes.size() + idx, v_node_to[idx], capacity); + } + + // add edge from temp group node to sink node + for (int idx = 0; idx < v_group_capacity.size(); ++idx) { + int capacity = v_group_capacity[idx].second; + add_edge(l_nodes.size() + r_nodes.size() + idx, sink_id, capacity); } // add edge from left nodes to right nodes @@ -269,6 +364,301 @@ namespace Slic3r return m_solver->solve(); } +// ==================== GeneralMinCostLowerBoundsSolver ==================== + GeneralMinCostLowerBoundsSolver::~GeneralMinCostLowerBoundsSolver() = default; + + GeneralMinCostLowerBoundsSolver::GeneralMinCostLowerBoundsSolver(const std::vector &matrix_, + const std::vector &u_nodes, + const std::vector &v_nodes, + const std::vector &v_nodes_group, + const std::unordered_map> &uv_link_limits, + const std::unordered_map> &uv_unlink_limits) + { + flush_matrix = matrix_; + l_nodes = u_nodes; + r_nodes = v_nodes; + r_nodes_group = v_nodes_group; + m_uv_link_limits = uv_link_limits; + m_uv_unlink_limits = uv_unlink_limits; + num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1; + + m_solver_lower_bounds = std::make_unique(); + m_solver_min_cost = std::make_unique(); + } + + std::vector GeneralMinCostLowerBoundsSolver::solve() + { + // group nodes that do not need a lower-bound constraint + std::unordered_set no_lower_group; + for (int i = 0; i < r_nodes.size(); i++) { + if (r_nodes[i] >= 0) + no_lower_group.insert(r_nodes_group[i]); + } + + // 1. build the lower-bound network graph + build_feasible_graph(no_lower_group); + + // 2. compute the max flow + int need = 0; + for (int d : demand) + if (d > 0) need += d; + std::vector feasible_matching; + int pushed_flow = m_solver_lower_bounds->solve(feasible_matching); + assert(need == pushed_flow); + + // 3. convert the lower-bound max-flow network into a min-cost-max-flow network + build_graph_with_feasible_result(); + // 4. compute the min-cost max-flow + auto min_cost_matching = m_solver_min_cost->solve(); + + return min_cost_matching; + } + + void GeneralMinCostLowerBoundsSolver::build_feasible_graph(const std::unordered_set &no_lower_groups) + { + m_solver_lower_bounds->l_nodes = l_nodes; + m_solver_lower_bounds->r_nodes = r_nodes; + m_solver_lower_bounds->total_nodes = l_nodes.size() + r_nodes.size() + num_groups + 2; + + m_solver_lower_bounds->source_id = m_solver_lower_bounds->total_nodes - 2; + m_solver_lower_bounds->sink_id = m_solver_lower_bounds->total_nodes - 1; + m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes); + demand.resize(m_solver_lower_bounds->total_nodes, 0); + + const int L = m_solver_lower_bounds->l_nodes.size(); + const int R = m_solver_lower_bounds->r_nodes.size(); + + // source -> l + for (int i = 0; i < L; ++i) + m_solver_lower_bounds->add_edge(m_solver_lower_bounds->source_id, i, 1); + + // u -> v (with link/unlink limits) + for (int i = 0; i < L; ++i) { + if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) { + for (int j : it->second) + m_solver_lower_bounds->add_edge(i, L + j, 1); + continue; + } + + std::optional> unlink_limits; + if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end()) + unlink_limits = it->second; + + for (int j = 0; j < R; ++j) { + if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end()) + continue; + m_solver_lower_bounds->add_edge(i, L + j, 1); + } + } + + // r -> group + for (int j = 0; j < R; ++j) { + int g = r_nodes_group[j]; + m_solver_lower_bounds->add_edge(L + j, L + R + g, 1); + } + + // group -> sink (lower bound = 1) + for (int g = 0; g < num_groups; ++g) { + if (no_lower_groups.count(g)) + m_solver_lower_bounds->add_edge(L + R + g, m_solver_lower_bounds->sink_id, R); + else + add_edge_with_lower_bound(L + R + g, m_solver_lower_bounds->sink_id, 1, R, 0); + } + + max_flow_edges = m_solver_lower_bounds->edges.size(); + + // support lower bounds, add super source super sink + super_source = m_solver_lower_bounds->total_nodes++; + super_sink = m_solver_lower_bounds->total_nodes++; + + m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes); + demand.resize(m_solver_lower_bounds->total_nodes, 0); + + for (int i = 0; i < super_source; ++i) { + if (demand[i] > 0) { + m_solver_lower_bounds->add_edge(super_source, i, demand[i]); + } else if (demand[i] < 0) { + m_solver_lower_bounds->add_edge(i, super_sink, -demand[i]); + } + } + m_solver_lower_bounds->add_edge(m_solver_lower_bounds->sink_id, m_solver_lower_bounds->source_id, MaxFlowGraph::INF); + source_id = m_solver_lower_bounds->source_id; + sink_id = m_solver_lower_bounds->sink_id; + m_solver_lower_bounds->source_id = super_source; + m_solver_lower_bounds->sink_id = super_sink; + } + + void GeneralMinCostLowerBoundsSolver::build_graph_with_feasible_result() + { + for (auto&lb:lower_bound_edges){ + m_solver_lower_bounds->edges[lb.edge_id].flow += lb.lower; + m_solver_lower_bounds->edges[lb.edge_id ^ 1].flow -= lb.lower; + } + + m_solver_min_cost->l_nodes = m_solver_lower_bounds->l_nodes; + m_solver_min_cost->r_nodes = m_solver_lower_bounds->r_nodes; + + m_solver_min_cost->source_id = source_id; + m_solver_min_cost->sink_id = sink_id; + m_solver_min_cost->total_nodes = sink_id + 1; + + m_solver_min_cost->edges = m_solver_lower_bounds->edges; + m_solver_min_cost->edges.erase(m_solver_min_cost->edges.begin() + max_flow_edges, m_solver_min_cost->edges.end()); + + m_solver_min_cost->adj = m_solver_lower_bounds->adj; + m_solver_min_cost->adj.resize(m_solver_min_cost->total_nodes); + for (auto &node_edges : m_solver_min_cost->adj) { + node_edges.erase(std::remove_if(node_edges.begin(), node_edges.end(), [this](int val) {return val >= this->max_flow_edges;}), node_edges.end()); + } + + + for (auto& e : m_solver_min_cost->edges) { + int L = m_solver_min_cost->l_nodes.size(); + int R = m_solver_min_cost->r_nodes.size(); + + if (e.from < L && e.to >= L && e.to < L + R) { + int idx_in_left = e.from; + int idx_in_right = e.to - L; + int group_id = r_nodes_group[idx_in_right]; + + if (r_nodes[idx_in_right] == -1) continue; + e.cost = flush_matrix[group_id][l_nodes[idx_in_left]][r_nodes[idx_in_right]]; + } + } + } + void GeneralMinCostLowerBoundsSolver::add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost) + { + int eid = m_solver_lower_bounds->edges.size(); + m_solver_lower_bounds->add_edge(from, to, upper - lower); + + lower_bound_edges.push_back({eid, lower}); + demand[from] -= lower; + demand[to] += lower; + } + +// ==================== GroupMinCostFlowSolver ==================== + GroupMinCostFlowSolver::~GroupMinCostFlowSolver() = default; + + GroupMinCostFlowSolver::GroupMinCostFlowSolver(const std::vector &matrix_, + const std::vector &u_nodes, + const std::vector &v_nodes, + const std::vector &v_nodes_group, + const std::unordered_map> &uv_link_limits, + const std::unordered_map> &uv_unlink_limits) + { + flush_matrix = matrix_; + l_nodes = u_nodes; + r_nodes = v_nodes; + r_nodes_group = v_nodes_group; + m_uv_link_limits = uv_link_limits; + m_uv_unlink_limits = uv_unlink_limits; + num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1; + + m_solver = std::make_unique(); + build_graph(); + } + + int GroupMinCostFlowSolver::get_flush_cost(int l_idx, int r_idx) + { + if (r_nodes[r_idx] == -1) + return 0; + int group_id = r_nodes_group[r_idx]; + return (int)flush_matrix[group_id][l_nodes[l_idx]][r_nodes[r_idx]]; + } + + void GroupMinCostFlowSolver::build_graph() + { + const int L = (int)l_nodes.size(); + const int R = (int)r_nodes.size(); + const int G = num_groups; + + m_solver->l_nodes = l_nodes; + m_solver->r_nodes = r_nodes; + m_solver->total_nodes = L + R + G + 2; + m_solver->source_id = L + R + G; + m_solver->sink_id = L + R + G + 1; + m_solver->adj.resize(m_solver->total_nodes); + + int max_flush = 0; + for (const auto &mat : flush_matrix) + for (const auto &row : mat) + for (float v : row) + max_flush = std::max(max_flush, (int)v); + int bonus = max_flush * L + 1; + + // source -> l_i + for (int i = 0; i < L; ++i) + m_solver->add_edge(m_solver->source_id, i, 1, 0); + + // l_i -> r_j (with link/unlink limits) + for (int i = 0; i < L; ++i) { + if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) { + for (int j : it->second) + m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j)); + continue; + } + + std::optional> unlink_limits; + if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end()) + unlink_limits = it->second; + + for (int j = 0; j < R; ++j) { + if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end()) + continue; + m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j)); + } + } + + // r_j -> group_g + // Compute per-nozzle incoming edge count as capacity upper bound. + // When unlink_limits restrict multiple filaments to the same nozzle, + // capacity=1 would block valid assignments. Using the actual in-degree + // allows the necessary flow while still preserving nozzle-level balance + // (a nozzle with fewer forced filaments keeps a tighter cap). + // The first unit carries a small nozzle-bonus to encourage spreading + // filaments across distinct nozzles within the same group. + int nozzle_bonus = max_flush + 1; + std::vector r_in_degree(R, 0); + for (int i = 0; i < L; ++i) { + if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) { + for (int j : it->second) + r_in_degree[j]++; + continue; + } + std::optional> unlink_limits; + if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end()) + unlink_limits = it->second; + for (int j = 0; j < R; ++j) { + if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end()) + continue; + r_in_degree[j]++; + } + } + + for (int j = 0; j < R; ++j) { + int g = r_nodes_group[j]; + int cap = std::max(r_in_degree[j], 1); + // First unit gets -nozzle_bonus to prefer using distinct nozzles + m_solver->add_edge(L + j, L + R + g, 1, -nozzle_bonus); + if (cap > 1) + m_solver->add_edge(L + j, L + R + g, cap - 1, 0); + } + + // group_g -> sink (split: first unit gets -bonus, rest gets 0) + // bonus >> nozzle_bonus, so group coverage always takes priority + for (int g = 0; g < G; ++g) { + m_solver->add_edge(L + R + g, m_solver->sink_id, 1, -bonus); + if (L > 1) + m_solver->add_edge(L + R + g, m_solver->sink_id, L - 1, 0); + } + } + + std::vector GroupMinCostFlowSolver::solve() + { + return m_solver->solve(); + } + +// ==================== MinFlushFlowSolver ==================== MinFlushFlowSolver::~MinFlushFlowSolver() { } @@ -277,7 +667,8 @@ namespace Slic3r const std::unordered_map>& uv_link_limits, const std::unordered_map>& uv_unlink_limits, const std::vector& u_capacity, - const std::vector& v_capacity) + const std::vector& v_capacity, + const std::vector,int>>&v_group_capacity) { assert(u_capacity.empty() || u_capacity.size() == u_nodes.size()); assert(v_capacity.empty() || v_capacity.size() == v_nodes.size()); @@ -286,13 +677,19 @@ namespace Slic3r m_solver->l_nodes = u_nodes; m_solver->r_nodes = v_nodes; - m_solver->total_nodes = u_nodes.size() + v_nodes.size() + 2; + m_solver->total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2; m_solver->source_id =m_solver->total_nodes - 2; m_solver->sink_id = m_solver->total_nodes - 1; m_solver->adj.resize(m_solver->total_nodes); + std::vector v_node_to(v_nodes.size(), m_solver->sink_id); + for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) { + for (auto vid : v_group_capacity[gid].first) + v_node_to[vid] = m_solver->l_nodes.size() + m_solver->r_nodes.size() + gid; + } + // add edge from source to left nodes,cost to 0 for (int i = 0; i < m_solver->l_nodes.size(); ++i) { int capacity = u_capacity.empty() ? 1 : u_capacity[i]; @@ -301,7 +698,12 @@ namespace Slic3r // add edge from right nodes to sink,cost to 0 for (int i = 0; i < m_solver->r_nodes.size(); ++i) { int capacity = v_capacity.empty() ? 1 : v_capacity[i]; - m_solver->add_edge(m_solver->l_nodes.size() + i, m_solver->sink_id, capacity, 0); + m_solver->add_edge(m_solver->l_nodes.size() + i, v_node_to[i], capacity, 0); + } + // add edge from temp group node to sink node + for(int i=0;iadd_edge(m_solver->l_nodes.size() + m_solver->r_nodes.size() + i, m_solver->sink_id, capacity, 0); } // add edge from left node to right nodes for (int i = 0; i < m_solver->l_nodes.size(); ++i) { @@ -602,12 +1004,125 @@ namespace Slic3r } + // Single-nozzle flush-minimizing reorder over one filament set / one flush matrix, with an + // optional seed filament. Extracted from the group loop so the multi-nozzle reorder can call it + // per physical nozzle. + // TODO: add custom sequence + static int reorder_filaments_for_minimum_flush_volume_base(const std::vector& filament_lists, + const std::vector>& layer_filaments, + const FlushMatrix& flush_matrix, + const std::function&)> get_custom_seq, + std::vector>* filament_sequences, + std::optional initial_filament_id = std::nullopt) + { + constexpr int max_n_with_forcast = 5; + using uint128_t = boost::multiprecision::uint128_t; + + if (filament_sequences) { + filament_sequences->clear(); + filament_sequences->reserve(layer_filaments.size()); + } + auto filament_list_to_hash_key = [](const std::vector& curr_layer_filaments, const std::vector& next_layer_filaments, + const std::optional& prev_filament, bool use_forcast) -> uint128_t { + uint128_t hash_key = 0; + // 31-0 bit define current layer extruder,63-32 bit define next layer extruder,95~64 define prev extruder + if (prev_filament) hash_key |= (uint128_t(1) << (64 + *prev_filament)); + + if (use_forcast) { + for (auto item : next_layer_filaments) { hash_key |= (uint128_t(1) << (32 + item)); } + } + + for (auto item : curr_layer_filaments) { hash_key |= (uint128_t(1) << item); } + return hash_key; + }; + + int cost = 0; + std::map> custom_layer_sequence_map; + std::unordered_map>> caches; + std::unordered_set filament_sets(filament_lists.begin(), filament_lists.end()); + std::optional curr_filament_id; + // use the provided initial filament id as the starting state when it is valid + if (initial_filament_id.has_value() && *initial_filament_id < flush_matrix.size()) { + curr_filament_id = initial_filament_id; + } + + for (size_t layer = 0; layer < layer_filaments.size(); ++layer){ + const auto& curr_lf = layer_filaments[layer]; + std::vector custom_filament_seq; + if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) { + std::vector unsign_custom_extruder_seq; + for (int extruder : custom_filament_seq) { + unsigned int unsign_extruder = static_cast(extruder) - 1; + auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder); + if (it != layer_filaments[layer].end()) + unsign_custom_extruder_seq.emplace_back(unsign_extruder); + } + assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size()); + + custom_layer_sequence_map[layer] = unsign_custom_extruder_seq; + } + } + + for (size_t layer = 0; layer < layer_filaments.size(); ++layer) { + const auto& curr_lf = layer_filaments[layer]; + + if(auto iter = custom_layer_sequence_map.find(layer); iter != custom_layer_sequence_map.end()){ + auto sequence_in_group = collect_filaments_in_groups(std::unordered_set(filament_lists.begin(),filament_lists.end()), iter->second); + + std::optional prev = curr_filament_id; + for (auto& f: sequence_in_group){ + if(prev) + cost += flush_matrix[*prev][f]; + prev = f; + } + + if(!sequence_in_group.empty()){ + curr_filament_id = sequence_in_group.back(); + } + + if(filament_sequences) + filament_sequences->emplace_back(sequence_in_group); + + continue; + } + + std::vector filament_used = collect_filaments_in_groups(filament_sets, curr_lf); + std::vector next_lf; + if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1]; + std::vector filament_used_next_layer = collect_filaments_in_groups(filament_sets, next_lf); + + bool use_forcast = false; + float tmp_cost = 0; + std::vector sequence; + uint128_t hash_key = filament_list_to_hash_key(filament_used, filament_used_next_layer, curr_filament_id, use_forcast); + if (auto iter = caches.find(hash_key); iter != caches.end()) { + tmp_cost = iter->second.first; + sequence = iter->second.second; + } + else { + sequence = get_extruders_order(flush_matrix, filament_used, filament_used_next_layer, curr_filament_id, use_forcast, &tmp_cost); + caches[hash_key] = { tmp_cost,sequence }; + } + + if (filament_sequences) + filament_sequences->emplace_back(sequence); + + if (!sequence.empty()) + curr_filament_id = sequence.back(); + + cost += tmp_cost; + } + + return cost; + } + int reorder_filaments_for_minimum_flush_volume(const std::vector& filament_lists, const std::vector& filament_maps, const std::vector>& layer_filaments, const std::vector& flush_matrix, std::optional&)>> get_custom_seq, - std::vector>* filament_sequences) + std::vector>* filament_sequences, + const std::unordered_map& nozzle_status) { //only when layer filament num <= 5,we do forcast constexpr int max_n_with_forcast = 5; @@ -670,6 +1185,12 @@ namespace Slic3r if (groups[idx].empty()) continue; std::optionalcurrent_extruder_id; + // seed the group (nozzle) with the filament already loaded, if nozzle_status supplies one + if (auto it = nozzle_status.find(static_cast(idx)); it != nozzle_status.end() && it->second >= 0) { + unsigned int initial_fil = static_cast(it->second); + if (initial_fil < flush_matrix[idx].size()) + current_extruder_id = initial_fil; + } std::unordered_map>> caches; @@ -775,4 +1296,174 @@ namespace Slic3r return cost; } + + int reorder_filaments_for_multi_nozzle_extruder(const std::vector& filament_lists, + const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result, + const std::vector>& layer_filaments, + const std::vector& flush_matrix, + const std::function&)> get_custom_seq, + std::vector>* filament_sequences, + const MultiNozzleUtils::NozzleStatusRecorder& initial_status) + { + std::map> nozzle_filament_groups; + std::map> extruder_to_nozzle; + + for(auto filament_idx : filament_lists){ + auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1); + if (!nozzle_info) + continue; + nozzle_filament_groups[nozzle_info->group_id].insert(filament_idx); + extruder_to_nozzle[nozzle_info->extruder_id].insert(nozzle_info->group_id); + } + + std::map>custom_layer_sequence_map;// save the filament sequences of custom layer + for (size_t layer = 0; layer < layer_filaments.size(); ++layer){ + const auto& curr_lf = layer_filaments[layer]; + std::vector custom_filament_seq; + if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) { + std::vector unsign_custom_extruder_seq; + for (int extruder : custom_filament_seq) { + unsigned int unsign_extruder = static_cast(extruder) - 1; + auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder); + if (it != layer_filaments[layer].end()) + unsign_custom_extruder_seq.emplace_back(unsign_extruder); + } + assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size()); + + custom_layer_sequence_map[layer] = unsign_custom_extruder_seq; + } + } + + + std::map>> nozzle_filament_sequences; + bool store_sequence = filament_sequences != nullptr; + + int cost = 0; + for(auto& group : nozzle_filament_groups){ + int nozzle_id = group.first; + auto& filament_in_nozzle = group.second; + + int extruder_id = 0; + for(auto& [ext, nozzle_set] : extruder_to_nozzle){ + if(nozzle_set.count(nozzle_id)){ + extruder_id = ext; + break; + } + } + + if(filament_in_nozzle.empty()) + continue; + + std::vector filament_vec_in_nozzle(filament_in_nozzle.begin(), filament_in_nozzle.end()); + + int initial_fil = initial_status.get_filament_in_nozzle(nozzle_id); + std::optional initial_fil_id = (initial_fil >= 0 && initial_fil < flush_matrix[extruder_id].size())? std::optional(initial_fil) : std::nullopt; + + std::vector> filament_seq; + cost += reorder_filaments_for_minimum_flush_volume_base(filament_vec_in_nozzle, layer_filaments, flush_matrix[extruder_id], get_custom_seq, + store_sequence ? &filament_seq : nullptr, initial_fil_id); + if(store_sequence) + nozzle_filament_sequences.emplace(nozzle_id, std::move(filament_seq)); + + } + + if(!store_sequence) + return cost; + + std::vector extruders; + std::map> nozzles_per_extruder; + for (auto& [extruder_id, nozzle_set] : extruder_to_nozzle) { + extruders.push_back(extruder_id); + nozzles_per_extruder[extruder_id] = std::vector( + nozzle_set.begin(), nozzle_set.end() + ); + } + + filament_sequences->clear(); + filament_sequences->resize(layer_filaments.size()); + + // No filament in filament_lists resolved to a nozzle in nozzle_group_result + // (e.g. a degenerate input where a layer references a filament index outside the range's + // grouping map). Emit each layer's filaments in their given order so the caller still gets a + // valid per-layer sequence, and skip the cross-nozzle reorder. Guards the unchecked + // max_element(extruders) below, which would dereference end() on an empty range. + if (extruders.empty()) { + for (size_t layer = 0; layer < layer_filaments.size(); ++layer) + (*filament_sequences)[layer] = layer_filaments[layer]; + return cost; + } + + auto get_extruder_for_filament = [nozzle_group_result](unsigned int filament_idx) { + auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1); + if (!nozzle) + return -1; + return nozzle->extruder_id; + }; + + auto get_nozzle_idx_for_filament = [nozzles_per_extruder, nozzle_group_result](unsigned int filament_idx)->int { + auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1); + if (!nozzle) + return -1; + return std::find(nozzles_per_extruder.at(nozzle->extruder_id).begin(), nozzles_per_extruder.at(nozzle->extruder_id).end(), nozzle->group_id) - nozzles_per_extruder.at(nozzle->extruder_id).begin(); + }; + + int initial_extruder = initial_status.get_current_extruder_id(); + int last_extruder_idx = (initial_extruder >= 0 && initial_extruder < extruders.size())? initial_extruder : 0; + // set size to max extruder_id in case extruder_id is not continuous + std::vector last_nozzle_idx(*std::max_element(extruders.begin(),extruders.end()) + 1,0); + for (int ext_id = 0; ext_id < static_cast(last_nozzle_idx.size()); ext_id++) { + int initial_nozzle = initial_status.get_nozzle_in_extruder(ext_id); + auto ext_nozzles = nozzles_per_extruder[ext_id]; + auto it = std::find(ext_nozzles.begin(), ext_nozzles.end(), initial_nozzle); + if (it != ext_nozzles.end()) + last_nozzle_idx[ext_id] = static_cast(std::distance(ext_nozzles.begin(), it)); + } + + for (size_t layer = 0; layer < layer_filaments.size(); ++layer) { + auto& out_seq = (*filament_sequences)[layer]; + + if (custom_layer_sequence_map.find(layer) != custom_layer_sequence_map.end()) { + out_seq = custom_layer_sequence_map[layer]; + if (!out_seq.empty()) { + last_extruder_idx = get_extruder_for_filament(out_seq.back()); + for (auto filament : out_seq) { + int cur_ext_id = get_extruder_for_filament(filament); + last_nozzle_idx[cur_ext_id] = get_nozzle_idx_for_filament(filament); + } + } + continue; + } + + if (last_extruder_idx == -1) + last_extruder_idx = 0; + + int curr_last_extruder_idx = last_extruder_idx; + auto curr_last_nozzle_idx = last_nozzle_idx; + for (int i = 0; i < extruders.size(); ++i) { + int extruder_id = extruders[(last_extruder_idx + i) % extruders.size()]; + auto& base_nozzles = nozzles_per_extruder[extruder_id]; + + bool has_seq = false; + if (last_nozzle_idx[extruder_id] == -1) + last_nozzle_idx[extruder_id] = 0; + + for (int j = 0; j < base_nozzles.size(); ++j) { + int nozzle_idx = (last_nozzle_idx[extruder_id] + j) % base_nozzles.size(); + int nozzle_id = base_nozzles[nozzle_idx]; + const auto& frag = nozzle_filament_sequences[nozzle_id][layer]; + if (frag.empty()) + continue; + has_seq = true; + curr_last_nozzle_idx[extruder_id] = nozzle_idx; + out_seq.insert(out_seq.end(), frag.begin(), frag.end()); + } + + if (has_seq) + curr_last_extruder_idx = extruder_id; + } + last_extruder_idx = curr_last_extruder_idx; + last_nozzle_idx = curr_last_nozzle_idx; + } + return cost; + } } diff --git a/src/libslic3r/GCode/ToolOrderUtils.hpp b/src/libslic3r/GCode/ToolOrderUtils.hpp index 26c9a01186..7b103531b1 100644 --- a/src/libslic3r/GCode/ToolOrderUtils.hpp +++ b/src/libslic3r/GCode/ToolOrderUtils.hpp @@ -6,7 +6,10 @@ #include #include #include +#include #include +#include +#include "../MultiNozzleUtils.hpp" namespace Slic3r { @@ -15,21 +18,27 @@ using FlushMatrix = std::vector>; namespace MaxFlowGraph { const int INF = std::numeric_limits::max(); const int INVALID_ID = -1; + // Upper bound for MCMF edge cost to prevent int overflow in SPFA causing infinite loops + constexpr int MCMF_MAX_EDGE_COST = 10000000; } +// Namespace-scope edge shared by the max-flow / min-cost-max-flow solvers below. +// The default cost keeps the plain max-flow solvers (which never read cost) source-compatible. +struct Edge +{ + int from, to, capacity, cost, flow; + Edge(int u, int v, int cap, int cst = 0) : from(u), to(v), capacity(cap), cost(cst), flow(0) {} +}; + class MaxFlowSolver { -private: - struct Edge { - int from, to, capacity, flow; - Edge(int u, int v, int cap) :from(u), to(v), capacity(cap), flow(0) {} - }; public: MaxFlowSolver(const std::vector& u_nodes, const std::vector& v_nodes, const std::unordered_map>& uv_link_limits = {}, const std::unordered_map>& uv_unlink_limits = {}, const std::vector& u_capacity = {}, - const std::vector& v_capacity = {} + const std::vector& v_capacity = {}, + const std::vector, int>>& v_group_capacity = {} ); std::vector solve(); @@ -47,6 +56,7 @@ private: struct MinCostMaxFlow; +struct MaxFlowWithLowerBounds; class GeneralMinCostSolver { @@ -61,6 +71,84 @@ private: std::unique_ptr m_solver; }; +class GeneralMinCostLowerBoundsSolver +{ +public: + GeneralMinCostLowerBoundsSolver( + const std::vector &matrix_, + const std::vector& u_nodes, + const std::vector& v_nodes, + const std::vector& v_nodes_group, + const std::unordered_map>& uv_link_limits = {}, + const std::unordered_map>& uv_unlink_limits = {}); + + std::vector solve(); + ~GeneralMinCostLowerBoundsSolver(); + +private: + void build_feasible_graph(const std::unordered_set& no_lower_groups); + + void build_graph_with_feasible_result(); + + void add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost); + + int get_distance(const int idx_in_left,const int idx_in_right); + +private: + std::unique_ptr m_solver_lower_bounds; + std::unique_ptr m_solver_min_cost; + + std::vector flush_matrix; + std::vector l_nodes; + std::vector r_nodes; + std::vector r_nodes_group; + std::unordered_map> m_uv_link_limits; + std::unordered_map> m_uv_unlink_limits; + int num_groups = 0; + + // support lower bounds + struct LowerBoundEdge{ + int edge_id; + int lower; + }; + + std::vector demand; + std::vector lower_bound_edges; + + int super_source = -1; + int super_sink = -1; + int source_id = -1; + int sink_id = -1; + int max_flow_edges = 0; +}; + +class GroupMinCostFlowSolver +{ +public: + GroupMinCostFlowSolver( + const std::vector &matrix_, + const std::vector &u_nodes, + const std::vector &v_nodes, + const std::vector &v_nodes_group, + const std::unordered_map> &uv_link_limits = {}, + const std::unordered_map> &uv_unlink_limits = {}); + + std::vector solve(); + ~GroupMinCostFlowSolver(); + +private: + void build_graph(); + int get_flush_cost(int l_idx, int r_idx); + + std::unique_ptr m_solver; + std::vector flush_matrix; + std::vector l_nodes; + std::vector r_nodes; + std::vector r_nodes_group; + std::unordered_map> m_uv_link_limits; + std::unordered_map> m_uv_unlink_limits; + int num_groups = 0; +}; class MinFlushFlowSolver { @@ -71,7 +159,8 @@ public: const std::unordered_map>& uv_link_limits = {}, const std::unordered_map>& uv_unlink_limits = {}, const std::vector& u_capacity = {}, - const std::vector& v_capacity = {} + const std::vector& v_capacity = {}, + const std::vector, int>>& v_group_capacity = {} ); std::vector solve(); ~MinFlushFlowSolver(); @@ -108,7 +197,19 @@ int reorder_filaments_for_minimum_flush_volume(const std::vector & const std::vector> &layer_filaments, const std::vector &flush_matrix, std::optional &)>> get_custom_seq, - std::vector> *filament_sequences); + std::vector> *filament_sequences, + const std::unordered_map& nozzle_status = {}); + +// Order filaments within a per-nozzle grouping result (multi-nozzle extruders). Threads a +// NozzleStatusRecorder describing the initial physical nozzle occupancy so the reorder can reward +// keeping an already-loaded filament in place. +int reorder_filaments_for_multi_nozzle_extruder(const std::vector& filament_lists, + const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result, + const std::vector>& layer_filaments, + const std::vector& flush_matrix, + const std::function&)> get_custom_seq, + std::vector> * filament_sequences, + const MultiNozzleUtils::NozzleStatusRecorder& initial_status = {}); } #endif // !TOOL_ORDER_UTILS_HPP diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 5cb66ba5e5..e36c177a90 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -6,8 +6,12 @@ #include "ParameterUtils.hpp" #include "GCode/ToolOrderUtils.hpp" #include "FilamentGroupUtils.hpp" +#include "MultiNozzleUtils.hpp" +#include "Utils.hpp" #include "I18N.hpp" +#include + // #define SLIC3R_DEBUG // Make assert active if SLIC3R_DEBUG @@ -149,26 +153,47 @@ static double calc_max_layer_height(const PrintConfig &config, double max_object } //calculate the flush weight (first value) and filament change count(second value) -static FilamentChangeStats calc_filament_change_info_by_toolorder(const PrintConfig* config, const std::vector& filament_map, const std::vector& flush_matrix, const std::vector>& layer_sequences) +// Nozzle-aware flush-stat calculator. Resolves each +// filament in the print sequence to its physical nozzle via the grouping result and tracks a +// per-nozzle NozzleStatusRecorder, so flush weight and flush_filament_change_count are charged +// per physical nozzle. For single-nozzle-per-extruder printers (H2D/X1/...) nozzle_id == extruder_id, +// so every returned value is identical to the extruder-level calculation. Out-of-range +// filament ids resolve to no nozzle and are skipped. +static FilamentChangeStats calc_filament_change_info_by_toolorder(const PrintConfig* config, const MultiNozzleUtils::LayeredNozzleGroupResult& group_result, const std::vector& flush_matrix, const std::vector>& layer_sequences) { FilamentChangeStats ret; std::unordered_map flush_volume_per_filament; - int max_extruder_id = *std::max_element(filament_map.begin(), filament_map.end()); - assert(max_extruder_id >= 0); - std::vectorlast_filament_per_extruder(max_extruder_id + 1, -1); + MultiNozzleUtils::NozzleStatusRecorder recorder; int total_filament_change_count = 0; + int total_flush_filament_change_count = 0; float total_filament_flush_weight = 0; - for (const auto& ls : layer_sequences) { - for (const auto& item : ls) { - int extruder_id = filament_map[item]; - int last_filament = last_filament_per_extruder[extruder_id]; - if (last_filament != -1 && last_filament != item) { - int flush_volume = flush_matrix[extruder_id][last_filament][item]; - flush_volume_per_filament[item] += flush_volume; - total_filament_change_count += 1; + + int old_filament_id = -1; + for (size_t layer_idx = 0; layer_idx < layer_sequences.size(); ++layer_idx) { + const auto& ls = layer_sequences[layer_idx]; + for (const auto& filament : ls) { + auto nozzle = group_result.get_nozzle_for_filament(filament, layer_idx); + if (!nozzle) + continue; + + int new_extruder_id = nozzle->extruder_id; + int new_nozzle_id_in_extruder = nozzle->group_id; + int new_filament_id_in_nozzle = filament; + int old_filament_id_in_nozzle = recorder.get_filament_in_nozzle(new_nozzle_id_in_extruder); + + bool filament_in_nozzle_change = old_filament_id_in_nozzle != -1 && new_filament_id_in_nozzle != old_filament_id_in_nozzle; + bool filament_change = old_filament_id != -1 && old_filament_id != new_filament_id_in_nozzle; + + if (filament_in_nozzle_change) { + total_flush_filament_change_count++; + int flush_volume = flush_matrix[new_extruder_id][old_filament_id_in_nozzle][new_filament_id_in_nozzle]; + flush_volume_per_filament[filament] += flush_volume; } - last_filament_per_extruder[extruder_id] = item; + if (filament_change) + total_filament_change_count++; + old_filament_id = new_filament_id_in_nozzle; + recorder.set_nozzle_status(new_nozzle_id_in_extruder, new_filament_id_in_nozzle, new_extruder_id); } } @@ -177,8 +202,9 @@ static FilamentChangeStats calc_filament_change_info_by_toolorder(const PrintCon total_filament_flush_weight += weight; } - ret.filament_change_count = total_filament_change_count; - ret.filament_flush_weight = (int)total_filament_flush_weight; + ret.filament_change_count = total_filament_change_count; + ret.flush_filament_change_count = total_flush_filament_change_count; + ret.filament_flush_weight = (int)total_filament_flush_weight; return ret; } @@ -1104,27 +1130,47 @@ float get_flush_volume(const std::vector &filament_maps, const std::vector< return flush_volume; } -std::vector ToolOrdering::get_recommended_filament_maps(const std::vector>& layer_filaments, const Print* print, const FilamentMapMode mode,const std::vector>&physical_unprintables,const std::vector>&geometric_unprintables) +// Forward declaration — the single-nozzle-per-extruder nozzle list (defined below). +static std::vector build_default_nozzle_list(const PrintConfig &print_config, size_t extruder_nums); + +// Best-effort readers for the multi-nozzle dev config keys. These are registered in the ConfigDef +// but not (yet) static PrintConfig members, so the slicing PrintConfig reads them as inert defaults, +// which keeps the auto grouping path bit-exact (all these degrade to the flush-only, non-switcher, +// hybrid-flow case). +static bool cfg_bool(const ConfigBase& c, const char* key, bool def) { - using namespace FilamentGroupUtils; - if (!print || layer_filaments.empty()) - return std::vector(); + if (auto* o = c.option(key)) return o->value; + return def; +} +static double cfg_float(const ConfigBase& c, const char* key, double def) +{ + if (auto* o = c.option(key)) return o->value; + return def; +} +static std::vector cfg_ints(const ConfigBase& c, const char* key) +{ + if (auto* o = c.option(key)) return o->values; + return {}; +} - const auto& print_config = print->config(); - const unsigned int filament_nums = (unsigned int)(print_config.filament_colour.values.size() + EPSILON); - - // get flush matrix - std::vector nozzle_flush_mtx; +// Prepare per-extruder flush matrices. The prime_volume_mode==pvmFast branch: Default reads +// flush_multiplier, Fast reads flush_multiplier_fast. +static std::vector prepare_flush_matrices(const PrintConfig& print_config) +{ size_t extruder_nums = print_config.nozzle_diameter.values.size(); + size_t filament_nums = print_config.filament_colour.values.size(); + std::vector nozzle_flush_mtx; for (size_t nozzle_id = 0; nozzle_id < extruder_nums; ++nozzle_id) { - std::vector flush_matrix(cast(get_flush_volumes_matrix(print_config.flush_volumes_matrix.values, nozzle_id, extruder_nums))); + std::vector flush_matrix(cast(get_flush_volumes_matrix(print_config.flush_volumes_matrix.values, nozzle_id, extruder_nums))); std::vector> wipe_volumes; for (unsigned int i = 0; i < filament_nums; ++i) wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums)); - nozzle_flush_mtx.emplace_back(wipe_volumes); } - auto flush_multiplies = print_config.flush_multiplier.values; + + // Fast purge mode uses flush_multiplier_fast; Default is inert. + auto flush_multiplies = (print_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? print_config.flush_multiplier_fast.values + : print_config.flush_multiplier.values; flush_multiplies.resize(extruder_nums, 1); for (size_t nozzle_id = 0; nozzle_id < extruder_nums; ++nozzle_id) { for (auto& vec : nozzle_flush_mtx[nozzle_id]) { @@ -1132,88 +1178,355 @@ std::vector ToolOrdering::get_recommended_filament_maps(const std::vector other_layers_seqs = get_other_layers_print_sequence(print_config.other_layers_print_sequence_nums.value, print_config.other_layers_print_sequence.values); - - // other_layers_seq: the layer_idx and extruder_idx are base on 1 - auto get_custom_seq = [&other_layers_seqs](int layer_idx, std::vector& out_seq) -> bool { - for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) { - const auto& other_layers_seq = other_layers_seqs[idx]; - if (layer_idx + 1 >= other_layers_seq.first.first && layer_idx + 1 <= other_layers_seq.first.second) { - out_seq = other_layers_seq.second; - return true; +// Per-extruder physical nozzle groups. +// Orca: bounds-guards the nozzle_volume_type / extruder_max_nozzle_count arrays, which may be +// shorter than the extruder count on some profiles. +static std::vector build_nozzle_groups(const PrintConfig& print_config, size_t extruder_nums) +{ + std::vector nozzle_groups; + auto extruder_nozzle_counts = get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values); + const auto& nozzle_volume_types = print_config.nozzle_volume_type.values; + for (size_t idx = 0; idx < extruder_nums; ++idx) { + std::string diameter = format_diameter_to_str(print_config.nozzle_diameter.values[idx]); + NozzleVolumeType vt = idx < nozzle_volume_types.size() ? NozzleVolumeType(nozzle_volume_types[idx]) : nvtStandard; + int max_count = idx < print_config.extruder_max_nozzle_count.values.size() ? print_config.extruder_max_nozzle_count.values[idx] : 1; + if (idx >= extruder_nozzle_counts.size() || extruder_nozzle_counts[idx].empty()) { + nozzle_groups.emplace_back(diameter, vt, (int)idx, max_count); + } else { + if (vt == nvtHybrid) { + for (auto& [volume_type, count] : extruder_nozzle_counts[idx]) + nozzle_groups.emplace_back(diameter, volume_type, (int)idx, count); + } else { + nozzle_groups.emplace_back(diameter, vt, (int)idx, extruder_nozzle_counts[idx][vt]); } } + } + return nozzle_groups; +} + +// Build the nozzle-centric FilamentGroupContext. +// Orca deviations, all inert for the shipping fleet: +// * no print->get_filament_usage_type() → FilamentInfo::usage_type stays ModelOnly (the default); +// * no print->get_filament_print_time() → speed_info.filament_print_time empty (TimeEvaluator → 0); +// * the fmmAutoForQuality and Bowden-PA-calibration limit blocks are omitted (Orca has no +// fmmAutoForQuality mode and no Calib_Params::has_bowden_extruder). +// prefer_non_model_filament (Bowden extruders) is all-false for the Direct-Drive BBL fleet, so the +// support-preference reward path stays dormant. +static FilamentGroupContext build_filament_group_context( + const Print* print, + const std::vector>& layer_filaments, + const std::vector>& physical_unprintables, + const std::vector>& geometric_unprintables, + const std::map>& unprintable_volumes, + FilamentMapMode mode, + const std::unordered_map& nozzle_status) +{ + using namespace MultiNozzleUtils; + using namespace FilamentGroupUtils; + + FilamentGroupContext context; + + const auto& print_config = print->config(); + const size_t filament_nums = print_config.filament_colour.values.size(); + const size_t extruder_nums = print_config.nozzle_diameter.values.size(); + bool has_multiple_nozzle = std::any_of(print_config.extruder_max_nozzle_count.values.begin(), print_config.extruder_max_nozzle_count.values.end(), + [](int v) { return v > 1; }); + + auto nozzle_flush_mtx = prepare_flush_matrices(print_config); + auto nozzle_groups = build_nozzle_groups(print_config, extruder_nums); + + std::vector> ext_unprintable_filaments; + collect_unprintable_limits(physical_unprintables, geometric_unprintables, ext_unprintable_filaments); + + bool ignore_ext_filament = false; + auto extruder_ams_counts = get_extruder_ams_count(print_config.extruder_ams_count.values); + std::vector group_size = calc_max_group_size(extruder_ams_counts, ignore_ext_filament); + + // When a filament switcher is connected, disable the AMS capacity limit for grouping. + const bool has_filament_switcher = cfg_bool(print_config, "has_filament_switcher", false); + if (has_filament_switcher) { + int total_filaments = (int)filament_nums; + for (auto& s : group_size) + s = std::max(s, total_filaments); + } + + std::vector prefer_non_model_filament(extruder_nums, false); + for (size_t idx = 0; idx < extruder_nums; ++idx) + if (idx < print_config.extruder_type.values.size()) + prefer_non_model_filament[idx] = (print_config.extruder_type.values[idx] == ExtruderType::etBowden); + + auto machine_filament_info = build_machine_filaments(print->get_extruder_filament_info(), extruder_ams_counts, ignore_ext_filament); + + std::vector filament_types = print_config.filament_type.values; + std::vector filament_colours = print_config.filament_colour.values; + std::vector filament_is_support = print_config.filament_is_support.values; + std::vector filament_ids = print_config.filament_ids.values; + + FGMode fg_mode = mode == FilamentMapMode::fmmAutoForMatch ? FGMode::MatchMode : FGMode::FlushMode; + context.model_info.flush_matrix = std::move(nozzle_flush_mtx); + context.model_info.unprintable_filaments = ext_unprintable_filaments; + context.model_info.layer_filaments = layer_filaments; + context.model_info.filament_ids = filament_ids; + context.model_info.unprintable_volumes = unprintable_volumes; + + for (size_t idx = 0; idx < filament_types.size(); ++idx) { + FilamentGroupUtils::FilamentInfo info; + info.color = filament_colours[idx]; + info.type = filament_types[idx]; + info.is_support = filament_is_support[idx]; + context.model_info.filament_info.emplace_back(std::move(info)); + } + + context.speed_info.group_with_time = cfg_bool(print_config, "group_algo_with_time", false); + context.speed_info.filament_change_time = print_config.machine_load_filament_time + print_config.machine_unload_filament_time; + context.speed_info.extruder_change_time = cfg_float(print_config, "machine_switch_extruder_time", 0.0); + { + double load_time = print_config.machine_load_filament_time; + double unload_time = print_config.machine_unload_filament_time; + context.speed_info.change_time_params.standard_load_time = static_cast(load_time); + context.speed_info.change_time_params.standard_unload_time = static_cast(unload_time); + context.speed_info.change_time_params.selector_load_time = static_cast(load_time / 2); + context.speed_info.change_time_params.selector_unload_time = static_cast(unload_time / 2); + } + + context.machine_info.machine_filament_info = machine_filament_info; + context.machine_info.max_group_size = std::move(group_size); + context.machine_info.master_extruder_id = print_config.master_extruder_id.value - 1; + context.machine_info.prefer_non_model_filament = prefer_non_model_filament; + + context.group_info.total_filament_num = (int)(filament_nums); + context.group_info.max_gap_threshold = 0.01; + context.group_info.strategy = FGStrategy::BestCost; + context.group_info.mode = fg_mode; + context.group_info.ignore_ext_filament = ignore_ext_filament; + context.group_info.has_filament_switcher = has_filament_switcher; + + // hybrid flow means no special per-filament nozzle-volume request. In manual mode we honour the + // config's per-filament volume map when it is present and correctly sized; otherwise (the key is + // not a static PrintConfig member) fall back to hybrid so rebuild_nozzle_unprintables + // never indexes out of range. + if (mode == FilamentMapMode::fmmManual) { + auto fvm = cfg_ints(print_config, "filament_volume_map"); + if (fvm.size() == filament_nums) + context.group_info.filament_volume_map = std::move(fvm); + else + context.group_info.filament_volume_map = std::vector(filament_nums, (int)(NozzleVolumeType::nvtHybrid)); + } else { + context.group_info.filament_volume_map = std::vector(filament_nums, (int)(NozzleVolumeType::nvtHybrid)); + } + + context.nozzle_info.nozzle_list = build_nozzle_list(nozzle_groups); + context.nozzle_info.extruder_nozzle_list = build_extruder_nozzle_list(context.nozzle_info.nozzle_list); + + if (context.nozzle_info.nozzle_list.empty()) + throw Slic3r::RuntimeError("No valid nozzle found. Please check nozzle count."); + + if (!nozzle_status.empty()) + context.nozzle_info.nozzle_status = nozzle_status; + + auto used_filaments = collect_sorted_used_filaments(layer_filaments); + + // add_volume_type_limits: only for single-nozzle-per-extruder machines (H2D and the like). A + // filament whose forbidden nozzle-volume-type matches an extruder's (only) nozzle becomes + // unprintable on that extruder; conflicts printable nowhere are dropped. + if (!has_multiple_nozzle) { + std::vector> ext_unprintable_filaments_with_volume = ext_unprintable_filaments; + for (auto& nozzle : context.nozzle_info.nozzle_list) { + for (auto fil_id : used_filaments) { + auto unprintable_vols = context.model_info.unprintable_volumes[fil_id]; + if (unprintable_vols.count(nozzle.volume_type) && nozzle.extruder_id >= 0 && nozzle.extruder_id < (int)ext_unprintable_filaments_with_volume.size()) + ext_unprintable_filaments_with_volume[nozzle.extruder_id].insert(fil_id); + } + } + for (auto fil_id : used_filaments) { + if (ext_unprintable_filaments_with_volume[0].count(fil_id) && ext_unprintable_filaments_with_volume[1].count(fil_id)) { + ext_unprintable_filaments_with_volume[0].erase(fil_id); + ext_unprintable_filaments_with_volume[1].erase(fil_id); + } + } + context.model_info.unprintable_filaments = ext_unprintable_filaments_with_volume; + } + + return context; +} + +// Orca: restore the master-extruder preference. Orca historically ran +// optimize_group_for_master_extruder / can_swap_groups after grouping so a light-filament print stays +// on the primary/master extruder. A weak in-enum penalty alone cannot overcome a pre-existing +// non-zero right-extruder self-flush term in the flush matrix (which otherwise pulls a lone filament +// onto the non-master extruder and changes H2D/H2C g-code). We re-apply the preference ONLY in this +// slicing wrapper — never in the FilamentGroup engine or the test harness — so existing prints keep +// their extruder assignment while the new engine's genuine multi-filament grouping deltas still land. +static bool can_swap_extruder_groups(int extruder_id_0, const std::set& group_0, int extruder_id_1, const std::set& group_1, const FilamentGroupContext& ctx) +{ + using namespace FilamentGroupUtils; + std::vector> extruder_unprintables(2); + { + std::vector> unprintable_filaments = ctx.model_info.unprintable_filaments; + if (unprintable_filaments.size() > 1) + remove_intersection(unprintable_filaments[0], unprintable_filaments[1]); + std::map> unplaceable_limits; + for (int group_id : {extruder_id_0, extruder_id_1}) + if (group_id >= 0 && group_id < (int)unprintable_filaments.size()) + for (auto f : unprintable_filaments[group_id]) + unplaceable_limits[f].emplace_back(group_id); + for (auto& elem : unplaceable_limits) sort_remove_duplicates(elem.second); + for (auto& elem : unplaceable_limits) + for (auto& eid : elem.second) { + if (eid == extruder_id_0) extruder_unprintables[0].insert(elem.first); + if (eid == extruder_id_1) extruder_unprintables[1].insert(elem.first); + } + } + for (auto fid : group_0) if (extruder_unprintables[1].count(fid) > 0) return false; + for (auto fid : group_1) if (extruder_unprintables[0].count(fid) > 0) return false; + const auto& mgs = ctx.machine_info.max_group_size; + if (extruder_id_0 < (int)mgs.size() && extruder_id_1 < (int)mgs.size() && + mgs[extruder_id_0] >= (int)group_0.size() && mgs[extruder_id_1] >= (int)group_1.size() && + (mgs[extruder_id_0] < (int)group_1.size() || mgs[extruder_id_1] < (int)group_0.size())) return false; + return true; +} + +// Balance a filament->nozzle map toward the master extruder (2-extruder machines only). If the +// non-master extruder holds strictly more used filaments than the master and the swap is valid, move +// each group's filaments onto nozzles of the opposite extruder. The exact nozzle within an extruder +// does not affect static g-code (which emits H-1), so re-nozzling round-robin is byte-safe. +static std::vector apply_master_extruder_preference(const FilamentGroupContext& ctx, const std::vector& used_filaments, std::vector nozzle_ret) +{ + const auto& extruder_nozzle_list = ctx.nozzle_info.extruder_nozzle_list; + int master = ctx.machine_info.master_extruder_id; + if (extruder_nozzle_list.size() != 2 || master < 0 || master > 1) return nozzle_ret; + int other = 1 - master; + if (extruder_nozzle_list.count(master) == 0 || extruder_nozzle_list.count(other) == 0) return nozzle_ret; + auto ext_of_nozzle = [&](int nid) -> int { + return (nid >= 0 && nid < (int)ctx.nozzle_info.nozzle_list.size()) ? ctx.nozzle_info.nozzle_list[nid].extruder_id : -1; + }; + std::set group_master, group_other; + for (auto fu : used_filaments) { + int f = (int)fu; + if (f >= (int)nozzle_ret.size()) continue; + int e = ext_of_nozzle(nozzle_ret[f]); + if (e == master) group_master.insert(f); + else if (e == other) group_other.insert(f); + } + if (group_other.size() > group_master.size() && + can_swap_extruder_groups(other, group_other, master, group_master, ctx)) { + const auto& master_nozzles = extruder_nozzle_list.at(master); + const auto& other_nozzles = extruder_nozzle_list.at(other); + if (!master_nozzles.empty() && !other_nozzles.empty()) { + int mi = 0, oi = 0; + for (auto f : group_other) nozzle_ret[f] = master_nozzles[(mi++) % master_nozzles.size()]; + for (auto f : group_master) nozzle_ret[f] = other_nozzles[(oi++) % other_nozzles.size()]; + } + } + return nozzle_ret; +} + +// Nozzle-centric grouping. Dispatches by FilamentMapMode and returns a nozzle-aware +// LayeredNozzleGroupResult. For single-extruder printers (X1/P1/A1/H2S) the grouping engine is not +// invoked — the trivial all-master map is wrapped in a single-nozzle result, so their g-code is +// unaffected. Multi-extruder (H2D) grouping runs the nozzle-centric FilamentGroup engine; +// multi-nozzle (H2C/A2L) resolves to a nozzle-granular result. +MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::get_recommended_filament_maps(const std::vector>& layer_filaments, const Print* print, const FilamentMapMode mode, const std::vector>& physical_unprintables, const std::vector>& geometric_unprintables, const std::map>& unprintable_volumes, const std::unordered_map& nozzle_status) +{ + using namespace FilamentGroupUtils; + using namespace MultiNozzleUtils; + + if (!print || layer_filaments.empty()) + return LayeredNozzleGroupResult(); + + const auto& print_config = print->config(); + size_t filament_nums = print_config.filament_colour.values.size(); + size_t extruder_nums = print_config.nozzle_diameter.values.size(); + auto used_filaments = collect_sorted_used_filaments(layer_filaments); + bool has_multiple_nozzle = std::any_of(print_config.extruder_max_nozzle_count.values.begin(), print_config.extruder_max_nozzle_count.values.end(), + [](int v) { return v > 1; }); + bool has_multiple_extruder = extruder_nums > 1; + + auto nozzle_list = build_default_nozzle_list(print_config, extruder_nums); + + // Manual mode: build directly from the user's filament->extruder map. + if (mode == FilamentMapMode::fmmManual && !has_multiple_nozzle) { + auto manual_filament_map = print_config.filament_map.values; + std::transform(manual_filament_map.begin(), manual_filament_map.end(), manual_filament_map.begin(), [](int v) { return v - 1; }); + auto result = LayeredNozzleGroupResult::create(manual_filament_map, nozzle_list, used_filaments); + return result ? *result : LayeredNozzleGroupResult(); + } + + // Fully-manual mode: build the nozzle-granular result from the config nozzle map. + if (mode == FilamentMapMode::fmmNozzleManual) { + auto manual_filament_map = print_config.filament_map.values; + std::transform(manual_filament_map.begin(), manual_filament_map.end(), manual_filament_map.begin(), [](int v) { return v - 1; }); + float diameter = print_config.nozzle_diameter.values.empty() ? 0.4f : (float)print_config.nozzle_diameter.values.front(); + auto nozzle_result = LayeredNozzleGroupResult::create(used_filaments, manual_filament_map, cfg_ints(print_config, "filament_volume_map"), cfg_ints(print_config, "filament_nozzle_map"), get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values), diameter); + return nozzle_result ? *nozzle_result : LayeredNozzleGroupResult(); + } + + int master_extruder_id = print_config.master_extruder_id.value - 1; + std::vector ret(filament_nums, master_extruder_id); + + // Non-BBL multi-extruder printers do not support filament grouping: filament id == extruder id. + if (has_multiple_extruder && !print->is_BBL_printer()) { + for (size_t i = 0; i < filament_nums && i < extruder_nums; i++) + ret[i] = (int)i; + auto result_opt = LayeredNozzleGroupResult::create(ret, nozzle_list, used_filaments); + return result_opt ? *result_opt : LayeredNozzleGroupResult(); + } + + if (has_multiple_extruder || has_multiple_nozzle) { + auto context = build_filament_group_context(print, layer_filaments, physical_unprintables, geometric_unprintables, unprintable_volumes, mode, nozzle_status); + + // other_layers_seq custom-sequence lambda (1-based layer/extruder). Only threaded for the + // single-nozzle-per-extruder engine. + std::vector other_layers_seqs = get_other_layers_print_sequence(print_config.other_layers_print_sequence_nums.value, print_config.other_layers_print_sequence.values); + auto get_custom_seq = [other_layers_seqs](int layer_idx, std::vector& out_seq) -> bool { + for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) { + const auto& other_layers_seq = other_layers_seqs[idx]; + if (layer_idx + 1 >= other_layers_seq.first.first && layer_idx + 1 <= other_layers_seq.first.second) { + out_seq = other_layers_seq.second; + return true; + } + } + return false; }; - int master_extruder_id = print_config.master_extruder_id.value -1; // switch to 0 based idx - std::vectorret(filament_nums, master_extruder_id); - bool ignore_ext_filament = false; // TODO: read from config - // if mutli_extruder, calc group,otherwise set to 0 - if (extruder_nums == 2 && print->is_BBL_printer()) { - std::vector extruder_ams_count_str = print_config.extruder_ams_count.values; - auto extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); - std::vector group_size = calc_max_group_size(extruder_ams_counts, ignore_ext_filament); - - auto machine_filament_info = build_machine_filaments(print->get_extruder_filament_info(), extruder_ams_counts, ignore_ext_filament); - - std::vector filament_types = print_config.filament_type.values; - std::vector filament_colours = print_config.filament_colour.values; - std::vector filament_is_support = print_config.filament_is_support.values; - std::vector filament_ids = print_config.filament_ids.values; - // speacially handle tpu filaments - auto used_filaments = collect_sorted_used_filaments(layer_filaments); - auto tpu_filaments = get_filament_by_type(used_filaments, &print_config, "TPU"); - FGMode fg_mode = mode == FilamentMapMode::fmmAutoForMatch ? FGMode::MatchMode: FGMode::FlushMode; - - std::vector> ext_unprintable_filaments; - collect_unprintable_limits(physical_unprintables, geometric_unprintables, ext_unprintable_filaments); - - FilamentGroupContext context; - { - context.model_info.flush_matrix = std::move(nozzle_flush_mtx); - context.model_info.unprintable_filaments = ext_unprintable_filaments; - context.model_info.layer_filaments = layer_filaments; - context.model_info.filament_ids = filament_ids; - - for (size_t idx = 0; idx < filament_types.size(); ++idx) { - FilamentGroupUtils::FilamentInfo info; - info.color = filament_colours[idx]; - info.type = filament_types[idx]; - info.is_support = filament_is_support[idx]; - context.model_info.filament_info.emplace_back(std::move(info)); + if (has_multiple_nozzle && mode == FilamentMapMode::fmmManual) { + auto manual_filament_map = print_config.filament_map.values; + std::transform(manual_filament_map.begin(), manual_filament_map.end(), manual_filament_map.begin(), [](int v) { return v - 1; }); + ret = calc_filament_group_for_manual_multi_nozzle(manual_filament_map, context); + } else if (has_multiple_nozzle && mode == FilamentMapMode::fmmAutoForMatch) { + ret = calc_filament_group_for_match_multi_nozzle(context); + } else { + // TPU: keep the dedicated TPU split for single-nozzle-per-extruder printers. + auto tpu_filaments = get_filament_by_type(used_filaments, &print_config, "TPU"); + if (!has_multiple_nozzle && !tpu_filaments.empty()) { + ret = std::vector(context.group_info.total_filament_num, context.machine_info.master_extruder_id); + for (size_t fidx = 0; fidx < (size_t)context.group_info.total_filament_num; ++fidx) + ret[fidx] = tpu_filaments.count((int)fidx) ? context.machine_info.master_extruder_id : (1 - context.machine_info.master_extruder_id); + } else { + FilamentGroup fg(context); + if (!has_multiple_nozzle) + fg.get_custom_seq = get_custom_seq; + ret = fg.calc_filament_group(); + // Flush-mode auto grouping: restore the master-extruder preference + // (optimize_group_for_master_extruder). Match mode assigns by AMS colour and never had + // it. Kept out of the FilamentGroup engine so the test harness is unaffected. + if (context.group_info.mode == FGMode::FlushMode) + ret = apply_master_extruder_preference(context, used_filaments, ret); } - - context.machine_info.machine_filament_info = machine_filament_info; - context.machine_info.max_group_size = std::move(group_size); - context.machine_info.master_extruder_id = master_extruder_id; - - context.group_info.total_filament_num = (int)(filament_nums); - context.group_info.max_gap_threshold = 0.01; - context.group_info.strategy = FGStrategy::BestCost; - context.group_info.mode = fg_mode; - context.group_info.ignore_ext_filament = ignore_ext_filament; } - - if (!tpu_filaments.empty()) { - ret = calc_filament_group_for_tpu(tpu_filaments, context.group_info.total_filament_num, context.machine_info.master_extruder_id); - } - else { - FilamentGroup fg(context); - fg.get_custom_seq = get_custom_seq; - ret = fg.calc_filament_group(); - } - } else if (extruder_nums > 1) { - // For non-bbl multi-extruder printers we don't support filament group yet, and we use filament id as extruder id - assert(extruder_nums == filament_nums); - for (int i = 0; i < filament_nums; i++) { - ret[i] = i; + if (has_multiple_nozzle) { + auto result_opt = LayeredNozzleGroupResult::create(ret, context.nozzle_info.nozzle_list, used_filaments); + return result_opt ? *result_opt : LayeredNozzleGroupResult(); } } - return ret; + auto result_opt = LayeredNozzleGroupResult::create(ret, nozzle_list, used_filaments); + return result_opt ? *result_opt : LayeredNozzleGroupResult(); } FilamentChangeStats ToolOrdering::get_filament_change_stats(FilamentChangeMode mode) @@ -1232,6 +1545,319 @@ FilamentChangeStats ToolOrdering::get_filament_change_stats(FilamentChangeMode m return m_stats_by_single_extruder; } +// Build one logical nozzle per extruder. This is the single-nozzle grouping: +// nozzle group_id == extruder_id. Kept file-local. +static std::vector build_default_nozzle_list(const PrintConfig &print_config, size_t extruder_nums) +{ + using namespace MultiNozzleUtils; + std::vector nozzle_list; + for (size_t idx = 0; idx < extruder_nums; ++idx) { + NozzleInfo tmp; + tmp.diameter = format_diameter_to_str(print_config.nozzle_diameter.values[idx]); + tmp.group_id = static_cast(idx); + tmp.extruder_id = static_cast(idx); + // nozzle_volume_type may be shorter than nozzle_diameter on some Orca profiles; default to Standard. + tmp.volume_type = idx < print_config.nozzle_volume_type.values.size() + ? NozzleVolumeType(print_config.nozzle_volume_type.values[idx]) + : nvtStandard; + nozzle_list.emplace_back(std::move(tmp)); + } + return nozzle_list; +} + +// Build a LayeredNozzleGroupResult from an already-resolved 0-based +// filament->extruder map. Used by the by-object (sequential) path, whose grouping is decided +// earlier in Print.cpp — here we only wrap the config map. For single-nozzle-per-extruder printers +// (the common case incl. H2D) each filament resolves to its extruder's one logical nozzle +// (nozzle_id == extruder_id). For a multi-nozzle printer the config's per-filament nozzle/volume +// choice is resolved via the 6-argument create (the per-layer engine owns the auto +// sequential path proper). +static MultiNozzleUtils::LayeredNozzleGroupResult build_group_result_from_map( + const PrintConfig& print_config, + const std::vector& filament_map_0based, + const std::vector& used_filaments) +{ + using namespace MultiNozzleUtils; + const size_t extruder_nums = print_config.nozzle_diameter.values.size(); + const bool has_multiple_nozzle = std::any_of(print_config.extruder_max_nozzle_count.values.begin(), print_config.extruder_max_nozzle_count.values.end(), + [](int v) { return v > 1; }); + if (has_multiple_nozzle) { + float diameter = print_config.nozzle_diameter.values.empty() ? 0.4f : static_cast(print_config.nozzle_diameter.values.front()); + if (auto g = LayeredNozzleGroupResult::create(used_filaments, filament_map_0based, cfg_ints(print_config, "filament_volume_map"), cfg_ints(print_config, "filament_nozzle_map"), get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values), diameter)) + return *g; + } + auto nozzle_list = build_default_nozzle_list(print_config, extruder_nums); + if (auto group = LayeredNozzleGroupResult::create(filament_map_0based, nozzle_list, used_filaments)) + return *group; + return LayeredNozzleGroupResult(); +} + +// Per-layer nozzle-state refinement. Given a per-range grouping result and the +// physical nozzle occupancy (nozzles_state: nozzle_id -> filament currently loaded), it re-matches +// each logical nozzle to a physical nozzle *within its extruder* by a MinFlushFlowSolver that +// rewards keeping an already-loaded filament (cost -1) and otherwise charges the averaged flush. +// Returns a new result carrying the remapped default filament->nozzle map (falls back to the input +// result if the remap cannot be built). File-local: only the per-layer engine below calls it. +static MultiNozzleUtils::LayeredNozzleGroupResult refine_groups_by_Nozzle_State( + const FilamentGroupContext& ctx, + const MultiNozzleUtils::LayeredNozzleGroupResult& group, + const std::unordered_map& nozzles_state) +{ + std::vector> nozzle_fils(ctx.nozzle_info.nozzle_list.size()); + auto fils = group.get_used_filaments(0); + auto fil_noz_map = group.get_layer_filament_nozzle_map(0); + + for (auto fil : fils) + nozzle_fils[fil_noz_map[fil]].emplace_back(fil); + + // 1. Collect the nozzles each filament may NOT use. + std::map> fil_unplaceable_nozs; + for (auto fil : fils) { + std::set unprintable_volumes; + if (ctx.model_info.unprintable_volumes.count(fil)) + unprintable_volumes = ctx.model_info.unprintable_volumes.at(fil); + auto expected_volume = ctx.group_info.filament_volume_map[fil]; + + for (int noz = 0; noz < (int) ctx.nozzle_info.nozzle_list.size(); noz++) { + auto noz_info = ctx.nozzle_info.nozzle_list[noz]; + int ext_id = noz_info.extruder_id; + auto ext_unprintable_fils = ctx.model_info.unprintable_filaments[ext_id]; + if (ext_unprintable_fils.count(fil) > 0 || + (expected_volume != nvtHybrid && expected_volume != noz_info.volume_type) || (unprintable_volumes.count(noz_info.volume_type) != 0)) + fil_unplaceable_nozs[fil].insert(noz); + } + } + + // 2. Global nozzle-match result. + std::unordered_map global_uv_match; + + // 3. Solve one min-cost flow per extruder. + for (const auto& [ext_id, ext_nozzles] : ctx.nozzle_info.extruder_nozzle_list) { + if (ext_nozzles.empty()) continue; + + // 3.1. u_nodes / v_nodes for this extruder. + std::vector u_nodes = ext_nozzles; + std::vector v_nodes = ext_nozzles; + + // 3.2. global nozzle id -> local index. + std::unordered_map global_to_local; + for (size_t i = 0; i < ext_nozzles.size(); ++i) + global_to_local[ext_nozzles[i]] = static_cast(i); + + // 3.3. cost matrix for this extruder. + std::vector> cost_matrix(u_nodes.size(), std::vector(v_nodes.size(), std::numeric_limits::max())); + std::unordered_map> uv_unlink_limits; + + for (size_t local_u = 0; local_u < u_nodes.size(); ++local_u) { + int u_node = u_nodes[local_u]; + std::set unlink_v_local; + auto u_fils = nozzle_fils[u_node]; + + // Collect the v_nodes this u_node may NOT connect to (as local indices). + for (auto fil : u_fils) { + for (auto unplaceable_noz : fil_unplaceable_nozs[fil]) { + if (global_to_local.count(unplaceable_noz)) + unlink_v_local.insert(global_to_local[unplaceable_noz]); + } + } + uv_unlink_limits[static_cast(local_u)].assign(unlink_v_local.begin(), unlink_v_local.end()); + + // 3.4. compute costs. + for (size_t local_v = 0; local_v < v_nodes.size(); ++local_v) { + int v_node = v_nodes[local_v]; + float cost = 0; + if (unlink_v_local.count(static_cast(local_v))) continue; + + std::optional v_fil_opt = std::nullopt; + if (nozzles_state.count(v_node)) + v_fil_opt = nozzles_state.at(v_node); + + if (!v_fil_opt.has_value() || v_fil_opt.value() >= ctx.model_info.filament_info.size()) { + cost = 0; + } else { + int v_fil = v_fil_opt.value(); + if (std::find(u_fils.begin(), u_fils.end(), v_fil) != u_fils.end()) + cost = -1; + else { + for (auto u_fil : u_fils) + cost += ctx.model_info.flush_matrix[ext_id][u_fil][v_fil]; + if (u_fils.size() > 0) + cost /= u_fils.size(); + } + } + + cost_matrix[local_u][local_v] = cost; + } + } + + // 3.5. min-cost flow -> nozzle match for this extruder. + std::vector local_u_nodes(u_nodes.size()); + std::vector local_v_nodes(v_nodes.size()); + std::iota(local_u_nodes.begin(), local_u_nodes.end(), 0); + std::iota(local_v_nodes.begin(), local_v_nodes.end(), 0); + + MinFlushFlowSolver solver(cost_matrix, local_u_nodes, local_v_nodes, {}, uv_unlink_limits); + auto local_match = solver.solve(); + + // 3.6. local match -> global match. + for (size_t local_u = 0; local_u < u_nodes.size(); ++local_u) { + int global_u = u_nodes[local_u]; + int local_v = local_match[static_cast(local_u)]; + if (local_v == MaxFlowGraph::INVALID_ID || local_v < 0 || local_v >= static_cast(v_nodes.size())) + continue; + int global_v = v_nodes[local_v]; + global_uv_match[global_u] = global_v; + } + } + + // 4. Build the new group_result. + std::vector new_default_filament_nozzle_maps = group.get_layer_filament_nozzle_map(-1); + + for (auto fil : fils) { + int ori_noz = new_default_filament_nozzle_maps[fil]; + if (global_uv_match.count(ori_noz)) + new_default_filament_nozzle_maps[fil] = global_uv_match[ori_noz]; + } + + auto new_group = MultiNozzleUtils::LayeredNozzleGroupResult::create(new_default_filament_nozzle_maps, ctx.nozzle_info.nozzle_list, fils); + if (!new_group.has_value()) new_group = group; + + return *new_group; +} + +// Used as an unordered_map key over a filament-set (the per-layer filament combo). +struct VectorHash +{ + size_t operator()(const std::vector& v) const + { + size_t seed = v.size(); + for (auto& elem : v) + seed ^= std::hash()(elem) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + return seed; + } +}; + +// The per-layer regroup engine. Layers are grouped into +// contiguous runs sharing the same filament set ("combo ranges"); a NozzleStatusRecorder carries the +// physical nozzle occupancy across ranges so the selector rewards keeping an already-loaded filament. +// Per range: get_recommended_filament_maps -> refine_groups_by_Nozzle_State (nozzle re-match) -> +// reorder_filaments_for_multi_nozzle_extruder (in-range ordering). Emits a per-layer +// filament->nozzle match + filament order. Orca: there is no ToolOrdering::OrderingContext here, so +// the custom-sequence function is passed directly instead. Only the dynamic branch +// (H2C selector, is_dynamic_group_reorder) calls this. +static std::vector plan_filament_mapping_and_order_by_combo_ranges( + Print* print, + const FilamentGroupContext& ctx, + const std::function&)> get_custom_seq, + const FilamentMapMode mode, + const std::vector>& physical_unprintables, + const std::vector>& geometric_unprintables, + const std::map>& unprintable_volumes, + MultiNozzleUtils::NozzleStatusRecorder* io_nozzle_status) +{ + std::vector results; + + const auto& layer_fils = ctx.model_info.layer_filaments; + if (layer_fils.empty()) + return results; + + results.resize(layer_fils.size()); + + // key: the sorted+deduped filament set used by a layer; value: the contiguous [start,end] runs. + std::unordered_map, std::vector>, VectorHash> filament_combo_ranges; + for (int layer_idx = 0; layer_idx < static_cast(layer_fils.size()); ++layer_idx) { + std::vector cur_combo = layer_fils[layer_idx]; + std::sort(cur_combo.begin(), cur_combo.end()); + cur_combo.erase(std::unique(cur_combo.begin(), cur_combo.end()), cur_combo.end()); + if (cur_combo.empty()) + continue; + + auto& ranges = filament_combo_ranges[cur_combo]; + if (ranges.empty() || ranges.back().second != layer_idx - 1) + ranges.emplace_back(layer_idx, layer_idx); + else + ranges.back().second = layer_idx; + } + + std::map, std::vector> range_filas_map; + for (auto& [combo, ranges] : filament_combo_ranges) + for (auto& range : ranges) + range_filas_map[range] = combo; + + std::set used_filaments; + + // Per combo range: build the range's layer_filaments, group + refine + reorder. + MultiNozzleUtils::NozzleStatusRecorder tool_status; + if (io_nozzle_status) tool_status = *io_nozzle_status; + + std::vector fil_noz_map(ctx.group_info.total_filament_num, -1); // global filament -> nozzle map + std::unordered_map fil_first_nozzle_map; // filament -> first nozzle it used + for (auto& [range, combo] : range_filas_map) { + auto [start_layer, end_layer] = range; + // 1. layer_filaments for this range. + std::vector> range_layer_fils; + range_layer_fils.reserve(end_layer - start_layer + 1); + for (int layer_idx = start_layer; layer_idx <= end_layer; ++layer_idx) + range_layer_fils.push_back(layer_fils[layer_idx]); + used_filaments.insert(combo.begin(), combo.end()); + + // 2. group the range. + auto nozzle_filament_map = tool_status.get_nozzle_filament_map(); + auto group_result = ToolOrdering::get_recommended_filament_maps(range_layer_fils, print, mode, physical_unprintables, geometric_unprintables, unprintable_volumes, nozzle_filament_map); + + // 3. re-match logical nozzles to physical nozzles by the current nozzle state. + auto new_group_result = refine_groups_by_Nozzle_State(ctx, group_result, nozzle_filament_map); + + auto range_seq_function = [&get_custom_seq, start_layer_ = start_layer, end_layer_ = end_layer](int layer_idx, std::vector& out_seq) -> bool { + if (layer_idx <= end_layer_ - start_layer_) { + int global_idx = start_layer_ + layer_idx; + return get_custom_seq ? get_custom_seq(global_idx, out_seq) : false; + } + return false; + }; + + // 4. order the filaments within the range. + std::vector> fils_sequences; + reorder_filaments_for_multi_nozzle_extruder(range_layer_fils.front(), new_group_result, range_layer_fils, ctx.model_info.flush_matrix, range_seq_function, + &fils_sequences, tool_status); + + // 5. store the range result + advance the nozzle state. + for (auto fil_id : fils_sequences.back()) { + auto noz = new_group_result.get_nozzle_for_filament(fil_id); + if (noz.has_value()) { + int noz_id = noz->group_id; + int ext_id = noz->extruder_id; + + fil_noz_map[fil_id] = noz_id; + fil_first_nozzle_map.emplace(static_cast(fil_id), noz_id); + + tool_status.set_current_extruder_id(ext_id); + tool_status.set_nozzle_status(noz_id, fil_id, ext_id); + } + } + + assert(fils_sequences.size() == range_layer_fils.size()); + for (size_t layer_id = 0; layer_id < fils_sequences.size(); ++layer_id) { + int g_layer_id = start_layer + static_cast(layer_id); + results[g_layer_id].fil_nozzle_match = fil_noz_map; + results[g_layer_id].fil_order = std::vector(fils_sequences[layer_id].begin(), fils_sequences[layer_id].end()); + } + } + + // Fill any never-assigned slot with the filament's first nozzle (or nozzle 0). + for (auto& res : results) { + for (int fil_id = 0; fil_id < (int) res.fil_nozzle_match.size(); fil_id++) { + auto& noz_id = res.fil_nozzle_match[fil_id]; + if (noz_id == -1) + noz_id = (used_filaments.count(fil_id) && fil_first_nozzle_map.count(fil_id)) ? fil_first_nozzle_map[fil_id] : 0; + } + } + + if (io_nozzle_status) *io_nozzle_status = tool_status; + + return results; +} + void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer) { const PrintConfig* print_config = m_print_config_ptr; @@ -1263,7 +1889,9 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first nozzle_flush_mtx.emplace_back(wipe_volumes); } - auto flush_multiplies = print_config->flush_multiplier.values; + // Fast purge mode uses flush_multiplier_fast; Default is inert. + auto flush_multiplies = (print_config->prime_volume_mode == PrimeVolumeMode::pvmFast) ? print_config->flush_multiplier_fast.values + : print_config->flush_multiplier.values; flush_multiplies.resize(nozzle_nums, 1); for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) { for (auto& vec : nozzle_flush_mtx[nozzle_id]) { @@ -1287,31 +1915,14 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first filament_maps = m_print->get_filament_maps(); map_mode = m_print->get_filament_map_mode(); - // only check and map in sequence mode, in by object mode, we check the map in print.cpp - if (print_config->print_sequence != PrintSequence::ByObject || m_print->objects().size() == 1) { - if (map_mode < FilamentMapMode::fmmManual) { - const PrintConfig* print_config = m_print_config_ptr; - if (!print_config && m_print_object_ptr) { - print_config = &(m_print_object_ptr->print()->config()); - } - filament_maps = ToolOrdering::get_recommended_filament_maps(layer_filaments, m_print, map_mode, physical_unprintables, geometric_unprintables); - - if (filament_maps.empty()) - return; - std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value + 1; }); - m_print->update_filament_maps_to_config(filament_maps); - } - std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; }); - - if (m_print->is_BBL_printer()) - check_filament_printable_after_group(used_filaments, filament_maps, print_config); - } - else { - // we just need to change the map to 0 based - std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; }); - } + // Grouping now yields a nozzle-aware LayeredNozzleGroupResult; the + // extruder-level filament_maps that feeds the ordering/stats below is derived from it. + MultiNozzleUtils::LayeredNozzleGroupResult grouping_result; + // The custom-sequence machinery is built before the grouping decision so both the static reorder + // and the dynamic per-layer plan can share it. Pure local setup (no dependency on the + // grouping result), so hoisting it above the branch does not change the static path's output. std::vector>filament_sequences; std::vectorfilament_lists(number_of_extruders); std::iota(filament_lists.begin(), filament_lists.end(), 0); @@ -1346,20 +1957,108 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first return false; }; + // Dynamic (per-layer filament-selector) regroup. is_dynamic_group_reorder() + // gates on enable_filament_dynamic_map (unset on every current profile), so this is closed for the + // whole shipping fleet AND H2C static mode — the static branch below is the only one they take, so + // their g-code is byte-identical. Only an H2C profile that enables the selector opens this branch. + const bool dynamic_reorder = m_print && m_print->is_dynamic_group_reorder(); + + if (dynamic_reorder) { + // Build the grouping context, plan per-combo-range nozzle maps + filament orders, then wrap the + // per-layer maps in a selector (4-arg create) result — which sets support_dynamic_nozzle_map and + // lights the GCode per-layer hotend/nozzle placeholders. filament_sequences is produced here, so + // the static reorder below is skipped for this branch. + auto grouping_context = build_filament_group_context(m_print, layer_filaments, physical_unprintables, geometric_unprintables, {}, FilamentMapMode::fmmAutoForFlush, + m_initial_nozzle_status.get_nozzle_filament_map()); + // The time estimator's per-extruder print times are global and do not apply to per-range + // grouping. + grouping_context.speed_info.group_with_time = false; + + m_nozzle_status = m_initial_nozzle_status; + auto dynamic_plan_res = plan_filament_mapping_and_order_by_combo_ranges(m_print, grouping_context, get_custom_seq, FilamentMapMode::fmmAutoForFlush, + physical_unprintables, geometric_unprintables, {}, &m_nozzle_status); + + std::vector> nozzle_map_per_layer; + for (auto& res : dynamic_plan_res) { + filament_sequences.emplace_back(cast(res.fil_order)); + nozzle_map_per_layer.emplace_back(res.fil_nozzle_match); + } + + auto result = MultiNozzleUtils::LayeredNozzleGroupResult::create(nozzle_map_per_layer, grouping_context.nozzle_info.nozzle_list, used_filaments, filament_sequences); + grouping_result = result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult(); + + // Derive the extruder-level map (0-based) for the stats path + config write-back. The full + // per-nozzle config write-back is not yet wired. + std::vector derived_maps = grouping_result.get_extruder_map(false); // 1-based + if (!derived_maps.empty()) { + filament_maps = derived_maps; + m_print->update_filament_maps_to_config(filament_maps); + } + std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; }); + } + // only check and map in sequence mode, in by object mode, we check the map in print.cpp + else if (print_config->print_sequence != PrintSequence::ByObject || m_print->objects().size() == 1) { + grouping_result = ToolOrdering::get_recommended_filament_maps(layer_filaments, m_print, map_mode, physical_unprintables, geometric_unprintables); + std::vector derived_maps = grouping_result.get_extruder_map(false); // 1-based extruder map + + if (map_mode < FilamentMapMode::fmmManual) { + if (derived_maps.empty()) + return; + filament_maps = derived_maps; + m_print->update_filament_maps_to_config(filament_maps); + } else if (!derived_maps.empty()) { + // Manual modes: the result mirrors the user's config map; adopt it for consistency. + filament_maps = derived_maps; + } + std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; }); + + if (m_print->is_BBL_printer()) + check_filament_printable_after_group(used_filaments, filament_maps, print_config); + } + else { + // by-object: grouping was decided in Print.cpp; just wrap the (0-based) config map. + std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; }); + grouping_result = build_group_result_from_map(*print_config, filament_maps, used_filaments); + } + + // The grouping result comes from the nozzle-centric engine. For single-nozzle-per-extruder printers + // (incl. H2D) each filament resolves to nozzle_id == extruder_id, so GCode's static hotend/nozzle + // placeholders are unchanged; H2C/A2L resolve to a nozzle-granular result (dynamic mode + // resolves per-layer). GCode consumes this via Print::get_layered_nozzle_group_result(). + m_nozzle_group_result = grouping_result; + { + // Orca: the ToolOrdering member is stored unconditionally, but the Print-level store is gated + // behind a not-sequential check. There is no is_sequential_print() here, so it is mirrored + // with the same predicate the branch above uses. + const bool not_sequential = print_config->print_sequence != PrintSequence::ByObject || + (m_print && m_print->objects().size() == 1); + if (m_print != nullptr && not_sequential) + m_print->set_nozzle_group_result(std::make_shared(m_nozzle_group_result)); + } + auto maps_without_group = filament_maps; for (auto& item : maps_without_group) item = 0; - reorder_filaments_for_minimum_flush_volume( - filament_lists, - m_print->is_BBL_printer() ? filament_maps : maps_without_group, // non-bbl printers do not support filament group yet - layer_filaments, - nozzle_flush_mtx, - get_custom_seq, - &filament_sequences - ); + // The dynamic branch produced filament_sequences itself (per-layer selector plan); only the static + // path needs the extruder-map flush reorder here. + if (!dynamic_reorder) { + reorder_filaments_for_minimum_flush_volume( + filament_lists, + m_print->is_BBL_printer() ? filament_maps : maps_without_group, // non-bbl printers do not support filament group yet + layer_filaments, + nozzle_flush_mtx, + get_custom_seq, + &filament_sequences + ); + } - auto curr_flush_info = calc_filament_change_info_by_toolorder(print_config, filament_maps, nozzle_flush_mtx, filament_sequences); + // The three-mode flush-stat caches are now computed from the nozzle-aware grouping result via the + // nozzle-aware calc_filament_change_info_by_toolorder. Stats are GUI-only (surfaced by + // get_filament_change_stats for the mode comparison); they never feed g-code, so this block is + // byte-inert. For single-nozzle-per-extruder printers (H2D/X1/...) nozzle_id == extruder_id, so + // every cached value equals the extruder-level stats. + auto curr_flush_info = calc_filament_change_info_by_toolorder(print_config, grouping_result, nozzle_flush_mtx, filament_sequences); if (nozzle_nums <= 1) m_stats_by_single_extruder = curr_flush_info; else { @@ -1368,35 +2067,71 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first m_stats_by_multi_extruder_best = curr_flush_info; } - // in multi extruder mode,collect data with other mode + // in multi extruder mode, collect data under the other modes (for the GUI mode comparison) if (nozzle_nums > 1) { - // always calculate the info by one extruder + // always calculate the info as if a single extruder were used { - std::vector>filament_sequences_one_extruder; + std::vector> single_extruder_sequences; reorder_filaments_for_minimum_flush_volume( filament_lists, maps_without_group, layer_filaments, nozzle_flush_mtx, get_custom_seq, - &filament_sequences_one_extruder + &single_extruder_sequences ); - m_stats_by_single_extruder = calc_filament_change_info_by_toolorder(print_config, maps_without_group, nozzle_flush_mtx, filament_sequences_one_extruder); + // One logical nozzle (extruder 0, nozzle 0); every filament resolves to it. + // diameter/volume_type are unused by the stat calc. + MultiNozzleUtils::NozzleInfo single_nozzle; + single_nozzle.volume_type = NozzleVolumeType::nvtStandard; + single_nozzle.extruder_id = 0; + single_nozzle.group_id = 0; + auto single_result = MultiNozzleUtils::LayeredNozzleGroupResult::create(maps_without_group, {single_nozzle}, used_filaments); + if (single_result) + m_stats_by_single_extruder = calc_filament_change_info_by_toolorder(print_config, *single_result, nozzle_flush_mtx, single_extruder_sequences); } - // if not in best for flush mode,also calculate the info by best for flush mode + // if not already in best-for-flush mode, also calculate the info under best-for-flush grouping if (map_mode != fmmAutoForFlush) { - std::vector>filament_sequences_one_extruder; - std::vectorfilament_maps_auto = get_recommended_filament_maps(layer_filaments, m_print, fmmAutoForFlush, physical_unprintables, geometric_unprintables); - reorder_filaments_for_minimum_flush_volume( - filament_lists, - filament_maps_auto, - layer_filaments, - nozzle_flush_mtx, - get_custom_seq, - &filament_sequences_one_extruder - ); - m_stats_by_multi_extruder_best = calc_filament_change_info_by_toolorder(print_config, filament_maps_auto, nozzle_flush_mtx, filament_sequences_one_extruder); + std::vector> best_sequences; + if (dynamic_reorder) { + // When the filament selector is active the "best" plan + // is the per-combo-range dynamic regroup, computed over a *copy* of the initial nozzle + // status so it cannot perturb the chosen m_nozzle_status / primary result. NOTE: + // is_dynamic_group_reorder() implies filament_map_mode == fmmAutoForFlush, contradicting + // this map_mode != fmmAutoForFlush guard, so this sub-branch is unreachable under the + // current predicate. It is provably inert. + auto best_context = build_filament_group_context(m_print, layer_filaments, physical_unprintables, geometric_unprintables, {}, FilamentMapMode::fmmAutoForFlush, + m_initial_nozzle_status.get_nozzle_filament_map()); + best_context.speed_info.group_with_time = false; + MultiNozzleUtils::NozzleStatusRecorder best_nozzle_status = m_initial_nozzle_status; + auto best_plan = plan_filament_mapping_and_order_by_combo_ranges(m_print, best_context, get_custom_seq, FilamentMapMode::fmmAutoForFlush, + physical_unprintables, geometric_unprintables, {}, &best_nozzle_status); + std::vector> best_nozzle_map_per_layer; + for (auto& res : best_plan) { + best_sequences.emplace_back(cast(res.fil_order)); + best_nozzle_map_per_layer.emplace_back(res.fil_nozzle_match); + } + auto best_result = MultiNozzleUtils::LayeredNozzleGroupResult::create(best_nozzle_map_per_layer, best_context.nozzle_info.nozzle_list, used_filaments, best_sequences); + if (best_result) + m_stats_by_multi_extruder_best = calc_filament_change_info_by_toolorder(print_config, *best_result, nozzle_flush_mtx, best_sequences); + } + else { + // Best-for-flush grouping (nozzle-aware result). The extruder-level map fed to the flush + // reorder is derived exactly as before, so best_sequences (and the flush weight) are + // identical; only flush_filament_change_count is now charged per physical nozzle. + auto best_group_result = get_recommended_filament_maps(layer_filaments, m_print, fmmAutoForFlush, physical_unprintables, geometric_unprintables); + std::vector best_maps = best_group_result.get_extruder_map(); + reorder_filaments_for_minimum_flush_volume( + filament_lists, + best_maps, + layer_filaments, + nozzle_flush_mtx, + get_custom_seq, + &best_sequences + ); + m_stats_by_multi_extruder_best = calc_filament_change_info_by_toolorder(print_config, best_group_result, nozzle_flush_mtx, best_sequences); + } } } diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 11e351b99a..b67fea119a 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -9,6 +9,7 @@ #include #include "../FilamentGroup.hpp" +#include "../MultiNozzleUtils.hpp" #include "../ExtrusionEntity.hpp" #include "../PrintConfig.hpp" @@ -98,19 +99,23 @@ private: struct FilamentChangeStats { int filament_flush_weight{0}; + // flush_filament_change_count counts filament changes that actually flush a physical nozzle. + // It replaces the former (dead, never populated) extruder_change_count. For single-nozzle-per- + // extruder printers it equals the per-extruder filament_change_count, so GUI stat displays are + // unchanged. + int flush_filament_change_count{0}; int filament_change_count{0}; - int extruder_change_count{0}; void clear(){ filament_flush_weight = 0; filament_change_count = 0; - extruder_change_count = 0; + flush_filament_change_count = 0; } FilamentChangeStats& operator+=(const FilamentChangeStats& other) { this->filament_flush_weight += other.filament_flush_weight; this->filament_change_count += other.filament_change_count; - this->extruder_change_count += other.extruder_change_count; + this->flush_filament_change_count += other.flush_filament_change_count; return *this; } @@ -118,7 +123,7 @@ struct FilamentChangeStats FilamentChangeStats ret; ret.filament_flush_weight = this->filament_flush_weight + other.filament_flush_weight; ret.filament_change_count = this->filament_change_count + other.filament_change_count; - ret.extruder_change_count = this->extruder_change_count + other.extruder_change_count; + ret.flush_filament_change_count = this->flush_filament_change_count + other.flush_filament_change_count; return ret; } @@ -236,12 +241,21 @@ public: bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().has_wipe_tower; } int get_most_used_extruder() const { return most_used_extruder; } + + // Logical (extruder, nozzle) grouping of the used filaments, built during reorder. + // For single-nozzle printers this is one logical nozzle per extruder (nozzle id == extruder id). + // Consumed by GCode (get_nozzle_id / get_first_nozzle_for_filament). + const MultiNozzleUtils::LayeredNozzleGroupResult &get_layered_nozzle_group_result() const { return m_nozzle_group_result; } /* * called in single extruder mode, the value in map are all 0 * called in dual extruder mode, the value in map will be 0 or 1 * 0 based group id */ - static std::vector get_recommended_filament_maps(const std::vector>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector>& physical_unprintables, const std::vector>& geometric_unprintables); + // Nozzle-centric grouping. Returns a nozzle-aware LayeredNozzleGroupResult instead of a plain + // extruder-level std::vector. Callers derive the 0/1-based extruder map via + // result.get_extruder_map(). unprintable_volumes / nozzle_status default empty for the static + // path; the per-layer engine supplies non-empty values. + static MultiNozzleUtils::LayeredNozzleGroupResult get_recommended_filament_maps(const std::vector>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector>& physical_unprintables, const std::vector>& geometric_unprintables, const std::map>& unprintable_volumes = {}, const std::unordered_map& nozzle_status = {}); // should be called after doing reorder FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode); @@ -283,6 +297,13 @@ private: FilamentChangeStats m_stats_by_single_extruder; FilamentChangeStats m_stats_by_multi_extruder_curr; FilamentChangeStats m_stats_by_multi_extruder_best; + MultiNozzleUtils::LayeredNozzleGroupResult m_nozzle_group_result; + // Physical nozzle occupancy threaded through the per-layer selector regroup. + // m_initial_nozzle_status seeds the first combo range (empty for a fresh slice — there is no + // device continuation state); m_nozzle_status carries the running state out of the plan. Inert + // for every printer except an H2C profile that enables the filament selector (is_dynamic_group_reorder). + MultiNozzleUtils::NozzleStatusRecorder m_initial_nozzle_status; + MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status; int most_used_extruder; }; diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index 13807c191b..38b7b56176 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -1493,6 +1494,21 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) { m_flat_ironing = (m_flat_ironing && m_use_gap_wall); + + // Prime-tower heating during wipe. m_is_multiple_nozzle mirrors the gate used in ToolOrdering/GCode + // (std::any_of extruder_max_nozzle_count > 1); it is false for every current printer, so the + // heating-during-wipe logic in toolchange_wipe_new is inert. + m_hotend_heating_rate = config.hotend_heating_rate.values; + m_physical_extruder_map = config.physical_extruder_map.values; + m_is_multiple_nozzle = std::any_of(config.extruder_max_nozzle_count.values.begin(), + config.extruder_max_nozzle_count.values.end(), + [](int v) { return v > 1; }); + + // Per-extruder printable-height clamp. Empty for single-extruder printers + // (extruder_printable_height = []), so is_valid_last_layer is inert there. + m_printable_height = config.extruder_printable_height.values; + m_last_layer_id.assign(config.nozzle_diameter.size(), -1); + // Read absolute value of first layer speed, if given as percentage, // it is taken over following default. Speeds from config are not // easily accessible here. @@ -1558,8 +1574,12 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config) } m_filpar[idx].tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx); m_filpar[idx].tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx); + // PETG pre-extrusion offset reuses the tower-interface pre-extrusion distance. Only read by the + // has_filament_switcher-gated PETG branch in get_next_pos (inert fleet-wide). + m_filpar[idx].petg_pre_extrusion_offset_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx); m_filpar[idx].tower_ironing_area = config.filament_tower_ironing_area.get_at(idx); m_filpar[idx].tower_interface_purge_length = config.filament_tower_interface_purge_volume.get_at(idx); + m_filpar[idx].filament_cooling_before_tower = config.filament_cooling_before_tower.get_at(idx); // If this is a single extruder MM printer, we will use all the SE-specific config values. // Otherwise, the defaults will be used to turn off the SE stuff. @@ -1651,6 +1671,27 @@ Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, fl break; default: break; } + // Shift the wipe start outward for a PETG pre-extrusion on filament-switcher devices, clamped to the + // shared printable bed. Gated on m_has_filament_switcher, which is false for the whole shipping fleet + // (no profile sets the key), so is_petg_pre_extrusion is always false and res is returned unchanged. + // The tower-interface contact branch is deliberately NOT applied here (enable_tower_interface_features + // DOES ship on H2C/X2D; applying it would change their g-code); is_contact_pre_extrusion is computed + // only as the guard that gives the contact path priority over PETG. + bool is_contact_pre_extrusion = interface_layer && m_enable_tower_interface_features; + bool is_petg_pre_extrusion = !is_contact_pre_extrusion && is_petg_filament(m_current_tool) && m_has_filament_switcher; + if (is_petg_pre_extrusion) { + Vec2f stop_pos = res; + float offset_dist = m_filpar[m_current_tool].petg_pre_extrusion_offset_dist; + auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); // BoundingBoxBase + printer_bbx.translate((-m_wipe_tower_pos - m_rib_offset).cast()); + if (stop_pos.x() < m_wipe_tower_width / 2.f) + stop_pos = Vec2f(stop_pos.x() - offset_dist, stop_pos.y()); + else + stop_pos = Vec2f(stop_pos.x() + offset_dist, stop_pos.y()); + if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = printer_bbx.min[0]; + if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = printer_bbx.max[0]; + res = stop_pos; + } return res; } @@ -2645,6 +2686,11 @@ bool WipeTower::is_tpu_filament(int filament_id) const return m_filpar[filament_id].material == "TPU"; } +bool WipeTower::is_petg_filament(int filament_id) const +{ + return m_filpar[filament_id].material == "PETG"; +} + // BBS: consider both soluable and support properties // Return index of first toolchange that switches to non-soluble and non-support extruder // ot -1 if there is no such toolchange. @@ -2717,6 +2763,9 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer) float spacing = m_layer_info->extra_spacing; if (has_tpu_filament() && m_layer_info->extra_spacing < m_tpu_fixed_spacing) spacing = 1; float nozzle_change_depth = tool_change.nozzle_change_depth * spacing; + // Drop the nozzle-change depth on an extruder's final layer above its printable height + // (inert unless is_valid_last_layer clamps, i.e. multi-extruder near Z-max). + if (!is_valid_last_layer(old_filament, m_cur_layer_id, layer.z)) nozzle_change_depth = 0.f; //float nozzle_change_depth = tool_change.nozzle_change_depth * (has_tpu_filament() ? m_tpu_fixed_spacing : layer.extra_spacing); auto* block = get_block_by_category(m_filpar[new_filament].category, false); if (!block) @@ -2760,7 +2809,10 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer) WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool solid_toolchange,bool solid_nozzlechange) { m_nozzle_change_result.gcode.clear(); - if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]) { + // Skip the cross-extruder nozzle change (ramming) on an extruder's final layer above its printable + // height. is_valid_last_layer is inert unless multi-extruder near Z-max. + if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool] + && is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) { m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange); } @@ -3411,23 +3463,103 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat x_to_wipe = solid_tool_toolchange ? std::numeric_limits::max(): x_to_wipe; float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f; target_speed = solid_tool_toolchange ? 20.f * 60.f : target_speed; - float wipe_speed = 0.33f * target_speed; + // Nominal wipe-speed schedule. The applied wipe_speed is nominal_speed * speed_factor; speed_factor + // stays 1.0 unless the H2C prime-tower heating-during-wipe model below slows the wipe so the hotend + // can reach temperature (nominal_speed == wipe_speed when speed_factor == 1, i.e. single-nozzle). + float nominal_speed = 0.33f * target_speed; m_left_to_right = ((m_cur_layer_id + 3) % 4 >= 2); bool is_from_up = (m_cur_layer_id % 2 == 1); + // Prime-tower heating during wipe. Everything here is gated on m_is_multiple_nozzle (false for every + // current printer); the lambdas emit nothing until add_M104_by_requirement's gate opens, so the + // single-nozzle wipe is untouched. + // WipeSpeedMap mirrors the nominal schedule above and is read only by estimate_wipe_time. It is a + // std::array (stack, no per-call heap allocation); values depend on runtime target_speed so it + // cannot be static const. + const std::array WipeSpeedMap{0.33f * target_speed, 0.375f * target_speed, 0.458f * target_speed, + 0.875f * target_speed, std::min(target_speed, 0.875f * target_speed + 50.f)}; + auto estimate_wipe_time = [&cleaning_box, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange]() -> float { + int n = std::ceil(x_to_wipe / (xr - xl)); + if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy; + float one_line_len = xr - xl; + float time = std::numeric_limits::max(); + if (n <= 1) + time = one_line_len / WipeSpeedMap[0]; + else if (n <= 2) + time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1]; + else if (n <= 3) + time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2]; + else if (n <= 4) + time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3]; + else { + time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3]; + time += (n - 4) * one_line_len / WipeSpeedMap[4]; + } + return time * 60.f; + }; + // Emit the arriving-hotend pre-heat inside the M632/M633 nozzle-change barrier. `M632 S[ H] + // M N` opens the barrier (M = firmware nozzle-change flag, N = slicer generated), the M104 sets the + // arriving hotend temp, and `M633` closes it. H2C's grouping is static (no dynamic nozzle map), so the + // H field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 => no + // H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally + // omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from + // add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C + // only; every other printer's wipe tower is untouched. The M632 M-flag is itself a firmware barrier, so + // a preceding M400 wait is subsumed. + auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") { + std::string buffer; + buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n"; + buffer += "M104"; + if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size()) + buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder])); + buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer + if (!comment.empty()) buffer += " ;" + comment; + buffer += '\n'; + buffer += "M633\n"; + (void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait + return buffer; + }; + // Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating). + // m_is_multiple_nozzle folds in the H2C gate so single-nozzle output is untouched. + // Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (layer-static) because Orca's + // wipe tower is extruder-level rather than tracking a per-layer nozzle map. + bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON && + !solid_tool_toolchange && !is_first_layer(); + auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() { + if (m_filpar[m_current_tool].filament_cooling_before_tower < EPSILON) return; + if (!should_heating) return; + float target_temp = is_first_layer() ? m_filpar[m_current_tool].nozzle_temperature_initial_layer : m_filpar[m_current_tool].nozzle_temperature; + writer.append(format_line_M104(target_temp, m_filament_map[m_current_tool] - 1)); + }; + float speed_factor = 1.f; + if (should_heating) { + // The heating-slowdown scaling is disabled — no additional heating time is required, so + // speed_factor stays 1.0. The structure and estimate_wipe_time/WipeSpeedMap are retained for + // future H2C tuning; the divide-by-zero/bounds guard is preserved in the commented body below. + // int extruder_id = m_filament_map[m_current_tool] - 1; + // if (extruder_id >= 0 && extruder_id < (int) m_hotend_heating_rate.size() && m_hotend_heating_rate[extruder_id] > 0.) { + // float estimate_time = estimate_wipe_time(); + // float heat_time = m_filpar[m_current_tool].filament_cooling_before_tower / m_hotend_heating_rate[extruder_id]; + // if (estimate_time < heat_time) speed_factor = estimate_time / heat_time; + // } + (void) estimate_wipe_time; // retain scaffolding above without an unused-lambda warning + } + float wipe_speed = nominal_speed * speed_factor; + // now the wiping itself: for (int i = 0; true; ++i) { if (i != 0) { - if (wipe_speed < 0.34f * target_speed) - wipe_speed = 0.375f * target_speed; - else if (wipe_speed < 0.377 * target_speed) - wipe_speed = 0.458f * target_speed; - else if (wipe_speed < 0.46f * target_speed) - wipe_speed = 0.875f * target_speed; + if (nominal_speed < 0.34f * target_speed) + nominal_speed = 0.375f * target_speed; + else if (nominal_speed < 0.377 * target_speed) + nominal_speed = 0.458f * target_speed; + else if (nominal_speed < 0.46f * target_speed) + nominal_speed = 0.875f * target_speed; else - wipe_speed = std::min(target_speed, wipe_speed + 50.f); + nominal_speed = std::min(target_speed, nominal_speed + 50.f); + wipe_speed = nominal_speed * speed_factor; } bool need_change_flow = need_thick_bridge_flow(writer.y()); @@ -3453,6 +3585,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat } else writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.); writer.retract(-retract_length, retract_speed); + add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed); } else { float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x(); @@ -3468,9 +3601,11 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat }else writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.); writer.retract(-retract_length, retract_speed); + add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed); } } else { + if (i == 0) add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe if (m_left_to_right) writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed); else @@ -3571,6 +3706,47 @@ bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2) return m_filament_map[filament_id_1] == m_filament_map[filament_id_2]; } +// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer, +// or is it its final layer above the extruder's printable height? +// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static), +// because Orca's wipe tower is extruder-level rather than tracking a per-layer nozzle map (the same +// idiom the pre-heat path uses in toolchange_wipe_new). Gated on m_is_multi_extruder so that +// single-extruder printers (including ones whose extruder_printable_height defaults to {0}) always +// return true and leave wipe-tower g-code unchanged. +bool WipeTower::is_valid_last_layer(int tool, int layer_id, double layer_z) const +{ + if (!m_is_multi_extruder) + return true; + int extruder_id = (tool >= 0 && tool < (int) m_filament_map.size()) ? m_filament_map[tool] - 1 : -1; + if (extruder_id < 0 || extruder_id >= (int) m_printable_height.size() || extruder_id >= (int) m_last_layer_id.size()) + return true; + if (m_last_layer_id[extruder_id] == layer_id && layer_z > m_printable_height[extruder_id]) + return false; + return true; +} + +// Records, per extruder, the last wipe-tower layer index that uses it, so is_valid_last_layer can +// recognise the extruder's final layer. Inert for single-extruder printers (early return); +// bounds-checked because m_filament_map may be empty/short. +void WipeTower::set_nozzle_last_layer_id() +{ + if (!m_is_multi_extruder) + return; + for (int idx = 0; idx < (int) m_plan.size(); ++idx) { + const auto &info = m_plan[idx]; + for (const auto &tc : info.tool_changes) { + int old_tool = (int) tc.old_tool; + int new_tool = (int) tc.new_tool; + int old_ext = (old_tool >= 0 && old_tool < (int) m_filament_map.size()) ? m_filament_map[old_tool] - 1 : -1; + int new_ext = (new_tool >= 0 && new_tool < (int) m_filament_map.size()) ? m_filament_map[new_tool] - 1 : -1; + if (old_ext >= 0 && old_ext < (int) m_last_layer_id.size()) + m_last_layer_id[old_ext] = idx; + if (new_ext >= 0 && new_ext < (int) m_last_layer_id.size()) + m_last_layer_id[new_ext] = idx; + } + } +} + void WipeTower::reset_block_status() { for (auto &block : m_wipe_tower_blocks) { @@ -3797,6 +3973,7 @@ void WipeTower::plan_tower_new() } update_all_layer_depth(max_depth); + set_nozzle_last_layer_id(); // record per-extruder last layer for is_valid_last_layer float diagonal = sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width); m_rib_length = std::max({m_rib_length, diagonal}); m_rib_length += m_extra_rib_length; @@ -3916,9 +4093,12 @@ void WipeTower::generate_new(std::vector m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder) + std::vector m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param) + + // Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height + // (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id + // records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on + // m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a + // multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable + // height (near the Z limit). + std::vector m_printable_height; + std::vector m_last_layer_id; + // Bed properties enum { RectangularBed, @@ -501,6 +530,11 @@ private: bool m_flat_ironing=false; bool m_enable_tower_interface_features=false; bool m_enable_tower_interface_cooldown_during_tower=false; + // Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset. + // m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so + // the PETG branch in get_next_pos never runs -> no change fleet-wide. + bool m_has_filament_switcher=false; + Polygons m_shared_print_bed; bool m_prev_layer_had_interface=false; bool m_current_layer_has_interface=false; // Calculates length of extrusion line to extrude given volume @@ -520,6 +554,7 @@ private: void save_on_last_wipe(); bool is_tpu_filament(int filament_id) const; + bool is_petg_filament(int filament_id) const; // BBS box_coordinates align_perimeter(const box_coordinates& perimeter_box); @@ -586,6 +621,12 @@ private: const box_coordinates &cleaning_box, float wipe_volume); void get_wall_skip_points(const WipeTowerInfo &layer); + + // Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns + // false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that + // extruder's printable height; returns true (no clamp) in every other case. + bool is_valid_last_layer(int tool, int layer_id, double layer_z) const; + void set_nozzle_last_layer_id(); }; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index a3d8e66ac3..f32840f614 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -632,6 +632,25 @@ std::string GCodeWriter::toolchange(unsigned int filament_id) return gcode.str(); } +// Current parked-retract length of the filament's extruder, share-aware. m_filament_extruders is +// sorted by id (see toolchange), so a lower_bound lookup finds the entry; unknown filament ids +// degrade to 0 rather than dereferencing end(). +double GCodeWriter::get_extruder_retracted_length(const int filament_id) +{ + double res = 0.0; + auto filament_extruder_iter = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(), + [filament_id](const Extruder &e) { return (int) e.id() < filament_id; }); + if (filament_extruder_iter == m_filament_extruders.end() || (int) filament_extruder_iter->id() != filament_id) + return res; + + if (filament_extruder_iter->is_share_extruder()) + res = filament_extruder_iter->get_share_retracted_length(); + else + res = filament_extruder_iter->get_single_retracted_length(); + + return res; +} + std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker) { assert(F > 0.); @@ -1101,7 +1120,7 @@ std::string GCodeWriter::_retract(double length, double restart_extra, const std return gcode; } -std::string GCodeWriter::unretract() +std::string GCodeWriter::unretract(float extra_retract) { std::string gcode; @@ -1117,7 +1136,9 @@ std::string GCodeWriter::unretract() //BBS // use G1 instead of G0 because G0 will blend the restart with the previous travel move GCodeG1Formatter w; - w.emit_e(filament()->E()); + // extra_retract over-extrudes for the PETG pre-extrusion; 0 by + // default -> identical to the plain deretract E position. + w.emit_e(filament()->E() + extra_retract); w.emit_f(filament()->deretract_speed() * 60.); //BBS w.emit_comment(GCodeWriter::full_gcode_comment, " ; unretract"); diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index d3f419df7c..205ad0cfbb 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -66,6 +66,9 @@ public: bool need_toolchange(unsigned int filament_id) const; std::string set_extruder(unsigned int filament_id); void init_extruder(unsigned int filament_id); + // Current parked-retract length of a filament's extruder (share-aware). Used for the + // new_extruder_retracted_length change-filament placeholder. Returns 0 if the filament is unknown. + double get_extruder_retracted_length(const int filament_id); // Prefix of the toolchange G-code line, to be used by the CoolingBuffer to separate sections of the G-code // printed with the same extruder. std::string toolchange_prefix() const; @@ -83,7 +86,9 @@ public: std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); - std::string unretract(); + // extra_retract adds a small over-extrusion to the deretract move (PETG pre-extrusion). + // Default 0 -> byte-identical to the plain deretract. + std::string unretract(float extra_retract = 0.f); // do lift instantly std::string eager_lift(const LiftType type); // record a lift request, do realy lift in next travel diff --git a/src/libslic3r/MultiNozzleUtils.cpp b/src/libslic3r/MultiNozzleUtils.cpp new file mode 100644 index 0000000000..e8ceadbbb7 --- /dev/null +++ b/src/libslic3r/MultiNozzleUtils.cpp @@ -0,0 +1,922 @@ +#include "MultiNozzleUtils.hpp" +#include "Utils.hpp" +#include "ProjectTask.hpp" // Slic3r::FilamentInfo (StaticNozzleGroupResult / load_nozzle_infos_with_compatibility) +#include +#include +#include +#include +#include +#include + +// Multi-nozzle support. + +namespace Slic3r { namespace MultiNozzleUtils { +// ==================== tool function implementations ==================== +std::vector build_nozzle_list(std::vector nozzle_groups) +{ + std::vector ret; + std::sort(nozzle_groups.begin(), nozzle_groups.end()); + int nozzle_id = 0; + for (auto& group : nozzle_groups) { + for (int i = 0; i < group.nozzle_count; ++i) { + NozzleInfo tmp; + tmp.diameter = group.diameter; + tmp.extruder_id = group.extruder_id; + tmp.volume_type = group.volume_type; + tmp.group_id = nozzle_id++; + ret.emplace_back(std::move(tmp)); + } + } + return ret; +} + +std::vector build_nozzle_list(double diameter, const std::vector& filament_nozzle_map, const std::vector& filament_volume_map, const std::vector& filament_map) +{ + std::string diameter_str = format_diameter_to_str(diameter); + std::map> nozzle_to_filaments; + for(size_t idx = 0; idx < filament_nozzle_map.size(); ++idx){ + int nozzle_id = filament_nozzle_map[idx]; + nozzle_to_filaments[nozzle_id].emplace_back(static_cast(idx)); + } + std::vector ret; + for(auto& elem : nozzle_to_filaments){ + int nozzle_id = elem.first; + auto& filaments = elem.second; + NozzleInfo info; + info.diameter = diameter_str; + info.group_id = nozzle_id; + info.extruder_id = filament_map[filaments.front()]; + info.volume_type = NozzleVolumeType(filament_volume_map[filaments.front()]); + ret.emplace_back(std::move(info)); + } + return ret; +} + +// ==================== LayeredNozzleGroupResult ==================== +static bool has_filament_mapped_to_multiple_nozzles(const std::vector> &layer_filament_nozzle_maps, + const std::vector &used_filaments) +{ + if (layer_filament_nozzle_maps.empty() || used_filaments.empty()) + return false; + + for (auto filament_id_u : used_filaments) { + int filament_id = static_cast(filament_id_u); + std::set nozzle_ids; + + for (size_t layer_id = 0; layer_id < layer_filament_nozzle_maps.size(); ++layer_id) { + const auto &map = layer_filament_nozzle_maps[layer_id]; + if (filament_id < 0 || filament_id >= static_cast(map.size())) + continue; + + int nozzle_id = map[filament_id]; + if (nozzle_id < 0) + continue; + + nozzle_ids.insert(nozzle_id); + if (nozzle_ids.size() > 1) + return true; + } + } + + return false; +} + +std::optional LayeredNozzleGroupResult::create( + const std::vector& filament_nozzle_map, + const std::vector& nozzle_list, + const std::vector& used_filaments) +{ + if (filament_nozzle_map.empty() || nozzle_list.empty()) { + return std::nullopt; + } + + LayeredNozzleGroupResult result(false); + result._default_filament_nozzle_map = filament_nozzle_map; + result._nozzle_list = nozzle_list; + result._used_filaments = used_filaments; + + return result; +} + +std::optional LayeredNozzleGroupResult::create( + const std::vector>& layer_filament_nozzle_maps, + const std::vector& nozzle_list, + const std::vector& used_filaments, + const std::vector>& layer_filament_sequences) +{ + if (layer_filament_nozzle_maps.empty() || nozzle_list.empty()) { + return std::nullopt; + } + + bool support_dynamic_nozzle_map = has_filament_mapped_to_multiple_nozzles(layer_filament_nozzle_maps, used_filaments); + LayeredNozzleGroupResult result(support_dynamic_nozzle_map); + result._layer_filament_nozzle_maps = layer_filament_nozzle_maps; + result._layer_filament_sequences = layer_filament_sequences; + result._nozzle_list = nozzle_list; + result._used_filaments = used_filaments; + + if (!layer_filament_nozzle_maps.empty()) { + result._default_filament_nozzle_map = layer_filament_nozzle_maps[0]; + } + + return result; +} + +std::optional LayeredNozzleGroupResult::create( + const std::vector& used_filaments, + const std::vector& filament_map, + const std::vector& filament_volume_map, + const std::vector& filament_nozzle_map, + const std::vector> &nozzle_count, + float diameter) +{ + std::vector nozzle_groups; + for (size_t extruder_id = 0; extruder_id < nozzle_count.size(); ++extruder_id) { + for (auto elem : nozzle_count[extruder_id]) { + NozzleGroupInfo group_info; + group_info.diameter = format_diameter_to_str(diameter); + group_info.volume_type = elem.first; + group_info.nozzle_count = elem.second; + group_info.extruder_id = static_cast(extruder_id); + nozzle_groups.emplace_back(group_info); + } + } + + auto nozzle_list = build_nozzle_list(nozzle_groups); + std::vector used_nozzle(nozzle_list.size(), false); + std::map input_nozzle_id_to_output; + std::vector output_nozzle_map(filament_nozzle_map.size(), 0); + + for (auto filament_idx : used_filaments) { + NozzleVolumeType req_type = NozzleVolumeType(filament_volume_map[filament_idx]); + int req_extruder = filament_map[filament_idx]; + int input_nozzle_idx = filament_nozzle_map[filament_idx]; + + if (input_nozzle_id_to_output.find(input_nozzle_idx) != input_nozzle_id_to_output.end()) { + output_nozzle_map[filament_idx] = input_nozzle_id_to_output[input_nozzle_idx]; + continue; + } + + int output_nozzle_idx = -1; + for (size_t nozzle_idx = 0; nozzle_idx < nozzle_list.size(); ++nozzle_idx) { + if (used_nozzle[nozzle_idx]) continue; + + auto &nozzle_info = nozzle_list[nozzle_idx]; + if (!(nozzle_info.extruder_id == req_extruder && nozzle_info.volume_type == req_type)) continue; + + output_nozzle_idx = static_cast(nozzle_idx); + input_nozzle_id_to_output[input_nozzle_idx] = output_nozzle_idx; + used_nozzle[nozzle_idx] = true; + break; + } + + if (output_nozzle_idx == -1) { return std::nullopt; } + output_nozzle_map[filament_idx] = output_nozzle_idx; + } + + return create(output_nozzle_map, nozzle_list, used_filaments); +} + +bool LayeredNozzleGroupResult::are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id) const +{ + std::optional nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id); + std::optional nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id); + + if (!nozzle_info1 || !nozzle_info2) return false; + + return nozzle_info1->extruder_id == nozzle_info2->extruder_id; +} + +bool LayeredNozzleGroupResult::are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id) const +{ + std::optional nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id); + std::optional nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id); + if (!nozzle_info1 || !nozzle_info2) return false; + + return nozzle_info1->group_id == nozzle_info2->group_id; +} + +int LayeredNozzleGroupResult::get_extruder_count() const +{ + std::set extruder_ids; + for (const auto &nozzle : _nozzle_list) { extruder_ids.insert(nozzle.extruder_id); } + return static_cast(extruder_ids.size()); +} + +std::vector LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const +{ + return get_used_nozzles_in_extruder(target_extruder_id, -1); +} + +std::vector LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const +{ + std::set nozzle_ids; + std::vector result; + + std::vector target_filaments = get_used_filaments(layer_id); + + for (unsigned int filament_id : target_filaments) { + if (layer_id != -1) { + auto nozzle_opt = get_nozzle_for_filament(static_cast(filament_id), layer_id); + if (nozzle_opt) { + if (target_extruder_id == -1 || nozzle_opt->extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle_opt->group_id); } + } + } else { + auto nozzles = get_nozzles_for_filament(static_cast(filament_id)); + for (const auto &nozzle : nozzles) { + if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle.group_id); } + } + } + } + for (int nozzle_id : nozzle_ids) { + if (nozzle_id >= 0 && nozzle_id < static_cast(_nozzle_list.size())) { result.push_back(_nozzle_list[nozzle_id]); } + } + return result; +} + +std::vector LayeredNozzleGroupResult::get_used_extruders() const +{ + return get_used_extruders(-1); +} + +std::vector LayeredNozzleGroupResult::get_used_extruders(int layer_id) const +{ + std::set used_extruders; + // used filaments on the given layer (or globally) + std::vector target_filaments = get_used_filaments(layer_id); + for (auto filament_id : target_filaments) { + if (layer_id != -1) { + // single-layer: nozzle used by this filament on this layer + auto nozzle_opt = get_nozzle_for_filament(static_cast(filament_id), layer_id); + if (nozzle_opt) { used_extruders.insert(nozzle_opt->extruder_id); } + } else { + // global: every nozzle this filament uses across all layers + auto nozzles = get_nozzles_for_filament(static_cast(filament_id)); + for (const auto &nozzle : nozzles) { used_extruders.insert(nozzle.extruder_id); } + } + } + return std::vector(used_extruders.begin(), used_extruders.end()); +} + +std::vector LayeredNozzleGroupResult::get_extruder_map(bool zero_based, int layer_id) const +{ + const std::vector &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id); + std::vector extruder_map(filament_nozzle_map.size()); + for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) { + int nozzle_id = filament_nozzle_map[idx]; + if (nozzle_id >= 0 && nozzle_id < static_cast(_nozzle_list.size())) { + extruder_map[idx] = _nozzle_list[nozzle_id].extruder_id; + } else { + extruder_map[idx] = -1; + } + } + + if (zero_based) return extruder_map; + + auto new_filament_map = extruder_map; + std::transform(new_filament_map.begin(), new_filament_map.end(), new_filament_map.begin(), [](int val) { return val + 1; }); + return new_filament_map; +} + +std::vector LayeredNozzleGroupResult::get_nozzle_map(int layer_id) const +{ + const std::vector &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id); + std::vector nozzle_map(filament_nozzle_map.size()); + for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) { + int nozzle_id = filament_nozzle_map[idx]; + if (nozzle_id >= 0 && nozzle_id < static_cast(_nozzle_list.size())) { + nozzle_map[idx] = _nozzle_list[nozzle_id].group_id; + } else { + nozzle_map[idx] = -1; + } + } + return nozzle_map; +} + +std::vector LayeredNozzleGroupResult::get_volume_map(int layer_id) const +{ + const std::vector &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id); + std::vector volume_map(filament_nozzle_map.size()); + for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) { + int nozzle_id = filament_nozzle_map[idx]; + if (nozzle_id >= 0 && nozzle_id < static_cast(_nozzle_list.size())) { + volume_map[idx] = _nozzle_list[nozzle_id].volume_type; + } else { + volume_map[idx] = -1; + } + } + return volume_map; +} + +std::vector LayeredNozzleGroupResult::get_used_filaments(int layer_id) const +{ + if (layer_id < 0) { return _used_filaments; } + if (layer_id >= static_cast(_layer_filament_nozzle_maps.size())) { return _used_filaments; } + + if (!_layer_filament_sequences.empty() && layer_id < static_cast(_layer_filament_sequences.size())) { + return _layer_filament_sequences[layer_id]; + } + return {}; +} + +std::optional LayeredNozzleGroupResult::get_nozzle_for_filament(int filament_id, int layer_id) const +{ + const std::vector &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id); + + if (filament_id < 0 || filament_id >= static_cast(filament_nozzle_map.size())) { return std::nullopt; } + + int nozzle_id = filament_nozzle_map[filament_id]; + return get_nozzle_from_id(nozzle_id); +} + +std::vector LayeredNozzleGroupResult::get_nozzles_for_filament(int filament_id) const +{ + std::set nozzle_ids; + + if (!support_dynamic_nozzle_map) { + if (filament_id >= 0 && filament_id < static_cast(_default_filament_nozzle_map.size())) { + nozzle_ids.insert(_default_filament_nozzle_map[filament_id]); + } + } else { + int start_layer = 0; + int end_layer = static_cast(_layer_filament_nozzle_maps.size()); + + for (int i = start_layer; i < end_layer; ++i) { + const auto &map = _layer_filament_nozzle_maps[i]; + if (filament_id >= 0 && filament_id < static_cast(map.size())) { + nozzle_ids.insert(map[filament_id]); + } + } + } + + std::vector result; + for (int id : nozzle_ids) { + if (id >= 0 && id < static_cast(_nozzle_list.size())) { result.push_back(_nozzle_list[id]); } + } + return result; +} + +std::optional LayeredNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const +{ + if (filament_id < 0) return std::nullopt; + + if (!support_dynamic_nozzle_map) { + if (filament_id >= static_cast(_default_filament_nozzle_map.size())) return std::nullopt; + return get_nozzle_from_id(_default_filament_nozzle_map[filament_id]); + } + + for (size_t layer = 0; layer < _layer_filament_nozzle_maps.size(); ++layer) { + auto layer_used_filaments = get_used_filaments(layer); + if (std::find(layer_used_filaments.begin(), layer_used_filaments.end(), static_cast(filament_id)) == layer_used_filaments.end()){ + continue; + } + const auto &map = _layer_filament_nozzle_maps[layer]; + if (filament_id >= 0 && filament_id < static_cast(map.size())) { + int nozzle_id = map[filament_id]; + auto nozzle = get_nozzle_from_id(nozzle_id); + if (nozzle) return nozzle; + } + } + + return std::nullopt; +} + +std::optional LayeredNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const +{ + if (nozzle_id < 0 || nozzle_id >= static_cast(_nozzle_list.size())) { return std::nullopt; } + return _nozzle_list[nozzle_id]; +} + +int LayeredNozzleGroupResult::get_extruder_id(int filament_id, int layer_id) const +{ + auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id); + return nozzle_info ? nozzle_info->extruder_id : -1; +} + +int LayeredNozzleGroupResult::get_nozzle_id(int filament_id, int layer_id) const +{ + auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id); + return nozzle_info ? nozzle_info->group_id : -1; +} + +const std::vector &LayeredNozzleGroupResult::get_layer_filament_nozzle_map(int layer_id) const +{ + if (layer_id >= 0 && layer_id < static_cast(_layer_filament_nozzle_maps.size())) { return _layer_filament_nozzle_maps[layer_id]; } + return _default_filament_nozzle_map; +} + +// ==================== filament-change-time model ==================== +FilamentChangeSimResult simulate_filament_change_time( + const std::vector& logical_filaments, + const std::vector& nozzle_list, + const std::vector& filament_change_seq, + const std::vector& nozzle_change_seq, + const std::vector& group_of_filament, + const FilamentChangeTimeParams& time_params, + const std::vector& ams_preload_enabled, + bool calc_sliced_time) +{ + FilamentChangeSimResult result; + if (logical_filaments.empty() || nozzle_list.empty() || filament_change_seq.empty() || nozzle_change_seq.empty()) + return result; + + // Re-map the parameter semantics: + // standard = AMS -> selector -> extruder (full path), selector = selector -> extruder (short path) + // so AMS -> selector = standard - selector + const float load_ams_to_selector = time_params.standard_load_time - time_params.selector_load_time; + const float unload_ams_to_selector = time_params.standard_unload_time - time_params.selector_unload_time; + const float load_selector_to_ext = time_params.selector_load_time; + const float unload_ext_to_selector = time_params.selector_unload_time; + + // nozzle_id -> extruder_id + std::unordered_map nozzle_to_extruder; + nozzle_to_extruder.reserve(nozzle_list.size()); + for (const auto& nozzle : nozzle_list) + nozzle_to_extruder[nozzle.group_id] = nozzle.extruder_id; + + // filament_id -> AMS group + std::unordered_map filament_to_group; + filament_to_group.reserve(logical_filaments.size()); + for (size_t i = 0; i < logical_filaments.size(); ++i) + filament_to_group[logical_filaments[i]] = group_of_filament[i]; + + const auto get_group = [&](int filament_id) -> int { + auto it = filament_to_group.find(filament_id); + return it != filament_to_group.end() ? it->second : -1; + }; + + const auto is_preload_enabled = [&](int group_id) -> bool { + if (group_id < 0 || group_id >= static_cast(ams_preload_enabled.size())) + return false; + return ams_preload_enabled[group_id]; + }; + + // Filament location states + enum class Location { IN_AMS, IN_SELECTOR, IN_EXTRUDER }; + std::unordered_map filament_location; // filament_id -> current location + std::unordered_map filament_extruder; // filament_id -> extruder it sits in (only valid when IN_EXTRUDER) + std::unordered_map extruder_filament; // extruder_id -> currently loaded filament + // group_id -> filaments currently occupying that AMS channel (IN_SELECTOR or IN_EXTRUDER) + std::unordered_map> ams_group_occupied; + + filament_location.reserve(logical_filaments.size()); + filament_extruder.reserve(logical_filaments.size()); + + // Initial state: every filament is in the AMS, every extruder is empty + for (int f : logical_filaments) + filament_location[f] = Location::IN_AMS; + + // Slicer-estimate simulator: use NozzleStatusRecorder to track what each nozzle/extruder holds during slicing + NozzleStatusRecorder sliced_recorder; + + const size_t seq_len = std::min(filament_change_seq.size(), nozzle_change_seq.size()); + double actual_time = 0.0; + double sliced_time = 0.0; + + for (size_t i = 0; i < seq_len; ++i) { + int B = filament_change_seq[i]; + int nozzle_id = nozzle_change_seq[i]; + + auto nozzle_iter = nozzle_to_extruder.find(nozzle_id); + if (nozzle_iter == nozzle_to_extruder.end()) continue; + + int E = nozzle_iter->second; // target extruder + + // Step 0: compute the slicer-estimated time + // Slicer estimate: simulate the slicer's view (no selector awareness); + // count a load/unload when nozzle_in_extruder_change || filament_in_nozzle_change + if (calc_sliced_time) { + int old_nozzle_in_E = sliced_recorder.get_nozzle_in_extruder(E); + int old_filament_in_nozzle = sliced_recorder.get_filament_in_nozzle(nozzle_id); + int old_filament_in_ext = sliced_recorder.get_filament_in_nozzle(old_nozzle_in_E); + + bool nozzle_change = (old_nozzle_in_E != nozzle_id); + bool filament_change = (old_filament_in_nozzle != B); + + if (nozzle_change || filament_change) { + if (old_filament_in_ext != -1) + sliced_time += time_params.standard_unload_time; + sliced_time += time_params.standard_load_time; + } + sliced_recorder.set_nozzle_status(nozzle_id, B, E); + } + + // Step 1: find the filament A currently loaded in the target extruder E + int A = -1; + { + auto it = extruder_filament.find(E); + if (it != extruder_filament.end()) + A = it->second; + } + + int group_B = get_group(B); + int group_A = (A != -1) ? get_group(A) : -1; + + // Step 2: clear B's AMS-channel occupancy + auto group_it = ams_group_occupied.find(group_B); + if (group_it != ams_group_occupied.end()) { + for (int X : group_it->second) { + if (X == B) continue; + // X shares B's AMS channel, retreat it to the AMS to make way + Location loc_X = filament_location[X]; + if (loc_X == Location::IN_EXTRUDER) { + actual_time += unload_ext_to_selector + unload_ams_to_selector; + int E2 = filament_extruder[X]; + extruder_filament.erase(E2); + filament_extruder.erase(X); + } else if (loc_X == Location::IN_SELECTOR) { + actual_time += unload_ams_to_selector; + } + filament_location[X] = Location::IN_AMS; + } + group_it->second.clear(); + } + + // Step 3: A exits E (while A is still in the extruder) + // Step 3.5: pre-load B (in parallel with Step 3) + // actual time = max(Step 3, Step 3.5) + bool step3_executed = false; + float step3_time = 0.0f; + if (A != -1 && A != B && filament_location[A] == Location::IN_EXTRUDER) { + if (is_preload_enabled(group_A) && group_A != group_B) { + step3_time = unload_ext_to_selector; + filament_location[A] = Location::IN_SELECTOR; + } else { + step3_time = unload_ext_to_selector + unload_ams_to_selector; + filament_location[A] = Location::IN_AMS; + ams_group_occupied[group_A].erase(A); + } + extruder_filament.erase(E); + filament_extruder.erase(A); + step3_executed = true; + } + + float step3_5_time = 0.0f; + if (step3_executed && + filament_location[B] == Location::IN_AMS && + group_A != group_B && + is_preload_enabled(group_B)) { + step3_5_time = load_ams_to_selector; + filament_location[B] = Location::IN_SELECTOR; + ams_group_occupied[group_B].insert(B); + } + + actual_time += std::max(step3_time, step3_5_time); + + // Step 4: push B into E + // Step 6: pre-load the next filament C (in parallel with Step 4) + // actual time = max(Step 4, Step 6) + float step4_time = 0.0f; + Location loc_B = filament_location[B]; + if (loc_B == Location::IN_AMS) { + step4_time = load_ams_to_selector + load_selector_to_ext; + } else if (loc_B == Location::IN_SELECTOR) { + step4_time = load_selector_to_ext; + } + + // Step 5: update state + extruder_filament[E] = B; + filament_location[B] = Location::IN_EXTRUDER; + filament_extruder[B] = E; + ams_group_occupied[group_B].insert(B); + + float step6_time = 0.0f; + if (i + 1 < seq_len) { + int C = filament_change_seq[i + 1]; + int group_C = get_group(C); + if (filament_location[C] == Location::IN_AMS && + group_C != group_B && + is_preload_enabled(group_C) && + ams_group_occupied[group_C].empty()) { + step6_time = load_ams_to_selector; + filament_location[C] = Location::IN_SELECTOR; + ams_group_occupied[group_C].insert(C); + } + } + + actual_time += std::max(step4_time, step6_time); + } + + result.actual_time = actual_time; + result.sliced_time = sliced_time; + return result; +} + +// ==================== NozzleStatusRecorder implementation ==================== + +bool NozzleStatusRecorder::is_nozzle_empty(int nozzle_id) const +{ + auto iter = nozzle_filament_status.find(nozzle_id); + if (iter == nozzle_filament_status.end()) return true; + return false; +} + +int NozzleStatusRecorder::get_filament_in_nozzle(int nozzle_id) const +{ + auto iter = nozzle_filament_status.find(nozzle_id); + if (iter == nozzle_filament_status.end()) return -1; + return iter->second; +} + +int NozzleStatusRecorder::get_nozzle_in_extruder(int extruder_id) const +{ + auto iter = extruder_nozzle_status.find(extruder_id); + if (iter == extruder_nozzle_status.end()) return -1; + return iter->second; +} + +void NozzleStatusRecorder::set_nozzle_status(int nozzle_id, int filament_id, int extruder_id) +{ + nozzle_filament_status[nozzle_id] = filament_id; + if (extruder_id != -1) { + extruder_nozzle_status[extruder_id] = nozzle_id; + } +} + +void NozzleStatusRecorder::clear_nozzle_status(int nozzle_id) +{ + auto iter = nozzle_filament_status.find(nozzle_id); + if (iter == nozzle_filament_status.end()) return; + nozzle_filament_status.erase(iter); +} + +int LayeredNozzleGroupResult::estimate_seq_flush_weight(const std::vector>>& flush_matrix, const std::vector& filament_change_seq) const +{ + auto get_weight_from_volume = [](float volume){ + return static_cast(volume * 1.26 * 0.01); + }; + + float total_flush_volume = 0; + NozzleStatusRecorder recorder; + for(auto filament: filament_change_seq){ + auto nozzle = get_nozzle_for_filament(filament, -1); + if(!nozzle) + continue; + + int extruder_id = nozzle->extruder_id; + int nozzle_id = nozzle->group_id; + int last_filament = recorder.get_filament_in_nozzle(nozzle_id); + + if(last_filament!= -1 && last_filament != filament){ + // bounds check to avoid out-of-range access + if (extruder_id >= 0 && extruder_id < static_cast(flush_matrix.size()) && + last_filament >= 0 && last_filament < static_cast(flush_matrix[extruder_id].size()) && + filament >= 0 && filament < static_cast(flush_matrix[extruder_id][last_filament].size())) { + float flush_volume = flush_matrix[extruder_id][last_filament][filament]; + total_flush_volume += flush_volume; + } + } + recorder.set_nozzle_status(nozzle_id, filament); + } + + return get_weight_from_volume(total_flush_volume); +} + +// ==================== StaticNozzleGroupResult ==================== + +std::optional StaticNozzleGroupResult::create( + const std::vector& filaments_info, + const std::vector& nozzles_info, + const std::vector& filament_change_seq, + const std::vector& nozzle_change_seq, + bool support_dynamic_nozzle_map) +{ + if (filaments_info.empty() || nozzles_info.empty()) return std::nullopt; + + std::map nozzle_list_map; + std::map> filament_to_nozzles; + + for (auto nozzle_info : nozzles_info) + nozzle_list_map[nozzle_info.group_id] = nozzle_info; + + for (auto filament_info : filaments_info) { + auto fil_id = filament_info.id; + auto nozzles_id = filament_info.group_id; + std::set nozzles_set(nozzles_id.begin(), nozzles_id.end()); + // Backward compat with older (single-nozzle) gcode.3mf: filament has no group_id, avoid an empty map. + if (nozzles_set.empty()) { + for (const auto& nozzle_entry : nozzle_list_map) + nozzles_set.insert(nozzle_entry.first); + } + filament_to_nozzles[fil_id] = nozzles_set; + } + + StaticNozzleGroupResult result(support_dynamic_nozzle_map); + result._filament_to_nozzles = filament_to_nozzles; + result._nozzle_list_map = nozzle_list_map; + result._filament_change_seq = filament_change_seq; + result._nozzle_change_seq = nozzle_change_seq; + + return result; +} + +std::optional StaticNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const +{ + auto iter = _nozzle_list_map.find(nozzle_id); + if (iter == _nozzle_list_map.end()) { return std::nullopt; } + return iter->second; +} + +int StaticNozzleGroupResult::get_extruder_count() const +{ + std::set extruder_ids; + for (const auto &elem : _nozzle_list_map) { extruder_ids.insert(elem.second.extruder_id); } + return static_cast(extruder_ids.size()); +} + +std::vector StaticNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const +{ + std::vector result; + for (const auto &elem : _nozzle_list_map) { + const auto &nozzle = elem.second; + if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) { + result.push_back(nozzle); + } + } + return result; +} + +std::vector StaticNozzleGroupResult::get_used_extruders() const +{ + std::set used_extruders; + for (const auto &elem : _nozzle_list_map) { used_extruders.insert(elem.second.extruder_id); } + return std::vector(used_extruders.begin(), used_extruders.end()); +} + +std::vector StaticNozzleGroupResult::get_used_filaments() const +{ + std::vector used_filaments; + used_filaments.reserve(_filament_to_nozzles.size()); + for (const auto &elem : _filament_to_nozzles) { + if (elem.first >= 0) { + used_filaments.push_back(static_cast(elem.first)); + } + } + return used_filaments; +} + +std::vector StaticNozzleGroupResult::get_nozzles_for_filament(int filament_id) const +{ + auto iter = _filament_to_nozzles.find(filament_id); + if (iter == _filament_to_nozzles.end()) { return std::vector(); } + + std::vector result; + for (int nozzle_id : iter->second) { + auto nozzle_iter = _nozzle_list_map.find(nozzle_id); + if (nozzle_iter != _nozzle_list_map.end()) { + result.push_back(nozzle_iter->second); + } + } + return result; +} + +std::optional StaticNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const +{ + if (filament_id < 0) return std::nullopt; + + if (!_filament_change_seq.empty() && _filament_change_seq.size() == _nozzle_change_seq.size()) { + for (size_t idx = 0; idx < _filament_change_seq.size(); ++idx) { + if (_filament_change_seq[idx] == filament_id) { + int nozzle_id = _nozzle_change_seq[idx]; + auto nozzle = get_nozzle_from_id(nozzle_id); + if (nozzle) return nozzle; + } + } + } + + auto iter = _filament_to_nozzles.find(filament_id); + if (iter == _filament_to_nozzles.end()) return std::nullopt; + + for (int nozzle_id : iter->second) { + auto nozzle = get_nozzle_from_id(nozzle_id); + if (nozzle) return nozzle; + } + + return std::nullopt; +} + +// ==================== serialization ==================== + +std::string NozzleInfo::serialize() const +{ + std::ostringstream oss; + oss << "id=\"" << group_id << "\" " + << "extruder_id=\"" << extruder_id + 1 << "\" " + << "nozzle_diameter=\"" << diameter << "\" " + << "volume_type=\"" << get_nozzle_volume_type_string(volume_type) << "\""; + return oss.str(); +} + +std::string NozzleGroupInfo::serialize() const +{ + std::ostringstream oss; + oss << extruder_id << "-" + << std::setprecision(2) << diameter << "-" + << get_nozzle_volume_type_string(volume_type) << "-" + << nozzle_count; + return oss.str(); +} + +std::optional NozzleGroupInfo::deserialize(const std::string &str) +{ + std::istringstream iss(str); + std::string token; + std::vector tokens; + + while (std::getline(iss, token, '-')) { tokens.push_back(token); } + + if (tokens.size() != 4) { return std::nullopt; } + + try { + int extruder_id = std::stoi(tokens[0]); + std::string diameter = tokens[1]; + NozzleVolumeType volume_type = NozzleVolumeType(ConfigOptionEnum::get_enum_values().at(tokens[2])); + int nozzle_count = std::stoi(tokens[3]); + + return NozzleGroupInfo(diameter, volume_type, extruder_id, nozzle_count); + } catch (const std::exception &) { + return std::nullopt; + } +} + +std::vector load_nozzle_infos_with_compatibility( + const std::vector& nozzle_infos, + const std::vector& filament_infos, + const std::vector& filament_map, + const std::vector& extruder_volume_types, + const std::vector& nozzle_diameter +) +{ + bool has_nozzle_info = !nozzle_infos.empty(); + bool has_valid_filament_info = !filament_infos.empty() && std::all_of(filament_infos.begin(), filament_infos.end(), [](const FilamentInfo& info){ + return info.group_id.size() == 1; + }); + + if(!has_nozzle_info && !has_valid_filament_info){ + BOOST_LOG_TRIVIAL(warning)<<__FUNCTION__ << ": building nozzle list from filament map and volume types"; + + // Backward compatibility for older gcode.3mf: + // - nozzle_diameter is always present and its size defines extruder count. + // - filament_map may be missing; treat it as [0, 0, ...] for each extruder. + // - extruder_volume_types may be missing; treat it as all Standard. + const size_t extruder_count = nozzle_diameter.size(); + + std::vector volume_types_fixed = extruder_volume_types; + volume_types_fixed.resize(extruder_count, NozzleVolumeType::nvtStandard); + + std::vector result; + result.reserve(extruder_count); + for (size_t extruder_id = 0; extruder_id < extruder_count; ++extruder_id) { + NozzleInfo info; + info.diameter = format_diameter_to_str(nozzle_diameter[extruder_id]); + info.group_id = static_cast(extruder_id); + info.extruder_id = static_cast(extruder_id); + info.volume_type = volume_types_fixed[extruder_id]; + result.emplace_back(std::move(info)); + } + return result; + } + + if(!has_nozzle_info){ + BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": building nozzle list from filament info"; + std::map nozzle_map; // group_id -> NozzleInfo + for(auto& filament : filament_infos){ + int group_id = filament.group_id.front(); + if(group_id < 0 || nozzle_map.find(group_id) != nozzle_map.end()){ + continue; + } + + auto volume_type_str_to_enum = ConfigOptionEnum::get_enum_values(); + + NozzleInfo info; + info.diameter = format_diameter_to_str(filament.nozzle_diameter); + info.group_id = group_id; + // Orca: bounds-check filament_map[filament.id] so a malformed 3mf (filament id + // beyond the map) degrades to extruder 0 instead of dereferencing out of range. + info.extruder_id = (filament.id >= 0 && filament.id < static_cast(filament_map.size())) + ? filament_map[filament.id] - 1 + : 0; // to 0-based + + if (volume_type_str_to_enum.count(filament.nozzle_volume_type)) + info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(filament.nozzle_volume_type)); + else { + info.volume_type = NozzleVolumeType::nvtStandard; + } + + nozzle_map[group_id] = std::move(info); + } + + std::vector ret; + for(auto& elem : nozzle_map){ + ret.emplace_back(elem.second); + } + return ret; + } + + auto result = nozzle_infos; + std::sort(result.begin(), result.end()); + BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": using new 3mf format with " << result.size() << " nozzle infos."; + return result; +} + +}} // namespace Slic3r::MultiNozzleUtils diff --git a/src/libslic3r/MultiNozzleUtils.hpp b/src/libslic3r/MultiNozzleUtils.hpp new file mode 100644 index 0000000000..f9b96bd79a --- /dev/null +++ b/src/libslic3r/MultiNozzleUtils.hpp @@ -0,0 +1,288 @@ +#ifndef MULTI_NOZZLE_UTILS_HPP +#define MULTI_NOZZLE_UTILS_HPP + +#include +#include +#include +#include +#include +#include "PrintConfig.hpp" + +// Multi-nozzle support types. +// Declares the filament-grouping result types the slicing pipeline needs, plus the analytic +// filament-change-time model (FilamentChangeTimeParams, NozzleStatusRecorder, +// FilamentChangeSimResult, simulate_filament_change_time) — self-contained analytic code that +// never touches the time estimator; its first consumer is the filament_group golden harness. +// The gcode.3mf serialization surface lives here too: NozzleInfo/NozzleGroupInfo +// serialize+deserialize, the device-side StaticNozzleGroupResult, +// load_nozzle_infos_with_compatibility (the backward-compat 3mf reader) and +// LayeredNozzleGroupResult::estimate_seq_flush_weight. The change-time-tuning helpers +// calc_filament_change_gap_for_assignment / find_optimal_physical_assignment (used only by the +// AMS pre-load optimizer, a later feature) are not implemented here. + +namespace Slic3r { +struct FilamentInfo; // Slic3r::FilamentInfo (ProjectTask.hpp) — consumed by StaticNozzleGroupResult / the 3mf reader +namespace MultiNozzleUtils { + +// Information about a single logical nozzle. +struct NozzleInfo +{ + std::string diameter; + NozzleVolumeType volume_type; + int extruder_id{-1}; // logical extruder id + int group_id{-1}; // logical nozzle id + + std::string serialize() const; + + bool operator<(const NozzleInfo& other) const { + if(group_id != other.group_id) return group_id < other.group_id; + if(extruder_id != other.extruder_id) return extruder_id < other.extruder_id; + if(volume_type != other.volume_type) return volume_type < other.volume_type; + return diameter < other.diameter; + } +}; + +// A group of identical nozzles on one extruder (diameter + volume type + count). +struct NozzleGroupInfo +{ + std::string diameter; + NozzleVolumeType volume_type; + int extruder_id; + int nozzle_count; + + NozzleGroupInfo() = default; + + NozzleGroupInfo(const std::string& nozzle_diameter_, const NozzleVolumeType volume_type_, const int extruder_id_, const int nozzle_count_) + : diameter(nozzle_diameter_), volume_type(volume_type_), extruder_id(extruder_id_), nozzle_count(nozzle_count_) + {} + + inline bool operator<(const NozzleGroupInfo &rhs) const + { + if (extruder_id != rhs.extruder_id) return extruder_id < rhs.extruder_id; + if (diameter != rhs.diameter) return diameter < rhs.diameter; + if (volume_type != rhs.volume_type) return volume_type < rhs.volume_type; + return nozzle_count < rhs.nozzle_count; + } + + bool is_same_type(const NozzleGroupInfo &rhs) const + { + return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id; + } + + inline bool operator==(const NozzleGroupInfo &rhs) const + { + return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id && nozzle_count == rhs.nozzle_count; + } + + std::string serialize() const; + static std::optional deserialize(const std::string& str); +}; + +// Load/unload time constants used by the filament-change-time model. +// Consumed by simulate_filament_change_time() below and carried by the grouping-context +// substrate (FilamentGroupContext::SpeedInfo). +struct FilamentChangeTimeParams +{ + float selector_load_time{0.0f}; + float selector_unload_time{0.0f}; + float standard_load_time{0.0f}; + float standard_unload_time{0.0f}; +}; + +/** + * @brief Abstract base for a nozzle-grouping result. + */ +class NozzleGroupResultBase +{ +protected: + bool support_dynamic_nozzle_map{false}; // whether dynamic (selector) mapping is used + +public: + NozzleGroupResultBase(bool support_dynamic_map = false) : support_dynamic_nozzle_map(support_dynamic_map) {} + virtual ~NozzleGroupResultBase() = default; + + virtual std::optional get_nozzle_from_id(int nozzle_id) const = 0; + virtual std::optional get_first_nozzle_for_filament(int filament_id) const = 0; // logical nozzle a filament first uses + + virtual std::vector get_nozzles_for_filament(int filament_id) const = 0; // every nozzle a filament may use (across all layers) + + bool is_support_dynamic_nozzle_map() const { return support_dynamic_nozzle_map; } + + virtual int get_extruder_count() const = 0; + + virtual std::vector get_used_nozzles_in_extruder(int extruder_id =-1) const = 0; + virtual std::vector get_used_extruders() const = 0; + virtual std::vector get_used_filaments() const = 0; +}; + +/** + * @brief Layer-aware nozzle-grouping result. + * Used by the back-end slicing code; supports per-layer nozzle mapping. + */ +class LayeredNozzleGroupResult : public NozzleGroupResultBase +{ +private: + std::vector> _layer_filament_nozzle_maps; // per-layer filament -> nozzle map + std::vector> _layer_filament_sequences; // per-layer filament print order + std::vector _default_filament_nozzle_map; // global filament -> nozzle map + std::vector _used_filaments; // all used filament indices + std::vector _nozzle_list; // global nozzle list + +public: + LayeredNozzleGroupResult(bool support_dynamic_map = false) : NozzleGroupResultBase(support_dynamic_map) {} + + // No selector: one global filament->nozzle map. + static std::optional create( + const std::vector& filament_nozzle_map, + const std::vector& nozzle_list, + const std::vector& used_filaments); + + // Selector: built from per-layer maps (each layer may differ). + static std::optional create( + const std::vector>& layer_filament_nozzle_maps, + const std::vector& nozzle_list, + const std::vector& used_filaments, + const std::vector>& layer_filament_sequences); + + // Multi-nozzle without selector: resolve each requested logical nozzle to a physical nozzle. + static std::optional create( + const std::vector& used_filaments, + const std::vector& filament_map, + const std::vector& filament_volume_map, + const std::vector& filament_nozzle_map, + const std::vector>& nozzle_count, + float diameter); + + bool are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id = -1) const; + bool are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id = -1) const; + int get_extruder_count() const override; + + std::vector get_used_nozzles_in_extruder(int target_extruder_id = -1) const override; + std::vector get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const; // layer_id=-1 uses default map + std::vector get_used_extruders() const override; + std::vector get_used_extruders(int layer_id) const; // layer_id=-1 returns global extruders + + std::vector get_extruder_map(bool zero_based = true, int layer_id = -1) const; + std::vector get_nozzle_map(int layer_id = -1) const; + std::vector get_volume_map(int layer_id = -1) const; + + std::vector get_used_filaments() const override { return _used_filaments; } + std::vector get_used_filaments(int layer_id) const; + + std::optional get_nozzle_for_filament(int filament_id, int layer_id = -1) const; + std::vector get_nozzles_for_filament(int filament_id) const override; + + std::optional get_nozzle_from_id(int nozzle_id) const override; + std::optional get_first_nozzle_for_filament(int filament_id) const override; + int get_extruder_id(int filament_id, int layer_id = -1) const; + int get_nozzle_id(int filament_id, int layer_id = -1) const; + + size_t get_layer_count() const { return _layer_filament_nozzle_maps.size(); } + const std::vector& get_layer_filament_nozzle_map(int layer_id) const; + const std::vector> &get_layer_filament_nozzle_maps() const { return _layer_filament_nozzle_maps; } + const std::vector>& get_layer_filament_sequences() const { return _layer_filament_sequences; } + + // Estimate the flush weight of a filament-change sequence given the per-extruder flush matrix + // (extruder -> from-filament -> to-filament). + int estimate_seq_flush_weight(const std::vector>>& flush_matrix, const std::vector& filament_change_seq) const; +}; + +/** + * @brief Layer-less nozzle-grouping result for the device side (static nozzle mapping only). + * Reconstructed from a loaded gcode.3mf together with the filament/nozzle change sequences. + */ +class StaticNozzleGroupResult : public NozzleGroupResultBase +{ +private: + std::map> _filament_to_nozzles; // every nozzle a filament may map to + std::map _nozzle_list_map; // used nozzles, keyed by logical nozzle id + std::vector _filament_change_seq; // filament sequence used to resolve first-use + std::vector _nozzle_change_seq; // logical-nozzle sequence paired with the filament sequence + +public: + StaticNozzleGroupResult(bool support_dynamic_map) : NozzleGroupResultBase(support_dynamic_map) {} + // Build from a loaded 3mf, with the filament/nozzle change sequences. + static std::optional create( + const std::vector& filaments_info, + const std::vector& nozzles_info, + const std::vector& filament_change_seq, + const std::vector& nozzle_change_seq, + bool support_dynamic_map); + + int get_extruder_count() const override; + std::vector get_used_nozzles_in_extruder(int extruder_id = -1) const override; + std::vector get_used_extruders() const override; + std::vector get_used_filaments() const override; + + std::optional get_nozzle_from_id(int nozzle_id) const override; + + std::vector get_nozzles_for_filament(int filament_id) const override; + std::optional get_first_nozzle_for_filament(int filament_id) const override; +}; + +// Tracks, during the filament-change simulation, which filament sits in each physical nozzle +// and which nozzle each extruder currently carries. +class NozzleStatusRecorder +{ +private: + std::unordered_map nozzle_filament_status; // Track filament in each nozzle + std::unordered_map extruder_nozzle_status; // Track nozzle for each extruder + int current_extruder_id_ = -1; // Track current extruder id + +public: + NozzleStatusRecorder() = default; + bool is_nozzle_empty(int nozzle_id) const; + int get_filament_in_nozzle(int nozzle_id) const; + int get_nozzle_in_extruder(int extruder_id) const; + int get_current_extruder_id() const { return current_extruder_id_; } + + void clear_nozzle_status(int nozzle_id); + void set_current_extruder_id(int extruder_id) { current_extruder_id_ = extruder_id; } + + // Update the status of a nozzle with new filament and extruder information + void set_nozzle_status(int nozzle_id, int filament_id, int extruder_id = -1); + + // key: nozzle id, value: filament id (-1 = the nozzle carries no filament) + const std::unordered_map& get_nozzle_filament_map() const { return nozzle_filament_status; } + // key: extruder id, value: nozzle id (-1 = the extruder carries no nozzle) + const std::unordered_map& get_extruder_nozzle_map() const { return extruder_nozzle_status; } +}; + +struct FilamentChangeSimResult { + double actual_time = 0.0; + double sliced_time = 0.0; +}; + +// Analytic filament-change-time model. Given the used filaments, the nozzle +// list, the filament/nozzle change sequences, each filament's AMS group and the load/unload time +// constants, it simulates AMS->selector->extruder transport (with optional AMS pre-load overlap) +// and returns the actual print time plus the slicer-estimated time. Self-contained: it never +// touches the g-code time estimator. +FilamentChangeSimResult simulate_filament_change_time( + const std::vector& logical_filaments, + const std::vector& nozzle_list, + const std::vector& filament_change_seq, + const std::vector& nozzle_change_seq, + const std::vector& group_of_filament, + const FilamentChangeTimeParams& time_params, + const std::vector& ams_preload_enabled = {}, + bool calc_sliced_time = false); + +// ==================== tool functions ==================== +std::vector build_nozzle_list(std::vector info); +std::vector build_nozzle_list(double diameter, const std::vector& filament_nozzle_map, + const std::vector& filament_volume_map, const std::vector& filament_map); +// Load nozzle infos from a gcode.3mf, handling backward compatibility with older 3mf that did not +// record standalone tags: falls back to the per-filament group_id/diameter/volume_type, and +// (for the oldest single-nozzle 3mf) to the filament_map + extruder volume types + nozzle diameters. +std::vector load_nozzle_infos_with_compatibility( + const std::vector& nozzle_infos, + const std::vector& filament_infos, + const std::vector& filament_map, + const std::vector& extruder_volume_types, + const std::vector& nozzle_diameter +); +} // namespace MultiNozzleUtils +} // namespace Slic3r + +#endif // MULTI_NOZZLE_UTILS_HPP diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 98a5a4fcb6..dea9e78906 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -252,7 +252,7 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil auto replace_nil_and_resize = [&](const std::string & key, int length){ ConfigOption* raw_ptr = config.option(key); ConfigOptionVectorBase* opt_vec = static_cast(raw_ptr); - if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), key) == filament_extruder_override_keys.end()){ + if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && !is_filament_extruder_override_key(key)){ opt_vec->clear(); opt_vec->resize(length, defaults.option(key)); } @@ -1312,7 +1312,7 @@ static std::vector s_Preset_print_options{ }; static std::vector s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type", - "filament_soluble", "filament_is_support", "filament_printable", + "filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility", "filament_max_volumetric_speed", "filament_adaptive_volumetric_speed", "filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower", "filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume", @@ -1347,7 +1347,13 @@ static std::vector s_Preset_filament_options {/*"filament_colour", "filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow", "activate_chamber_temp_control", "chamber_minimal_temperature", "filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature", //BBS filament change length while the extruder color - "filament_change_length","filament_flush_volumetric_speed","filament_flush_temp", "filament_cooling_before_tower", + "filament_change_length","filament_flush_volumetric_speed","filament_flush_temp","filament_flush_temp_fast", "filament_cooling_before_tower", + // Multi-nozzle pre-cooling / ramming / nozzle-change (nc) filament overrides + "filament_ramming_volumetric_speed", "filament_ramming_volumetric_speed_nc", + "filament_ramming_travel_time", "filament_ramming_travel_time_nc", + "filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc", + "filament_preheat_temperature_delta", "filament_retract_length_nc", + "filament_change_length_nc", "filament_prime_volume_nc", "long_retractions_when_ec", "retraction_distances_when_ec" }; @@ -1358,6 +1364,8 @@ static std::vector s_Preset_machine_limits_options { "machine_min_extruding_rate", "machine_min_travel_rate", "machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e", "machine_max_junction_deviation", + // Bedslinger mass/force limits + "machine_max_force_Y", "machine_bed_mass_Y", "machine_max_printed_mass", //resonance avoidance ported from qidi slicer "resonance_avoidance", "min_resonance_avoidance_speed", "max_resonance_avoidance_speed", // Orca: input shaping @@ -1375,7 +1383,7 @@ static std::vector s_Preset_printer_options { "default_print_profile", "inherits", "silent_mode", "scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode", - "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure", + "nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","cooling_filter_enabled","printer_structure","farthest_point_timelapse", "best_object_pos", "head_wrap_detect_zone", "host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent", "print_host_webui", @@ -1387,7 +1395,13 @@ static std::vector s_Preset_printer_options { "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", - "bed_temperature_formula", "nozzle_flush_dataset" + "bed_temperature_formula", "nozzle_flush_dataset", + // Multi-nozzle count + pre-heat model printer options + "extruder_max_nozzle_count", "group_algo_with_time", "enable_pre_heating", "hotend_heating_rate", "hotend_cooling_rate", + "machine_hotend_change_time", "machine_prepare_compensation_time", + // Fast-purge printer flag + device/firmware-facing per-variant extruder-change + // deretraction speed (unconsumed by the slicer; carried by H2D/A2L/X2D/P2S machine profiles). + "support_fast_purge_mode", "deretract_speed_extruder_change" }; static std::vector s_Preset_sla_print_options { diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index bbbdc6c5ff..75150f85c9 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -72,6 +72,7 @@ #define BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME "bottom_texture_end_name" #define BBL_JSON_KEY_USE_DOUBLE_EXTRUDER_DEFAULT_TEXTURE "use_double_extruder_default_texture" #define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT "bottom_texture_rect" +#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER "bottom_texture_rect_longer" #define BBL_JSON_KEY_MIDDLE_TEXTURE_RECT "middle_texture_rect" #define BBL_JSON_KEY_HOTEND_MODEL "hotend_model" @@ -150,6 +151,7 @@ public: std::string bottom_texture_end_name; std::string use_double_extruder_default_texture; std::string bottom_texture_rect; + std::string bottom_texture_rect_longer; std::string middle_texture_rect; std::string hotend_model; PrinterVariant* variant(const std::string &name) { diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index fccbd13b98..610ca41619 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -52,6 +52,9 @@ static std::vector s_project_options { "wipe_tower_rotation_angle", "curr_bed_type", "flush_multiplier", + // Fast-purge mode: project-level purge control, inert at Default. + "flush_multiplier_fast", + "prime_volume_mode", "nozzle_volume_type", "filament_map_mode", "filament_map" @@ -348,7 +351,7 @@ PresetBundle::PresetBundle() auto& default_config = this->filaments.default_preset().config; for(const std::string& opt_key : default_config.keys()){ ConfigOption* opt = default_config.optptr(opt_key, false); - bool is_override_key = std::find(filament_extruder_override_keys.begin(),filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end(); + bool is_override_key = is_filament_extruder_override_key(opt_key); if(!is_override_key || !opt->nullable()) continue; opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable); @@ -721,6 +724,8 @@ std::optional PresetBundle::get_filament_by_filament_id(const auto iter = std::find(compatible_printers.begin(), compatible_printers.end(), printer_name); if (iter != compatible_printers.end() && config.has("filament_printable")) { info.filament_printable = config.option("filament_printable")->values[0]; + if (config.has("filament_extruder_compatibility")) + info.set_filament_extruder_compatibility(config.option("filament_extruder_compatibility")->values[0]); return info; } } @@ -3872,10 +3877,46 @@ DynamicPrintConfig PresetBundle::full_config_secure(std::optional>> PresetBundle::get_full_flush_matrix(bool with_multiplier) const +{ + auto full_config = this->full_config(); + int extruder_nums = full_config.option("nozzle_diameter")->values.size(); + std::vector flush_volume_value = full_config.option("flush_volumes_matrix")->values; + int filament_nums = full_config.option("filament_type")->values.size(); + + std::vector>> matrix; + for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) { + std::vector flush_matrix(cast(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums))); + std::vector> wipe_volumes; + for (unsigned int i = 0; i < filament_nums; ++i) + wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums)); + + matrix.emplace_back(wipe_volumes); + } + + if (with_multiplier) { + // Fast purge mode uses flush_multiplier_fast; the default prime_volume_mode==Default + // (or the key absent) reads flush_multiplier, so this is inert. + auto* mode_opt = project_config.option>("prime_volume_mode"); + const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast; + auto* mult_opt = project_config.option(use_fast ? "flush_multiplier_fast" : "flush_multiplier"); + auto flush_multiplies = mult_opt ? mult_opt->values : project_config.option("flush_multiplier")->values; + flush_multiplies.resize(extruder_nums, 1); + for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) { + for (auto& vec : matrix[extruder_id]) { + for (auto& v : vec) + v *= flush_multiplies[extruder_id]; + } + } + } + + return matrix; +} + const std::set ignore_settings_list ={ "inherits", "print_settings_id", "filament_settings_id", "printer_settings_id" @@ -4770,6 +4811,8 @@ std::pair PresetBundle::load_vendor_configs_ model.use_double_extruder_default_texture = it.value(); } else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT)) { model.bottom_texture_rect = it.value(); + } else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER)) { + model.bottom_texture_rect_longer = it.value(); } else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) { model.middle_texture_rect = it.value(); } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index fc03ff8d3b..4bdccd4663 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -72,6 +72,25 @@ struct FilamentBaseInfo bool is_support{ false }; bool is_system{ true }; int filament_printable = 3; + + // filament_extruder_compatibility packs one compatibility level per extruder into a single + // 32-bit int, 3 bits per extruder (up to 10 extruders). Levels: 0 = printable, 1 = error, + // 2 = critical warning, 3 = warning (4-7 reserved). extruder_id is 0-based. + int get_extruder_compatibility(int extruder_id) const { + constexpr int bits_per_extruder = 3; + constexpr int extruder_mask = (1 << bits_per_extruder) - 1; // 0x7 + constexpr int max_extruder_count = 32 / bits_per_extruder; // 10 + + if (extruder_id < 0 || extruder_id >= max_extruder_count) + return 0; + return (m_filament_extruder_compatibility >> (bits_per_extruder * extruder_id)) & extruder_mask; + } + + void set_filament_extruder_compatibility(int value) { m_filament_extruder_compatibility = value; } + int get_filament_extruder_compatibility() const { return m_filament_extruder_compatibility; } + +private: + int m_filament_extruder_compatibility = 0; }; enum BundleType{ @@ -368,6 +387,11 @@ public: // full_config() with the some "useless" config removed. DynamicPrintConfig full_config_secure(std::optional>filament_maps = std::nullopt) const; + // Per-extruder flush matrix [extruder_id][from_filament][to_filament] in mm^3, optionally scaled + // by the per-extruder flush_multiplier (or flush_multiplier_fast when prime_volume_mode==Fast). + // Used by the print-dispatch nozzle-mapping flush-weight estimate. + std::vector>> get_full_flush_matrix(bool with_multiplier = true) const; + //BBS: add some functions for multiple extruders int get_printer_extruder_count() const; bool support_different_extruders() const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index a676d24cc0..19be87bc32 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2506,9 +2506,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) auto map_mode = get_filament_map_mode(); // get recommended filament map if (map_mode < FilamentMapMode::fmmManual) { - filament_maps = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables); - std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value + 1; }); - update_filament_maps_to_config(filament_maps); + // Grouping returns a nozzle-aware result; the 1-based extruder map + // for the by-object path is derived from it. + auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables); + auto derived_maps = grouping_result.get_extruder_map(false); + if (!derived_maps.empty()) { + filament_maps = derived_maps; + update_filament_maps_to_config(filament_maps); + } } // check map valid both in auto and mannual mode std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; }); @@ -2658,8 +2663,14 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor gcode.do_export(this, path.c_str(), result, thumbnail_cb); gcode.export_layer_filaments(result); //BBS - if (result != nullptr) + if (result != nullptr) { result->conflict_result = m_conflict_result; + // Surface the slicer's per-filament nozzle grouping onto the post-slice result + // the device GUI reads. This is the static L/R + rack subset the multi-nozzle path computes; + // null for single-nozzle prints where nothing computes it. It is assigned after g-code + // generation and read by no emitter, so it does not affect the emitted g-code. + result->nozzle_group_result = this->get_layered_nozzle_group_result(); + } return path.c_str(); } @@ -3306,6 +3317,47 @@ size_t Print::get_extruder_id(unsigned int filament_id) const return 0; } +// Region reachable by every extruder = intersection of all per-extruder printable areas. +// For single-nozzle printers, or whenever extruder_printable_area is unpopulated / degenerate (all +// current single/dual profiles), fall back to the full printable_area so the wipe-tower-center clamp +// is identical to the previous full-bed clamp. +Polygons Print::get_extruder_shared_printable_polygon() const +{ + const std::vector& extruder_printable_areas = m_config.extruder_printable_area.values; + if (m_config.nozzle_diameter.size() < 2 || extruder_printable_areas.empty()) + return {Polygon::new_scale(m_config.printable_area.values)}; + for (const Vec2ds& area : extruder_printable_areas) + if (area.size() < 3) + return {Polygon::new_scale(m_config.printable_area.values)}; + + Polygons shared_printable_polys = {Polygon::new_scale(extruder_printable_areas.front())}; + for (size_t i = 1; i < extruder_printable_areas.size(); ++i) + shared_printable_polys = intersection(shared_printable_polys, Polygons{Polygon::new_scale(extruder_printable_areas[i])}); + return shared_printable_polys; +} + +// Narrow the stored grouping result to the layer-aware type the slicing pipeline uses. +std::shared_ptr Print::get_layered_nozzle_group_result() const +{ + return std::dynamic_pointer_cast(m_nozzle_group_result); +} + +// Dynamic (per-layer selector) regroup predicate. +// Orca: enable_filament_dynamic_map is a develop-only config key registered in the ConfigDef but NOT +// a static PrintConfig member, so it is read from the applied full config; it is absent for every +// shipping printer/profile -> nullptr -> false, which keeps the static grouping path (identical +// output) the only one the current fleet takes. There is no mixed-colour-filament guard (mixed-colour +// filaments are not supported). The remaining gates (auto-for-flush mode, multi-extruder machine) +// read the static PrintConfig members. +bool Print::is_dynamic_group_reorder() const +{ + const auto *opt = m_full_print_config.option("enable_filament_dynamic_map"); + const bool enabled = opt && opt->value; + if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1) + return false; + return true; +} + // Wipe tower support. bool Print::has_wipe_tower() const { @@ -3466,6 +3518,14 @@ void Print::_make_wipe_tower() m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders()); wipe_tower.set_has_tpu_filament(this->has_tpu_filament()); wipe_tower.set_filament_map(this->get_filament_maps()); + // Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from + // the full config — no shipping profile sets it) and the shared printable bed used by the PETG + // pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set. + { + const ConfigOptionBool* hfs = m_full_print_config.option("has_filament_switcher"); + wipe_tower.set_has_filament_switcher(hfs && hfs->value); + } + wipe_tower.set_shared_print_bed(this->get_extruder_shared_printable_polygon()); // Set the extruder & material properties at the wipe tower object. for (size_t i = 0; i < number_of_extruders; ++i) wipe_tower.set_extruder(i, m_config); @@ -3517,7 +3577,10 @@ void Print::_make_wipe_tower() float volume_to_purge = 0; if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) { volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id]; - volume_to_purge *= m_config.flush_multiplier.get_at(nozzle_id); + // Fast purge mode uses flush_multiplier_fast; Default is inert. + float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id) + : m_config.flush_multiplier.get_at(nozzle_id); + volume_to_purge *= flush_multiplier; volume_to_purge = pre_filament_id == -1 ? 0 : layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge); } @@ -3526,8 +3589,10 @@ void Print::_make_wipe_tower() float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4 volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume); + // Saving mode reduces the prime volume to 15 mm3; Default is inert. + float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume; wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id, - m_config.prime_volume, volume_to_purge); + prime_volume, volume_to_purge); current_filament_id = filament_id; nozzle_cur_filament_ids[nozzle_id] = filament_id; } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b9f70506fb..bf9b5bfd48 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -38,6 +38,7 @@ class SupportLayer; class TreeSupportData; class TreeSupport; class ExtrusionLayers; +namespace MultiNozzleUtils { class NozzleGroupResultBase; class LayeredNozzleGroupResult; } #define MAX_OUTER_NOZZLE_DIAMETER 4 // BBS: move from PrintObjectSlice.cpp @@ -1012,6 +1013,25 @@ public: // get the group label of filament size_t get_extruder_id(unsigned int filament_id) const; + // The region every extruder can reach, + // i.e. the intersection of all per-extruder printable areas. Falls back to the full printable_area + // for single-nozzle printers and whenever extruder_printable_area is not populated (all current + // single/dual profiles), so the wipe-tower-center clamp is byte-identical to full-bed clamping there. + Polygons get_extruder_shared_printable_polygon() const; + + // Logical (extruder, nozzle) grouping result produced by ToolOrdering during reorder. + // Consumed by GCode via get_layered_nozzle_group_result()->get_nozzle_id(filament, layer) etc. + void set_nozzle_group_result(std::shared_ptr result) { m_nozzle_group_result = result; } + std::shared_ptr get_nozzle_group_result() const { return m_nozzle_group_result; } + std::shared_ptr get_layered_nozzle_group_result() const; + + // True only when the printer opts into the per-layer filament selector + // (enable_filament_dynamic_map) in auto-for-flush mode on a multi-extruder machine. Gates the + // dynamic (per-layer) regroup branch in ToolOrdering::reorder_extruders_for_minimum_flush_volume. + // Closed for every current profile, so the static grouping path (byte-identical output) is the + // only one taken by the shipping fleet. + bool is_dynamic_group_reorder() const; + const std::vector>& get_extruder_filament_info() const { return m_extruder_filament_info; } void set_extruder_filament_info(const std::vector>& filament_info) { m_extruder_filament_info = filament_info; } @@ -1176,6 +1196,9 @@ private: std::vector> m_extruder_filament_info; + // Logical (extruder, nozzle) grouping result, set by ToolOrdering during reorder. + std::shared_ptr m_nozzle_group_result; + // Following section will be consumed by the GCodeGenerator. ToolOrdering m_tool_ordering; WipeTowerData m_wipe_tower_data {m_tool_ordering}; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index dda24e4e5b..d324553868 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -83,6 +83,15 @@ const std::vector filament_extruder_override_keys = { "filament_retraction_distances_when_cut" }; +// Some filament override parameters are generated from filament_extruder_override_keys, +// while filament_retract_length_nc is defined separately. Keep the generator list +// unchanged and use this helper for behavior checks that need the full override set. +bool is_filament_extruder_override_key(const std::string &opt_key) +{ + return std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end() || + opt_key == "filament_retract_length_nc"; +} + size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id) { if (filament_id < config.filament_map.size()) { @@ -569,17 +578,28 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ExtruderType) static const t_config_enum_values s_keys_map_NozzleVolumeType = { { "Standard", nvtStandard }, - { "High Flow", nvtHighFlow } + { "High Flow", nvtHighFlow }, + { "TPU High Flow", nvtTPUHighFlow }, + { "Hybrid", nvtHybrid } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType) static const t_config_enum_values s_keys_map_FilamentMapMode = { { "Auto For Flush", fmmAutoForFlush }, { "Auto For Match", fmmAutoForMatch }, - { "Manual", fmmManual } + { "Manual", fmmManual }, + { "Nozzle Manual", fmmNozzleManual } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FilamentMapMode) +// PrimeVolumeMode. Serialized string keys must stay stable; they round-trip through .3mf. +static const t_config_enum_values s_keys_map_PrimeVolumeMode = { + { "Default", pvmDefault }, + { "Saving", pvmSaving }, + { "Fast", pvmFast } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrimeVolumeMode) + //BBS std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type) @@ -650,6 +670,60 @@ std::vector save_extruder_ams_count_to_string(const std::vector#". Empty string => no per-type stats for that extruder. +std::vector> get_extruder_nozzle_stats(const std::vector &strs) +{ + std::vector> extruder_nozzle_counts; + for (const std::string &str : strs) { + std::map nozzle_count_map; + if (str.empty()) { + extruder_nozzle_counts.emplace_back(nozzle_count_map); + continue; + } + std::vector nozzle_infos; + boost::algorithm::split(nozzle_infos, str, boost::algorithm::is_any_of("|")); + for (auto &nozzle_info : nozzle_infos) { + std::vector attr; + boost::algorithm::split(attr, nozzle_info, boost::algorithm::is_any_of("#")); + // Guard against malformed device/project data: skip tokens without a "#" + // shape or with an unknown volume-type key instead of throwing (.at) or reading attr[1] + // out of bounds. Corrupt tokens are dropped and logged; the rest still parse. + if (attr.size() < 2) { + BOOST_LOG_TRIVIAL(warning) << "get_extruder_nozzle_stats: skipping malformed token '" << nozzle_info << "'"; + continue; + } + auto it = s_keys_map_NozzleVolumeType.find(attr[0]); + if (it == s_keys_map_NozzleVolumeType.end()) { + BOOST_LOG_TRIVIAL(warning) << "get_extruder_nozzle_stats: skipping unknown nozzle volume type '" << attr[0] << "'"; + continue; + } + NozzleVolumeType volume_type = NozzleVolumeType(it->second); + int nozzle_count = std::atoi(attr[1].c_str()); + nozzle_count_map[volume_type] = nozzle_count; + } + extruder_nozzle_counts.emplace_back(nozzle_count_map); + } + return extruder_nozzle_counts; +} + +std::vector save_extruder_nozzle_stats_to_string(const std::vector> &extruder_nozzle_stats) +{ + std::vector extruder_nozzle_count_str; + for (size_t idx = 0; idx < extruder_nozzle_stats.size(); ++idx) { + std::ostringstream oss; + const auto &item = extruder_nozzle_stats[idx]; + for (auto it = item.begin(); it != item.end(); ++it) { + oss << get_nozzle_volume_type_string(it->first) << "#" << it->second; + if (std::next(it) != item.end()) + oss << "|"; + } + extruder_nozzle_count_str.emplace_back(oss.str()); + } + return extruder_nozzle_count_str; +} + static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology) { for (std::pair &kvp : options) @@ -2548,6 +2622,25 @@ void PrintConfigDef::init_fff_params() def->mode = comDevelop; def->set_default_value(new ConfigOptionInts{1}); + // Multi-nozzle: per-filament map to the config slot identified by (extruder, nozzle_volume_type). + // Forward-compat-only registration with no consumer yet. + // Orca: resolves per-variant slots via get_index_for_extruder at PrintApply time rather than a + // read-time filament-config-index lookup, so nothing in src/ reads this key. Kept registered so an + // H2C project/config that carries it loads without an unknown-option substitution warning. + // Internal use only, no translation. + def = this->add("filament_map_2", coInts); + def->label = "Filament map plus for multi nozzle"; + def->tooltip = "Filament map to the index identified by extruder and nozzle_volume_type"; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionInts{1}); + + // Per-filament nozzle-volume-type override (multi-volume/Hybrid). Registered so the value + // round-trips through .3mf plate metadata (filament_volume_maps); load/save-only for now and + // inert for existing printers (unread by slicing until the multi-volume pipeline lands). + def = this->add("filament_volume_map", coInts); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionInts{(int)(NozzleVolumeType::nvtStandard)}); + def = this->add("physical_extruder_map",coInts); // internal use only, don't need translation def->label = "Map the logical extruder to physical extruder"; @@ -2563,14 +2656,22 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("Auto For Flush"); def->enum_values.push_back("Auto For Match"); def->enum_values.push_back("Manual"); + def->enum_values.push_back("Nozzle Manual"); def->enum_values.push_back("Default"); def->enum_labels.push_back(L("Auto For Flush")); def->enum_labels.push_back(L("Auto For Match")); def->enum_labels.push_back(L("Manual")); + def->enum_labels.push_back(L("Nozzle Manual")); def->enum_labels.push_back(L("Default")); def->mode = comAdvanced; def->set_default_value(new ConfigOptionEnum(fmmAutoForFlush)); + // Fully-manual mode (fmmNozzleManual): per-filament -> physical-nozzle assignment. + // Inert until the nozzle-assignment engine consumes it. Internal use only, no translation. + def = this->add("filament_nozzle_map", coInts); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionInts{1}); + def = this->add("filament_flush_temp", coInts); def->label = L("Flush temperature"); def->tooltip = L("Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range."); @@ -2581,6 +2682,19 @@ void PrintConfigDef::init_fff_params() def->sidetext = L(u8"\u2103" /* °C */); // degrees Celsius, CIS languages need translation def->set_default_value(new ConfigOptionIntsNullable{0}); + // Fast-purge flush temperature: used only when prime_volume_mode==Fast. + // Default 0 (fall back to the recommended-range upper bound, identical to filament_flush_temp), + // so the key is inert for the shipping fleet. Kept out of the g-code config block (banned_keys). + def = this->add("filament_flush_temp_fast", coInts); + def->label = L("Flush temperature"); + def->tooltip = L("Flush temperature used in fast purge mode."); + def->mode = comAdvanced; + def->nullable = true; + def->min = 0; + def->max = max_temp; + def->sidetext = L(u8"℃" /* °C */); // degrees Celsius, CIS languages need translation + def->set_default_value(new ConfigOptionIntsNullable{0}); + def = this->add("filament_flush_volumetric_speed", coFloats); def->label = L("Flush volumetric speed"); def->tooltip = L("Volumetric speed when flushing filament. 0 indicates the max volumetric speed."); @@ -2954,6 +3068,18 @@ void PrintConfigDef::init_fff_params() def->mode = comDevelop; def->set_default_value(new ConfigOptionInts{3}); + // A single 32-bit int encodes the compatibility level of a filament across all extruders (up to 10). + // Every 3 bits represent one extruder: bits [3*i, 3*i+2] -> extruder i. + // Compatibility levels: 0 = printable, 1 = error, 2 = critical warning, 3 = warning (4-7 reserved). + // Default 0 (printable) keeps the key inert for filaments/printers that do not declare it. + def = this->add("filament_extruder_compatibility", coInts); + def->label = L("Filament-extruder compatibility"); + def->tooltip = L("A single 32-bit int encoding the compatibility level of a filament across all extruders (up to 10). " + "Every 3 bits represent one extruder (bits [3*i, 3*i+2] for extruder i). " + "0: printable, 1: error, 2: critical warning, 3: warning, 4-7: reserved."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionInts{0}); + // BBS def = this->add("temperature_vitrification", coInts); def->label = L("Softening temperature"); @@ -3857,6 +3983,12 @@ void PrintConfigDef::init_fff_params() def->mode=comDevelop; def->set_default_value(new ConfigOptionBool(true)); + def = this->add("cooling_filter_enabled", coBool); + def->label = L("Use cooling filter"); + def->tooltip = L("Enable this if printer support cooling filter"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("gcode_flavor", coEnum); def->label = L("G-code flavor"); def->tooltip = L("What kind of G-code the printer is compatible with."); @@ -4659,6 +4791,39 @@ void PrintConfigDef::init_fff_params() def->mode = comDevelop; def->set_default_value(new ConfigOptionFloats{ 0., 0. }); + // Bedslinger mass/force limits. + // Consumed by GCode::mass_load_limited_machine_acceleration (curr_y_acceleration_limit) + // and the printed-mass check. Default 0 keeps them inactive for existing printers. + def = this->add("machine_max_force_Y", coFloat); + def->full_label = L("Maximum force of the Y axis"); + def->category = L("Machine limits"); + def->readonly = false; + def->tooltip = L("The allowed maximum output force of Y axis"); + def->sidetext = L("N"); + def->min = 0; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("machine_bed_mass_Y", coFloat); + def->full_label = L("Bed mass of the Y axis"); + def->category = L("Machine limits"); + def->readonly = false; + def->tooltip = L("The machine bed mass load of Y axis"); + def->sidetext = L("g"); + def->min = 0; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionFloat(0)); + + def = this->add("machine_max_printed_mass", coFloat); + def->full_label = L("The allowed max printed mass"); + def->category = L("Machine limits"); + def->readonly = false; + def->tooltip = L("The allowed max printed mass on a plate"); + def->sidetext = L("g"); + def->min = 0; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionFloat(0)); + // M204 P... [mm/sec^2] def = this->add("machine_max_acceleration_extruding", coFloats); def->full_label = L("Maximum acceleration for extruding"); @@ -5404,10 +5569,15 @@ void PrintConfigDef::init_fff_params() def->label = "Nozzle Volume Type"; def->tooltip = "Nozzle volume type for extruders."; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + // Order must match the NozzleVolumeType enum values (Standard=0, High Flow=1, Hybrid=2, TPU High Flow=3). def->enum_values.push_back(L("Standard")); def->enum_values.push_back(L("High Flow")); + def->enum_values.push_back(L("Hybrid")); + def->enum_values.push_back(L("TPU High Flow")); def->enum_labels.push_back(L("Standard")); def->enum_labels.push_back(L("High Flow")); + def->enum_labels.push_back(L("Hybrid")); + def->enum_labels.push_back(L("TPU High Flow")); def->mode = comSimple; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -5418,8 +5588,12 @@ void PrintConfigDef::init_fff_params() def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back(L("Standard")); def->enum_values.push_back(L("High Flow")); + def->enum_values.push_back(L("Hybrid")); + def->enum_values.push_back(L("TPU High Flow")); def->enum_labels.push_back(L("Standard")); def->enum_labels.push_back(L("High Flow")); + def->enum_labels.push_back(L("Hybrid")); + def->enum_labels.push_back(L("TPU High Flow")); def->mode = comDevelop; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -5436,6 +5610,43 @@ void PrintConfigDef::init_fff_params() def->tooltip = "AMS counts per extruder."; def->set_default_value(new ConfigOptionStrings { }); + // Multi-nozzle: per-extruder physical nozzle inventory by volume type, + // "#" tokens joined by "|". Internal use only, no translation. + def = this->add("extruder_nozzle_stats", coStrings); + def->label = "Extruder nozzle stats"; + def->tooltip = "Physical nozzle counts per extruder, keyed by nozzle volume type."; + def->set_default_value(new ConfigOptionStrings { }); + + // Multi-nozzle: number of physical nozzles per extruder. Forward-compat-only registration + // with no slicing consumer: the nozzle-assignment engine derives its per-extruder physical nozzle + // inventory from the richer `extruder_nozzle_stats` key, not from this scalar count, so nothing in + // src/ reads it. Kept registered so an H2C project/config that carries the key loads without an + // unknown-option substitution warning. Default {1} == today's single-nozzle-per-extruder assumption, + // so absent/default is a no-op for every shipping printer. Internal use only, no translation. + def = this->add("extruder_nozzle_count", coInts); + def->label = "extruder nozzle count"; + def->tooltip = "extruder nozzle count"; + def->mode = comDevelop; + def->set_default_value(new ConfigOptionInts{1}); + + // Per-nozzle volume type. Forward-compat-only registration with no slicing consumer — nothing in + // src/ reads it; the engine resolves per-nozzle volume types from `extruder_nozzle_stats` tokens + // instead. Kept registered so a project/config carrying it loads without an unknown-option + // substitution warning. Registers Standard/High Flow/TPU High Flow only (no Hybrid). + // Internal use only, no translation. + def = this->add("extruder_nozzle_volume_type", coEnums); + def->label = "Extruder nozzle volume type"; + def->tooltip = "Nozzle volume type per physical nozzle of an extruder."; + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("Standard"); + def->enum_values.push_back("High Flow"); + def->enum_values.push_back("TPU High Flow"); + def->enum_labels.push_back("Standard"); + def->enum_labels.push_back("High Flow"); + def->enum_labels.push_back("TPU High Flow"); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); + def = this->add("enable_filament_dynamic_map", coBool); def->label = L("Enable filament dynamic map"); def->tooltip = L("Enable dynamic filament mapping during print."); @@ -5534,6 +5745,19 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloats { 0. }); + // Per-(extruder,variant) deretraction speed at an extruder change. This value + // ships in multi-extruder machine profiles but has no slicer consumer — it is a device/firmware-facing + // profile key. Registered here (ConfigDef-only, no FullPrintConfig static member) so X2D/P2S profiles + // that already carry it — and the H2D/A2L leaves — validate as a known key; unread by the slicer. + // nullable so absent leaves stay nil (excluded from the g-code config block). + def = this->add("deretract_speed_extruder_change", coFloats); + def->label = L("Deretraction speed (extruder change)"); + def->tooltip = L("Speed for reloading filament into the nozzle when switching extruder."); + def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation + def->mode = comDevelop; + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable { 0. }); + def = this->add("use_firmware_retraction", coBool); def->label = L("Use firmware retraction"); def->tooltip = L("This experimental setting uses G10 and G11 commands to have the firmware " @@ -5952,6 +6176,20 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionEnum(tlTraditional)); + // Farthest-point timelapse. Corexy-only refinement layered on top of the existing + // timelapse_type: when enabled the traditional-mode snapshot is + // taken at the extrusion point farthest from the camera (0,0) instead of traveling to a safe/chute + // position. Gated in GCode::process_layer by timelapse_type==tlTraditional && printer_structure!=psI3, + // so it is inert for i3 (A1/A2L) printers and whenever the toggle is off (default false → every + // existing printer's g-code is byte-identical). Only H2C/H2D machine profiles set it =1. + def = this->add("farthest_point_timelapse", coBool); + def->label = L("Farthest point timelapse"); + def->tooltip = L("When enabled, the timelapse snapshot is taken at the farthest point from camera " + "instead of traveling to the wipe tower or excess chute. " + "Only effective in traditional timelapse mode on non-I3 printers."); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("standby_temperature_delta", coInt); def->label = L("Temperature variation"); // TRN PrintSettings : "Ooze prevention" > "Temperature variation" @@ -6875,6 +7113,14 @@ void PrintConfigDef::init_fff_params() //def->sidetext = ""; def->set_default_value(new ConfigOptionFloats{0.3}); + // Fast-purge mode: the flush multiplier used when prime_volume_mode==Fast. + // Only consumed on the pvmFast branch (default prime_volume_mode==Default reads flush_multiplier), + // so this key is inert for the shipping fleet. Kept out of the g-code config block (banned_keys). + def = this->add("flush_multiplier_fast", coFloats); + def->label = L("Flush multiplier (Fast mode)"); + def->tooltip = L("The flush multiplier used in fast purge mode."); + def->set_default_value(new ConfigOptionFloats{1.2}); + // BBS def = this->add("prime_volume", coFloat); def->label = L("Prime volume"); @@ -6884,6 +7130,22 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionFloat(45.)); + // Dual-extruder purge control: Default reproduces current behaviour + // (per-extruder flush_multiplier + filament_prime_volume); Saving reduces the prime volume, + // Fast selects flush_multiplier_fast + filament_flush_temp_fast. Default pvmDefault = inert. + def = this->add("prime_volume_mode", coEnum); + def->label = L("Prime volume mode"); + def->tooltip = L("Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("Default"); + def->enum_values.push_back("Saving"); + def->enum_values.push_back("Fast"); + def->enum_labels.push_back(L("Default")); + def->enum_labels.push_back(L("Saving")); + def->enum_labels.push_back(L("Fast")); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionEnum(pvmDefault)); + def = this->add("wipe_tower_x", coFloats); //def->label = L("Position X"); //def->tooltip = L("X coordinate of the left front corner of a wipe tower."); @@ -7340,6 +7602,162 @@ void PrintConfigDef::init_fff_params() } } + // ---- Multi-nozzle + pre-heat + nozzle-change (nc) config keys ---- + // These back 6-nozzle cluster grouping, the pre-heat/pre-cool time model and wipe-tower + // nozzle-change handling. Defaults are no-ops for existing single-nozzle printers (all new + // readers gate on extruder_max_nozzle_count > 1). + def = this->add("machine_hotend_change_time", coFloat); + def->label = L("Hotend change time"); + def->tooltip = L("Time to change hotend."); + def->sidetext = L("s"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + // Printer-owned toggle selecting the time-aware grouping objective (flush + change/print + // time) over the flush-only objective. Default false == today's flush-only grouping, so + // absent/default is inert. Consumed by the grouping solver (SpeedInfo). + // Orca: pinned to comDevelop (rather than the default comSimple) to match the sibling multi-nozzle + // dev keys and keep it out of user selectors. + def = this->add("group_algo_with_time", coBool); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("machine_prepare_compensation_time", coFloat); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionFloat(260)); + + def = this->add("enable_pre_heating", coBool); + def->set_default_value(new ConfigOptionBool(false)); + + // Pre-heat injector context key. handle_hotend_as_extruder is read defensively by the injector; + // registered here (comDevelop, default off) as a known inert key. + def = this->add("handle_hotend_as_extruder", coBool); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBool(false)); + + // filament_max_temperature_drop_when_ec: inert (default {0}, passed into the injector context but + // never consumed). Registered inert-only (comDevelop); not wired into any estimator read, and not + // added to any per-variant expansion list. + def = this->add("filament_max_temperature_drop_when_ec", coFloats); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionFloats{0}); + + def = this->add("hotend_cooling_rate", coFloats); + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{2}); + + def = this->add("hotend_heating_rate", coFloats); + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{2}); + + def = this->add("filament_change_length_nc", coFloats); + def->label = L("Hotend change"); + def->tooltip = L("When changing the hotend, it is recommended to extrude a certain length of filament from the original nozzle. This helps minimize nozzle oozing."); + def->sidetext = L("mm"); + def->min = 0; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloats{10}); + + def = this->add("filament_ramming_travel_time", coFloats); + def->label = L("Extruder change"); + def->tooltip = L("To prevent oozing, the nozzle will perform a reverse travel movement for a certain period after " + "the ramming is complete. The setting define the travel time."); + def->mode = comAdvanced; + def->sidetext = "s"; + def->min = 0; + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{0}); + + def = this->add("filament_pre_cooling_temperature", coInts); + def->label = L("Extruder change"); + def->tooltip = L("To prevent oozing, the nozzle temperature will be cooled during ramming. Therefore, the ramming time must be greater than the cooldown time. 0 means disabled."); + def->mode = comAdvanced; + def->sidetext = "°C"; + def->min = 0; + def->nullable = true; + def->set_default_value(new ConfigOptionIntsNullable{0}); + + def = this->add("filament_ramming_volumetric_speed", coFloats); + def->label = L("Extruder change"); + def->tooltip = L("The maximum volumetric speed for ramming before extruder change, where -1 means using the maximum volumetric speed."); + def->sidetext = L("mm³/s"); + def->min = -1; + def->max = 200; + def->mode = comAdvanced; + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{-1}); + + def = this->add("filament_ramming_travel_time_nc", coFloats); + def->label = L("Hotend change"); + def->tooltip = L("To prevent oozing, the nozzle will perform a reverse travel movement for a certain period after " + "the ramming is complete. The setting define the travel time."); + def->mode = comAdvanced; + def->sidetext = "s"; + def->min = 0; + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{0}); + + def = this->add("filament_pre_cooling_temperature_nc", coInts); + def->label = L("Hotend change"); + def->tooltip = L( + "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled."); + def->mode = comAdvanced; + def->sidetext = "°C"; + def->min = 0; + def->nullable = true; + def->set_default_value(new ConfigOptionIntsNullable{0}); + + def = this->add("filament_ramming_volumetric_speed_nc", coFloats); + def->label = L("Hotend change"); + def->tooltip = L("The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed."); + def->sidetext = L("mm³/s"); + def->min = -1; + def->max = 200; + def->mode = comAdvanced; + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{-1}); + + def = this->add("filament_retract_length_nc", coFloats); + def->label = L("length when change hotend"); + def->tooltip = L("When this retraction value is modified, it will be used as the amount of filament retracted " + "inside the hotend before changing hotends."); + def->sidetext = L("mm"); + def->mode = comDevelop; + def->nullable = true; + def->min = 0; + def->max = 18; + def->set_default_value(new ConfigOptionFloatsNullable { 10. }); + + def = this->add("extruder_max_nozzle_count", coInts); + def->mode = comDevelop; + def->nullable = true; + def->set_default_value(new ConfigOptionIntsNullable{ 1 }); + + // Printer flag gating the fast-purge mode selector in the Multi-Filament + // UI. comDevelop, default false; no shipping profile sets it, so the UI stays hidden. + def = this->add("support_fast_purge_mode", coBool); + def->label = L("Support fast purge mode"); + def->tooltip = L("Whether this printer supports fast purge mode with optimized temperature and multiplier."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("filament_prime_volume_nc", coFloats); + def->label = L("Hotend change"); + def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower."); + def->sidetext = L("mm³"); + def->min = 1.0; + def->mode = comSimple; + def->set_default_value(new ConfigOptionFloats{60.}); + + def = this->add("filament_preheat_temperature_delta", coFloats); + def->label = L("Preheat temperature delta"); + def->tooltip = L("Temperature delta applied during pre-heating before tool change."); + def->sidetext = "°C"; + def->mode = comDevelop; + def->nullable = true; + def->set_default_value(new ConfigOptionFloatsNullable{0}); + def = this->add("detect_narrow_internal_solid_infill", coBool); def->label = L("Detect narrow internal solid infills"); def->category = L("Strength"); @@ -7389,13 +7807,14 @@ void PrintConfigDef::init_filament_option_keys() m_filament_option_keys = { "filament_diameter", "min_layer_height", "max_layer_height","volumetric_speed_coefficients", "retraction_length", "z_hop", "z_hop_types", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retraction_speed", "deretraction_speed", - "retract_before_wipe", "retract_restart_extra", "retraction_minimum_travel", "wipe", "wipe_distance", + "retract_before_wipe", "filament_retract_length_nc", "retract_restart_extra", "retraction_minimum_travel", "wipe", "wipe_distance", "retract_when_changing_layer", "retract_length_toolchange", "retract_restart_extra_toolchange", "filament_colour", "default_filament_profile","retraction_distances_when_cut","long_retractions_when_cut"/*,"filament_seam_gap"*/ }; m_filament_retract_keys = { "deretraction_speed", + "filament_retract_length_nc", "long_retractions_when_cut", "retract_before_wipe", "retract_lift_above", @@ -8414,6 +8833,15 @@ std::set print_options_with_variant = { std::set filament_options_with_variant = { "filament_flow_ratio", "filament_max_volumetric_speed", + // Per-variant ramming / pre-cooling / nozzle-change filament overrides + "filament_ramming_volumetric_speed", + "filament_pre_cooling_temperature", + "filament_ramming_travel_time", + "filament_ramming_volumetric_speed_nc", + "filament_pre_cooling_temperature_nc", + "filament_ramming_travel_time_nc", + "filament_retract_length_nc", + "filament_preheat_temperature_delta", //"filament_extruder_id", "filament_extruder_variant", "filament_retraction_length", @@ -8461,7 +8889,8 @@ std::set printer_extruder_options = { "extruder_printable_area", "extruder_printable_height", "min_layer_height", - "max_layer_height" + "max_layer_height", + "extruder_max_nozzle_count" // Per-extruder max (sub-)nozzle count }; std::set printer_options_with_variant_1 = { @@ -8489,6 +8918,9 @@ std::set printer_options_with_variant_1 = { "nozzle_type", "printer_extruder_id", "printer_extruder_variant", + // Per-variant hotend heat-up / cool-down rates (pre-heat time model) + "hotend_cooling_rate", + "hotend_heating_rate", "nozzle_flush_dataset" }; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3b8b809c78..5a2f94bec8 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -46,6 +46,14 @@ enum GCodeFlavor : unsigned char { gcfNoExtrusion }; +// How a filament is used across the model. Part of the multi-nozzle grouping data; not yet +// read by the shipping slicer — the nozzle-centric FilamentGroup engine consumes it. +enum FilamentUsageType { + SupportOnly, + ModelOnly, + Hybrid +}; + enum class FuzzySkinType { None, @@ -418,24 +426,37 @@ enum ExtruderType { enum NozzleVolumeType { nvtStandard = 0, nvtHighFlow, - nvtMaxNozzleVolumeType = nvtHighFlow + nvtHybrid, // match-any / mixed sentinel; hidden from user selectors, kept out of the shipping slicer + nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only + // Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable. + nvtMaxNozzleVolumeType = nvtTPUHighFlow }; enum FilamentMapMode { fmmAutoForFlush, fmmAutoForMatch, fmmManual, + fmmNozzleManual, // Fully-manual filament->physical-nozzle mapping (filament_nozzle_map). Kept ordered right after fmmManual so every `< fmmManual` "is-auto" check stays correct. fmmDefault }; +// Dual-extruder purge control. Default reproduces the current +// per-extruder flush_multiplier + filament_prime_volume behaviour, so absent/default is inert. +// Saving -> reduce prime volume to 15 mm3; Fast -> use flush_multiplier_fast + filament_flush_temp_fast. +enum PrimeVolumeMode { + pvmDefault = 0, + pvmSaving, + pvmFast +}; + extern std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type); static std::set get_valid_nozzle_volume_type() { std::set type; for (int i = 0; i <= nvtMaxNozzleVolumeType; ++i) { auto t = static_cast(i); - // TODO: Orca: Support hybrid - //if (t == nvtHybrid) continue; + // Hybrid is a "match-any / mixed" sentinel, never a user-selectable physical type. + if (t == nvtHybrid) continue; type.insert(t); } return type; @@ -521,11 +542,17 @@ static std::string get_bed_temp_1st_layer_key(const BedType type) } extern const std::vector filament_extruder_override_keys; +// Full override-key check incl. filament_retract_length_nc (defined outside the generator list). +extern bool is_filament_extruder_override_key(const std::string &opt_key); // for parse extruder_ams_count extern std::vector> get_extruder_ams_count(const std::vector &strs); extern std::vector save_extruder_ams_count_to_string(const std::vector> &extruder_ams_count); +// for parse extruder_nozzle_stats (per-extruder physical nozzle inventory by volume type) +extern std::vector> get_extruder_nozzle_stats(const std::vector &strs); +extern std::vector save_extruder_nozzle_stats_to_string(const std::vector> &extruder_nozzle_stats); + #define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \ template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names(); \ template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values(); @@ -1297,6 +1324,12 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloats, machine_min_travel_rate)) // M205 S... [mm/sec] ((ConfigOptionFloats, machine_min_extruding_rate)) + // Bedslinger mass/force model: drive the per-layer Y-axis + // acceleration limit (curr_y_acceleration_limit) and the printed-mass check. + // Default 0 => inactive for every existing printer (mass model reads them as disabled). + ((ConfigOptionFloat, machine_max_force_Y)) + ((ConfigOptionFloat, machine_bed_mass_Y)) + ((ConfigOptionFloat, machine_max_printed_mass)) //resonance avoidance ported from qidi slicer ((ConfigOptionBool, resonance_avoidance)) @@ -1351,6 +1384,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, filament_vendor)) ((ConfigOptionBools, filament_is_support)) ((ConfigOptionInts, filament_printable)) + ((ConfigOptionInts, filament_extruder_compatibility)) ((ConfigOptionFloats, filament_change_length)) ((ConfigOptionFloats, filament_cost)) ((ConfigOptionStrings, default_filament_colour)) @@ -1367,6 +1401,8 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionIntsNullable, nozzle_flush_dataset)) ((ConfigOptionFloatsNullable, filament_flush_volumetric_speed)) ((ConfigOptionIntsNullable, filament_flush_temp)) + // Fast-purge flush temperature; consumed only when prime_volume_mode==pvmFast. + ((ConfigOptionIntsNullable, filament_flush_temp_fast)) // BBS ((ConfigOptionBool, scan_first_layer)) ((ConfigOptionEnum, enable_power_loss_recovery)) @@ -1428,11 +1464,13 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionInt, nozzle_hrc)) ((ConfigOptionBool, auxiliary_fan)) ((ConfigOptionBool, support_air_filtration)) + ((ConfigOptionBool, cooling_filter_enabled)) ((ConfigOptionEnum,printer_structure)) ((ConfigOptionBool, support_chamber_temp_control)) ((ConfigOptionEnumsGeneric, extruder_type)) ((ConfigOptionEnumsGeneric, nozzle_volume_type)) ((ConfigOptionStrings, extruder_ams_count)) + ((ConfigOptionStrings, extruder_nozzle_stats)) ((ConfigOptionInts, printer_extruder_id)) ((ConfigOptionInt, master_extruder_id)) ((ConfigOptionStrings, printer_extruder_variant)) @@ -1490,6 +1528,26 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionStrings, small_area_infill_flow_compensation_model)) ((ConfigOptionBool, has_scarf_joint_seam)) + + // Multi-nozzle + pre-heating + nozzle-change (nc) keys. Defaults are no-ops for existing + // single-nozzle printers; new slicing paths gate on extruder_max_nozzle_count > 1. + ((ConfigOptionFloat, machine_hotend_change_time)) + ((ConfigOptionFloat, machine_prepare_compensation_time)) + ((ConfigOptionBool, enable_pre_heating)) + ((ConfigOptionFloatsNullable, hotend_cooling_rate)) + ((ConfigOptionFloatsNullable, hotend_heating_rate)) + ((ConfigOptionFloats, filament_change_length_nc)) + ((ConfigOptionFloatsNullable, filament_ramming_travel_time)) + ((ConfigOptionIntsNullable, filament_pre_cooling_temperature)) + ((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed)) + ((ConfigOptionFloatsNullable, filament_ramming_travel_time_nc)) + ((ConfigOptionIntsNullable, filament_pre_cooling_temperature_nc)) + ((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed_nc)) + ((ConfigOptionFloatsNullable, filament_retract_length_nc)) + ((ConfigOptionIntsNullable, extruder_max_nozzle_count)) + // Printer flag: whether the printer offers the fast-purge mode selector. + // Default false; no shipping profile sets it, so the fast-purge UI stays hidden. + ((ConfigOptionBool, support_fast_purge_mode)) ) // This object is mapped to Perl as Slic3r::Config::Print. @@ -1636,7 +1694,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( // BBS: wipe tower is only used for priming ((ConfigOptionFloat, prime_volume)) + // Nozzle-change (nc) prime volume + pre-heat delta + ((ConfigOptionFloats, filament_prime_volume_nc)) + ((ConfigOptionFloatsNullable, filament_preheat_temperature_delta)) ((ConfigOptionFloats, flush_multiplier)) + // Fast-purge mode. Kept out of the g-code config block (banned_keys in + // GCode::append_full_config) so registering them leaves the shipping fleet's g-code byte-identical; + // consumed only on the prime_volume_mode==pvmFast / pvmSaving branch (default pvmDefault = inert). + ((ConfigOptionEnum, prime_volume_mode)) + ((ConfigOptionFloats, flush_multiplier_fast)) ((ConfigOptionFloat, z_offset)) // BBS: project filaments ((ConfigOptionFloats, filament_colour_new)) @@ -1644,6 +1710,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloatsNullable, nozzle_volume)) ((ConfigOptionPoints, start_end_points)) ((ConfigOptionEnum, timelapse_type)) + // Corexy farthest-point timelapse (default false → inert for existing printers) + ((ConfigOptionBool, farthest_point_timelapse)) ((ConfigOptionString, thumbnails)) // BBS: move from PrintObjectConfig ((ConfigOptionBool, independent_support_layer_height)) diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index a76857dc4b..62b2eeb78e 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -102,6 +102,9 @@ const std::string& var_dir(); // Return a full resource path for a file_name. std::string var(const std::string &file_name); +// Snap a nozzle diameter to the closest supported value and format it as a string (e.g. 0.4 -> "0.4"). +std::string format_diameter_to_str(double diameter, int precision = 1); + // Set a path with various static definition data (for example the initial config bundles). void set_resources_dir(const std::string &path); // Return a full path to the resources directory. diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index f69ad3e1a3..fc8e0cddc0 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -83,6 +83,8 @@ public: NozzleVolumeType nozzle_volume_type; BedType bed_type; float nozzle_diameter; + int nozzle_pos_id{-1}; + std::string nozzle_sn; std::string filament_id; std::string setting_id; std::string name; @@ -93,6 +95,8 @@ public: this->extruder_id = other.extruder_id; this->nozzle_volume_type = other.nozzle_volume_type; this->nozzle_diameter = other.nozzle_diameter; + this->nozzle_pos_id = other.nozzle_pos_id; + this->nozzle_sn = other.nozzle_sn; this->filament_id = other.filament_id; this->setting_id = other.setting_id; this->name = other.name; @@ -123,7 +127,9 @@ public: int ams_id = 0; int slot_id = 0; int cali_idx = -1; + int nozzle_pos_id = -1; //-1 means no nozzle pos float nozzle_diameter; + std::string nozzle_sn; std::string filament_id; std::string setting_id; std::string name; @@ -140,7 +146,9 @@ struct PACalibIndexInfo int ams_id = 0; int slot_id = 0; int cali_idx = -1; // -1 means default + int nozzle_pos_id = -1; //-1 means no nozzle pos float nozzle_diameter; + std::string nozzle_sn; std::string filament_id; }; @@ -148,7 +156,9 @@ struct PACalibExtruderInfo { int extruder_id = 0; NozzleVolumeType nozzle_volume_type; + int nozzle_pos_id = -1; //-1 means no nozzle pos float nozzle_diameter; + std::string nozzle_sn; std::string filament_id = ""; bool use_extruder_id{true}; bool use_nozzle_volume_type{true}; diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 131369d212..5f429f076a 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -8,6 +8,10 @@ #include #include #include +#include +#include +#include +#include #include "format.hpp" #include "Platform.hpp" @@ -1497,6 +1501,15 @@ std::string format_memsize(size_t bytes, unsigned int decimals) } } +std::string format_diameter_to_str(double diameter, int precision) +{ + double candidates[] = {0.2, 0.4, 0.6, 0.8}; + double best = *std::min_element(std::begin(candidates), std::end(candidates), [diameter](double a, double b) { return std::abs(a - diameter) < std::abs(b - diameter); }); + std::ostringstream oss; + oss << std::fixed << std::setprecision(precision) << best; + return oss.str(); +} + // Returns platform-specific string to be used as log output or parsed in SysInfoDialog. // The latter parses the string with (semi)colons as separators, it should look about as // "desc1: value1; desc2: value2" or similar (spaces should not matter).