Merge upstream/main into cad-mainline (530 commits)

Catches the fork up from 449a4cf9fc (2026-06-28) to d6cb667b89 (2026-07-24).
Upstream touched 2326 files; 17 of them overlap the 164 this branch touches.

16 of the 17 auto-merged, including all three CMakeLists.txt, the build_all.yml
CI workflow, and every GUI file. The CAD core never conflicts: CadDocument,
SketchEngine, SketchSolver, McpControl and test_caddocument are files this fork
adds, so upstream does not touch them.

The one conflict, tests/libslic3r/test_3mf.cpp, was purely additive in all three
hunks and is resolved as a union: our test pinning that store_bbs_3mf embeds the
CAD recipe as Metadata/SnapOrca_cad.bin, upstream's multi-nozzle plate-metadata
round-trip tests, and both sets of includes. All three were verified present
after resolution rather than assumed.

NOT BUILD-VERIFIED, for a reason that predates this merge and is not caused by
it: this fork cannot be configured on nativedev at all. Its CMakeLists has
required Eigen3 5.0.1 since before the merge (line 592 pre-merge), while the
only deps image on the machine is snaporca-deps, built for snaporca's
find_package(Eigen3 3.3). CMake fails at configure, so nothing compiles.

That means this fork's Catch2 suite has never run. Every "suite green" figure
recorded for M1-M8 was snaporca's suite; the ports were verified by patch-apply
plus the CAD sources being byte-identical to snaporca's. Building an orca_cad
deps image with Eigen 5.0.1 is what would finally close that gap.

