X2D Support (#13388)

# Description

Adresses #13294 

- Adds the X2D printer definition, machine presets, process presets,
filament presets, BBL profile index entries, CLI config entries,
filament blacklist updates, and printer/load/calibration/cover assets.
- Updates dual-nozzle handling to use configured toolhead labels and
match Bambu X2D hotend placeholders.
- Adds X2D-specific wipe tower cooling placeholder support and 3MF
filament/nozzle change sequence metadata import/export plumbing.

# Note

I own a P2S and an X2D. That's all. I frankly have no idea if my changes
cause regression on other printers, and have no capability to test. I
know that for my X2D, which runs an AMS, .2mm nozzles, SuperTack, and in
LAN mode, this has been working without issue.

# Screenshots/Recordings/Graphs

<img width="606" height="380" alt="Dual nozzle control"
src="https://github.com/user-attachments/assets/0d1c1063-4621-4097-b97c-d739557bf18c"
/>

*Dual nozzle control*

<img width="726" height="260" alt="image"
src="https://github.com/user-attachments/assets/270355b7-ca67-4ca3-ad19-582b8f11411b"
/>

*Multi nozzle filament override*

<img width="416" height="202" alt="X2D Machine config and dual nozzle
support"
src="https://github.com/user-attachments/assets/6a5c07b2-0d20-4819-8f42-d60731313249"
/>

*X2D Machine config and dual nozzle support*

<img width="397" height="142" alt="Filament for Supports test prints"
src="https://github.com/user-attachments/assets/3c7546bd-0e27-4d56-89b7-d9ca18c976f9"
/>

*Filament for Supports has been used in over 20 hours of test prints*

<img width="210" height="263" alt="Left vs Right filament distinction"
src="https://github.com/user-attachments/assets/03322268-b669-4f14-8d77-c4d96843d219"
/>

*Left vs Right filament distinction*

<img width="557" height="327" alt="Custom filament mapping"
src="https://github.com/user-attachments/assets/c1c4396f-7359-474e-80bd-78fec22f9c82"
/>

*Custom filament mapping*

<img width="556" height="314" alt="Auto map"
src="https://github.com/user-attachments/assets/d83e3217-edce-4340-886e-043962003a30"
/>

*Auto map*

<img width="689" height="664" alt="LAN mode send print with X2D preview
and no errors"
src="https://github.com/user-attachments/assets/76009bbf-31d3-4a6c-979c-8643b487c824"
/>

*LAN mode send print with X2D preview and no errors, dual nozzle
selection*


## Tests

- 20 hours of dual-nozzle printing.
- 100% CTest tests passed
- Validated 208 changed JSON files.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
Fix #13294
This commit is contained in:
glowstab
2026-05-09 13:21:13 -05:00
committed by GitHub
parent 7bef75b2cd
commit 9956ad5b48
242 changed files with 53863 additions and 83 deletions

View File

@@ -210,6 +210,7 @@ const std::string BBS_PROJECT_CONFIG_FILE = "Metadata/project_settings.config";
const std::string BBS_MODEL_CONFIG_FILE = "Metadata/model_settings.config";
const std::string BBS_MODEL_CONFIG_RELS_FILE = "Metadata/_rels/model_settings.config.rels";
const std::string SLICE_INFO_CONFIG_FILE = "Metadata/slice_info.config";
const std::string FILAMENT_SEQUENCE_FILE = "Metadata/filament_sequence.json";
const std::string BBS_LAYER_HEIGHTS_PROFILE_FILE = "Metadata/layer_heights_profile.txt";
const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/layer_config_ranges.xml";
const std::string BRIM_EAR_POINTS_FILE = "Metadata/brim_ear_points.txt";
@@ -251,6 +252,12 @@ static constexpr const char* FILAMENT_TYPE_TAG = "type";
static constexpr const char *FILAMENT_COLOR_TAG = "color";
static constexpr const char *FILAMENT_USED_M_TAG = "used_m";
static constexpr const char *FILAMENT_USED_G_TAG = "used_g";
static constexpr const char *FILAMENT_USED_FOR_SUPPORT = "used_for_support";
static constexpr const char *FILAMENT_USED_FOR_OBJECT = "used_for_object";
static constexpr const char *FILAMENT_NOZZLE_GROUP_ID_TAG = "group_id";
static constexpr const char *FILAMENT_NOZZLE_DIAMETER_TAG = "nozzle_diameter";
static constexpr const char *FILAMENT_NOZZLE_VOLUME_TYPE_TAG = "volume_type";
static constexpr const char *NOZZLE_TAG = "nozzle";
static constexpr const char *FILAMENT_TRAY_INFO_ID_TAG = "tray_info_idx";
static constexpr const char *LAYER_FILAMENT_LISTS_TAG = "layer_filament_lists";
static constexpr const char *LAYER_FILAMENT_LIST_TAG = "layer_filament_list";
@@ -365,6 +372,8 @@ static constexpr const char* TIMELAPSE_TYPE_ATTR = "timelapse_type";
static constexpr const char* OUTSIDE_ATTR = "outside";
static constexpr const char* SUPPORT_USED_ATTR = "support_used";
static constexpr const char* LABEL_OBJECT_ENABLED_ATTR = "label_object_enabled";
static constexpr const char* ENABLE_FILAMENT_DYNAMIC_MAP_ATTR = "enable_filament_dynamic_map";
static constexpr const char* HAS_FILAMENT_SWITCHER_ATTR = "has_filament_switcher";
static constexpr const char* SKIPPED_ATTR = "skipped";
static constexpr const char* OBJECT_TYPE = "object";
@@ -528,6 +537,40 @@ void add_vector(std::stringstream &stream, const std::vector<T> &values)
}
}
std::vector<int> parse_int_list(const std::string& value)
{
std::vector<int> out;
if (value.empty())
return out;
std::vector<std::string> tokens;
boost::split(tokens, value, boost::is_any_of(" ,"), boost::token_compress_on);
out.reserve(tokens.size());
for (const std::string& token : tokens) {
if (token.empty())
continue;
try {
out.emplace_back(boost::lexical_cast<int>(token));
} catch (...) {
}
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::string join_int_list_comma(const std::vector<int>& values)
{
std::stringstream stream;
for (size_t i = 0; i < values.size(); ++i) {
stream << values[i];
if (i + 1 < values.size())
stream << ",";
}
return stream.str();
}
Slic3r::Vec3f get_vec3_from_string(const std::string &pos_str)
{
Slic3r::Vec3f pos(0, 0, 0);
@@ -655,6 +698,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
info.id = it->first;
info.used_g = used_filament_g;
info.used_m = used_filament_m;
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;
info.used_for_support = support_volume_it != ps.support_volumes_per_extruder.end() && support_volume_it->second > EPSILON;
slice_filaments_info.push_back(info);
}
@@ -1142,6 +1189,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
void _extract_brim_ear_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat);
void _extract_custom_gcode_per_print_z_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat);
void _extract_filament_sequence_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat);
void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename);
//BBS: add project config file logic
@@ -1535,6 +1583,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
_extract_xml_from_archive(archive, stat, _handle_start_config_xml_element, _handle_end_config_xml_element);
m_parsing_slice_info = false;
}
else if (boost::algorithm::iequals(name, FILAMENT_SEQUENCE_FILE)) {
_extract_filament_sequence_from_archive(archive, stat);
}
}
}
@@ -1568,6 +1619,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
plate->slice_filaments_info = it->second->slice_filaments_info;
plate->printer_model_id = it->second->printer_model_id;
plate->nozzle_diameters = it->second->nozzle_diameters;
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;
plate->optimal_assignment = it->second->optimal_assignment;
plate->warnings = it->second->warnings;
plate->thumbnail_file = it->second->thumbnail_file;
if (plate->thumbnail_file.empty()) {
@@ -1911,6 +1966,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
_extract_xml_from_archive(archive, stat, _handle_start_config_xml_element, _handle_end_config_xml_element);
m_parsing_slice_info = false;
}
else if (!dont_load_config && boost::algorithm::iequals(name, FILAMENT_SEQUENCE_FILE)) {
_extract_filament_sequence_from_archive(archive, stat);
}
else if (boost::algorithm::istarts_with(name, AUXILIARY_DIR)) {
// extract auxiliary directory to temp directory, do nothing for restore
if (m_load_aux && !m_load_restore)
@@ -2231,6 +2289,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
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]->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]->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;
plate_data_list[it->first-1]->optimal_assignment = it->second->optimal_assignment;
plate_data_list[it->first-1]->warnings = it->second->warnings;
plate_data_list[it->first-1]->thumbnail_file = (m_load_restore || it->second->thumbnail_file.empty()) ? it->second->thumbnail_file : m_backup_path + "/" + it->second->thumbnail_file;
//plate_data_list[it->first-1]->pattern_file = (m_load_restore || it->second->pattern_file.empty()) ? it->second->pattern_file : m_backup_path + "/" + it->second->pattern_file;
@@ -3226,6 +3290,62 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
void _BBS_3MF_Importer::_extract_filament_sequence_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)
{
if (stat.m_uncomp_size == 0) {
add_error("Error while reading filament sequence data to buffer");
return;
}
std::string buffer((size_t) stat.m_uncomp_size, 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*) buffer.data(), (size_t) stat.m_uncomp_size, 0);
if (res == 0) {
add_error("Error while reading filament sequence data to buffer");
return;
}
try {
const nlohmann::json sequence_json = nlohmann::json::parse(buffer);
for (auto& elem : m_plater_data) {
const std::string plate_key = "plate_" + std::to_string(elem.first);
auto plate_it = sequence_json.find(plate_key);
if (plate_it == sequence_json.end() || !plate_it->is_object())
continue;
auto filament_it = plate_it->find("filament_sequence");
if (filament_it == plate_it->end())
filament_it = plate_it->find("sequence");
auto nozzle_it = plate_it->find("nozzle_sequence");
if (filament_it == plate_it->end() || !filament_it->is_array() || nozzle_it == plate_it->end() || !nozzle_it->is_array())
continue;
std::vector<unsigned int> filament_sequence;
std::vector<unsigned int> nozzle_sequence;
std::vector<int> optimal_assignment;
for (const auto& item : *filament_it) {
const unsigned int filament_id = item.get<unsigned int>();
filament_sequence.push_back(filament_id > 0 ? filament_id - 1 : 0);
}
for (const auto& item : *nozzle_it)
nozzle_sequence.push_back(item.get<unsigned int>());
auto optimal_assignment_it = plate_it->find("optimal_assignment");
if (optimal_assignment_it != plate_it->end() && optimal_assignment_it->is_array()) {
for (const auto& item : *optimal_assignment_it)
optimal_assignment.emplace_back(item.get<int>());
}
elem.second->filament_change_sequence = std::move(filament_sequence);
elem.second->nozzle_change_sequence = std::move(nozzle_sequence);
if (!optimal_assignment.empty())
elem.second->optimal_assignment = std::move(optimal_assignment);
}
} catch (const std::exception& e) {
add_error(std::string("Error while parsing filament sequence JSON: ") + e.what());
}
}
void _BBS_3MF_Importer::_handle_start_model_xml_element(const char* name, const char** attributes)
{
if (m_xml_parser == nullptr)
@@ -4294,7 +4414,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (key == BED_TYPE_ATTR)
{
BedType bed_type = BedType::btPC;
ConfigOptionEnum<BedType>::from_string(value, bed_type);
const std::string bed_type_value = value == "SuperTack Plate" ? "Supertack Plate" : value;
ConfigOptionEnum<BedType>::from_string(bed_type_value, bed_type);
m_curr_plater->config.set_key_value("curr_bed_type", new ConfigOptionEnum<BedType>(bed_type));
}
else if (key == PRINT_SEQUENCE_ATTR)
@@ -4334,6 +4455,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
filament_map[idx] = 1;
}
}
m_curr_plater->filament_maps = filament_map;
m_curr_plater->config.set_key_value("filament_map", new ConfigOptionInts(filament_map));
}
}
@@ -4425,6 +4547,22 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (m_curr_plater)
std::istringstream(value) >> std::boolalpha >> m_curr_plater->is_label_object_enabled;
}
else if (key == ENABLE_FILAMENT_DYNAMIC_MAP_ATTR)
{
if (m_curr_plater) {
bool enable_filament_dynamic_map = false;
std::istringstream(value) >> std::boolalpha >> enable_filament_dynamic_map;
m_curr_plater->config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(enable_filament_dynamic_map));
}
}
else if (key == HAS_FILAMENT_SWITCHER_ATTR)
{
if (m_curr_plater) {
bool has_filament_switcher = false;
std::istringstream(value) >> std::boolalpha >> has_filament_switcher;
m_curr_plater->config.set_key_value("has_filament_switcher", new ConfigOptionBool(has_filament_switcher));
}
}
else if (key == PRINTER_MODEL_ID_ATTR)
{
if (m_curr_plater)
@@ -4455,6 +4593,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::string used_m = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_USED_M_TAG);
std::string used_g = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_USED_G_TAG);
std::string filament_id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_TRAY_INFO_ID_TAG);
std::string used_for_object = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_USED_FOR_OBJECT);
std::string used_for_support = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_USED_FOR_SUPPORT);
std::string group_id = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_NOZZLE_GROUP_ID_TAG);
std::string nozzle_diameter = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_NOZZLE_DIAMETER_TAG);
std::string volume_type = bbs_get_attribute_value_string(attributes, num_attributes, FILAMENT_NOZZLE_VOLUME_TYPE_TAG);
FilamentInfo filament_info;
filament_info.id = atoi(id.c_str()) - 1;
filament_info.type = type;
@@ -4462,6 +4605,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
filament_info.used_m = atof(used_m.c_str());
filament_info.used_g = atof(used_g.c_str());
filament_info.filament_id = filament_id;
std::istringstream(used_for_object) >> std::boolalpha >> filament_info.used_for_object;
std::istringstream(used_for_support) >> std::boolalpha >> filament_info.used_for_support;
filament_info.group_id = parse_int_list(group_id);
filament_info.nozzle_diameter = atof(nozzle_diameter.c_str());
filament_info.nozzle_volume_type = volume_type;
m_curr_plater->slice_filaments_info.push_back(filament_info);
}
return true;
@@ -5756,6 +5904,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx = -1, bool save_gcode = true, bool use_loaded_id = false);
bool _add_cut_information_file_to_archive(mz_zip_archive &archive, Model &model);
bool _add_slice_info_config_file_to_archive(mz_zip_archive &archive, const Model &model, PlateDataPtrs &plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config);
bool _add_filament_sequence_file_to_archive(mz_zip_archive& archive, const PlateDataPtrs& plate_data_list);
bool _add_gcode_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, Export3mfProgressFn proFn = nullptr);
bool _add_custom_gcode_per_print_z_file_to_archive(mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config);
bool _add_auxiliary_dir_to_archive(mz_zip_archive &archive, const std::string &aux_dir, PackingTemporaryData &data);
@@ -6294,6 +6443,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
if (!_add_filament_sequence_file_to_archive(archive, plate_data_list)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" << __LINE__ << boost::format(", _add_filament_sequence_file_to_archive failed\n");
return false;
}
//BBS progress point
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", before add auxiliary dir to 3mf\n");
if (proFn) {
@@ -7950,6 +8104,39 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Exporter::_add_filament_sequence_file_to_archive(mz_zip_archive& archive, const PlateDataPtrs& plate_data_list)
{
nlohmann::json sequence_json;
for (size_t idx = 0; idx < plate_data_list.size(); ++idx) {
const PlateData* plate_data = plate_data_list[idx];
if (!plate_data)
continue;
std::vector<unsigned int> filament_sequence = plate_data->filament_change_sequence;
std::transform(filament_sequence.begin(), filament_sequence.end(), filament_sequence.begin(),
[](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;
sequence_json[plate_key]["nozzle_sequence"] = plate_data->nozzle_change_sequence;
sequence_json[plate_key]["optimal_assignment"] = plate_data->optimal_assignment;
}
if (sequence_json.empty())
return true;
const std::string out = sequence_json.dump();
if (!mz_zip_writer_add_mem(&archive, FILAMENT_SEQUENCE_FILE.c_str(), out.c_str(), out.size(), MZ_DEFAULT_COMPRESSION)) {
add_error("Unable to add filament sequence file to archive");
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" << __LINE__
<< boost::format(", store filament sequence to 3mf, length %1%, failed\n") % out.length();
return false;
}
return true;
}
bool _BBS_3MF_Exporter::_add_slice_info_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config)
{
std::stringstream stream;
@@ -7987,6 +8174,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector<int> extruder_types = config.option<ConfigOptionEnumsGeneric>("extruder_type")->values;
std::vector<int> nozzle_volume_types = config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")->values;
auto* nozzle_volume_type_option = dynamic_cast<const ConfigOptionEnumsGeneric*>(config.option("nozzle_volume_type"));
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << EXTRUDER_TYPE_ATTR << "\" " << VALUE_ATTR << "=\"";
add_vector(stream, extruder_types);
@@ -8010,6 +8198,11 @@ 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";
{
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";
}
std::vector<int> filament_maps = plate_data->filament_maps;
if (filament_maps.empty())
@@ -8053,20 +8246,69 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
<< "\" />\n";
}
auto get_nozzle_group_id = [&filament_maps](int filament_id) {
if (filament_id >= 0 && filament_id < static_cast<int>(filament_maps.size()) && filament_maps[filament_id] > 0)
return filament_maps[filament_id] - 1;
return 0;
};
auto get_nozzle_diameter = [nozzle_diameter_option](int nozzle_group_id) {
if (!nozzle_diameter_option || nozzle_diameter_option->values.empty())
return 0.0;
if (nozzle_group_id >= 0 && nozzle_group_id < static_cast<int>(nozzle_diameter_option->values.size()))
return nozzle_diameter_option->values[nozzle_group_id];
return nozzle_diameter_option->values.front();
};
auto get_nozzle_diameter_str = [&get_nozzle_diameter](int nozzle_group_id) {
std::ostringstream diameter_stream;
diameter_stream << std::defaultfloat << get_nozzle_diameter(nozzle_group_id);
return diameter_stream.str();
};
auto get_nozzle_volume_type = [nozzle_volume_type_option](int nozzle_group_id) {
if (!nozzle_volume_type_option || nozzle_volume_type_option->values.empty())
return std::string();
int nozzle_volume_type = nozzle_volume_type_option->values.front();
if (nozzle_group_id >= 0 && nozzle_group_id < static_cast<int>(nozzle_volume_type_option->values.size()))
nozzle_volume_type = nozzle_volume_type_option->values[nozzle_group_id];
if (nozzle_volume_type < 0 || nozzle_volume_type > nvtMaxNozzleVolumeType)
nozzle_volume_type = nvtStandard;
return get_nozzle_volume_type_string(static_cast<NozzleVolumeType>(nozzle_volume_type));
};
std::vector<int> used_nozzle_groups;
for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++)
{
int nozzle_group_id = get_nozzle_group_id(it->id);
if (std::find(used_nozzle_groups.begin(), used_nozzle_groups.end(), nozzle_group_id) == used_nozzle_groups.end())
used_nozzle_groups.push_back(nozzle_group_id);
const std::string filament_nozzle_group_id = it->group_id.empty() ? std::to_string(nozzle_group_id) : join_int_list_comma(it->group_id);
const double filament_nozzle_diameter = it->nozzle_diameter > 0.0 ? it->nozzle_diameter : get_nozzle_diameter(nozzle_group_id);
const std::string filament_nozzle_volume_type = it->nozzle_volume_type.empty() ? get_nozzle_volume_type(nozzle_group_id) : it->nozzle_volume_type;
stream << " <" << FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id + 1) << "\" "
<< FILAMENT_TRAY_INFO_ID_TAG <<"=\""<< it->filament_id <<"\" "
<< FILAMENT_TYPE_TAG << "=\"" << it->type << "\" "
<< FILAMENT_COLOR_TAG << "=\"" << it->color << "\" "
<< FILAMENT_USED_M_TAG << "=\"" << it->used_m << "\" "
<< FILAMENT_USED_G_TAG << "=\"" << it->used_g << "\" />\n";
<< FILAMENT_USED_G_TAG << "=\"" << it->used_g << "\" "
<< FILAMENT_NOZZLE_GROUP_ID_TAG << "=\"" << filament_nozzle_group_id << "\" "
<< FILAMENT_NOZZLE_DIAMETER_TAG << "=\"" << filament_nozzle_diameter << "\" "
<< FILAMENT_NOZZLE_VOLUME_TYPE_TAG << "=\"" << filament_nozzle_volume_type << "\" "
<< FILAMENT_USED_FOR_OBJECT << "=\"" << std::boolalpha << it->used_for_object << "\" "
<< FILAMENT_USED_FOR_SUPPORT << "=\"" << std::boolalpha << it->used_for_support << "\"/>\n";
}
for (auto it = plate_data->warnings.begin(); it != plate_data->warnings.end(); it++) {
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";
}
if (!plate_data->layer_filaments.empty()) {
stream << " <" << LAYER_FILAMENT_LISTS_TAG << ">\n";
for (auto iter = plate_data->layer_filaments.begin(); iter != plate_data->layer_filaments.end(); ++iter) {