mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-13 12:07:41 +00:00
Published 3MF: import Full Publish materials as standalone detached presets inside the project
This commit is contained in:
@@ -3053,6 +3053,81 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
|
|||||||
this->get_selected_preset().save(nullptr);
|
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 "<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)
|
||||||
|
{
|
||||||
|
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<ConfigOptionString>("print_settings_id", true)->value = final_name;
|
||||||
|
else if (m_type == Preset::TYPE_FILAMENT)
|
||||||
|
preset.config.option<ConfigOptionStrings>("filament_settings_id", true)->values[0] = final_name;
|
||||||
|
else if (m_type == Preset::TYPE_PRINTER)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
bool PresetCollection::delete_current_preset()
|
bool PresetCollection::delete_current_preset()
|
||||||
{
|
{
|
||||||
Preset &selected = this->get_selected_preset();
|
Preset &selected = this->get_selected_preset();
|
||||||
|
|||||||
@@ -631,6 +631,24 @@ public:
|
|||||||
// All presets are marked as not modified and the new preset is activated.
|
// All presets are marked as not modified and the new preset is activated.
|
||||||
//BBS: add project embedded preset logic
|
//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);
|
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:
|
||||||
|
// "<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);
|
||||||
|
|
||||||
// Delete the current preset, activate the first visible preset.
|
// Delete the current preset, activate the first visible preset.
|
||||||
// returns true if the preset was deleted successfully.
|
// returns true if the preset was deleted successfully.
|
||||||
|
|||||||
@@ -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.)
|
// slots wrote to it; that compound case is not chased.)
|
||||||
const bool edited_survives_load = this->filament_presets.empty() ||
|
const bool edited_survives_load = this->filament_presets.empty() ||
|
||||||
this->filament_presets.front() == this->filaments.get_edited_preset().name;
|
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<std::string, std::string> published_full_dedup;
|
||||||
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
for (const PublishedMaterialEntry &entry : published_config->material_keys) {
|
||||||
if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size())
|
if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size())
|
||||||
continue; // out of range: nothing to do for this slot
|
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.publish_type_value.empty() ? entry.filament_type : entry.publish_type_value)
|
||||||
: entry.filament_id;
|
: 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<int>(static_cast<const ConfigOptionVectorBase*>(src_opt)->size()))
|
||||||
|
continue;
|
||||||
|
ConfigOption *dst_opt = new_cfg.option(base_key);
|
||||||
|
if (dst_opt == nullptr || !dst_opt->is_vector() ||
|
||||||
|
static_cast<const ConfigOptionVectorBase*>(dst_opt)->empty() ||
|
||||||
|
dst_opt->type() != src_opt->type())
|
||||||
|
continue;
|
||||||
|
static_cast<ConfigOptionVectorBase*>(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<ConfigOptionStrings>("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<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;
|
||||||
|
}
|
||||||
|
} 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<ConfigOptionStrings>("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<const ConfigOptionStrings*>(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;
|
bool apply_slot = true;
|
||||||
// The gate compares against the slot's effective material type: the edited
|
// The gate compares against the slot's effective material type: the edited
|
||||||
// layer when the slot references the collection's edited preset and that
|
// layer when the slot references the collection's edited preset and that
|
||||||
|
|||||||
@@ -6,12 +6,19 @@
|
|||||||
#include "MaterialType.hpp"
|
#include "MaterialType.hpp"
|
||||||
|
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <boost/algorithm/string/trim.hpp>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
std::string publish_base_key(const std::string &key)
|
||||||
|
{
|
||||||
|
const size_t pos = key.find('#');
|
||||||
|
return pos == std::string::npos ? key : key.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
std::string normalize_filament_type(const std::string& type)
|
std::string normalize_filament_type(const std::string& type)
|
||||||
{
|
{
|
||||||
if (type.empty())
|
if (type.empty())
|
||||||
@@ -29,6 +36,32 @@ std::string normalize_filament_type(const std::string& type)
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void make_publish_universal(DynamicPrintConfig &config)
|
||||||
|
{
|
||||||
|
// Lists: empty => compatible with every printer / every print preset. Conditions:
|
||||||
|
// empty so a leftover expression left behind by the baseline clone can never
|
||||||
|
// re-narrow the match (see is_compatible_with_printer, Preset.cpp:840). All four
|
||||||
|
// keys exist on filament presets; nil-guard for hand-crafted future schemas.
|
||||||
|
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_printers", false))
|
||||||
|
opt->values.clear();
|
||||||
|
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_prints", false))
|
||||||
|
opt->values.clear();
|
||||||
|
if (auto *opt = config.opt<ConfigOptionString>("compatible_printers_condition", false))
|
||||||
|
opt->value.clear();
|
||||||
|
if (auto *opt = config.opt<ConfigOptionString>("compatible_prints_condition", false))
|
||||||
|
opt->value.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string publish_material_base_name(const std::string &preset_name)
|
||||||
|
{
|
||||||
|
if (preset_name.empty())
|
||||||
|
return preset_name;
|
||||||
|
const size_t at = preset_name.find('@');
|
||||||
|
std::string base = (at == std::string::npos) ? preset_name : preset_name.substr(0, at);
|
||||||
|
boost::trim_right(base);
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
const std::set<std::string>& publish_structural_keys()
|
const std::set<std::string>& publish_structural_keys()
|
||||||
{
|
{
|
||||||
// Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty
|
// Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ namespace Slic3r {
|
|||||||
class PresetBundle;
|
class PresetBundle;
|
||||||
|
|
||||||
// Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length").
|
// Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length").
|
||||||
inline std::string publish_base_key(const std::string &key)
|
std::string publish_base_key(const std::string &key);
|
||||||
{
|
|
||||||
const size_t pos = key.find('#');
|
|
||||||
return pos == std::string::npos ? key : key.substr(0, pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Structural keys that are never applied onto the receiver's presets when loading a published
|
// 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
|
// 3MF (single source of truth for the denylist): applying them would rewrite the user's preset
|
||||||
@@ -56,14 +52,20 @@ struct PublishedMaterialEntry {
|
|||||||
// 0-based author filament slot; -1 (hand-crafted files) is skipped.
|
// 0-based author filament slot; -1 (hand-crafted files) is skipped.
|
||||||
int slot{-1};
|
int slot{-1};
|
||||||
std::vector<std::string> keys;
|
std::vector<std::string> keys;
|
||||||
// "Full Publish": serialize the whole filament preset (full_keys); the type gate then
|
// "Full Publish": the whole filament preset (full_keys) is published. On the receiver
|
||||||
// decides whether the receiver keeps its material (type match) or is replaced.
|
// 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).
|
||||||
bool full{false};
|
bool full{false};
|
||||||
// All non-structural filament keys of the author's slot preset; values travel in the file
|
// All non-structural filament keys of the author's slot preset; values travel in the file
|
||||||
// config, masked to the author's slot index.
|
// config, masked to the author's slot index.
|
||||||
std::vector<std::string> full_keys;
|
std::vector<std::string> full_keys;
|
||||||
// Vendor-agnostic (MaterialType) filament type the author requires for this slot; on
|
// Vendor-agnostic (MaterialType) filament type the author requires for this slot; on
|
||||||
// mismatch the slot is replaced with a same-type filament from the receiver's library.
|
// mismatch the slot is replaced with a same-type filament from the receiver's library.
|
||||||
|
// For Full entries the baked filament_type on the created copy satisfies the type gate.
|
||||||
bool publish_type{false};
|
bool publish_type{false};
|
||||||
std::string publish_type_value;
|
std::string publish_type_value;
|
||||||
// Required filament colour, applied on load regardless of the type match.
|
// Required filament colour, applied on load regardless of the type match.
|
||||||
@@ -74,9 +76,21 @@ struct PublishedMaterialEntry {
|
|||||||
// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
|
// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
|
||||||
std::string normalize_filament_type(const std::string& type);
|
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).
|
||||||
|
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".
|
||||||
|
std::string publish_material_base_name(const std::string &preset_name);
|
||||||
|
|
||||||
// Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys,
|
// Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys,
|
||||||
// material keys, identity fields and plate geometry keys.
|
// material keys, identity fields and plate geometry keys.
|
||||||
class DynamicPrintConfig;
|
|
||||||
DynamicPrintConfig filter_published_config(
|
DynamicPrintConfig filter_published_config(
|
||||||
const DynamicPrintConfig &full_config,
|
const DynamicPrintConfig &full_config,
|
||||||
const std::vector<std::string> &published_keys,
|
const std::vector<std::string> &published_keys,
|
||||||
|
|||||||
Reference in New Issue
Block a user