Pre-merge state is preserved at branch cad-mainline-pre-upstream-2026-07-25
(30d54f0074).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-07-25 12:28:33 +02:00
co-authored by Claude Opus 5
2326 changed files with 2562372 additions and 1267981 deletions
+11 -11
View File
@@ -230,8 +230,8 @@ static void getNamedSolids(const TopLoc_Location& location,
}
//bool load_step(const char *path, Model *model, bool& is_cancel,
// double linear_defletion/*=0.003*/,
// double angle_defletion/*= 0.5*/,
// double linear_deflection/*=0.003*/,
// double angle_deflection/*= 0.5*/,
// bool isSplitCompound,
// ImportStepProgressFn stepFn, StepIsUtf8Fn isUtf8Fn, long& mesh_face_num)
//{
@@ -288,7 +288,7 @@ static void getNamedSolids(const TopLoc_Location& location,
// stl.resize(namedSolids.size());
// tbb::parallel_for(tbb::blocked_range<size_t>(0, namedSolids.size()), [&](const tbb::blocked_range<size_t> &range) {
// for (size_t i = range.begin(); i < range.end(); i++) {
// BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_defletion, false, angle_defletion, true);
// BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_deflection, false, angle_deflection, true);
// // BBS: calculate total number of the nodes and triangles
// int aNbNodes = 0;
// int aNbTriangles = 0;
@@ -511,8 +511,8 @@ Step::Step_Status Step::load()
Step::Step_Status Step::mesh(Model* model,
bool& is_cancel,
bool isSplitCompound,
double linear_defletion/*=0.003*/,
double angle_defletion/*= 0.5*/)
double linear_deflection/*=0.003*/,
double angle_deflection/*= 0.5*/)
{
bool task_result = false;
@@ -544,7 +544,7 @@ Step::Step_Status Step::mesh(Model* model,
stl.resize(namedSolids.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, namedSolids.size()), [&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); i++) {
BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_defletion, false, angle_defletion, true);
BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_deflection, false, angle_deflection, true);
// BBS: calculate total number of the nodes and triangles
int aNbNodes = 0;
int aNbTriangles = 0;
@@ -689,15 +689,15 @@ void Step::clean_mesh_data()
}
}
unsigned int Step::get_triangle_num(double linear_defletion, double angle_defletion)
unsigned int Step::get_triangle_num(double linear_deflection, double angle_deflection)
{
unsigned int tri_num = 0;
try {
Handle(StepProgressIncdicator) progress = new StepProgressIncdicator(m_stop_mesh);
clean_mesh_data();
IMeshTools_Parameters param;
param.Deflection = linear_defletion;
param.Angle = angle_defletion;
param.Deflection = linear_deflection;
param.Angle = angle_deflection;
param.InParallel = true;
for (int i = 0; i < m_name_solids.size(); ++i) {
BRepMesh_IncrementalMesh mesh(m_name_solids[i].solid, param, progress->Start());
@@ -719,7 +719,7 @@ unsigned int Step::get_triangle_num(double linear_defletion, double angle_deflet
return tri_num;
}
unsigned int Step::get_triangle_num_tbb(double linear_defletion, double angle_defletion)
unsigned int Step::get_triangle_num_tbb(double linear_deflection, double angle_deflection)
{
unsigned int tri_num = 0;
clean_mesh_data();
@@ -727,7 +727,7 @@ unsigned int Step::get_triangle_num_tbb(double linear_defletion, double angle_de
[&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); i++) {
unsigned int solids_tri_num = 0;
BRepMesh_IncrementalMesh mesh(m_name_solids[i].solid, linear_defletion, false, angle_defletion, true);
BRepMesh_IncrementalMesh mesh(m_name_solids[i].solid, linear_deflection, false, angle_deflection, true);
for (TopExp_Explorer anExpSF(m_name_solids[i].solid, TopAbs_FACE); anExpSF.More(); anExpSF.Next()) {
TopLoc_Location aLoc;
Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation(TopoDS::Face(anExpSF.Current()), aLoc);
+6 -6
View File
@@ -38,8 +38,8 @@ struct NamedSolid
//BBS: Load an step file into a provided model.
extern bool load_step(const char *path, Model *model,
bool& is_cancel,
double linear_defletion = 0.003,
double angle_defletion = 0.5,
double linear_deflection = 0.003,
double angle_deflection = 0.5,
bool isSplitCompound = false,
ImportStepProgressFn proFn = nullptr,
StepIsUtf8Fn isUtf8Fn = nullptr,
@@ -98,14 +98,14 @@ public:
Step(std::string path, ImportStepProgressFn stepFn = nullptr, StepIsUtf8Fn isUtf8Fn = nullptr);
~Step();
Step_Status load();
unsigned int get_triangle_num(double linear_defletion, double angle_defletion);
unsigned int get_triangle_num_tbb(double linear_defletion, double angle_defletion);
unsigned int get_triangle_num(double linear_deflection, double angle_deflection);
unsigned int get_triangle_num_tbb(double linear_deflection, double angle_deflection);
void clean_mesh_data();
Step_Status mesh(Model* model,
bool& is_cancel,
bool isSplitCompound,
double linear_defletion = 0.003,
double angle_defletion = 0.5);
double linear_deflection = 0.003,
double angle_deflection = 0.5);
std::atomic<bool> m_stop_mesh{false};
void update_process(int load_stage, int current, int total, bool& cancel);
+168 -11
View File
@@ -348,6 +348,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";
@@ -700,6 +701,35 @@ 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. This block runs for every print: reorder_extruders_for_minimum_flush_volume
// runs unconditionally and stores a (non-null) 1-nozzle result even for a single-extruder print,
// so result->nozzle_group_result is non-null here for single-nozzle printers too. The stamped
// nozzle_diameter is the grouping result's rounded matching-key value; the 3mf writer decides the
// final saved diameter (see has_multi_nozzle_extruder). group_id and volume_type are unaffected.
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<double> diameters;
std::set<NozzleVolumeType> 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;
@@ -707,6 +737,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 <nozzle> 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<MultiNozzleUtils::LayeredNozzleGroupResult>(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;
@@ -1284,6 +1321,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();
@@ -1619,8 +1659,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;
@@ -2298,9 +2340,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;
@@ -3478,6 +3522,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)
@@ -3512,6 +3558,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)
@@ -4469,6 +4517,21 @@ 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) {
// The map feeds per-filament slot resolution and grouping. Clamp any
// higher volume-type back to Standard(0) on load: Hybrid(2) is only an
// in-memory grouping seed that is never persisted, and TPU High Flow(3)
// is clamped with the same information loss on every load.
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;
@@ -4578,6 +4641,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)
@@ -4631,6 +4699,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 <nozzle> tags. Older 3mf without <nozzle> 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<NozzleVolumeType>::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) {
@@ -5959,7 +6062,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_thumbnail_middle = iter->second;
}
boost::system::error_code ec;
std::string filename = std::string(store_params.path);
std::string filename = store_params.path;
boost::filesystem::remove(filename + ".tmp", ec);
bool result = _save_model_to_file(filename + ".tmp", *store_params.model, store_params.plate_data_list, store_params.project_presets, store_params.config,
@@ -8008,6 +8111,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << "\"/>\n";
}
ConfigOptionInts* filament_volume_maps_opt = plate_data->config.option<ConfigOptionInts>("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<int>& 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()) {
@@ -8147,7 +8262,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;
}
@@ -8233,6 +8353,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (nozzle_diameter_option)
nozzle_diameters_str = nozzle_diameter_option->serialize();
// True when any extruder carries a cluster of interchangeable nozzles (max nozzle count
// > 1). Such an extruder's per-nozzle diameters are not expressible in the per-extruder
// nozzle_diameter config, so the saved <filament>/<nozzle> diameters must come from the
// grouping result. For a single-nozzle-per-extruder printer the true diameter is the raw
// config value; the grouping result rounds it to the nearest of {0.2,0.4,0.6,0.8} for its
// internal matching key, so reading that back would rewrite a non-standard nozzle
// (e.g. 0.5 -> 0.4). Use this flag to keep the exact config value in that case.
auto* extruder_max_nozzle_count_option = dynamic_cast<const ConfigOptionInts*>(config.option("extruder_max_nozzle_count"));
const bool has_multi_nozzle_extruder = extruder_max_nozzle_count_option &&
std::any_of(extruder_max_nozzle_count_option->values.begin(), extruder_max_nozzle_count_option->values.end(),
[](int v) { return v > 1; });
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << PRINTER_MODEL_ID_ATTR << "\" " << VALUE_ATTR << "=\"" << plate_data->printer_model_id << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << NOZZLE_DIAMETERS_ATTR << "\" " << VALUE_ATTR << "=\"" << nozzle_diameters_str << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TIMELAPSE_TYPE_ATTR << "\" " << VALUE_ATTR << "=\"" << timelapse_type << "\"/>\n";
@@ -8242,7 +8374,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";
@@ -8357,7 +8497,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
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);
// Single-nozzle extruders: exact config diameter; clusters keep the result's rounded
// value (see has_multi_nozzle_extruder).
const double filament_nozzle_diameter = (has_multi_nozzle_extruder && 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) << "\" "
@@ -8377,12 +8520,26 @@ 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 <nozzle> tags from the grouping result. Single-nozzle-per-extruder printers
// override the diameter with the exact config value (see has_multi_nozzle_extruder); 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) {
if (!has_multi_nozzle_extruder && nozzle_diameter_option &&
used_nozzle.extruder_id >= 0 && used_nozzle.extruder_id < (int) nozzle_diameter_option->values.size()) {
used_nozzle.diameter = get_nozzle_diameter_str(used_nozzle.extruder_id);
}
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()) {
@@ -9016,7 +9173,7 @@ bool store_bbs_3mf(StoreParams& store_params)
// All export should use "C" locales for number formatting.
CNumericLocalesSetter locales_setter;
if (store_params.path == nullptr || store_params.model == nullptr)
if (store_params.path.empty() || store_params.model == nullptr)
return false;
_BBS_3MF_Exporter exporter;
+9 -1
View File
@@ -73,6 +73,7 @@ struct PlateData
std::map<int, std::pair<int, int>> 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<unsigned int> nozzle_change_sequence;
std::vector<int> optimal_assignment;
// Multi-nozzle grouping surface. nozzles_info accumulates the <nozzle> 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<MultiNozzleUtils::NozzleInfo> nozzles_info;
std::optional<MultiNozzleUtils::LayeredNozzleGroupResult> nozzle_group_result;
// Hexadecimal number,
// the 0th digit corresponds to extruder 1
// the 1th digit corresponds to extruder 2
@@ -226,7 +234,7 @@ typedef std::map<int, PlateData*> PlateDataMaps;
struct StoreParams
{
const char* path;
std::string path;
Model* model = nullptr;
PlateDataPtrs plate_data_list;
int export_plate_idx = -1;