mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +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:
@@ -4783,6 +4783,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
if (is_published) {
|
||||
std::vector<std::string> skipped_keys;
|
||||
std::set<std::string> applied_keys;
|
||||
// Set whenever the material overlay actually modifies a receiver filament preset
|
||||
// (applied key, colour or slot replacement). Only then must the edited preset be
|
||||
// re-snapshotted: re-selecting unconditionally would discard the user's unsaved
|
||||
// in-memory filament edits when the published file touches nothing.
|
||||
bool material_applied = false;
|
||||
// Structural keys must never be applied to the user's presets: doing so would
|
||||
// rewrite their preset inheritance/structure. This is the single source of truth
|
||||
// shared with PublishSettingsDialog.cpp (publish_structural_keys in
|
||||
@@ -4869,6 +4874,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
return true;
|
||||
};
|
||||
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
||||
// Entries using the filament-publishing-v2 features (full dump, published type or
|
||||
// colour) are handled by the positional per-slot pass below; the legacy identity
|
||||
// matching here applies only to files that predate those features.
|
||||
if (entry.full || entry.publish_type || entry.publish_color)
|
||||
continue;
|
||||
// Resolve the author's source slot and its ordinal among the author slots
|
||||
// carrying this entry's identity. A slotted entry (slot >= 0) names the exact
|
||||
// author slot and targets the receiver's Nth matching preset (N = ordinal);
|
||||
@@ -5002,11 +5012,212 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
continue;
|
||||
}
|
||||
static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, 0, author_slot);
|
||||
material_applied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing-v2: 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. The receiver's slot is matched positionally against the published type:
|
||||
// - colour: always applied to the slot, independent of the type gate;
|
||||
// - type match: a full dump is intentionally ignored (the receiver keeps its material),
|
||||
// a partial entry's keys are applied as usual;
|
||||
// - type mismatch: the slot is replaced with the first visible same-type filament from
|
||||
// the receiver's library; the author's values are applied on top of it (full) or the
|
||||
// published keys are applied (partial);
|
||||
// - no replacement available: a full entry falls back to applying the author's values
|
||||
// in-memory onto the receiver's current preset (no library import); a partial entry
|
||||
// keeps the receiver's material and reports its keys as skipped.
|
||||
{
|
||||
// Slot growth is tied to the author slots that carry published content (full,
|
||||
// type or colour): the file's total filament count is irrelevant, and a slot the
|
||||
// author left unpublished must not pull a filler material into the receiver's
|
||||
// setup. Grow only as far as the highest published slot (never shrink, never
|
||||
// remove the receiver's existing materials).
|
||||
bool has_new_semantics = false;
|
||||
size_t target_slots = this->filament_presets.size();
|
||||
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
||||
if (!entry.full && !entry.publish_type && !entry.publish_color)
|
||||
continue; // legacy entry, handled above
|
||||
has_new_semantics = true;
|
||||
if (entry.slot >= 0)
|
||||
target_slots = std::max(target_slots, size_t(entry.slot) + 1);
|
||||
}
|
||||
if (has_new_semantics) {
|
||||
// Defensive cap: never exceed the file's own filament count.
|
||||
target_slots = std::min(target_slots, num_filaments);
|
||||
while (this->filament_presets.size() < target_slots) {
|
||||
const size_t new_slot_idx = this->filament_presets.size();
|
||||
std::string initial_preset;
|
||||
// Proactively assign matching candidate preset if this slot carries a published type
|
||||
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()) {
|
||||
for (size_t i = 0; i < this->filaments.size(); ++i) {
|
||||
const Preset &candidate = this->filaments.preset(i);
|
||||
if (!candidate.is_visible)
|
||||
continue;
|
||||
if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) {
|
||||
initial_preset = candidate.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (initial_preset.empty())
|
||||
initial_preset = this->filaments.first_visible().name;
|
||||
this->filament_presets.emplace_back(initial_preset);
|
||||
}
|
||||
|
||||
auto apply_slot_keys = [&](Preset &preset, const std::vector<std::string> &slot_keys, int author_slot,
|
||||
const std::string &material_label) {
|
||||
for (const std::string &key : slot_keys) {
|
||||
const std::string base_key = key.substr(0, key.find('#'));
|
||||
if (structural_keys.count(base_key) != 0)
|
||||
continue;
|
||||
const ConfigOption *src_opt = config.option(base_key);
|
||||
if (src_opt == nullptr || !src_opt->is_vector() ||
|
||||
author_slot < 0 || author_slot >= static_cast<int>(static_cast<const ConfigOptionVectorBase*>(src_opt)->size())) {
|
||||
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
|
||||
continue;
|
||||
}
|
||||
ConfigOption *dst_opt = preset.config.option(base_key);
|
||||
if (dst_opt == nullptr || !dst_opt->is_vector() ||
|
||||
static_cast<const ConfigOptionVectorBase*>(dst_opt)->empty() ||
|
||||
dst_opt->type() != src_opt->type()) {
|
||||
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
|
||||
continue;
|
||||
}
|
||||
// Per-slot scalar copy: the receiver's filament preset holds a single
|
||||
// value per key (vector of size 1), the file holds the per-slot vector.
|
||||
static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, 0, author_slot);
|
||||
material_applied = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
||||
if (!entry.full && !entry.publish_type && !entry.publish_color)
|
||||
continue; // legacy entry, handled above
|
||||
if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size())
|
||||
continue; // out of range: nothing to do for this slot
|
||||
const size_t slot = size_t(entry.slot);
|
||||
// Modify the stored preset itself (real=true), never the edited snapshot:
|
||||
// find_preset would return &m_edited_preset for the currently selected slot,
|
||||
// and the re-select at the end of this block re-snapshots from the stored
|
||||
// preset, silently discarding any values applied to the snapshot.
|
||||
Preset *recv = this->filaments.find_preset(this->filament_presets[slot], false, true);
|
||||
if (recv == nullptr)
|
||||
continue;
|
||||
|
||||
const std::string material_label = entry.filament_id.empty()
|
||||
? (entry.publish_type_value.empty() ? entry.filament_type : entry.publish_type_value)
|
||||
: entry.filament_id;
|
||||
|
||||
bool apply_slot = true;
|
||||
if (entry.publish_type && !entry.publish_type_value.empty()) {
|
||||
const std::string recv_type = normalize_filament_type(recv->config.opt_string("filament_type", 0u));
|
||||
if (recv_type == entry.publish_type_value) {
|
||||
// Type match: the receiver keeps its material. A full dump is
|
||||
// intentionally ignored for this slot; partial keys still apply.
|
||||
if (entry.full)
|
||||
apply_slot = false;
|
||||
} else {
|
||||
// Type mismatch: replace the slot with the first visible same-type
|
||||
// filament from the receiver's library.
|
||||
std::string replacement;
|
||||
for (size_t i = 0; i < this->filaments.size(); ++i) {
|
||||
const Preset &candidate = this->filaments.preset(i);
|
||||
if (!candidate.is_visible)
|
||||
continue;
|
||||
if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) != entry.publish_type_value)
|
||||
continue;
|
||||
replacement = candidate.name;
|
||||
break;
|
||||
}
|
||||
if (!replacement.empty()) {
|
||||
const std::string old_name = recv->name;
|
||||
this->filament_presets[slot] = replacement;
|
||||
recv = this->filaments.find_preset(replacement, false, true);
|
||||
material_applied = true;
|
||||
published_config->material_replacements.emplace_back(
|
||||
"slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement);
|
||||
} else if (entry.full) {
|
||||
// No library match: create a temporary project-embedded custom preset
|
||||
// populated with default settings and overlaid with the author's values.
|
||||
std::string custom_name = entry.publish_type_value + " (Published)";
|
||||
for (size_t idx = 1; this->filaments.find_preset(custom_name, false) != nullptr; ++idx)
|
||||
custom_name = entry.publish_type_value + " (Published " + std::to_string(idx) + ")";
|
||||
|
||||
// Capture the slot's current name BEFORE load_preset: the custom
|
||||
// name sorts ahead of the slot's material, so the deque insertion
|
||||
// relocates it and recv would dangle after the call.
|
||||
const std::string old_name = recv->name;
|
||||
|
||||
DynamicPrintConfig custom_cfg = this->filaments.default_preset_for(config).config;
|
||||
// filament_type is a per-slot vector option: set it via the strings
|
||||
// accessor. opt_string(key, bool) would ask for the scalar
|
||||
// ConfigOptionString, fail the cast and dereference nullptr.
|
||||
if (ConfigOptionStrings *type_opt = custom_cfg.opt<ConfigOptionStrings>("filament_type", true)) {
|
||||
if (type_opt->values.empty())
|
||||
type_opt->values.emplace_back();
|
||||
type_opt->values[0] = entry.publish_type_value;
|
||||
}
|
||||
if (ConfigOptionStrings *id_opt = custom_cfg.opt<ConfigOptionStrings>("filament_settings_id", true))
|
||||
if (!id_opt->values.empty())
|
||||
id_opt->values[0] = custom_name;
|
||||
|
||||
Preset &created = this->filaments.load_preset("", custom_name, std::move(custom_cfg), false, file_version);
|
||||
created.is_project_embedded = true;
|
||||
created.is_visible = true;
|
||||
|
||||
this->filament_presets[slot] = custom_name;
|
||||
recv = &created;
|
||||
material_applied = true;
|
||||
published_config->material_replacements.emplace_back(
|
||||
"slot " + std::to_string(slot) + ": " + old_name + " -> " + custom_name);
|
||||
} else {
|
||||
// Partial publish with no replacement available: keep the
|
||||
// receiver's material and report this slot's keys as skipped.
|
||||
for (const std::string &key : entry.keys)
|
||||
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
|
||||
apply_slot = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Colour is slot-scoped and independent of the type gate: it is applied to
|
||||
// whichever material ends up in the slot (original, replacement or the
|
||||
// in-memory fallback), and synced into project_config for GUI rendering.
|
||||
if (entry.publish_color && !entry.color.empty()) {
|
||||
if (recv != nullptr) {
|
||||
// Create the key when the target preset lacks it (e.g. a replacement
|
||||
// built from the static defaults): the colour is a requirement, not
|
||||
// an optional override.
|
||||
if (ConfigOptionStrings *colour = recv->config.opt<ConfigOptionStrings>("filament_colour", true)) {
|
||||
if (colour->values.empty())
|
||||
colour->values.emplace_back();
|
||||
colour->values[0] = entry.color;
|
||||
material_applied = true;
|
||||
}
|
||||
}
|
||||
if (ConfigOptionStrings *proj_colour = this->project_config.opt<ConfigOptionStrings>("filament_colour")) {
|
||||
if (slot < proj_colour->values.size())
|
||||
proj_colour->values[slot] = entry.color;
|
||||
}
|
||||
if (ConfigOptionStrings *proj_multi_colour = this->project_config.opt<ConfigOptionStrings>("filament_multi_colour")) {
|
||||
if (slot < proj_multi_colour->values.size())
|
||||
proj_multi_colour->values[slot] = entry.color;
|
||||
}
|
||||
}
|
||||
|
||||
if (apply_slot && recv != nullptr)
|
||||
apply_slot_keys(*recv, entry.full ? entry.full_keys : entry.keys, entry.slot, material_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string &key : published_config->published_keys) {
|
||||
if (applied_keys.count(key) != 0)
|
||||
continue;
|
||||
@@ -5021,6 +5232,14 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
||||
skipped_keys.emplace_back(key);
|
||||
}
|
||||
published_config->skipped_keys = std::move(skipped_keys);
|
||||
|
||||
// The material overlay above modified the filament collection presets in place, but
|
||||
// the edited preset (what the GUI displays) is a snapshot taken when the preset was
|
||||
// last selected. Re-select the first slot's filament (mirroring a normal project load)
|
||||
// so the applied values (colour, type, keys and slot replacements) surface in the GUI;
|
||||
// selecting any other slot's filament afterwards snapshots its modified preset too.
|
||||
if (material_applied && !this->filament_presets.empty())
|
||||
this->filaments.select_preset_by_name(this->filament_presets.front(), true);
|
||||
}
|
||||
|
||||
//BBS
|
||||
|
||||
@@ -181,6 +181,9 @@ struct PublishedConfig
|
||||
// Keys that could not be applied (missing on the user's machine or vector size mismatch),
|
||||
// filled in by load_config_file_config for notification purposes.
|
||||
std::vector<std::string> skipped_keys;
|
||||
// Human-readable notices of the slot material replacements performed while loading a
|
||||
// published project (e.g. "Slot 2: replaced PETG with PLA"), for the load notification.
|
||||
std::vector<std::string> material_replacements;
|
||||
};
|
||||
|
||||
// Bundle of Print + Filament + Printer presets.
|
||||
|
||||
@@ -3,11 +3,31 @@
|
||||
#include "PresetBundle.hpp"
|
||||
#include "Preset.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::string normalize_filament_type(const std::string& type)
|
||||
{
|
||||
if (type.empty())
|
||||
return type;
|
||||
if (MaterialType::find(type) != nullptr)
|
||||
return type;
|
||||
// "PLA High Speed" -> "PLA": strip a space-separated modifier, but keep dash-separated
|
||||
// types like "PA-CF" / "PETG-CF" intact (they are distinct materials, not modifiers).
|
||||
const size_t sep = type.find(' ');
|
||||
if (sep != std::string::npos) {
|
||||
const std::string base = type.substr(0, sep);
|
||||
if (MaterialType::find(base) != nullptr)
|
||||
return base;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
const std::set<std::string>& publish_structural_keys()
|
||||
{
|
||||
// Structural / non-publishable keys. The *_settings_id keys are also part of
|
||||
@@ -111,6 +131,13 @@ DynamicPrintConfig filter_published_config(
|
||||
DynamicPrintConfig filtered;
|
||||
|
||||
std::set<std::string> base_keys_to_include;
|
||||
// Base keys that must never be masked: identity, plate geometry, process/printer keys and
|
||||
// partially-published material keys keep today's whole-vector serialization (all slots).
|
||||
std::set<std::string> mask_exempt_keys;
|
||||
// For keys carried only by "full" entries: base key -> author slots whose values must
|
||||
// survive; the other slots are masked to their defaults so a full publish does not leak
|
||||
// the author's unrelated slot data.
|
||||
std::map<std::string, std::set<int>> full_slot_map;
|
||||
|
||||
// 1. Mandatory material identity & slot count keys for 3MF validation/normalization
|
||||
static const std::vector<std::string> s_material_identity_keys = {
|
||||
@@ -122,8 +149,10 @@ DynamicPrintConfig filter_published_config(
|
||||
"filament_self_index",
|
||||
"filament_extruder_variant"
|
||||
};
|
||||
for (const std::string &key : s_material_identity_keys)
|
||||
for (const std::string &key : s_material_identity_keys) {
|
||||
base_keys_to_include.insert(key);
|
||||
mask_exempt_keys.insert(key);
|
||||
}
|
||||
|
||||
// 2. Published plate / bed geometry keys (wipe tower positioning)
|
||||
static const std::vector<std::string> s_plate_geometry_keys = {
|
||||
@@ -131,29 +160,69 @@ DynamicPrintConfig filter_published_config(
|
||||
"wipe_tower_y",
|
||||
"wipe_tower_rotation_angle"
|
||||
};
|
||||
for (const std::string &key : s_plate_geometry_keys)
|
||||
for (const std::string &key : s_plate_geometry_keys) {
|
||||
base_keys_to_include.insert(key);
|
||||
mask_exempt_keys.insert(key);
|
||||
}
|
||||
|
||||
// 3. Process and printer published keys
|
||||
for (const std::string &key : published_keys) {
|
||||
const std::string base_key = key.substr(0, key.find('#'));
|
||||
if (!base_key.empty())
|
||||
if (!base_key.empty()) {
|
||||
base_keys_to_include.insert(base_key);
|
||||
mask_exempt_keys.insert(base_key);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Material-specific published keys
|
||||
for (const PublishedMaterialEntry &entry : material_keys) {
|
||||
for (const std::string &key : entry.keys) {
|
||||
const std::string base_key = key.substr(0, key.find('#'));
|
||||
if (!base_key.empty())
|
||||
if (!base_key.empty()) {
|
||||
base_keys_to_include.insert(base_key);
|
||||
mask_exempt_keys.insert(base_key);
|
||||
}
|
||||
}
|
||||
// 4b. "Full publish" entries carry the entire slot; the values of the covered keys are
|
||||
// masked to the author's slot on export (see the copy loop below).
|
||||
for (const std::string &key : entry.full_keys) {
|
||||
const std::string base_key = key.substr(0, key.find('#'));
|
||||
if (base_key.empty())
|
||||
continue;
|
||||
base_keys_to_include.insert(base_key);
|
||||
if (entry.slot >= 0)
|
||||
full_slot_map[base_key].insert(entry.slot);
|
||||
}
|
||||
}
|
||||
|
||||
// Mask a vector option's slots that are not author-published: copy the option default over
|
||||
// each non-published index. Keys without an option default are left unmasked (the file then
|
||||
// carries the whole vector, matching the partial-publish behavior).
|
||||
auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set<int> &keep_slots) {
|
||||
auto *vec = dynamic_cast<ConfigOptionVectorBase*>(&opt);
|
||||
if (vec == nullptr || vec->size() == 0 || def == nullptr || !def->default_value)
|
||||
return;
|
||||
if (def->default_value->type() != opt.type())
|
||||
return;
|
||||
const auto *default_vec = dynamic_cast<const ConfigOptionVectorBase*>(def->default_value.get());
|
||||
if (default_vec == nullptr || default_vec->empty())
|
||||
return;
|
||||
for (size_t idx = 0; idx < vec->size(); ++idx)
|
||||
if (keep_slots.count(static_cast<int>(idx)) == 0)
|
||||
vec->set_at(def->default_value.get(), idx, 0);
|
||||
};
|
||||
|
||||
// Copy selected options from full_config into filtered config
|
||||
for (const std::string &key : base_keys_to_include) {
|
||||
if (const ConfigOption *opt = full_config.option(key))
|
||||
filtered.set_key_value(key, opt->clone());
|
||||
if (const ConfigOption *opt = full_config.option(key)) {
|
||||
ConfigOption *cloned = opt->clone();
|
||||
if (mask_exempt_keys.count(key) == 0) {
|
||||
const auto it = full_slot_map.find(key);
|
||||
if (it != full_slot_map.end() && !it->second.empty())
|
||||
mask_slots(*cloned, print_config_def.get(key), it->second);
|
||||
}
|
||||
filtered.set_key_value(key, cloned);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
|
||||
@@ -52,8 +52,30 @@ struct PublishedMaterialEntry {
|
||||
// entries apply to every matching receiver preset.
|
||||
int slot{-1};
|
||||
std::vector<std::string> keys;
|
||||
// "Full Publish": the entire filament preset of this slot is serialized (see full_keys),
|
||||
// not just the individually selected keys. On load the type gate (publish_type_value)
|
||||
// decides whether the receiver keeps its material (type match) or is replaced; a full
|
||||
// entry carries no partial keys.
|
||||
bool full{false};
|
||||
// All non-structural filament keys of the author's slot preset, present when full is true.
|
||||
// Values travel in the file config, masked to the author's slot index.
|
||||
std::vector<std::string> full_keys;
|
||||
// Vendor-agnostic, curated (MaterialType) filament type the author requires for this slot.
|
||||
// On load the receiver's slot material is matched against it; on mismatch the slot is
|
||||
// replaced with a same-type filament from the receiver's library.
|
||||
bool publish_type{false};
|
||||
std::string publish_type_value;
|
||||
// Required filament colour for this slot, applied on load regardless of the type match.
|
||||
bool publish_color{false};
|
||||
std::string color;
|
||||
};
|
||||
|
||||
// Normalizes a filament type string against the curated MaterialType list: an exact match
|
||||
// wins, then the value is stripped after its first space ("PLA High Speed" -> "PLA"); a
|
||||
// value still not recognized is returned unchanged. Shared by the Publish dialog's type row
|
||||
// default and by the published-3MF loader's type matching.
|
||||
std::string normalize_filament_type(const std::string& type);
|
||||
|
||||
// Constructs a minimal DynamicPrintConfig for a published 3MF export containing only the
|
||||
// author-selected published keys, material keys, material identity fields, and plate geometry keys.
|
||||
class DynamicPrintConfig;
|
||||
|
||||
Reference in New Issue
Block a user