diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index e4bafd0071..4c4114345e 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5220,29 +5220,18 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Material pass: positional per-slot entries. The author published, per slot, either the // entire filament (full) or specific keys plus optionally a curated type and/or colour. - // - full: the slot always lands on a freshly created standalone detached copy - // ("Detach from parent", project-embedded, universally compatible, within-load - // deduped) - handled up front in the entry loop below; no library preset is ever - // reused or mutated; - // - partial: a type requirement gates the application - on type match (or no - // requirement) the keys are applied onto the slot's effective preset; on mismatch - // the slot is replaced with the best visible candidate, scored by the published - // identity (exact preset name resolved through the collection's name machinery, - // then exact setting_id, exact filament_id, then vendor+type, then type only); a - // preset no other slot references wins on equal scores; with no replacement - // available the receiver's material is kept and the keys are reported as skipped; - // - colour: applied to the slot regardless of the type gate. - // - capacity: on a non-SEMM receiver whose printer has fewer nozzles than the - // authored slot needs, the entry becomes an empty mixed-filament placeholder - // appended at the tail (the GUI flags it; the user assigns components from their - // own filaments); on a single-physical-slot receiver it is dropped and reported - // instead, since an empty mix could never be edited there. + // - full: lands on a freshly created standalone detached copy (no library preset used + // or mutated); + // - partial: a type requirement gates application; on mismatch the slot is replaced + // with the best visible candidate by published identity (exact name, setting_id, + // filament_id, vendor+type, type); with no replacement the receiver's material is + // kept and the keys are reported as skipped; + // - colour: applied regardless of the type gate. + // - capacity: past the printer's physical nozzles the entry becomes an empty + // mixed-filament placeholder; on a single-physical-slot receiver it is dropped. // Applied partial values land on the collection's edited layer when the slot references - // it and that layer survives the load (visible as a modification, revertible, the user's - // unsaved edits preserved), otherwise on the stored preset in place. - // To keep slot-to-slot aliasing (several slots referencing one preset) from leaking one - // slot's values into another, published slots sharing a preset with another slot are - // re-pointed at distinct presets before the values are applied. + // it and that layer survives the load, otherwise on the stored preset in place. Slots + // aliasing a shared preset are re-pointed at distinct presets before the values apply. { // Grow the receiver's slots only as far as the highest published slot (never // shrink, never pull filler materials for unpublished slots). @@ -5281,26 +5270,13 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool grow_target = std::max(grow_target, size_t(entry.slot) + 1); } // Mixed-filament definitions live in project-level virtual slots, so applying one - // positionally onto a receiver slot that holds a real, physical filament would - // silently convert hardware-backed state into a virtual mix. Compute each mixed - // entry's destination before anything consumes entry.slot (growth, seeding, - // de-aliasing, the overlay below): - // - a definition landing on a receiver slot that already carries a mixed - // definition keeps its place (a like-for-like override of a virtual slot); - // - everything else goes through one monotone append counter preserving author - // order: dest = max(authored, next_free). With a receiver shorter than the - // publish this keeps the authored positions intact; past them (or around a - // collision with a real filament) the mixes pack onto consecutive fresh slots - // AFTER every positional (real-filament) territory. The definition's cells are - // shifted inside the file-side per-slot mixed arrays so they stay readable - // from the new index. No existing slot changes meaning. - // - destinations are also capped: appends past the extruder limit are dropped - // and reported instead of being forced onto a physical filament. - // Physical entries past the printer's capacity join the same append counter - // (dest = next_free, packed consecutively at the tail - never max(authored, - // next_free), which would grow filler physical slots past the capacity) and are - // flagged mixed_placeholder: they become empty mixed-filament placeholders the - // GUI flags for the user to assign components to. + // positionally onto a receiver slot holding a real filament would convert hardware + // state into a virtual mix. Compute each entry's destination before anything + // consumes entry.slot: a definition on a slot that already holds a mixed definition + // keeps its place (like-for-like); everything else follows one monotone append + // counter preserving author order (dest = max(authored, next_free)). Appends past + // the extruder limit are dropped and reported. Physical entries past capacity join + // the same counter as mixed_placeholder empties the GUI flags for assignment. size_t next_free_slot = this->filament_presets.size(); bool any_mixed_relocated = false; // All authored-slot -> destination moves decided by this pass, applied to the @@ -5441,20 +5417,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool const auto it = exact_name_by_slot.find(slot); return it == exact_name_by_slot.end() ? std::string() : it->second; }; - std::set used_preset_names(this->filament_presets.begin(), this->filament_presets.end()); // Mirror first_visible_idx()'s start index so suppressed default presets are // never picked as a slot material. const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; - // Candidate preference for a published entry: exact preset name (the raw author - // name and the collection-resolved name - renames, removed vendor-generic - // library fallback - both identify the exact preset, unambiguous even when ids - // are shared between variants or missing from older files), then the trimmed - // bare form / alias ("Generic PLA" from "Generic PLA @System"): a same-family - // match that must never outrank the exact preset, then exact setting_id - // (variant-level, since "Generic PLA" and "Generic PLA Matte" share - // filament_id), then exact filament_id, then vendor+type, then type only (a - // type-only pick may surface an unrelated preset, e.g. a different vendor's - // PLA). + // Candidate preference for a published entry: exact preset name (raw author and + // collection-resolved), then trimmed bare form / alias, then exact setting_id + // (variant-level), then exact filament_id, then vendor+type, then type only. auto candidate_score = [](const Preset& candidate, const PublishedMaterialEntry& entry, const std::string& resolved_name) -> int { // Exact preset name: raw and collection-resolved forms both outrank the @@ -5512,7 +5480,6 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name : this->filament_presets.back(); this->filament_presets.emplace_back(initial_preset); - used_preset_names.insert(initial_preset); } // Published slots that alias another slot (multi-extruder with one filament) // get re-pointed at distinct presets: the overlay writes onto the slot's diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index d7f2c13c5f..60ea9a8354 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -10,20 +10,16 @@ class PresetBundle; std::string publish_base_key(const std::string &key); // Structural keys that are never applied onto the receiver's presets when loading a published -// 3MF (single source of truth for the denylist): applying them would rewrite the user's preset -// inheritance/structure. filament_ids is nevertheless exported via the identity list in -// filter_published_config because 3MF validation needs it - exported, never applied. +// 3MF (single source of truth for the denylist); applying them would rewrite the user's preset +// inheritance/structure. filament_ids is still exported via the identity list (3MF validation +// needs it) - exported, never applied. const std::set& publish_structural_keys(); // The mixed-color filament project keys (parallel per-slot arrays, see PresetBundle's -// s_project_options): a mixed slot's full definition - which slots it blends, the sublayer -// ratios and the optional Z-gradient description. A published mixed slot always serializes -// these keys; on import they are applied into the receiver's project_config (not a filament -// preset), so the mix survives the round-trip. +// s_project_options). Import applies them into project_config, not a filament preset. const std::set& publish_mixed_keys(); -// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (key + tab icon id), kept -// together so the tab can later be migrated onto these lists. +// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (config key + tab icon id). struct PublishablePrinterOption { const char *key; // config key, e.g. "retraction_length" const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length" @@ -33,8 +29,8 @@ struct PublishablePrinterOption { const std::vector& publishable_printer_retraction_options(); const std::vector& publishable_printer_z_hop_options(); -// Union of the two optgroup option lists; the published-3MF overlay applies printer keys only -// when their base key is in this allowlist (anything else is contract-excluded). +// Union of the two optgroup option lists; printer keys apply on import only if their base +// key is in this allowlist. const std::set& publishable_printer_keys(); // Union of setting keys differing from the base/system preset across the current print, @@ -60,32 +56,25 @@ struct PublishedMaterialEntry { // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; std::vector keys; - // "Full Publish": the whole filament preset (full_keys) is published. On the receiver - // Full Publish always creates a standalone parentless copy (libslic3r's "Detach from - // parent"): a new project-embedded preset ("Preset Inside Project") with the full - // resolved config, universally compatible (compatible_printers/condition cleared). - // It lives inside the loaded project only - never written to the user's library, - // no existing preset is ever selected-by-reference or mutated. Identical Full - // entries inside one load share one created instance (within-load dedup). + // "Full Publish": the whole filament preset (full_keys) is published. On the receiver Full + // Publish always creates a standalone parentless copy (libslic3r's "Detach from parent"), + // universally compatible and project-embedded only - never written to the user's library. + // Identical Full entries within one load share one created instance (within-load dedup). bool full{false}; // All non-structural filament keys of the author's slot preset; values travel in the file // config, masked to the author's slot index. std::vector full_keys; // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on a - // partial entry's mismatch the slot is replaced with a same-type filament from the - // receiver's library. Full entries consult no gate: they detach unconditionally, and the - // copy carries whatever values the payload bakes. + // partial entry's mismatch the slot is replaced with a same-type filament. Full entries + // consult no gate. bool publish_type{false}; std::string publish_type_value; // Required filament colour, applied on load regardless of the type match. bool publish_color{false}; std::string color; - // Import-side only, never serialized: the entry's authored slot sits past the receiver - // printer's physical filament capacity, so instead of growing a physical slot the entry - // is appended as an empty mixed-filament placeholder (virtual tail slot; the GUI flags - // it and the user assigns components from their own filaments). The flag also keeps the - // entry out of the payload mixed-definition validation and the value-apply passes, - // which only make sense for a slot that carries a real material. + // Import-side only, never serialized: the authored slot sits past the receiver's physical + // capacity, so the entry is appended as an empty mixed-filament placeholder (virtual tail + // slot; the GUI flags it for the user to assign components). bool mixed_placeholder{false}; }; @@ -93,16 +82,12 @@ struct PublishedMaterialEntry { std::string normalize_filament_type(const std::string& type); class DynamicPrintConfig; -// Clear the compatibility lists/conditions on a filament config so it is compatible -// with every printer and every print profile. A detached published material is -// universally compatible by construction: the baseline clone may carry machine-specific -// restrictions. Empty lists + empty conditions => compatible with everything -// (see is_compatible_with_printer, Preset.cpp:840). +// Clear the compatibility lists/conditions on a filament config so it is universally +// compatible once detached (empty lists + empty conditions = compatible with everything). void make_publish_universal(DynamicPrintConfig &config); -// Naming base for a detached published-material copy: "Generic PLA @System" -> -// "Generic PLA" (truncate at the first '@' variant tail, right-trimmed). Unchanged -// when the name carries no '@'. Empty result means "fall back to identity fields". +// Naming base for a detached published-material copy: "Generic PLA @System" -> "Generic PLA" +// (truncate/right-trim at the first '@' tail). Empty result means "fall back to identity". std::string publish_material_base_name(const std::string &preset_name); // Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys, diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index df69fa5434..2f882e35aa 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -174,7 +174,7 @@ void KBShortcutsDialog::fill_shortcuts() { ctrl + "O", L("Open Project") }, { ctrl + "S", L("Save Project") }, { ctrl + shift + "S", L("Save Project as")}, - { ctrl + shift + "E", L("Publish") }, + { ctrl + shift + "E", L("Publish 3MF") }, // File>Import { ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") }, // File>Export diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0d2c5ac4c9..a1915a1200 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15300,11 +15300,12 @@ void Plater::load_project(wxString const& filename2, if (using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; p->set_project_filename(filename); - } else if (loaded_published) { + } else if (loaded_published && !res.empty()) { // A "published" 3MF loads as a new project: its path must not become the project // filename (Save/Ctrl-S prompts for a destination instead of overwriting it); // reset() already cleared the project name, so restore the default title and keep - // the file in recents. + // the file in recents. Only on a successful load (res not empty): a failed or + // cancelled load must not pollute "Recently opened". p->set_project_name(_L("Untitled")); if (!filename.IsEmpty()) wxGetApp().mainframe->add_to_recent_projects(filename); @@ -18316,8 +18317,18 @@ int Plater::export_published_3mf(const std::vector& published_keys, DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); std::string payload; - for (const std::string& key : filtered_cfg.keys()) - payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; + for (const std::string& key : filtered_cfg.keys()) { + // A value containing a newline would break the INI written below (read_ini throws), + // so load_from_ini_string discards the whole settings block on import. Skip such + // keys instead of silently dropping every setting. + std::string value = filtered_cfg.opt_serialize(key); + if (value.find('\n') != std::string::npos) { + BOOST_LOG_TRIVIAL(warning) << "publish: dropping key \"" << key + << "\" from the published payload (value contains a newline)"; + continue; + } + payload += key + " = " + value + "\n"; + } model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload); // Same file layout as save_project(), plus Silence (so export_3mf does not set the project