mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Publish 3MF: import-side hardening and test coverage
Validate mixed-filament definitions during the published material pass: definitions whose components reference slots that do not exist or hold other mixed filaments, or that carry fewer than two components, are reported through the shared skipped_keys channel instead of shipping a mix the GUI integrity check would only flag later. Fix the slot-limit exhaustion report being silently dropped: it wrote to published_config->skipped_keys, which the pass's final move-assignment from the local vector clobbers. All rejections now go through the local. Remove the unreachable persist branch from add_detached_preset: no caller passes save_to_project=false, so the parameter is gone and the copy is always project-embedded. Tests: cover the exhaustion path, the new definition validation, the identity-tier matching matrix (including substitute reporting), the structural-key denylist, whole-vector size-mismatch skips, relocation payload degradation, the "(Published 2)" uniquify chain, mixed blend colours staying out of shared preset configs, and duplicate-slot last-wins. Also fix the legacy-3mf scenario passing vacuously behind an if-guarded assertion. All existing published/3mf tests pass unchanged.
This commit is contained in:
@@ -3064,14 +3064,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
||||
// diff against a parent; the caller decides whether to select it.
|
||||
// The published entry's filament_id is forwarded so user bases keep their stable
|
||||
// material grouping (get_filament_presets() groups user bases by filament_id).
|
||||
// save_to_project=true (the Full Publish default) creates a project-embedded preset:
|
||||
// it lives inside the loaded project only (serialized into the saved .3mf, restored by
|
||||
// load_project_embedded_presets) and never touches the user's library directory;
|
||||
// Preset::save() early-returns for embedded presets, so persistence is skipped here too.
|
||||
// The copy is a project-embedded preset: it lives inside the loaded project only
|
||||
// (serialized into the saved .3mf, restored by load_project_embedded_presets) and
|
||||
// never touches the user's library directory; Preset::save() early-returns for
|
||||
// embedded presets, so persistence is skipped here too.
|
||||
// Returns the final (uniquified) name; on collision "<base>" -> "<base> (Published)" ->
|
||||
// "<base> (Published 2)" ...
|
||||
std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
|
||||
const std::string &filament_id, bool save_to_project)
|
||||
const std::string &filament_id)
|
||||
{
|
||||
if (name_base.empty())
|
||||
return std::string();
|
||||
@@ -3111,7 +3111,7 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base,
|
||||
preset.bundle_id.clear();
|
||||
preset.file = this->path_for_preset(preset);
|
||||
preset.is_visible = true;
|
||||
preset.is_project_embedded = save_to_project;
|
||||
preset.is_project_embedded = true;
|
||||
if (m_type == Preset::TYPE_PRINT)
|
||||
preset.config.option<ConfigOptionString>("print_settings_id", true)->value = final_name;
|
||||
else if (m_type == Preset::TYPE_FILAMENT)
|
||||
@@ -3120,14 +3120,6 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base,
|
||||
preset.config.option<ConfigOptionString>("printer_settings_id", true)->value = final_name;
|
||||
unlock();
|
||||
|
||||
if (!save_to_project) {
|
||||
// Persist the full resolved config (no parent). Project-embedded presets are
|
||||
// serialized into the .3mf instead; Preset::save() would early-return anyway.
|
||||
// find by final_name — m_presets may have reallocated, so don't keep a raw ref.
|
||||
auto persist_it = this->find_preset_internal(final_name);
|
||||
if (persist_it != m_presets.end() && persist_it->name == final_name)
|
||||
persist_it->save(nullptr);
|
||||
}
|
||||
return final_name;
|
||||
}
|
||||
|
||||
|
||||
@@ -639,16 +639,14 @@ public:
|
||||
// preset's stable material grouping (get_filament_presets groups user bases by
|
||||
// filament_id); the published entry's filament_id is forwarded so the copy keeps
|
||||
// the author's grouping.
|
||||
// With save_to_project=true (default) the copy is a project-embedded preset
|
||||
// ("Preset Inside Project"): it lives inside the loaded project only, is serialized
|
||||
// into the saved .3mf via get_current_project_embedded_presets(), and is never
|
||||
// written to the user's library directory. With false it persists as a normal
|
||||
// user preset file.
|
||||
// The copy is a project-embedded preset ("Preset Inside Project"): it lives inside
|
||||
// the loaded project only, is serialized into the saved .3mf via
|
||||
// get_current_project_embedded_presets(), and is never written to the user's
|
||||
// library directory.
|
||||
// Returns the final (uniquified) name; on collision the suffix rule is:
|
||||
// "<base>" -> "<base> (Published)" -> "<base> (Published 2)" ...
|
||||
std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
|
||||
const std::string &filament_id = std::string(),
|
||||
bool save_to_project = true);
|
||||
const std::string &filament_id = std::string());
|
||||
|
||||
// Delete the current preset, activate the first visible preset.
|
||||
// returns true if the preset was deleted successfully.
|
||||
|
||||
@@ -5466,8 +5466,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
const std::string material_label = !entry.filament_id.empty() ? entry.filament_id :
|
||||
!entry.publish_type_value.empty() ? entry.publish_type_value :
|
||||
entry.filament_type;
|
||||
published_config->skipped_keys.emplace_back("material:" + material_label +
|
||||
" (mixed filament definition: filament slot limit reached)");
|
||||
// The local skipped_keys is published wholesale at the end of the pass;
|
||||
// writing published_config->skipped_keys here would be clobbered by it.
|
||||
skipped_keys.emplace_back("material:" + material_label +
|
||||
" (mixed filament definition: filament slot limit reached)");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot
|
||||
<< " could not be placed: all " << next_free_slot << " slots exhausted";
|
||||
entry_it = published_config->material_keys.erase(entry_it);
|
||||
@@ -5880,6 +5882,19 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
// slots wrote to it; that compound case is not chased.)
|
||||
const bool edited_survives_load = this->filament_presets.empty() ||
|
||||
this->filament_presets.front() == this->filaments.get_edited_preset().name;
|
||||
// Final layout for mix-definition validation: every slot that will hold a
|
||||
// mixed definition once this load completes - the receiver's own virtual
|
||||
// slots plus each published mixed entry's final (possibly relocated) slot.
|
||||
// Mix components are 1-based slot numbers, so a component is valid only
|
||||
// when the slot it names exists and does not itself hold a mixed filament.
|
||||
std::set<int> mixed_final_slots;
|
||||
for (const PublishedMaterialEntry& mix_entry : published_config->material_keys)
|
||||
if (mix_entry.slot >= 0 && is_mixed_definition(mix_entry))
|
||||
mixed_final_slots.insert(mix_entry.slot);
|
||||
for (size_t i = 0; i < this->filament_presets.size(); ++i)
|
||||
if (this->is_mixed_filament(i))
|
||||
mixed_final_slots.insert(int(i));
|
||||
const size_t mixed_final_slot_count = this->filament_presets.size();
|
||||
// Full Publish within-load dedup: identical Full materials (same setting_id
|
||||
// + preset_name identity) share one created instance, so an author who
|
||||
// pointed two slots at one preset yields one standalone copy here.
|
||||
@@ -5900,6 +5915,40 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
entry.publish_type_value) :
|
||||
entry.filament_id;
|
||||
|
||||
// Import-side validation of a mixed-filament definition: the publish
|
||||
// dialog cannot produce a definition whose components reference slots
|
||||
// that do not exist or hold other mixed filaments, so a broken one here
|
||||
// means the payload itself is broken (hand-crafted or corrupt file).
|
||||
// Report it through the same channel as every other rejected input and
|
||||
// skip the entry, instead of shipping a mix the GUI integrity check
|
||||
// (check_mixed_filament_integrity) would only flag later. A payload
|
||||
// that omits the mixed arrays entirely is not an error here: the key
|
||||
// routing below reports those per key as usual.
|
||||
if (is_mixed_definition(entry)) {
|
||||
std::string mix_error;
|
||||
if (const ConfigOptionStrings* comp_opt = config.opt<ConfigOptionStrings>("filament_mixed_components");
|
||||
comp_opt != nullptr && entry.slot < static_cast<int>(comp_opt->values.size())) {
|
||||
const std::vector<unsigned int> comps = parse_mixed_components(comp_opt->values[entry.slot]);
|
||||
if (comps.size() < 2)
|
||||
// An empty definition cell counts as broken too: applying it
|
||||
// would ship a mix the sidebar would only flag later.
|
||||
mix_error = "needs at least two components";
|
||||
else
|
||||
for (unsigned int comp : comps)
|
||||
if (comp < 1 || size_t(comp) > mixed_final_slot_count ||
|
||||
mixed_final_slots.count(int(comp) - 1) != 0) {
|
||||
mix_error = "components reference missing slots";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mix_error.empty()) {
|
||||
skipped_keys.emplace_back("material:" + material_label + " (mixed filament definition: " + mix_error + ")");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot
|
||||
<< " rejected: " << mix_error;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Full Publish: always create a standalone detached copy (even on exact
|
||||
// identity match) as a "Preset Inside Project" (project-embedded: lives
|
||||
// in this project only, never written to the library), universally
|
||||
|
||||
Reference in New Issue
Block a user