#include "libslic3r/Model.hpp" #include "libslic3r/Format/3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Format/STL.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Semver.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/ProjectTask.hpp" #include "libslic3r/PublishSettings.hpp" #include "test_utils.hpp" #include #include #include #include #include #include // for std::enable_if_t #include // for typeid namespace Catch { template struct is_eigen_matrix : std::is_base_of, T> {}; template struct StringMaker::value>> { static std::string convert(const T& eigen_obj) { // Newline at end of rows Eigen::IOFormat fmt(4, 0, ", ", "\n", "[", "]"); std::stringstream ss; ss << "Matrix<" << typeid(eigen_obj).name() << "> = \n"; ss << eigen_obj.format(fmt); return ss.str(); } }; // We must manually specialize for Eigen::Transform as it doesn't derive from MatrixBase. // It's defined as: Eigen::Transform template struct StringMaker> { static std::string convert(const Eigen::Transform& trafo) { // We print the underlying matrix const auto& matrix = trafo.matrix(); // Newline at end of rows Eigen::IOFormat fmt(4, 0, ", ", "\n", "[", "]"); std::stringstream ss; ss << "Transform = \n"; ss << matrix.format(fmt); return ss.str(); } }; // Quaternions also need an explicit specialization template struct StringMaker> { static std::string convert(const Eigen::Quaternion& quat) { std::stringstream ss; ss << "Quaternion(w=" << quat.w() << ", x=" << quat.x() << ", y=" << quat.y() << ", z=" << quat.z() << ")"; return ss.str(); } }; } // end namespace Catch #include using namespace Slic3r; SCENARIO("Reading 3mf file", "[3mf]") { GIVEN("umlauts in the path of the file") { Model model; WHEN("3mf model is read") { std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; DynamicPrintConfig config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; bool ret = load_3mf(path.c_str(), config, ctxt, &model, false); THEN("load should succeed") { REQUIRE(ret); } } } } SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") { GIVEN("world vertices coordinates before save") { // load a model from stl file Model src_model; std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; load_stl(src_file.c_str(), &src_model); src_model.add_default_instances(); ModelObject* src_object = src_model.objects.front(); // apply generic transformation to the 1st volume Geometry::Transformation src_volume_transform; src_volume_transform.set_offset({ 10.0, 20.0, 0.0 }); src_volume_transform.set_rotation({ Geometry::deg2rad(25.0), Geometry::deg2rad(35.0), Geometry::deg2rad(45.0) }); src_volume_transform.set_scaling_factor({ 1.1, 1.2, 1.3 }); src_volume_transform.set_mirror({ -1.0, 1.0, -1.0 }); src_object->volumes.front()->set_transformation(src_volume_transform); // apply generic transformation to the 1st instance Geometry::Transformation src_instance_transform; src_instance_transform.set_offset({ 5.0, 10.0, 0.0 }); src_instance_transform.set_rotation({ Geometry::deg2rad(12.0), Geometry::deg2rad(13.0), Geometry::deg2rad(14.0) }); src_instance_transform.set_scaling_factor({ 0.9, 0.8, 0.7 }); src_instance_transform.set_mirror({ 1.0, -1.0, -1.0 }); src_object->instances.front()->set_transformation(src_instance_transform); WHEN("model is saved+loaded to/from 3mf file") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); store_3mf(test_file.c_str(), &src_model, nullptr, false); // load back the model from the 3mf file Model dst_model; DynamicPrintConfig dst_config; { ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Disable }; load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false); } // compare meshes TriangleMesh src_mesh = src_model.mesh(); TriangleMesh dst_mesh = dst_model.mesh(); bool res = src_mesh.its.vertices.size() == dst_mesh.its.vertices.size(); if (res) { for (size_t i = 0; i < dst_mesh.its.vertices.size(); ++i) { res &= dst_mesh.its.vertices[i].isApprox(src_mesh.its.vertices[i]); } } THEN("world vertices coordinates after load match") { REQUIRE(res); } } } } // .3mf multi-nozzle round-trip. // Locks the load/save handling for the H2C multi-nozzle plate metadata: // * filament_volume_maps -> plate config "filament_volume_map" (with the >1 -> 0 clamp) // * nozzle_volume_type -> PlateData::nozzle_volume_types (previously write-only) // and pins the deliberately-lossy keys (enable_filament_dynamic_map) so a future change has to // consciously unpin them. Uses a store_bbs_3mf -> load_bbs_3mf cycle (no external fixture needed). SCENARIO("H2C multi-nozzle .3mf round-trip", "[3mf][MultiNozzle]") { GIVEN("a plate carrying multi-nozzle filament assignment metadata") { 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(); // store_bbs_3mf stages Metadata/project_settings.config through the model's backup path; // point it at a writable temp dir (the default lives under a read-only root in CI). ScopedTemporaryDir backup_dir("orca_mn"); model.set_backup_path(backup_dir.string()); // Global (printer) config: give nozzle_volume_type a non-default value so the slice_info // read-back is a meaningful assertion (High Flow == 1). DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_key_value("nozzle_volume_type", new ConfigOptionEnumsGeneric({ (int) NozzleVolumeType::nvtHighFlow })); PlateData* plate = new PlateData(); plate->plate_index = 0; plate->is_sliced_valid = true; // gate for the slice_info.config writer (nozzle_volume_type) plate->filament_maps = { 1, 2, 1 }; // slice_info uses this; keep it == model_settings' value plate->config.set_key_value("filament_map_mode", new ConfigOptionEnum(fmmManual)); plate->config.set_key_value("filament_map", new ConfigOptionInts({ 1, 2, 1 })); // Deliberately include out-of-range volume-type ids (2 == Hybrid, 3 == TPU High Flow): // the loader must clamp them back to Standard (0). plate->config.set_key_value("filament_volume_map", new ConfigOptionInts({ 0, 2, 1, 3 })); // Known-lossy: a true value must NOT survive the round-trip (slice_info hardcodes false, // model_settings never writes it). plate->config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true)); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); 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 project_presets; bool is_bbl_3mf = false, is_orca_3mf = false; Semver file_version; // LoadConfig is required for slice_info.config (nozzle_volume_type) to be parsed — // matches how the app loads projects. 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); THEN("every multi-nozzle key round-trips as expected") { REQUIRE(loaded); REQUIRE(dst_plates.size() >= 1); PlateData* rt = dst_plates.front(); // filament_map (model_settings + slice_info; already round-tripped) auto* fmap = rt->config.option("filament_map"); REQUIRE(fmap != nullptr); REQUIRE(fmap->values == std::vector({ 1, 2, 1 })); // filament_volume_map (model_settings) with the >1 -> 0 clamp auto* fvmap = rt->config.option("filament_volume_map"); REQUIRE(fvmap != nullptr); REQUIRE(fvmap->values == std::vector({ 0, 0, 1, 0 })); // nozzle_volume_type read-back into PlateData::nozzle_volume_types REQUIRE(rt->nozzle_volume_types == "1"); // enable_filament_dynamic_map pinned lossy: model_settings never serializes it and // slice_info hardcodes false, so the `true` we set is dropped. Pinned here // (absent or false, never true) so a future change that persists it must update this. auto* dyn = rt->config.option("enable_filament_dynamic_map"); const bool persisted_true = (dyn != nullptr && dyn->value); REQUIRE_FALSE(persisted_true); } release_PlateData_list(dst_plates); } delete plate; // store_bbs_3mf does not take ownership of the source plate } } // 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 / 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(); ScopedTemporaryDir backup_dir("orca_nd"); model.set_backup_path(backup_dir.string()); // 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") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); 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 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); 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(); // tag: device-facing per-nozzle diameter string, written verbatim. REQUIRE(rt->nozzles_info.size() >= 1); REQUIRE(rt->nozzles_info.front().diameter == "0.5"); // 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 } } // 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]") { GIVEN("a project without any multi-nozzle metadata") { std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; Model model; DynamicPrintConfig config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; PlateDataPtrs plates; std::vector project_presets; bool is_bbl_3mf = false, is_orca_3mf = false; Semver file_version; WHEN("loaded through the BBS importer") { bool loaded = false; REQUIRE_NOTHROW(loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates, &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); THEN("it does not crash and invents no per-filament volume map") { for (PlateData* p : plates) { REQUIRE(p->config.option("filament_volume_map") == nullptr); } } release_PlateData_list(plates); } } } // Device-side nozzle-grouping serialization surface. // Direct unit coverage for the pure serialize/deserialize + StaticNozzleGroupResult helpers that the // gcode.3mf writer/reader lean on. SCENARIO("MultiNozzle serialization helpers", "[3mf][MultiNozzle]") { using namespace Slic3r::MultiNozzleUtils; GIVEN("NozzleInfo / NozzleGroupInfo") { NozzleInfo n0; n0.group_id = 0; n0.extruder_id = 0; n0.diameter = "0.4"; n0.volume_type = nvtStandard; NozzleInfo n1; n1.group_id = 1; n1.extruder_id = 1; n1.diameter = "0.4"; n1.volume_type = nvtHighFlow; THEN("NozzleInfo::serialize matches the tag attributes (extruder_id 1-based)") { REQUIRE(n0.serialize() == "id=\"0\" extruder_id=\"1\" nozzle_diameter=\"0.4\" volume_type=\"Standard\""); REQUIRE(n1.serialize() == "id=\"1\" extruder_id=\"2\" nozzle_diameter=\"0.4\" volume_type=\"High Flow\""); } THEN("NozzleGroupInfo serialize/deserialize round-trips and rejects malformed input") { NozzleGroupInfo g("0.4", nvtHighFlow, 1, 3); REQUIRE(g.serialize() == "1-0.4-High Flow-3"); auto rt = NozzleGroupInfo::deserialize(g.serialize()); REQUIRE(rt.has_value()); REQUIRE(*rt == g); REQUIRE_FALSE(NozzleGroupInfo::deserialize("1-0.4-Standard").has_value()); // too few tokens REQUIRE_FALSE(NozzleGroupInfo::deserialize("x-0.4-Standard-3").has_value()); // non-numeric extruder } } GIVEN("a StaticNozzleGroupResult built from filament + nozzle infos") { std::vector nozzles; { NozzleInfo n; n.group_id = 0; n.extruder_id = 0; n.diameter = "0.4"; n.volume_type = nvtStandard; nozzles.push_back(n); } { NozzleInfo n; n.group_id = 1; n.extruder_id = 1; n.diameter = "0.4"; n.volume_type = nvtHighFlow; nozzles.push_back(n); } std::vector filaments(3); filaments[0].id = 0; filaments[0].group_id = { 0 }; filaments[1].id = 1; filaments[1].group_id = { 1 }; filaments[2].id = 2; filaments[2].group_id = { 0, 1 }; auto result = StaticNozzleGroupResult::create(filaments, nozzles, { 0, 1, 2 }, { 0, 1, 0 }, false); REQUIRE(result.has_value()); THEN("filament->nozzle queries resolve to the stored mapping") { REQUIRE(result->get_extruder_count() == 2); REQUIRE(result->get_used_extruders() == std::vector({ 0, 1 })); REQUIRE(result->get_used_filaments() == std::vector({ 0, 1, 2 })); REQUIRE(result->get_nozzles_for_filament(0).size() == 1); REQUIRE(result->get_nozzles_for_filament(2).size() == 2); // first-use resolves through the (filament,nozzle) change sequences. auto first = result->get_first_nozzle_for_filament(1); REQUIRE(first.has_value()); REQUIRE(first->group_id == 1); } THEN("empty inputs yield nullopt") { REQUIRE_FALSE(StaticNozzleGroupResult::create({}, nozzles, {}, {}, false).has_value()); REQUIRE_FALSE(StaticNozzleGroupResult::create(filaments, {}, {}, {}, false).has_value()); } } GIVEN("load_nozzle_infos_with_compatibility fallbacks") { std::vector new_format; { NozzleInfo n; n.group_id = 1; n.extruder_id = 1; n.diameter = "0.4"; n.volume_type = nvtHighFlow; new_format.push_back(n); } { NozzleInfo n; n.group_id = 0; n.extruder_id = 0; n.diameter = "0.4"; n.volume_type = nvtStandard; new_format.push_back(n); } THEN("new-format tags are returned sorted by logical id") { auto out = load_nozzle_infos_with_compatibility(new_format, {}, {}, {}, {}); REQUIRE(out.size() == 2); REQUIRE(out[0].group_id == 0); REQUIRE(out[1].group_id == 1); } THEN("oldest single-nozzle 3mf (no tags, no filament group_id) rebuilds from diameters/volume types") { std::vector vt = { nvtStandard, nvtHighFlow }; std::vector dia = { 0.4, 0.4 }; auto out = load_nozzle_infos_with_compatibility({}, {}, {}, vt, dia); REQUIRE(out.size() == 2); REQUIRE(out[0].extruder_id == 0); REQUIRE(out[0].volume_type == nvtStandard); REQUIRE(out[1].volume_type == nvtHighFlow); } } } // The layer-aware grouping result must survive the gcode.3mf write/read as // tags and the enable_filament_dynamic_map flag. Proves the parse_filament_info stamping, // the NOZZLE_TAG writer, the _handle_config_nozzle reader, and the nozzles_info plate copy. SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { GIVEN("a plate carrying a two-nozzle LayeredNozzleGroupResult") { 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(); ScopedTemporaryDir backup_dir("orca_ng"); model.set_backup_path(backup_dir.string()); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); std::vector nozzles; { MultiNozzleUtils::NozzleInfo n; n.group_id = 0; n.extruder_id = 0; n.diameter = "0.4"; n.volume_type = NozzleVolumeType::nvtStandard; nozzles.push_back(n); } { MultiNozzleUtils::NozzleInfo n; n.group_id = 1; n.extruder_id = 1; n.diameter = "0.4"; n.volume_type = NozzleVolumeType::nvtHighFlow; nozzles.push_back(n); } auto group = MultiNozzleUtils::LayeredNozzleGroupResult::create( std::vector{ 0, 1, 0 }, nozzles, std::vector{ 0, 1, 2 }); REQUIRE(group.has_value()); PlateData* plate = new PlateData(); plate->plate_index = 0; plate->is_sliced_valid = true; plate->filament_maps = { 1, 2, 1 }; plate->nozzle_group_result = group; plate->config.set_key_value("filament_map_mode", new ConfigOptionEnum(fmmManual)); plate->config.set_key_value("filament_map", new ConfigOptionInts({ 1, 2, 1 })); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); 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 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); THEN("the tags round-trip into the loaded plate's nozzles_info") { REQUIRE(loaded); REQUIRE(dst_plates.size() >= 1); PlateData* rt = dst_plates.front(); REQUIRE(rt->nozzles_info.size() == 2); // reader stores extruder_id 0-based (tag is 1-based), diameter/volume_type preserved. std::sort(rt->nozzles_info.begin(), rt->nozzles_info.end()); REQUIRE(rt->nozzles_info[0].group_id == 0); REQUIRE(rt->nozzles_info[0].extruder_id == 0); REQUIRE(rt->nozzles_info[0].diameter == "0.4"); REQUIRE(rt->nozzles_info[0].volume_type == NozzleVolumeType::nvtStandard); REQUIRE(rt->nozzles_info[1].group_id == 1); REQUIRE(rt->nozzles_info[1].extruder_id == 1); REQUIRE(rt->nozzles_info[1].volume_type == NozzleVolumeType::nvtHighFlow); // A static (non-selector) result must persist enable_filament_dynamic_map = false. auto* dyn = rt->config.option("enable_filament_dynamic_map"); const bool persisted_true = (dyn != nullptr && dyn->value); REQUIRE_FALSE(persisted_true); } release_PlateData_list(dst_plates); } delete plate; } } // A mixed-color filament occupies an ordinary filament slot, and painting with it stores an // ordinary extruder state: a project saved by BambuStudio encodes filament 5 of a 5-slot setup // as paint state 5, with the mix described by the parallel filament_mixed_* project arrays. SCENARIO("Mixed-color filament setup and painting round-trip through a .3mf", "[3mf][MixedFilament]") { GIVEN("a painted model whose project config describes a mixed filament in the last slot") { 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(); // Both the exporter and the importer stage Metadata/project_settings.config through the // model's backup path; point them at writable temp dirs. ScopedTemporaryDir backup_dir("orca_mixed_src"); model.set_backup_path(backup_dir.string()); ModelVolume* mv = model.objects.front()->volumes.front(); { TriangleSelector selector(mv->mesh()); selector.set_facet(0, EnforcerBlockerType::Extruder5); // the mixed slot selector.set_facet(1, EnforcerBlockerType::Extruder2); REQUIRE(mv->mmu_segmentation_facets.set(selector)); } DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_key_value("filament_colour", new ConfigOptionStrings( { "#00AE42", "#FFFF00", "#FF0000", "#0000FF", "#FF6A26" })); config.set_key_value("filament_is_mixed", new ConfigOptionBools( { false, false, false, false, true })); config.set_key_value("filament_mixed_components", new ConfigOptionStrings( { "", "", "", "", "3,2" })); config.set_key_value("filament_mixed_sublayer_ratios", new ConfigOptionStrings( { "", "", "", "", "0.4200,0.5800" })); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); PlateData* plate = new PlateData(); plate->plate_index = 0; StoreParams store_params; store_params.path = test_file.c_str(); store_params.model = &model; store_params.config = &config; store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; store_params.plate_data_list.push_back(plate); REQUIRE(store_bbs_3mf(store_params)); Model dst_model; ScopedTemporaryDir dst_backup_dir("orca_mixed_dst"); dst_model.set_backup_path(dst_backup_dir.string()); DynamicPrintConfig dst_config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; PlateDataPtrs dst_plates; std::vector project_presets; bool is_bbl_3mf = false, is_orca_3mf = false; Semver file_version; REQUIRE(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)); THEN("the mixed-filament project keys survive") { auto* is_mixed = dst_config.option("filament_is_mixed"); REQUIRE(is_mixed != nullptr); REQUIRE(is_mixed->values == std::vector({ 0, 0, 0, 0, 1 })); auto* components = dst_config.option("filament_mixed_components"); REQUIRE(components != nullptr); REQUIRE(components->values.size() == 5); REQUIRE(components->values[4] == "3,2"); auto* ratios = dst_config.option("filament_mixed_sublayer_ratios"); REQUIRE(ratios != nullptr); REQUIRE(ratios->values.size() == 5); REQUIRE(ratios->values[4] == "0.4200,0.5800"); } THEN("the painted facets survive, including the one painted with the mixed slot") { REQUIRE(dst_model.objects.size() == 1); ModelVolume* dst_mv = dst_model.objects.front()->volumes.front(); REQUIRE_FALSE(dst_mv->mmu_segmentation_facets.empty()); REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder2)); REQUIRE(dst_mv->mmu_segmentation_facets.has_facets(*dst_mv, EnforcerBlockerType::Extruder5)); } release_PlateData_list(dst_plates); delete plate; // store_bbs_3mf does not take ownership of the source plate } } } // Locks the serialization contract of the "Publish" metadata: the orca_published flag and the // orca_published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf -> // load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior is exercised // headlessly in test_preset_bundle_loading.cpp.) SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") { GIVEN("a model carrying published metadata") { 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(); model.model_info = std::make_shared(); model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = R"(["layer_height","wall_thickness"])"; // store_bbs_3mf stages project_settings.config through the model's backup path; point // it at a writable temp dir (the default lives under a read-only root in CI). ScopedTemporaryDir backup_dir("orca_pub"); model.set_backup_path(backup_dir.string()); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); StoreParams store_params; store_params.path = test_file.c_str(); store_params.model = &model; store_params.config = &config; 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 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); THEN("the published metadata round-trips unchanged") { REQUIRE(loaded); REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1"); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height","wall_thickness"])"); // The orca_published_keys value is a JSON array of setting keys; it must parse back to // the same keys that were selected. nlohmann::json keys = nlohmann::json::parse(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG]); REQUIRE(keys.is_array()); REQUIRE(keys.size() == 2); REQUIRE(keys[0] == "layer_height"); REQUIRE(keys[1] == "wall_thickness"); } release_PlateData_list(dst_plates); } } } // A normal 3MF (no Publish metadata) must load identically: the loader must not fabricate a // "orca_published" flag or orca_published_keys for files that never carried them. SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { GIVEN("a model without any published metadata") { 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(); ScopedTemporaryDir backup_dir("orca_legacy"); model.set_backup_path(backup_dir.string()); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); StoreParams store_params; store_params.path = test_file.c_str(); store_params.model = &model; store_params.config = &config; 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 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); THEN("no published key is fabricated") { REQUIRE(loaded); REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_TAG) == 0); REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_KEYS_TAG) == 0); } release_PlateData_list(dst_plates); } } } // Locks the serialization contract of the orca_published_material_keys metadata: the per-entry JSON // must survive a store_bbs_3mf -> load_bbs_3mf round-trip verbatim, exactly like orca_published_keys. SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") { GIVEN("a model carrying published material keys metadata") { 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(); const std::string material_keys_json = R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99"},"slot":0,"keys":["filament_retraction_length","filament_z_hop"]}])"; model.model_info = std::make_shared(); model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = material_keys_json; ScopedTemporaryDir backup_dir("orca_pub_mat"); model.set_backup_path(backup_dir.string()); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); StoreParams store_params; store_params.path = test_file.c_str(); store_params.model = &model; store_params.config = &config; 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 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); THEN("the published material keys metadata round-trips unchanged") { REQUIRE(loaded); REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json); // The value must parse back to one material entry carrying the nested identity // object, the author slot ordinal and the key list. nlohmann::json entries = nlohmann::json::parse(material_keys_json); REQUIRE(entries.is_array()); REQUIRE(entries.size() == 1); REQUIRE(entries[0]["material"]["filament_type"] == "PLA"); REQUIRE(entries[0]["material"]["filament_vendor"] == "Generic"); REQUIRE(entries[0]["material"]["filament_id"] == "GFL99"); REQUIRE(entries[0]["slot"] == 0); REQUIRE(entries[0]["keys"].is_array()); REQUIRE(entries[0]["keys"].size() == 2); REQUIRE(entries[0]["keys"][0] == "filament_retraction_length"); } release_PlateData_list(dst_plates); } } } SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer tags", "[3mf]") { GIVEN("a multi-instance model carrying published metadata and a published_config payload") { 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(); // A second instance: tag-less third-party files get their multi-instance objects split, // published files must not (the loader recognizes them by their metadata). model.objects.front()->add_instance(); DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24)); full_cfg.set_key_value("retraction_length", new ConfigOptionFloats({ 1.2 })); const std::vector published_keys = { "layer_height", "retraction_length" }; const std::vector material_keys = { { "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } } }; // The payload builder keeps the published and identity keys and drops everything else. DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); REQUIRE(filtered_cfg.option("layer_height") != nullptr); REQUIRE(filtered_cfg.option("retraction_length") != nullptr); REQUIRE(filtered_cfg.option("filament_colour") != nullptr); REQUIRE(filtered_cfg.option("filament_type") != nullptr); REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr); REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr); REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr); // Serialize the payload exactly like export_published_3mf does. std::string payload; for (const std::string &key : filtered_cfg.keys()) payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; model.model_info = std::make_shared(); model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = R"(["layer_height","retraction_length"])"; model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = payload; ScopedTemporaryDir backup_dir("orca_min_pub"); model.set_backup_path(backup_dir.string()); WHEN("stored using SaveStrategy::MinimalPublished and reloaded") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); // Create a fake project preset to verify MinimalPublished omits it. Preset preset(Preset::TYPE_PRINT, "TestPrintPreset"); preset.config = full_cfg; std::vector project_presets = { &preset }; StoreParams store_params; store_params.path = test_file.c_str(); store_params.model = &model; store_params.config = &filtered_cfg; store_params.project_presets = project_presets; store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::MinimalPublished; REQUIRE(store_bbs_3mf(store_params)); Model dst_model; ScopedTemporaryDir loaded_backup_dir("orca_min_pub_loaded"); dst_model.set_backup_path(loaded_backup_dir.string()); DynamicPrintConfig dst_config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; PlateDataPtrs dst_plates; std::vector loaded_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, &loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig); THEN("the 3MF loads without project config or embedded presets") { REQUIRE(loaded); REQUIRE(dst_config.empty()); REQUIRE(loaded_presets.empty()); } THEN("the file carries no slicer tags and classifies as a generic 3MF") { REQUIRE_FALSE(is_bbl_3mf); REQUIRE_FALSE(is_orca_3mf); // No Application / OrcaSlicer tag: old receivers import the geometry silently // instead of showing a baked-in, wrong "old version" popup. REQUIRE_FALSE(file_version.valid()); } THEN("the geometry keeps BBS-grade handling: instances are not split") { REQUIRE(dst_model.objects.size() == 1); REQUIRE(dst_model.objects.front()->instances.size() == 2); } THEN("the published metadata and payload round-trip unchanged") { REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1"); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height","retraction_length"])"); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] == payload); } THEN("the payload parses back to the published values") { DynamicPrintConfig parsed_payload; parsed_payload.load_from_ini_string(dst_model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG], ForwardCompatibilitySubstitutionRule::Enable); REQUIRE(parsed_payload.option("layer_height") != nullptr); REQUIRE_THAT(parsed_payload.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6)); REQUIRE(parsed_payload.option("retraction_length") != nullptr); REQUIRE_THAT(parsed_payload.opt("retraction_length")->get_at(0), Catch::Matchers::WithinAbs(1.2, 1e-6)); } release_PlateData_list(dst_plates); } } } // An entry masks the non-published slots to their defaults so publishing slot 1 never leaks slot // 0's value into the file. Both a full entry (the whole-slot key list) and a partial entry (a // per-slot key) go through the same masking path in filter_published_config (keys and full_keys // are filtered identically), so the two forms are exercised together. SCENARIO("Published entries mask the other slots to their defaults", "[3mf]") { const bool full = GENERATE(true, false); GIVEN("a full print configuration with two filament slots") { DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); full_cfg.opt("filament_diameter")->values = { 1.75, 1.75 }; full_cfg.opt("filament_colour")->values = { "#111111", "#222222" }; // filament_flow_ratio carries a non-empty option default (1.0) of the same type, so the // mask can restore it on the non-published slot. full_cfg.opt("filament_flow_ratio", true)->values = { 1.02, 0.98 }; WHEN("filtering with a published entry for slot 1") { PublishedMaterialEntry entry; entry.slot = 1; if (full) { entry.full = true; entry.full_keys = { "filament_flow_ratio" }; } else { entry.keys = { "filament_flow_ratio" }; } DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { entry }); THEN("the selected key is present with the author's slot value") { REQUIRE(filtered_cfg.option("filament_flow_ratio") != nullptr); REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); } THEN("the non-published slot is masked to its default") { REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); } THEN("the identity keys stay present") { REQUIRE(filtered_cfg.option("filament_colour") != nullptr); } } } } // A key needing slot masking that cannot be masked (no registered option default of the same // type) is dropped from the payload entirely instead of shipping the author's whole vector. SCENARIO("Unmaskable keys are dropped from the published payload instead of leaking", "[3mf]") { GIVEN("a config carrying a synthetic def-less vector key and a maskable one") { DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); full_cfg.opt("filament_diameter")->values = { 1.75, 1.75 }; full_cfg.opt("filament_colour")->values = { "#111111", "#222222" }; // Not a PrintConfig key: print_config_def has no default to mask with. full_cfg.set_key_value("orca_synthetic_setting", new ConfigOptionFloats({ 9.9, 8.8 })); full_cfg.opt("filament_flow_ratio", true)->values = { 1.02, 0.98 }; PublishedMaterialEntry partial_entry; partial_entry.slot = 1; partial_entry.keys = { "orca_synthetic_setting", "filament_flow_ratio" }; WHEN("filtering with a partial entry for slot 1") { DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { partial_entry }); THEN("the unmaskable synthetic key is not published") { REQUIRE(filtered_cfg.option("orca_synthetic_setting") == nullptr); } THEN("the maskable key is present, author slot kept, other slot masked") { REQUIRE(filtered_cfg.opt("filament_flow_ratio") != nullptr); REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); } THEN("the identity keys stay present") { REQUIRE(filtered_cfg.option("filament_colour") != nullptr); } } } } // The extended per-entry fields (full dump list, published type and colour) travel inside the // published_material_keys metadata and round-trip unchanged. SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") { GIVEN("a model carrying extended published material keys metadata") { 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(); const std::string material_keys_json = R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf","name":"Generic PLA Matte @System"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])"; model.model_info = std::make_shared(); model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = material_keys_json; ScopedTemporaryDir backup_dir("orca_pub_mat2"); model.set_backup_path(backup_dir.string()); WHEN("stored to and reloaded from a .3mf") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); StoreParams store_params; store_params.path = test_file.c_str(); store_params.model = &model; store_params.config = &config; 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 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); THEN("the extended material metadata round-trips unchanged") { REQUIRE(loaded); REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json); // The value must parse back with every extended field intact. nlohmann::json entries = nlohmann::json::parse(material_keys_json); REQUIRE(entries.is_array()); REQUIRE(entries.size() == 1); REQUIRE(entries[0]["full"].get() == true); REQUIRE(entries[0]["full_keys"].is_array()); REQUIRE(entries[0]["full_keys"].size() == 2); REQUIRE(entries[0]["publish_type"].get() == true); REQUIRE(entries[0]["type"] == "PLA"); REQUIRE(entries[0]["publish_color"].get() == false); } release_PlateData_list(dst_plates); } } } // A published mixed filament serializes its whole definition (components, ratios, gradient) // masked to the author's slot: the mix slot's values survive, the non-published slots reset to // their defaults, so a partial publish never leaks another slot's mix data. SCENARIO("Published mixed-filament keys are masked to the author's slot", "[3mf]") { GIVEN("a full print configuration with three slots, one of them mixed") { DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); full_cfg.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; full_cfg.opt("filament_colour")->values = { "#111111", "#222222", "#333333" }; full_cfg.opt("filament_is_mixed")->values = { 0, 0, 1 }; full_cfg.opt("filament_mixed_components")->values = { "", "", "1,2" }; full_cfg.opt("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" }; full_cfg.opt("filament_mixed_gradient")->values = { 0, 0, 1 }; full_cfg.opt("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" }; full_cfg.opt("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" }; full_cfg.opt("filament_mixed_gradient_per_part")->values = { 0, 0, 1 }; PublishedMaterialEntry mix_entry; mix_entry.slot = 2; mix_entry.keys = { "filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios", "filament_mixed_gradient", "filament_mixed_gradient_range", "filament_mixed_gradient_curve", "filament_mixed_gradient_per_part" }; WHEN("filtering with a mixed entry for slot 2") { DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { mix_entry }); THEN("the author's mixed slot keeps its definition") { REQUIRE(filtered_cfg.option("filament_is_mixed") != nullptr); REQUIRE(filtered_cfg.opt("filament_is_mixed")->values == std::vector{ 0, 0, 1 }); const auto& components = filtered_cfg.opt("filament_mixed_components")->values; REQUIRE(components.size() == 3); CHECK(components[2] == "1,2"); CHECK(filtered_cfg.opt("filament_mixed_sublayer_ratios")->values[2] == "0.6,0.4"); CHECK(filtered_cfg.opt("filament_mixed_gradient_curve")->values[2] == "0,0.1|1,0.9"); CHECK(filtered_cfg.opt("filament_mixed_gradient")->values[2]); CHECK(filtered_cfg.opt("filament_mixed_gradient_per_part")->values[2]); } THEN("the non-published slots are masked to their defaults") { CHECK(filtered_cfg.opt("filament_mixed_components")->values[0] == ""); CHECK(filtered_cfg.opt("filament_mixed_components")->values[1] == ""); CHECK(filtered_cfg.opt("filament_is_mixed")->values[0] == 0); CHECK(filtered_cfg.opt("filament_is_mixed")->values[1] == 0); } THEN("the identity keys stay present") { REQUIRE(filtered_cfg.option("filament_colour") != nullptr); } } } } // The published flag is gated on the exact string "1": any other serialized value means "not // published", so a receiver never treats a file as published on a loose truthiness check. TEST_CASE("is_published_3mf_flag accepts only the literal \"1\"", "[3mf]") { CHECK(is_published_3mf_flag("1")); CHECK_FALSE(is_published_3mf_flag("0")); CHECK_FALSE(is_published_3mf_flag("false")); CHECK_FALSE(is_published_3mf_flag("true")); CHECK_FALSE(is_published_3mf_flag("")); CHECK_FALSE(is_published_3mf_flag("YES")); } // bbs_3mf_is_published is the lightweight metadata probe used to decide whether a file was // produced by the publish feature (GUI "recently published" tracking). It must return true only // for a file whose metadata carries the flag set to "1", and false for legacy files and for a // file whose flag is present but not "1" (which loads as a normal, non-published 3MF). SCENARIO("bbs_3mf_is_published detects only genuinely published 3MFs", "[3mf]") { auto store_model = [](const std::string &path, const std::string &flag_value, const std::string &keys_value) { 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(); model.model_info = std::make_shared(); // An empty flag_value means "don't write the flag at all" (a legacy file). if (!flag_value.empty()) model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = flag_value; model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = keys_value; ScopedTemporaryDir backup_dir("orca_is_pub"); model.set_backup_path(backup_dir.string()); DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); StoreParams store_params; store_params.path = path.c_str(); store_params.model = &model; store_params.config = &config; store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; REQUIRE(store_bbs_3mf(store_params)); }; GIVEN("a minimal published 3MF whose flag is \"1\"") { ScopedTemporaryFile temp(".3mf"); store_model(temp.string(), "1", R"(["layer_height"])"); WHEN("probed by bbs_3mf_is_published") { THEN("it is recognized as published") { CHECK(bbs_3mf_is_published(temp.string())); } } } GIVEN("a legacy 3MF without any published flag") { ScopedTemporaryFile temp(".3mf"); store_model(temp.string(), "", R"(["layer_height"])"); WHEN("probed by bbs_3mf_is_published") { THEN("it is not recognized as published") { CHECK_FALSE(bbs_3mf_is_published(temp.string())); } } } GIVEN("a 3MF carrying the flag set to \"0\"") { ScopedTemporaryFile temp(".3mf"); store_model(temp.string(), "0", R"(["layer_height"])"); WHEN("probed and loaded") { THEN("it is not recognized as published") { CHECK_FALSE(bbs_3mf_is_published(temp.string())); } THEN("it loads as a normal, non-published 3MF") { Model dst_model; DynamicPrintConfig dst_config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; PlateDataPtrs dst_plates; std::vector project_presets; bool is_bbl_3mf = false, is_orca_3mf = false; Semver file_version; REQUIRE(load_bbs_3mf(temp.string().c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); REQUIRE(dst_model.model_info != nullptr); // The key is present but not "1", so nothing treats the file as published; the // stored keys still round-trip verbatim. REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "0"); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height"])"); release_PlateData_list(dst_plates); } } } }