mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 02:57:39 +00:00
Published 3MF: match filament slots by exported identity without a Type requirement
This commit is contained in:
@@ -5020,7 +5020,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
const std::string vendor = (vendors != nullptr && !vendors->values.empty()) ? vendors->get_at(0) : std::string();
|
const std::string vendor = (vendors != nullptr && !vendors->values.empty()) ? vendors->get_at(0) : std::string();
|
||||||
if (!entry.filament_id.empty() && candidate.filament_id == entry.filament_id)
|
if (!entry.filament_id.empty() && candidate.filament_id == entry.filament_id)
|
||||||
return 2;
|
return 2;
|
||||||
if (normalize_filament_type(type) == entry.publish_type_value) {
|
// Tier 0/1 family gate: the explicit type requirement when published as
|
||||||
|
// such, otherwise the entry's own material family - so identity scoring
|
||||||
|
// also applies to entries published without a checked Type row.
|
||||||
|
const std::string required_type = !entry.publish_type_value.empty()
|
||||||
|
? entry.publish_type_value : normalize_filament_type(entry.filament_type);
|
||||||
|
if (!required_type.empty() && normalize_filament_type(type) == required_type) {
|
||||||
if (!entry.filament_vendor.empty() && vendor == entry.filament_vendor)
|
if (!entry.filament_vendor.empty() && vendor == entry.filament_vendor)
|
||||||
return 1;
|
return 1;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -5036,7 +5041,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
// presets first and falling back to incompatible ones only when no
|
// presets first and falling back to incompatible ones only when no
|
||||||
// compatible candidate exists...
|
// compatible candidate exists...
|
||||||
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
||||||
if (entry.slot != static_cast<int>(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty())
|
// Scored for every entry with an identity (name / ids / family),
|
||||||
|
// not only when a Type requirement was checked; candidate_score's
|
||||||
|
// family tiers fall back to the entry's own filament_type.
|
||||||
|
if (entry.slot != static_cast<int>(new_slot_idx))
|
||||||
continue;
|
continue;
|
||||||
const std::string resolved_name = resolved_name_for(entry.slot);
|
const std::string resolved_name = resolved_name_for(entry.slot);
|
||||||
int best_score = -1;
|
int best_score = -1;
|
||||||
@@ -5073,14 +5081,32 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF grew slot " << new_slot_idx
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF grew slot " << new_slot_idx
|
||||||
<< " with " << initial_preset << " (score " << best_score << ", preset_name \""
|
<< " with " << initial_preset << " (score " << best_score << ", preset_name \""
|
||||||
<< entry.preset_name << "\", type \"" << entry.publish_type_value << "\")";
|
<< entry.preset_name << "\", type \"" << entry.publish_type_value << "\")";
|
||||||
|
if (best_score >= 0 && best_score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty()))
|
||||||
|
published_config->material_replacements.emplace_back(
|
||||||
|
"slot " + std::to_string(new_slot_idx) + ": " + initial_preset +
|
||||||
|
" (substitute: no exact material match)");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// ...otherwise any visible preset not already used by another slot.
|
// ...otherwise any visible preset not already used by another slot,
|
||||||
|
// preferring the published material's own family when it is known.
|
||||||
if (initial_preset.empty()) {
|
if (initial_preset.empty()) {
|
||||||
|
std::string slot_family;
|
||||||
|
for (const PublishedMaterialEntry &entry : published_config->material_keys)
|
||||||
|
if (entry.slot == static_cast<int>(new_slot_idx)) {
|
||||||
|
slot_family = !entry.publish_type_value.empty()
|
||||||
|
? entry.publish_type_value : normalize_filament_type(entry.filament_type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
|
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
|
||||||
const Preset &candidate = this->filaments.preset(i);
|
const Preset &candidate = this->filaments.preset(i);
|
||||||
if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)
|
if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)
|
||||||
continue;
|
continue;
|
||||||
|
if (!slot_family.empty()) {
|
||||||
|
const ConfigOptionStrings *types = candidate.config.opt<ConfigOptionStrings>("filament_type");
|
||||||
|
const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string();
|
||||||
|
if (normalize_filament_type(cand_type) != slot_family)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
initial_preset = candidate.name;
|
initial_preset = candidate.name;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -5114,7 +5140,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
std::string replacement;
|
std::string replacement;
|
||||||
int best_score = -1;
|
int best_score = -1;
|
||||||
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
||||||
if (entry.slot != static_cast<int>(slot) || !entry.publish_type || entry.publish_type_value.empty())
|
// Scored for every entry with an identity, not only when a Type
|
||||||
|
// requirement was checked (same as the growth seeding above).
|
||||||
|
if (entry.slot != static_cast<int>(slot))
|
||||||
continue;
|
continue;
|
||||||
const std::string resolved_name = resolved_name_for(entry.slot);
|
const std::string resolved_name = resolved_name_for(entry.slot);
|
||||||
auto scan = [&](bool compatible_only) -> std::pair<int, std::string> {
|
auto scan = [&](bool compatible_only) -> std::pair<int, std::string> {
|
||||||
@@ -5153,20 +5181,39 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
<< entry.preset_name << "\", type \"" << entry.publish_type_value << "\")";
|
<< entry.preset_name << "\", type \"" << entry.publish_type_value << "\")";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// ...otherwise any distinct visible preset not referenced by another slot.
|
// ...otherwise any distinct visible preset not referenced by another slot,
|
||||||
|
// preferring the published material's own family when it is known.
|
||||||
if (replacement.empty()) {
|
if (replacement.empty()) {
|
||||||
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
|
std::string slot_family;
|
||||||
const Preset &candidate = this->filaments.preset(i);
|
for (const PublishedMaterialEntry &entry : published_config->material_keys)
|
||||||
if (candidate.is_visible && !referenced_elsewhere(candidate.name, size_t(-1))) {
|
if (entry.slot == static_cast<int>(slot)) {
|
||||||
replacement = candidate.name;
|
slot_family = !entry.publish_type_value.empty()
|
||||||
|
? entry.publish_type_value : normalize_filament_type(entry.filament_type);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
|
||||||
|
const Preset &candidate = this->filaments.preset(i);
|
||||||
|
if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1)))
|
||||||
|
continue;
|
||||||
|
if (!slot_family.empty()) {
|
||||||
|
const ConfigOptionStrings *types = candidate.config.opt<ConfigOptionStrings>("filament_type");
|
||||||
|
const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string();
|
||||||
|
if (normalize_filament_type(cand_type) != slot_family)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
replacement = candidate.name;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (replacement.empty())
|
if (replacement.empty())
|
||||||
continue; // every visible preset is referenced: aliasing is unavoidable
|
continue; // every visible preset is referenced: aliasing is unavoidable
|
||||||
|
const std::string aliased_name = this->filament_presets[slot];
|
||||||
this->filament_presets[slot] = replacement;
|
this->filament_presets[slot] = replacement;
|
||||||
material_applied = true;
|
material_applied = true;
|
||||||
|
// The re-point used to be silent; surface it like the other slot changes.
|
||||||
|
published_config->material_replacements.emplace_back(
|
||||||
|
"slot " + std::to_string(slot) + ": " + aliased_name + " -> " + replacement +
|
||||||
|
" (de-aliased: shared profile)");
|
||||||
}
|
}
|
||||||
// Grow the per-slot colour/type/map project vectors to the new slot count and
|
// Grow the per-slot colour/type/map project vectors to the new slot count and
|
||||||
// seed the new entries so the slots render with colours instead of blank chips
|
// seed the new entries so the slots render with colours instead of blank chips
|
||||||
@@ -5367,25 +5414,43 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
// No same-type library preset: fall back to the first available
|
// No same-type library preset: fall back to the first available
|
||||||
// visible preset, preferring one no other slot references, and
|
// visible preset, preferring one no other slot references, and
|
||||||
// apply the author's full values on top of it (the dump carries
|
// apply the author's full values on top of it (the dump carries
|
||||||
// filament_type, so the preset takes the author's type).
|
// filament_type, so the preset takes the author's type). A preset
|
||||||
|
// of the published material's own family is preferred overall -
|
||||||
|
// except that a referenced preset must never win just on family,
|
||||||
|
// since the dump would mutate it for every sharing slot too.
|
||||||
std::string fallback;
|
std::string fallback;
|
||||||
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
|
const std::string wanted_family = !entry.publish_type_value.empty()
|
||||||
const Preset &candidate = this->filaments.preset(i);
|
? entry.publish_type_value : normalize_filament_type(entry.filament_type);
|
||||||
if (!candidate.is_visible)
|
auto pick_fallback = [&](bool want_family) -> std::string {
|
||||||
continue;
|
std::string first;
|
||||||
if (fallback.empty())
|
for (size_t i = first_candidate; i < this->filaments.size(); ++i) {
|
||||||
fallback = candidate.name;
|
const Preset &candidate = this->filaments.preset(i);
|
||||||
bool referenced = false;
|
if (!candidate.is_visible)
|
||||||
for (size_t s = 0; s < this->filament_presets.size(); ++s)
|
continue;
|
||||||
if (this->filament_presets[s] == candidate.name) {
|
if (want_family) {
|
||||||
referenced = true;
|
if (wanted_family.empty())
|
||||||
break;
|
break;
|
||||||
|
const ConfigOptionStrings *cand_types = candidate.config.opt<ConfigOptionStrings>("filament_type");
|
||||||
|
if (normalize_filament_type(cand_types != nullptr && !cand_types->values.empty() ? cand_types->get_at(0) : std::string()) != wanted_family)
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (!referenced) {
|
bool referenced = false;
|
||||||
fallback = candidate.name;
|
for (size_t s = 0; s < this->filament_presets.size(); ++s)
|
||||||
break;
|
if (this->filament_presets[s] == candidate.name) {
|
||||||
|
referenced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!referenced)
|
||||||
|
return candidate.name;
|
||||||
|
if (first.empty())
|
||||||
|
first = candidate.name;
|
||||||
}
|
}
|
||||||
}
|
return first;
|
||||||
|
};
|
||||||
|
if (!wanted_family.empty())
|
||||||
|
fallback = pick_fallback(true);
|
||||||
|
if (fallback.empty())
|
||||||
|
fallback = pick_fallback(false);
|
||||||
if (!fallback.empty() && fallback != recv->name) {
|
if (!fallback.empty() && fallback != recv->name) {
|
||||||
const std::string old_name = recv->name;
|
const std::string old_name = recv->name;
|
||||||
this->filament_presets[slot] = fallback;
|
this->filament_presets[slot] = fallback;
|
||||||
|
|||||||
@@ -2010,6 +2010,72 @@ TEST_CASE("Published 3MF seeds published slots from unused presets and mutates t
|
|||||||
CHECK(bundle.project_config.opt<ConfigOptionInts>("filament_map")->values.size() == 4);
|
CHECK(bundle.project_config.opt<ConfigOptionInts>("filament_map")->values.size() == 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Without a checked Type row, a grown published slot is still seeded from the published
|
||||||
|
// material's identity - an exact filament_id outranks any arbitrary unused preset, and an
|
||||||
|
// entry carrying only a family constrains the pick to that family.
|
||||||
|
TEST_CASE("Published 3MF seeds a grown slot by published identity or family without a type requirement", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
auto make_file_config = [] {
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
PublishedMaterialEntry entry;
|
||||||
|
entry.slot = 2;
|
||||||
|
entry.filament_type = "PLA";
|
||||||
|
entry.filament_vendor = "Generic";
|
||||||
|
// An unused preset sorting before everything else: an unconstrained pick would take it.
|
||||||
|
PresetBundle bundle;
|
||||||
|
Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||||
|
mine.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
Preset &arbitrary = add_inmemory_preset(bundle.filaments, "Aaa PLA");
|
||||||
|
arbitrary.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
bundle.filament_presets = { "My PLA" };
|
||||||
|
|
||||||
|
SECTION("an exact filament_id outranks the first unused preset") {
|
||||||
|
Preset &authored = add_inmemory_preset(bundle.filaments, "Zzz PLA");
|
||||||
|
authored.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
authored.filament_id = "GFA00";
|
||||||
|
entry.filament_id = "GFA00";
|
||||||
|
|
||||||
|
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.size() == 3);
|
||||||
|
CHECK(bundle.filament_presets[1] == "My PLA");
|
||||||
|
CHECK(bundle.filament_presets[2] == "Zzz PLA");
|
||||||
|
// An exact identity match is not a substitute, so nothing is reported.
|
||||||
|
CHECK(pub.material_replacements.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a family-only entry picks an unused preset of that family") {
|
||||||
|
Preset &petg = add_inmemory_preset(bundle.filaments, "Bbb PETG");
|
||||||
|
petg.config.opt_string("filament_type", 0u) = "PETG";
|
||||||
|
entry.filament_type = "PETG";
|
||||||
|
|
||||||
|
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.size() == 3);
|
||||||
|
CHECK(bundle.filament_presets[2] == "Bbb PETG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The GUI displays the edited preset, a snapshot of the selected collection preset taken at
|
// The GUI displays the edited preset, a snapshot of the selected collection preset taken at
|
||||||
// selection time. Since the overlay mutates the collection presets in place, the load must
|
// selection time. Since the overlay mutates the collection presets in place, the load must
|
||||||
// re-select the first slot's filament so the applied values - and slot replacements - surface
|
// re-select the first slot's filament so the applied values - and slot replacements - surface
|
||||||
@@ -2415,6 +2481,59 @@ TEST_CASE("Published 3MF gives each published slot its own preset on an aliased
|
|||||||
CHECK(pub.skipped_keys.empty());
|
CHECK(pub.skipped_keys.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// De-aliasing runs on the exported identity even without a checked Type row: the re-pointed
|
||||||
|
// slot lands on the exact published material (by filament_id) rather than an arbitrary spare,
|
||||||
|
// and the formerly silent re-point is surfaced through the replacements notification list.
|
||||||
|
TEST_CASE("Published 3MF de-aliases an aliased slot by published identity without a type requirement", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
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", "PLA" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFL99" };
|
||||||
|
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.6, 0.9 };
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||||
|
mine.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
mine.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||||
|
// A spare sorting before the exact match: an unconstrained pick would take it.
|
||||||
|
Preset &spare = add_inmemory_preset(bundle.filaments, "Aaa PLA");
|
||||||
|
spare.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
spare.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||||
|
Preset &match = add_inmemory_preset(bundle.filaments, "Zzz PLA");
|
||||||
|
match.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
match.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||||
|
match.filament_id = "GFA00";
|
||||||
|
bundle.filament_presets = { "My PLA", "My PLA" };
|
||||||
|
|
||||||
|
PublishedMaterialEntry entry;
|
||||||
|
entry.slot = 1;
|
||||||
|
entry.filament_type = "PLA";
|
||||||
|
entry.filament_vendor = "Generic";
|
||||||
|
entry.filament_id = "GFA00";
|
||||||
|
entry.keys = { "filament_retraction_length" };
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { entry };
|
||||||
|
Preset::normalize(config);
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 2);
|
||||||
|
CHECK(bundle.filament_presets[0] == "My PLA");
|
||||||
|
CHECK(bundle.filament_presets[1] == "Zzz PLA");
|
||||||
|
// The published key was written onto the re-pointed slot's own preset.
|
||||||
|
Preset *target = bundle.filaments.find_preset("Zzz PLA", false, true);
|
||||||
|
REQUIRE(target != nullptr);
|
||||||
|
CHECK(target->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||||
|
REQUIRE(pub.material_replacements.size() == 1);
|
||||||
|
CHECK(pub.material_replacements[0].find("(de-aliased") != std::string::npos);
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
|
||||||
// Printer retraction keys are published per-extruder ("#N"): a receiver with a different
|
// Printer retraction keys are published per-extruder ("#N"): a receiver with a different
|
||||||
// extruder count still receives the in-range elements; out-of-range variants are reported as
|
// extruder count still receives the in-range elements; out-of-range variants are reported as
|
||||||
// skipped instead of corrupting the receiver's vector.
|
// skipped instead of corrupting the receiver's vector.
|
||||||
|
|||||||
Reference in New Issue
Block a user