mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-16 15:32:09 +00:00
save exact (not standard-rounded) nozzle diameter in 3mf metadata
This commit is contained in:
@@ -701,20 +701,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
info.used_g = used_filament_g;
|
||||
info.used_m = used_filament_m;
|
||||
|
||||
// Stamp each filament's logical-nozzle assignment onto the saved 3mf so the
|
||||
// device/monitor can reconstruct it. NOTE: this block runs for the whole fleet, not just
|
||||
// multi-nozzle prints: reorder_extruders_for_minimum_flush_volume runs unconditionally and
|
||||
// set_nozzle_group_result stores a (non-null) 1-nozzle result even for a single-extruder print,
|
||||
// so result->nozzle_group_result is non-null here for X1/P1/A1/H2S too. Saved-3mf byte-identity
|
||||
// vs the older filament_maps path therefore holds by VALUE-COINCIDENCE, not by skipping: for a
|
||||
// standard 0.4/0.2/0.6/0.8 nozzle the stamped group_id/nozzle_diameter/volume_type render the same
|
||||
// bytes as the filament_maps-derived fallback below (empirically confirmed on an X1C 0.4 saved 3mf:
|
||||
// slice_info.config emits group_id="0" nozzle_diameter="0.40" volume_type="Standard" and
|
||||
// <nozzle id="0" extruder_id="1" nozzle_diameter="0.4" volume_type="Standard"> == the else-path).
|
||||
// The one divergence is a non-standard nozzle (e.g. 0.3/0.5): format_diameter_to_str snaps the
|
||||
// stamped diameter to the nearest of {0.2,0.4,0.6,0.8} (0.5 -> "0.4"), whereas the older path wrote
|
||||
// the raw config value — a metadata-only carry. No g-code effect; the raw nozzle_diameters plate
|
||||
// metadata is unchanged.
|
||||
// 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()) {
|
||||
@@ -8331,6 +8323,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";
|
||||
@@ -8463,7 +8467,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) << "\" "
|
||||
@@ -8483,17 +8490,16 @@ 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";
|
||||
}
|
||||
|
||||
// Emit the <nozzle> tags from the grouping result when present. The result is present for
|
||||
// the whole fleet (see the value-coincidence note in parse_filament_info), so this if-branch
|
||||
// is normally taken; for single-nozzle-per-extruder prints get_used_nozzles_in_extruder()
|
||||
// yields one nozzle per used extruder whose serialize() output is byte-identical to the
|
||||
// filament_maps-derived else-path below for a standard nozzle diameter (a non-standard
|
||||
// diameter snaps via format_diameter_to_str — the metadata-only carry). Only genuinely
|
||||
// multi-nozzle prints emit per-nozzle tags the extruder map cannot express. The else
|
||||
// is a defensive fallback for a missing result.
|
||||
// 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 {
|
||||
|
||||
@@ -236,6 +236,88 @@ SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") {
|
||||
}
|
||||
}
|
||||
|
||||
// Saved nozzle diameter for a single-nozzle-per-extruder printer with a non-standard nozzle.
|
||||
// The grouping result rounds every nozzle diameter to the nearest of {0.2,0.4,0.6,0.8} for its
|
||||
// internal matching key. That rounded value must NOT reach the saved <filament>/<nozzle> metadata on
|
||||
// a printer whose extruders each carry one nozzle: the exact per-extruder config diameter is written
|
||||
// instead, so a 0.5 mm nozzle is preserved rather than saved as 0.4. (Only an extruder that carries a
|
||||
// nozzle cluster, which the per-extruder config cannot express, keeps the grouping result's diameter.)
|
||||
SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle printer", "[3mf][MultiNozzle]") {
|
||||
GIVEN("a single-extruder plate whose nozzle is 0.5 mm and whose stamped diameter was rounded to 0.4") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
std::string backup_dir =
|
||||
(boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("orca_nd_%%%%%%%%")).string();
|
||||
boost::filesystem::create_directories(backup_dir);
|
||||
model.set_backup_path(backup_dir);
|
||||
|
||||
// Single extruder with a non-standard 0.5 mm nozzle; extruder_max_nozzle_count stays at its
|
||||
// default (no nozzle cluster), so the writer must emit the exact config diameter.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({ 0.5 }));
|
||||
|
||||
PlateData* plate = new PlateData();
|
||||
plate->plate_index = 0;
|
||||
plate->is_sliced_valid = true; // gate for the slice_info.config writer
|
||||
plate->filament_maps = { 1 };
|
||||
|
||||
// Seed the stamped diameter with the grouping result's rounded value (0.5 -> 0.4) so the
|
||||
// assertion proves the writer ignores it and emits the exact config diameter instead.
|
||||
FilamentInfo fi;
|
||||
fi.id = 0;
|
||||
fi.type = "PLA";
|
||||
fi.color = "#FFFFFFFF";
|
||||
fi.group_id = { 0 };
|
||||
fi.nozzle_diameter = 0.4; // rounded; must NOT be the value written
|
||||
plate->slice_filaments_info.push_back(fi);
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
std::string test_file = std::string(TEST_DATA_DIR) + "/test_3mf/nd_roundtrip.3mf";
|
||||
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.plate_data_list.push_back(plate);
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
boost::filesystem::remove(test_file);
|
||||
|
||||
THEN("the saved nozzle diameter is the exact 0.5, not the rounded 0.4") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_plates.size() >= 1);
|
||||
PlateData* rt = dst_plates.front();
|
||||
|
||||
// <nozzle> tag: device-facing per-nozzle diameter string, written verbatim.
|
||||
REQUIRE(rt->nozzles_info.size() >= 1);
|
||||
REQUIRE(rt->nozzles_info.front().diameter == "0.5");
|
||||
|
||||
// <filament> tag: per-filament nozzle_diameter parsed back as 0.5, not 0.4.
|
||||
REQUIRE(rt->slice_filaments_info.size() >= 1);
|
||||
REQUIRE_THAT(rt->slice_filaments_info.front().nozzle_diameter, Catch::Matchers::WithinAbs(0.5, 1e-6));
|
||||
}
|
||||
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
delete plate; // store_bbs_3mf does not take ownership of the source plate
|
||||
boost::filesystem::remove_all(backup_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// A legacy / foreign project (no multi-nozzle metadata) must load crash-safe through the BBS
|
||||
// importer and must not fabricate a filament_volume_map.
|
||||
SCENARIO("Legacy project loads crash-safe via load_bbs_3mf", "[3mf][MultiNozzle]") {
|
||||
|
||||
Reference in New Issue
Block a user