diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index 1334bd4e7a..c422253dcd 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -3053,6 +3053,81 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
this->get_selected_preset().save(nullptr);
}
+// A detached standalone preset for the Full Publish receiver: create a user preset holding
+// the full resolved filament config (no inheritance, no vendor/alias links), parentless and
+// universally compatible. Mirrors save_current_preset(detach=true)'s creation branch but
+// does not force-select or 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.
+// Returns the final (uniquified) name; on collision "" -> " (Published)" ->
+// " (Published 2)" ...
+std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
+ const std::string &filament_id, bool save_to_project)
+{
+ if (name_base.empty())
+ return std::string();
+ Preset stored(m_type, name_base);
+ stored.config = std::move(config);
+ stored.filament_id = filament_id;
+
+ // Uniquify verbatim; only on collision append " (Published)" then " (Published 2)".
+ const std::string base_name = name_base;
+ std::string final_name = base_name;
+ auto exists = [this](const std::string &candidate) -> bool {
+ const auto it = this->find_preset_internal(candidate);
+ return it != m_presets.end() && it->name == candidate;
+ };
+ if (exists(final_name)) {
+ final_name = base_name + " (Published)";
+ for (int i = 2; exists(final_name); ++i)
+ final_name = base_name + " (Published " + std::to_string(i) + ")";
+ }
+
+ // Creation branch of save_current_preset(detach=true), without its selection side
+ // effects or project-embedded path.
+ lock();
+ const auto it = this->find_preset_internal(final_name);
+ Preset &preset = *m_presets.insert(it, stored);
+ stored.name.clear(); // avoid stale copied name being used below
+ stored.config.clear();
+ preset.name = final_name;
+ preset.vendor = nullptr;
+ preset.alias.clear();
+ preset.renamed_from.clear();
+ preset.m_excluded_from.clear();
+ preset.setting_id.clear();
+ preset.inherits().clear();
+ preset.version = Semver::parse(SoftFever_VERSION) ? *Semver::parse(SoftFever_VERSION) : Semver();
+ preset.is_default = false;
+ preset.is_system = false;
+ preset.is_external = false;
+ preset.bundle_id.clear();
+ preset.file = this->path_for_preset(preset);
+ preset.is_visible = true;
+ preset.is_project_embedded = save_to_project;
+ if (m_type == Preset::TYPE_PRINT)
+ preset.config.option("print_settings_id", true)->value = final_name;
+ else if (m_type == Preset::TYPE_FILAMENT)
+ preset.config.option("filament_settings_id", true)->values[0] = final_name;
+ else if (m_type == Preset::TYPE_PRINTER)
+ preset.config.option("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;
+}
+
bool PresetCollection::delete_current_preset()
{
Preset &selected = this->get_selected_preset();
diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp
index c9b3197a6f..e904851a1d 100644
--- a/src/libslic3r/Preset.hpp
+++ b/src/libslic3r/Preset.hpp
@@ -631,6 +631,24 @@ public:
// All presets are marked as not modified and the new preset is activated.
//BBS: add project embedded preset logic
void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr);
+ // Insert a standalone user preset holding the full resolved config (no inheritance,
+ // no vendor links): the libslic3r equivalent of "Detach from parent". Takes a
+ // resolved config, clears parent/vendor/alias metadata, stamps filament_settings_id.
+ // Unlike save_current_preset it does not force-select or diff against a parent.
+ // Used by the published-3MF Full Publish path. The optional filament_id seeds the
+ // 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.
+ // Returns the final (uniquified) name; on collision the suffix rule is:
+ // "" -> " (Published)" -> " (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);
// Delete the current preset, activate the first visible preset.
// returns true if the preset was deleted successfully.
diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp
index ce88c7a030..96373eae7c 100644
--- a/src/libslic3r/PresetBundle.cpp
+++ b/src/libslic3r/PresetBundle.cpp
@@ -5312,6 +5312,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// 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;
+ // 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.
+ std::map published_full_dedup;
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size())
continue; // out of range: nothing to do for this slot
@@ -5327,6 +5331,113 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
? (entry.publish_type_value.empty() ? entry.filament_type : entry.publish_type_value)
: entry.filament_id;
+ // 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
+ // compatible, named after the author's preset with its variant tail
+ // stripped ("(Published)" uniquification on collision). No existing
+ // preset is ever mutated.
+ if (entry.full) {
+ std::string dedup_key = entry.setting_id + std::string("\x1f") + entry.preset_name;
+ // Identity-less hand-crafted files (empty setting_id+name) must not
+ // collide: fall back to slot-scoped key so each slot gets its own copy
+ // unless the dedup above is meaningful.
+ if (dedup_key == std::string("\x1f"))
+ dedup_key = dedup_key + std::to_string(slot);
+ else if (!entry.filament_id.empty())
+ dedup_key += std::string("\x1f") + entry.filament_id;
+ std::string new_name;
+ const auto dedup_it = published_full_dedup.find(dedup_key);
+ if (dedup_it != published_full_dedup.end()) {
+ new_name = dedup_it->second;
+ } else {
+ // Baseline: clone the receiver slot's stored preset config (schema-
+ // complete across versions), then overlay the published full_keys.
+ DynamicPrintConfig new_cfg = recv != nullptr
+ ? recv->config : this->filaments.default_preset().config;
+ for (const std::string &key : entry.full_keys) {
+ const std::string base_key = publish_base_key(key);
+ if (structural_keys.count(base_key) != 0)
+ continue;
+ const ConfigOption *src_opt = config.option(base_key);
+ if (src_opt == nullptr || !src_opt->is_vector() ||
+ entry.slot < 0 ||
+ entry.slot >= static_cast(static_cast(src_opt)->size()))
+ continue;
+ ConfigOption *dst_opt = new_cfg.option(base_key);
+ if (dst_opt == nullptr || !dst_opt->is_vector() ||
+ static_cast(dst_opt)->empty() ||
+ dst_opt->type() != src_opt->type())
+ continue;
+ static_cast(dst_opt)->set_at(src_opt, 0, entry.slot);
+ }
+ // The published colour is authoritative even for Full (the payload's
+ // filament_colour plus the explicit publish_color field).
+ if (entry.publish_color && !entry.color.empty()) {
+ if (ConfigOptionStrings *col = new_cfg.opt("filament_colour", true)) {
+ if (col->values.empty())
+ col->values.emplace_back();
+ col->values[0] = entry.color;
+ }
+ }
+ make_publish_universal(new_cfg);
+ // Naming: stripped variant tail ("Generic PLA @System" -> "Generic
+ // PLA"), then identity fallbacks; collisions uniquify with
+ // "(Published)" / "(Published N)" inside add_detached_preset.
+ std::string base_name = entry.preset_name.empty() ? std::string() : publish_material_base_name(entry.preset_name);
+ if (base_name.empty()) {
+ base_name = !entry.filament_id.empty() ? entry.filament_id
+ : (!entry.publish_type_value.empty() ? entry.publish_type_value : entry.filament_type);
+ if (base_name.empty())
+ base_name = "Published Filament";
+ }
+ new_name = this->filaments.add_detached_preset(base_name, std::move(new_cfg), entry.filament_id);
+ published_full_dedup.emplace(dedup_key, new_name);
+ }
+ const std::string old_name = this->filament_presets[slot];
+ this->filament_presets[slot] = new_name;
+ material_applied = true;
+ published_config->material_replacements.emplace_back(
+ "slot " + std::to_string(slot) + ": " + old_name + " -> " + new_name +
+ " (published material imported)");
+ // Colour is slot-scoped and project-visible: sync into project_config
+ // (the copy already baked it, this makes the chips render).
+ if (entry.publish_color && !entry.color.empty()) {
+ if (ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour")) {
+ if (slot < proj_colour->values.size())
+ proj_colour->values[slot] = entry.color;
+ }
+ if (ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour")) {
+ if (slot < proj_multi_colour->values.size())
+ proj_multi_colour->values[slot] = entry.color;
+ }
+ } else if (proj_colour && new_name != old_name) {
+ // Fall back to the copy's colour so the chip is never blank: try
+ // the newly created preset's filament_colour, then the option default.
+ std::string seed;
+ if (const Preset *created = this->filaments.find_preset(new_name, false, true)) {
+ if (const ConfigOptionStrings *cols = created->config.opt("filament_colour"))
+ if (!cols->values.empty())
+ seed = cols->values.front();
+ }
+ if (seed.empty()) {
+ if (const ConfigOptionDef *colour_def = print_config_def.get("filament_colour"))
+ if (const auto *defaults = dynamic_cast(colour_def->default_value.get()))
+ if (!defaults->values.empty())
+ seed = defaults->values.front();
+ }
+ if (!seed.empty()) {
+ if (proj_colour && slot < proj_colour->values.size())
+ proj_colour->values[slot] = seed;
+ if (proj_multi_colour && slot < proj_multi_colour->values.size())
+ proj_multi_colour->values[slot] = seed;
+ }
+ }
+ if (proj_colour_type && slot < proj_colour_type->values.size())
+ proj_colour_type->values[slot] = "1";
+ continue;
+ }
+
bool apply_slot = true;
// The gate compares against the slot's effective material type: the edited
// layer when the slot references the collection's edited preset and that
diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp
index ac25d815f1..3a3d241e81 100644
--- a/src/libslic3r/PublishSettings.cpp
+++ b/src/libslic3r/PublishSettings.cpp
@@ -6,12 +6,19 @@
#include "MaterialType.hpp"
#include
+#include
#include