feat(libslic3r): multi-nozzle slicing engine for H2C/A2L

Port BambuStudio's dual-nozzle slicing core: H2C-era config keys, filament-to-nozzle grouping with per-layer dynamic regrouping, filament/nozzle/hotend gcode placeholder vocabulary, multi-nozzle wipe tower pre-heat/pre-cool, the two-pass pre-cooling injector, and corexy farthest-point timelapse.
This commit is contained in:
SoftFever
2026-07-09 00:06:26 +08:00
parent 781ecdc2c1
commit 237ef41b06
36 changed files with 7622 additions and 708 deletions

View File

@@ -0,0 +1,922 @@
#include "MultiNozzleUtils.hpp"
#include "Utils.hpp"
#include "ProjectTask.hpp" // Slic3r::FilamentInfo (StaticNozzleGroupResult / load_nozzle_infos_with_compatibility)
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <boost/log/trivial.hpp>
// Multi-nozzle support.
namespace Slic3r { namespace MultiNozzleUtils {
// ==================== tool function implementations ====================
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> nozzle_groups)
{
std::vector<NozzleInfo> 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<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map, const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map)
{
std::string diameter_str = format_diameter_to_str(diameter);
std::map<int, std::vector<int>> 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<int>(idx));
}
std::vector<NozzleInfo> 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<std::vector<int>> &layer_filament_nozzle_maps,
const std::vector<unsigned int> &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<int>(filament_id_u);
std::set<int> 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<int>(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> LayeredNozzleGroupResult::create(
const std::vector<int>& filament_nozzle_map,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& 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> LayeredNozzleGroupResult::create(
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::vector<unsigned int>>& 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> LayeredNozzleGroupResult::create(
const std::vector<unsigned int>& used_filaments,
const std::vector<int>& filament_map,
const std::vector<int>& filament_volume_map,
const std::vector<int>& filament_nozzle_map,
const std::vector<std::map<NozzleVolumeType, int>> &nozzle_count,
float diameter)
{
std::vector<NozzleGroupInfo> 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<int>(extruder_id);
nozzle_groups.emplace_back(group_info);
}
}
auto nozzle_list = build_nozzle_list(nozzle_groups);
std::vector<bool> used_nozzle(nozzle_list.size(), false);
std::map<int, int> input_nozzle_id_to_output;
std::vector<int> 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<int>(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<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
std::optional<NozzleInfo> 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<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
std::optional<NozzleInfo> 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<int> extruder_ids;
for (const auto &nozzle : _nozzle_list) { extruder_ids.insert(nozzle.extruder_id); }
return static_cast<int>(extruder_ids.size());
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
{
return get_used_nozzles_in_extruder(target_extruder_id, -1);
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const
{
std::set<int> nozzle_ids;
std::vector<NozzleInfo> result;
std::vector<unsigned int> 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<int>(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<int>(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<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[nozzle_id]); }
}
return result;
}
std::vector<int> LayeredNozzleGroupResult::get_used_extruders() const
{
return get_used_extruders(-1);
}
std::vector<int> LayeredNozzleGroupResult::get_used_extruders(int layer_id) const
{
std::set<int> used_extruders;
// used filaments on the given layer (or globally)
std::vector<unsigned int> 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<int>(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<int>(filament_id));
for (const auto &nozzle : nozzles) { used_extruders.insert(nozzle.extruder_id); }
}
}
return std::vector<int>(used_extruders.begin(), used_extruders.end());
}
std::vector<int> LayeredNozzleGroupResult::get_extruder_map(bool zero_based, int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> 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<int>(_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<int> LayeredNozzleGroupResult::get_nozzle_map(int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> 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<int>(_nozzle_list.size())) {
nozzle_map[idx] = _nozzle_list[nozzle_id].group_id;
} else {
nozzle_map[idx] = -1;
}
}
return nozzle_map;
}
std::vector<int> LayeredNozzleGroupResult::get_volume_map(int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> 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<int>(_nozzle_list.size())) {
volume_map[idx] = _nozzle_list[nozzle_id].volume_type;
} else {
volume_map[idx] = -1;
}
}
return volume_map;
}
std::vector<unsigned int> LayeredNozzleGroupResult::get_used_filaments(int layer_id) const
{
if (layer_id < 0) { return _used_filaments; }
if (layer_id >= static_cast<int>(_layer_filament_nozzle_maps.size())) { return _used_filaments; }
if (!_layer_filament_sequences.empty() && layer_id < static_cast<int>(_layer_filament_sequences.size())) {
return _layer_filament_sequences[layer_id];
}
return {};
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_for_filament(int filament_id, int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
if (filament_id < 0 || filament_id >= static_cast<int>(filament_nozzle_map.size())) { return std::nullopt; }
int nozzle_id = filament_nozzle_map[filament_id];
return get_nozzle_from_id(nozzle_id);
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
{
std::set<int> nozzle_ids;
if (!support_dynamic_nozzle_map) {
if (filament_id >= 0 && filament_id < static_cast<int>(_default_filament_nozzle_map.size())) {
nozzle_ids.insert(_default_filament_nozzle_map[filament_id]);
}
} else {
int start_layer = 0;
int end_layer = static_cast<int>(_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<int>(map.size())) {
nozzle_ids.insert(map[filament_id]);
}
}
}
std::vector<NozzleInfo> result;
for (int id : nozzle_ids) {
if (id >= 0 && id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[id]); }
}
return result;
}
std::optional<NozzleInfo> 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<int>(_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<unsigned int>(filament_id)) == layer_used_filaments.end()){
continue;
}
const auto &map = _layer_filament_nozzle_maps[layer];
if (filament_id >= 0 && filament_id < static_cast<int>(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<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
{
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(_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<int> &LayeredNozzleGroupResult::get_layer_filament_nozzle_map(int layer_id) const
{
if (layer_id >= 0 && layer_id < static_cast<int>(_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<int>& logical_filaments,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
const std::vector<int>& group_of_filament,
const FilamentChangeTimeParams& time_params,
const std::vector<bool>& 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<int, int> 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<int, int> 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<int>(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<int, Location> filament_location; // filament_id -> current location
std::unordered_map<int, int> filament_extruder; // filament_id -> extruder it sits in (only valid when IN_EXTRUDER)
std::unordered_map<int, int> extruder_filament; // extruder_id -> currently loaded filament
// group_id -> filaments currently occupying that AMS channel (IN_SELECTOR or IN_EXTRUDER)
std::unordered_map<int, std::unordered_set<int>> 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<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const
{
auto get_weight_from_volume = [](float volume){
return static_cast<int>(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<int>(flush_matrix.size()) &&
last_filament >= 0 && last_filament < static_cast<int>(flush_matrix[extruder_id].size()) &&
filament >= 0 && filament < static_cast<int>(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> StaticNozzleGroupResult::create(
const std::vector<FilamentInfo>& filaments_info,
const std::vector<NozzleInfo>& nozzles_info,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
bool support_dynamic_nozzle_map)
{
if (filaments_info.empty() || nozzles_info.empty()) return std::nullopt;
std::map<int, NozzleInfo> nozzle_list_map;
std::map<int, std::set<int>> 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<int> 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<NozzleInfo> 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<int> extruder_ids;
for (const auto &elem : _nozzle_list_map) { extruder_ids.insert(elem.second.extruder_id); }
return static_cast<int>(extruder_ids.size());
}
std::vector<NozzleInfo> StaticNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
{
std::vector<NozzleInfo> 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<int> StaticNozzleGroupResult::get_used_extruders() const
{
std::set<int> used_extruders;
for (const auto &elem : _nozzle_list_map) { used_extruders.insert(elem.second.extruder_id); }
return std::vector<int>(used_extruders.begin(), used_extruders.end());
}
std::vector<unsigned int> StaticNozzleGroupResult::get_used_filaments() const
{
std::vector<unsigned int> 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<unsigned int>(elem.first));
}
}
return used_filaments;
}
std::vector<NozzleInfo> 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<NozzleInfo>(); }
std::vector<NozzleInfo> 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<NozzleInfo> 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> NozzleGroupInfo::deserialize(const std::string &str)
{
std::istringstream iss(str);
std::string token;
std::vector<std::string> 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<NozzleVolumeType>::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<NozzleInfo> load_nozzle_infos_with_compatibility(
const std::vector<NozzleInfo>& nozzle_infos,
const std::vector<FilamentInfo>& filament_infos,
const std::vector<int>& filament_map,
const std::vector<NozzleVolumeType>& extruder_volume_types,
const std::vector<double>& 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<NozzleVolumeType> volume_types_fixed = extruder_volume_types;
volume_types_fixed.resize(extruder_count, NozzleVolumeType::nvtStandard);
std::vector<NozzleInfo> 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<int>(extruder_id);
info.extruder_id = static_cast<int>(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<int, NozzleInfo> 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<NozzleVolumeType>::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<int>(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<NozzleInfo> 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