mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
Extend Publish workflow with full-filament and type/color requirements
Per material slot, the Publish dialog can now embed the entire filament preset ("Full Publish") and require a curated filament type and/or colour:
- On export, full-publish vector options are masked to the author's slot so unrelated slot data never leaks into the published file.
- On load, slots are matched by the published type: a match keeps the receiver's material (full dumps ignored, partial keys applied); a mismatch replaces the slot with the first visible same-type library filament, falling back to a temporary embedded preset or skipped keys when none exists. Required colours apply regardless of the type match.
- The receiver's slot count grows only to the highest published slot.
- Published 3MFs load as a new project: the file's path is not adopted as the project filename, published metadata is stripped from the model, and the file is added to recent projects.
- Notifications list replaced slots, and the edited filament preset is refreshed so applied values surface in the GUI.
- Dialog: "Full Publish" toggle replaces the material opt-in and select-all headers; new Color/Type requirement rows with swatches.
- Add Ctrl+Shift+E shortcut for the Publish dialog (menu, key handling, and the keyboard shortcuts dialog).
- Tests for export slot masking, metadata round-trip, replacement semantics, slot growth, and skipped-key reporting.
This commit is contained in:
@@ -752,3 +752,98 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing v2: a "full publish" entry carries the whole slot's key list. Its vector
|
||||
// options keep only the author's slot value; the other slots are masked to their defaults so a
|
||||
// slot-1 full publish does not leak slot 0's data into the file.
|
||||
SCENARIO("Full-publish entries filter the whole slot and mask the other slots", "[3mf]") {
|
||||
GIVEN("a full print configuration with two filament slots") {
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
full_cfg.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
full_cfg.opt<ConfigOptionStrings>("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<ConfigOptionFloatsNullable>("filament_flow_ratio", true)->values = { 1.02, 0.98 };
|
||||
|
||||
PublishedMaterialEntry full_entry;
|
||||
full_entry.slot = 1;
|
||||
full_entry.full = true;
|
||||
full_entry.full_keys = { "filament_flow_ratio" };
|
||||
|
||||
WHEN("filtering with a full entry for slot 1") {
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { full_entry });
|
||||
|
||||
THEN("the full key list is present with the author's slot value") {
|
||||
REQUIRE(filtered_cfg.option("filament_flow_ratio") != nullptr);
|
||||
REQUIRE(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[1] == 0.98);
|
||||
}
|
||||
THEN("the non-published slot is masked to its default") {
|
||||
REQUIRE(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[0] == 1.0);
|
||||
}
|
||||
THEN("the identity keys stay present") {
|
||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing v2: 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 filament-publishing-v2 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"},"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<ModelInfo>();
|
||||
model.model_info->metadata_items["published_material_keys"] = 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<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);
|
||||
THEN("the extended material metadata round-trips unchanged") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json);
|
||||
|
||||
// The value must parse back with every filament-publishing-v2 field intact.
|
||||
nlohmann::json entries = nlohmann::json::parse(material_keys_json);
|
||||
REQUIRE(entries.is_array());
|
||||
REQUIRE(entries.size() == 1);
|
||||
REQUIRE(entries[0]["full"].get<bool>() == true);
|
||||
REQUIRE(entries[0]["full_keys"].is_array());
|
||||
REQUIRE(entries[0]["full_keys"].size() == 2);
|
||||
REQUIRE(entries[0]["publish_type"].get<bool>() == true);
|
||||
REQUIRE(entries[0]["type"] == "PLA");
|
||||
REQUIRE(entries[0]["publish_color"].get<bool>() == false);
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,6 +784,409 @@ TEST_CASE("Published 3MF applies material retraction keys onto the receiver's ma
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// Filament-publishing v2: a "full publish" slot serializes the entire filament of the slot. On
|
||||
// load the slot is matched positionally against the published (curated, vendor-agnostic) type:
|
||||
// a matching receiver type leaves the slot untouched, a mismatched type replaces it with the
|
||||
// first same-type visible preset (applying the author's full values on top), and a slot whose
|
||||
// type cannot be found in the receiver's library falls back to the author's values in-memory.
|
||||
TEST_CASE("Published 3MF full-published slots replace or ignore the receiver material by type", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
// Two author slots; filament_diameter drives the normalized per-slot vector sizes.
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PETG" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFT99" };
|
||||
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.9, 1.2 };
|
||||
return config;
|
||||
};
|
||||
// The full dump of slot 0, publishing the whole filament as type "ABS".
|
||||
auto make_full_abs_entry = [] {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.full = true;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "ABS";
|
||||
entry.full_keys = { "filament_retraction_length" };
|
||||
return entry;
|
||||
};
|
||||
|
||||
SECTION("type match leaves a full-published slot untouched") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
bundle.filament_presets = { "My PLA", "My PLA" };
|
||||
|
||||
PublishedMaterialEntry full = make_full_abs_entry();
|
||||
full.publish_type_value = "PLA"; // author requires PLA, receiver slot is PLA
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { full };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// The receiver keeps its own material and its own values: the full dump is ignored.
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.5 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
CHECK(pub.material_replacements.empty());
|
||||
}
|
||||
|
||||
SECTION("type mismatch replaces the slot with the first same-type preset and applies the full dump") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
Preset &abs = add_inmemory_preset(bundle.filaments, "My ABS");
|
||||
abs.config.opt_string("filament_type", 0u) = "ABS";
|
||||
abs.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.3 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_full_abs_entry() };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filament_presets[0] == "My ABS");
|
||||
// The author's slot-0 full values were applied onto the replacement.
|
||||
CHECK(bundle.filaments.find_preset("My ABS")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
REQUIRE(pub.material_replacements.size() == 1);
|
||||
}
|
||||
|
||||
SECTION("no same-type match creates a temporary project-embedded custom preset") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_full_abs_entry() };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// No ABS in the library: a temporary embedded preset is created and selected.
|
||||
CHECK(bundle.filament_presets[0] == "ABS (Published)");
|
||||
Preset *created = bundle.filaments.find_preset("ABS (Published)");
|
||||
REQUIRE(created != nullptr);
|
||||
CHECK(created->is_project_embedded);
|
||||
CHECK(created->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
// The original user preset remains untouched. Re-fetch by name: load_preset's deque
|
||||
// insertion relocated the presets, so the pre-load `pla` reference points at the
|
||||
// newly created "ABS (Published)" slot.
|
||||
CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.5 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
REQUIRE(pub.material_replacements.size() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing v2: a partially-published slot can carry a curated type and/or colour.
|
||||
// The colour is applied regardless of the type match; a type mismatch with no same-type
|
||||
// replacement keeps the receiver's material and reports the slot's keys as skipped. The
|
||||
// receiver's slot count grows only as far as the highest slot with published content.
|
||||
TEST_CASE("Published 3MF partial slots apply colour and gate keys by the published type", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PETG" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFT99" };
|
||||
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.9, 1.2 };
|
||||
return config;
|
||||
};
|
||||
|
||||
SECTION("matching type applies the keys and the colour") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "PLA";
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// Type matched: keys applied, colour applied.
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
CHECK(pub.material_replacements.empty());
|
||||
}
|
||||
|
||||
SECTION("type mismatch without a replacement keeps the material and skips the keys") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "ABS"; // no ABS in the receiver library
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// Colour still applies (type-independent); the material is kept and the keys skipped.
|
||||
CHECK(bundle.filament_presets[0] == "My PLA");
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.5 });
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(contains_key(pub.skipped_keys, "material:ABS (filament_retraction_length)"));
|
||||
CHECK(pub.material_replacements.empty());
|
||||
}
|
||||
|
||||
SECTION("receiver slot count grows to fit the highest published slot and assigns matching type preset") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG");
|
||||
petg.config.opt_string("filament_type", 0u) = "PETG";
|
||||
petg.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.8 };
|
||||
// The receiver has a single slot; the file carries two, only slot 1 is published.
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 1;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "PETG";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// The slot list was grown so author slot 1 has a material, automatically assigning the PETG preset.
|
||||
REQUIRE(bundle.filament_presets.size() == 2);
|
||||
CHECK(bundle.filament_presets[1] == "My PETG");
|
||||
}
|
||||
}
|
||||
|
||||
// The receiver's slot list grows only as far as the highest author slot that carries published
|
||||
// content: a 4-filament file whose author published nothing (or only a low slot) must not pull
|
||||
// filler materials into the receiver's setup, and the receiver never grows to the file's count.
|
||||
TEST_CASE("Published 3MF grows the receiver's slots only as far as the published slots", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
// Four author slots (a 4-filament model).
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2, 3, 4 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" };
|
||||
return config;
|
||||
};
|
||||
auto add_pla_preset = [](PresetBundle &bundle) {
|
||||
Preset &preset = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
preset.config.opt_string("filament_type", 0u) = "PLA";
|
||||
preset.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
return &preset;
|
||||
};
|
||||
auto make_color_entry = [](int slot) {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = slot;
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
return entry;
|
||||
};
|
||||
|
||||
// A file whose author published nothing for any slot: the receiver's setup is untouched.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true; // no material entries at all
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#123456" });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// Only slot 0 published: a single-slot receiver keeps its single slot; the file's other
|
||||
// three slots pull nothing in.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_color_entry(0) };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
}
|
||||
|
||||
// Slot 3 published: the receiver grows to 4 so the published slot exists; the filler is
|
||||
// the receiver's own first visible material.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
const std::string filler = bundle.filaments.first_visible().name;
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_color_entry(3) };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 4);
|
||||
CHECK(bundle.filament_presets[3] == filler);
|
||||
}
|
||||
|
||||
// Slots 0 and 2 published: the receiver grows to 3, never to the file's 4.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_color_entry(0), make_color_entry(2) };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 3);
|
||||
}
|
||||
}
|
||||
|
||||
// The GUI displays the edited preset, a snapshot of the selected collection preset taken at
|
||||
// selection time. The published overlay modifies the collection presets in place, so the load
|
||||
// must re-select the first slot's filament (mirroring a normal project load) for the applied
|
||||
// colour/type/keys - and slot replacements - to surface in the GUI.
|
||||
TEST_CASE("Published 3MF refreshes the edited preset so the applied material values surface", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99" };
|
||||
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.9 };
|
||||
return config;
|
||||
};
|
||||
auto make_entry = [] {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "PLA";
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
return entry;
|
||||
};
|
||||
|
||||
SECTION("the edited preset carries the applied colour and keys") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
// Mirror the GUI: the displayed preset is the collection's edited preset.
|
||||
REQUIRE(bundle.filaments.select_preset_by_name("My PLA", false));
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_entry() };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
const Preset &edited = bundle.filaments.get_edited_preset();
|
||||
CHECK(edited.config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(edited.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
SECTION("a slot replacement is reflected in the edited preset") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
Preset &abs = add_inmemory_preset(bundle.filaments, "My ABS");
|
||||
abs.config.opt_string("filament_type", 0u) = "ABS";
|
||||
abs.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.3 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
REQUIRE(bundle.filaments.select_preset_by_name("My PLA", false));
|
||||
|
||||
PublishedMaterialEntry entry = make_entry();
|
||||
entry.publish_type_value = "ABS"; // mismatch: replaced by the library's ABS
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets[0] == "My ABS");
|
||||
// The edited preset now displays the replacement with the author's values on top.
|
||||
const Preset &edited = bundle.filaments.get_edited_preset();
|
||||
CHECK(edited.name == "My ABS");
|
||||
CHECK(edited.config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(edited.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
}
|
||||
}
|
||||
|
||||
// Material-qualified keys whose receiver-side material match is missing or ambiguous must be
|
||||
// reported as skipped (material-qualified) and never applied; a single unqualified type
|
||||
// fallback still applies.
|
||||
|
||||
Reference in New Issue
Block a user