Files
OrcaSlicer/src/slic3r/GUI/DeviceCore/DevConfigUtil.h
glowstab 9956ad5b48 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
2026-05-10 02:21:13 +08:00

221 lines
8.2 KiB
C++

/**
* @file DevConfigUtil.h
* @brief Parses configuration files and provides access to printer options.
*
* This class loads a configuration file and allows querying options by key.
* The configuration file format is expected to be key-value pairs (e.g., INI or simple text).
*/
#pragma once
#include <string>
#include <map>
#include <vector>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp>
#include <wx/string.h>
#include "slic3r/GUI/DeviceCore/DevDefs.h"
namespace Slic3r
{
class dePrinterConfigFactory
{
public:
dePrinterConfigFactory() = default;
~dePrinterConfigFactory() = default;
};
enum class ToolHeadComponent {
Extruder,
Nozzle,
Hotend
};
enum class ToolHeadNameCase {
TitleCase = 0,
SentenceCase = 1,
LowerCase = 2
};
class DevPrinterConfigUtil
{
public:
DevPrinterConfigUtil() = default;
~DevPrinterConfigUtil() = default;
public:
static void InitFilePath(const std::string& res_file_path) { m_resource_file_path = res_file_path; };
/*printer*/
// info
static std::map<std::string, std::string> get_all_model_id_with_name();
static std::string get_printer_type(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_type"); }
static std::string get_printer_display_name(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "display_name"); }
static std::string get_printer_series_str(std::string type_str) { return get_value_from_config<std::string>(type_str, "printer_series"); }
static PrinterArch get_printer_arch(std::string type_str);
// images
static std::string get_printer_thumbnail_img(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_thumbnail_image"); }
static std::string get_printer_connect_help_img(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_connect_help_image"); }
static std::string get_printer_auto_pa_cali_image(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "auto_pa_cali_thumbnail_image"); }
/*media*/
static std::string get_ftp_folder(std::string type_str) { return get_value_from_config<std::string>(type_str, "ftp_folder"); }
static std::vector<std::string> get_resolution_supported(std::string type_str) { return get_value_from_config<std::vector<std::string>>(type_str, "camera_resolution"); }
static std::vector<std::string> get_compatible_machine(std::string type_str) { return get_value_from_config<std::vector<std::string>>(type_str, "compatible_machine"); }
static std::map<std::string, std::vector<std::string>> get_all_subseries(std::string type_str = "");
/*ams*/
static std::string get_printer_use_ams_type(std::string type_str) { return get_value_from_config<std::string>(type_str, "use_ams_type"); }
static std::string get_printer_ams_img(const std::string& type_str) { return get_value_from_config<std::string>(type_str, "printer_use_ams_image"); }
static std::string get_printer_ext_img(const std::string& type_str, int pos);//printer_ext_image
/*fan*/
static std::string get_fan_text(const std::string& type_str, const std::string& key);
static std::string get_fan_text(const std::string& type_str, int airduct_mode, int airduct_func, int submode);
/*extruder*/
static bool get_printer_can_set_nozzle(std::string type_str) { return get_value_from_config<bool>(type_str, "enable_set_nozzle_info"); }// can set nozzle from studio
static std::string get_toolhead_display_name(
const std::string& type_str,
int ext_id,
ToolHeadComponent component,
ToolHeadNameCase name_case = ToolHeadNameCase::TitleCase,
bool short_name = false);
/*print job*/
static bool support_ams_ext_mix_print(std::string type_str) { return get_value_from_config<bool>(type_str, "print", "support_ams_ext_mix_print"); }
/*calibration*/
static std::vector<std::string> get_unsupport_auto_cali_filaments(std::string type_str) { return get_value_from_config<std::vector<std::string>>(type_str, "auto_cali_not_support_filaments"); }
/*detection*/
static bool support_wrapping_detection(const std::string& type_str) { return get_value_from_config<bool>(type_str, "support_wrapping_detection"); }
/*safety options*/
static bool support_safety_options(const std::string &type_str) { return get_value_from_config<bool>(type_str, "support_safety_options"); }
/*print check*/
static bool support_print_check_extension_fan_f000_mounted(const std::string& type_str) { return get_value_from_config<bool>(type_str, "print", "support_print_check_extension_fan_f000_mounted"); }
public:
template<typename T>
static T get_value_from_config(const std::string& type_str, const std::string& item)
{
std::string config_file = m_resource_file_path + "/printers/" + type_str + ".json";
boost::nowide::ifstream json_file(config_file.c_str());
try
{
nlohmann::json jj;
if (json_file.is_open())
{
json_file >> jj;
if (jj.contains("00.00.00.00"))
{
nlohmann::json const& printer = jj["00.00.00.00"];
if (printer.contains(item))
{
return printer[item].get<T>();
}
}
}
}
catch (...) { assert(0 && "get_value_from_config failed"); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed"; }// there are file errors
return T();
};
template<typename T>
static T get_value_from_config(const std::string& type_str, const std::string& item1, const std::string& item2)
{
try
{
const auto& json_item1 = get_value_from_config<nlohmann::json>(type_str, item1);
if (json_item1.contains(item2))
{
return json_item1[item2].get<T>();
}
}
catch (...)
{
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to get " << item1 << ", " << item2;
}
return T();
}
static nlohmann::json get_json_from_config(const std::string& type_str, const std::string& key1, const std::string& key2 = std::string())
{
std::string config_file = m_resource_file_path + "/printers/" + type_str + ".json";
boost::nowide::ifstream json_file(config_file.c_str());
try
{
nlohmann::json jj;
if (json_file.is_open())
{
json_file >> jj;
if (jj.contains("00.00.00.00"))
{
nlohmann::json const& printer = jj["00.00.00.00"];
if (printer.contains(key1))
{
nlohmann::json const& key1_item = printer[key1];
if (key2.empty())
{
return key1_item;
}
if (key1_item.contains(key2))
{
return key1_item[key2];
}
}
}
}
}
catch (...) { assert(0 && "get_json_from_config failed"); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed"; }// there are file errors
return nlohmann::json();
}
private:
static std::string m_resource_file_path; // Path to the configuration file
};
/*special transform*/
static std::string _parse_printer_type(const std::string& type_str)
{
if (type_str.compare("3DPrinter-X1") == 0)
{
return "BL-P002";
}
else if (type_str.compare("3DPrinter-X1-Carbon") == 0)
{
return "BL-P001";
}
else if (type_str.compare("BL-P001") == 0)
{
return type_str;
}
else if (type_str.compare("BL-P002") == 0)
{
return type_str;
}
else
{
std::string result = DevPrinterConfigUtil::get_printer_type(type_str);
if (!result.empty())
{
return result;
}
}
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Unsupported printer type: " << type_str;
return type_str;
}
};// namespace Slic3r