mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 02:57:39 +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;
|
||||
|
||||
@@ -174,6 +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") },
|
||||
// File>Import
|
||||
{ ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") },
|
||||
// File>Export
|
||||
|
||||
@@ -741,6 +741,10 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
|
||||
if (m_plater) { m_plater->add_file(); }
|
||||
return;
|
||||
}
|
||||
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'E') {
|
||||
if (can_export_model()) publish_project();
|
||||
return;
|
||||
}
|
||||
evt.Skip();
|
||||
});
|
||||
|
||||
@@ -1728,6 +1732,16 @@ bool MainFrame::save_project_as(const wxString& filename)
|
||||
return ret;
|
||||
}
|
||||
|
||||
void MainFrame::publish_project()
|
||||
{
|
||||
if (m_plater == nullptr)
|
||||
return;
|
||||
PublishSettingsDialog dlg(this);
|
||||
if (dlg.ShowModal() != wxID_OK)
|
||||
return;
|
||||
m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
|
||||
}
|
||||
|
||||
bool MainFrame::can_upload() const
|
||||
{
|
||||
return true;
|
||||
@@ -2820,19 +2834,14 @@ void MainFrame::init_menubar_as_editor()
|
||||
|
||||
// BBS: publish
|
||||
fileMenu->AppendSeparator();
|
||||
auto publish_handler = [this](wxCommandEvent&) {
|
||||
if (!m_plater) return;
|
||||
PublishSettingsDialog dlg(this);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
|
||||
};
|
||||
auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
|
||||
|
||||
#ifndef __APPLE__
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots, _L("Export a 3MF file with the selected settings embedded"),
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
|
||||
publish_handler, "menu_publish", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
#else
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots, _L("Export a 3MF file with the selected settings embedded"),
|
||||
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
|
||||
publish_handler, "", nullptr,
|
||||
[this](){return can_export_model(); }, this);
|
||||
#endif
|
||||
|
||||
@@ -340,6 +340,8 @@ public:
|
||||
bool can_upload() const;
|
||||
void save_project();
|
||||
bool save_project_as(const wxString& filename = wxString());
|
||||
// Open the Publish dialog and export the selected settings as a published 3MF.
|
||||
void publish_project();
|
||||
|
||||
void add_to_recent_projects(const wxString& filename);
|
||||
void get_recent_projects(boost::property_tree::wptree &tree, int images);
|
||||
|
||||
@@ -5449,7 +5449,7 @@ struct Plater::priv
|
||||
BoundingBox scaled_bed_shape_bb() const;
|
||||
|
||||
// BBS: backup & restore
|
||||
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false);
|
||||
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false, bool* published_out = nullptr);
|
||||
std::vector<size_t> load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true);
|
||||
|
||||
fs::path get_export_file_path(GUI::FileType file_type);
|
||||
@@ -6759,7 +6759,7 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
|
||||
}
|
||||
|
||||
// BBS: backup & restore
|
||||
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi)
|
||||
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out)
|
||||
{
|
||||
std::vector<size_t> empty_result;
|
||||
bool dlg_cont = true;
|
||||
@@ -7257,6 +7257,22 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
for (const auto &k : *entry_keys_it)
|
||||
if (k.is_string())
|
||||
entry.keys.emplace_back(k.get<std::string>());
|
||||
// Filament-publishing-v2 fields; absent in legacy files.
|
||||
if (m.contains("full") && m["full"].is_boolean())
|
||||
entry.full = m["full"].get<bool>();
|
||||
const auto entry_full_keys_it = m.find("full_keys");
|
||||
if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array())
|
||||
for (const auto &k : *entry_full_keys_it)
|
||||
if (k.is_string())
|
||||
entry.full_keys.emplace_back(k.get<std::string>());
|
||||
if (m.contains("publish_type") && m["publish_type"].is_boolean())
|
||||
entry.publish_type = m["publish_type"].get<bool>();
|
||||
if (m.contains("type") && m["type"].is_string())
|
||||
entry.publish_type_value = m["type"].get<std::string>();
|
||||
if (m.contains("publish_color") && m["publish_color"].is_boolean())
|
||||
entry.publish_color = m["publish_color"].get<bool>();
|
||||
if (m.contains("color") && m["color"].is_string())
|
||||
entry.color = m["color"].get<std::string>();
|
||||
published_config.material_keys.emplace_back(std::move(entry));
|
||||
}
|
||||
} catch (...) {
|
||||
@@ -7266,6 +7282,18 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: a "published" 3MF behaves like a new project once loaded: the file's path
|
||||
// must not become the project filename (Save/Ctrl-S would otherwise overwrite the
|
||||
// shared file), and the published metadata is consumed by the overlay above and
|
||||
// stripped so a later save produces a normal, unpublished 3MF.
|
||||
if (published_out != nullptr && published_config.published)
|
||||
*published_out = true;
|
||||
if (published_config.published && load_config && this->model.model_info != nullptr) {
|
||||
this->model.model_info->metadata_items.erase("published");
|
||||
this->model.model_info->metadata_items.erase("published_keys");
|
||||
this->model.model_info->metadata_items.erase("published_material_keys");
|
||||
}
|
||||
|
||||
if (load_config) {
|
||||
if (!config.empty()) {
|
||||
Preset::normalize(config);
|
||||
@@ -7380,6 +7408,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
notify_manager->bbl_show_3mf_warn_notification(message);
|
||||
}
|
||||
|
||||
// BBS: notify the user about slot materials that were replaced while
|
||||
// loading a published project (type mismatch / no same-type match).
|
||||
if (!published_config.material_replacements.empty()) {
|
||||
NotificationManager *notify_manager = q->get_notification_manager();
|
||||
std::string message = _u8L("Some filament slots were changed to match the published materials:");
|
||||
for (const std::string &replacement : published_config.material_replacements)
|
||||
message += "\n-" + replacement;
|
||||
notify_manager->bbl_show_3mf_warn_notification(message);
|
||||
}
|
||||
|
||||
ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type");
|
||||
if (bed_type_opt != nullptr) {
|
||||
BedType bed_type = (BedType)bed_type_opt->getInt();
|
||||
@@ -7519,7 +7557,13 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
dynamic_map->value = false;
|
||||
}
|
||||
// Update filament combobox after loading config
|
||||
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
|
||||
if (published_config.published) {
|
||||
q->update_filament_colors_in_full_config();
|
||||
wxGetApp().plater()->sidebar().update_all_preset_comboboxes();
|
||||
wxGetApp().plater()->sidebar().update_dynamic_filament_list();
|
||||
} else {
|
||||
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
|
||||
}
|
||||
// The loaded project supplies nozzle_volume_type; refresh the sidebar
|
||||
// nozzle-count badges against it.
|
||||
if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")) {
|
||||
@@ -13244,14 +13288,15 @@ void Plater::load_project(wxString const& filename2,
|
||||
if (strategy & LoadStrategy::Restore)
|
||||
input_paths.push_back(into_u8(originfile));
|
||||
|
||||
std::vector<size_t> res = load_files(input_paths, strategy);
|
||||
bool loaded_published = false;
|
||||
std::vector<size_t> res = load_files(input_paths, strategy, false, &loaded_published);
|
||||
|
||||
reset_project_dirty_initial_presets();
|
||||
update_project_dirty_from_presets();
|
||||
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
|
||||
|
||||
// if res is empty no data has been loaded
|
||||
if (!res.empty() && (load_restore || !(strategy & LoadStrategy::Silence))) {
|
||||
if (!res.empty() && !loaded_published && (load_restore || !(strategy & LoadStrategy::Silence))) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << (load_restore ? originfile : filename);
|
||||
p->set_project_filename(load_restore ? originfile : filename);
|
||||
if (load_restore && originfile.IsEmpty()) {
|
||||
@@ -13263,6 +13308,15 @@ void Plater::load_project(wxString const& filename2,
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
|
||||
p->set_project_filename(filename);
|
||||
}
|
||||
else if (loaded_published) {
|
||||
// A "published" 3MF loads as a new project: the shared file's path must not become
|
||||
// the project filename, so Save/Ctrl-S prompts for a destination instead of
|
||||
// overwriting the published file. reset() above already cleared the project name
|
||||
// and folder; restore the default new-project title and keep the file in recents.
|
||||
p->set_project_name(_L("Untitled"));
|
||||
if (!filename.IsEmpty())
|
||||
wxGetApp().mainframe->add_to_recent_projects(filename);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14918,12 +14972,12 @@ void Plater::force_update_all_plate_thumbnails()
|
||||
}
|
||||
|
||||
// BBS: backup
|
||||
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi) {
|
||||
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) {
|
||||
//BBS: wish to reset state when load a new file
|
||||
p->m_slice_all_only_has_gcode = false;
|
||||
//BBS: wish to reset all plates stats item selected state when load a new file
|
||||
p->preview->get_canvas3d()->reset_select_plate_toolbar_selection();
|
||||
return p->load_files(input_files, strategy, ask_multi);
|
||||
return p->load_files(input_files, strategy, ask_multi, published_out);
|
||||
}
|
||||
|
||||
// To be called when providing a list of files to the GUI slic3r on command line.
|
||||
@@ -16186,7 +16240,10 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
||||
j.push_back(key);
|
||||
nlohmann::json jm = nlohmann::json::array();
|
||||
for (const Slic3r::PublishedMaterialEntry& e : material_keys)
|
||||
jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}}}, {"slot", e.slot}, {"keys", e.keys} });
|
||||
jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}}}, {"slot", e.slot}, {"keys", e.keys},
|
||||
{"full", e.full}, {"full_keys", e.full_keys},
|
||||
{"publish_type", e.publish_type}, {"type", e.publish_type_value},
|
||||
{"publish_color", e.publish_color}, {"color", e.color} });
|
||||
|
||||
Model& model = this->model();
|
||||
// Remember the previous metadata state so it can be restored after the export, keeping the
|
||||
|
||||
@@ -383,7 +383,7 @@ public:
|
||||
bool preview_zip_archive(const boost::filesystem::path& archive_path);
|
||||
|
||||
// BBS: restore
|
||||
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
|
||||
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false, bool* published_out = nullptr);
|
||||
// To be called when providing a list of files to the GUI slic3r on command line.
|
||||
std::vector<size_t> load_files(const std::vector<std::string>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
|
||||
// to be called on drag and drop
|
||||
|
||||
@@ -290,6 +290,25 @@ void PublishSettingsDialog::build_option_model()
|
||||
const PublishMaterialIdentity identity = material_identity(slot, full);
|
||||
const wxString title = material_title(slot, bundle, full);
|
||||
const size_t category_index = category_index_for(title, Section::Material, "custom-gcode_filament", g, slot, identity);
|
||||
|
||||
// Filament-publishing-v2 rows: the author may require a filament colour and/or
|
||||
// a vendor-agnostic material type for this slot. They live in their own
|
||||
// optgroup so they stay visually separated from the setting rows.
|
||||
{
|
||||
const size_t req_sub = subcategory_index_for(category_index, _L("Material"), "custom-gcode_filament");
|
||||
std::string hex;
|
||||
if (const auto* colours = full.opt<ConfigOptionStrings>("filament_colour"))
|
||||
if (slot < colours->size())
|
||||
hex = colours->get_at(slot);
|
||||
add_row_ui("filament_colour", _L("Color"), from_u8(hex), wxString(), category_index, req_sub, RowKind::Color);
|
||||
std::string type;
|
||||
if (const auto* types = full.opt<ConfigOptionStrings>("filament_type"))
|
||||
if (slot < types->size())
|
||||
type = types->get_at(slot);
|
||||
add_row_ui("filament_type", _L("Type"), from_u8(normalize_filament_type(type)), wxString(), category_index, req_sub,
|
||||
RowKind::Type);
|
||||
}
|
||||
|
||||
// A material section must not repeat a key; the same key may
|
||||
// appear in other material sections - that is intended.
|
||||
std::set<std::string> material_added;
|
||||
@@ -371,6 +390,10 @@ void PublishSettingsDialog::build_option_model()
|
||||
dirty_base.insert(n == std::string::npos ? key : key.substr(0, n));
|
||||
}
|
||||
for (Row& row : m_rows) {
|
||||
// The Color/Type requirement rows are not "dirty overrides": they are never
|
||||
// auto-checked by the dirty pre-check.
|
||||
if (row.kind != RowKind::Setting)
|
||||
continue;
|
||||
std::string base = row.key.substr(0, row.key.find('#'));
|
||||
row.dirty = dirty_base.count(base) > 0;
|
||||
if (row.dirty) {
|
||||
@@ -379,24 +402,11 @@ void PublishSettingsDialog::build_option_model()
|
||||
}
|
||||
}
|
||||
|
||||
// Wire the inner-page tri-state headers: clicking a header toggles all its children;
|
||||
// toggling any child re-syncs its header. Bind by index so the lambdas stay
|
||||
// valid even if the vectors are reallocated later.
|
||||
for (size_t c = 0; c < m_categories.size(); ++c) {
|
||||
if (m_categories[c].master_check != nullptr)
|
||||
m_categories[c].master_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_master_toggle(c); });
|
||||
if (m_categories[c].header != nullptr)
|
||||
m_categories[c].header->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_category_toggle(c); });
|
||||
for (size_t r : m_categories[c].rows)
|
||||
m_rows[r].check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { update_all_headers(); });
|
||||
update_category_header(m_categories[c]);
|
||||
}
|
||||
|
||||
// Material pages start gated (master OFF): their rows and tri-state are
|
||||
// disabled until the author opts the material in.
|
||||
// Wire the "Full Publish" checkboxes: toggling one disables/enables the material's
|
||||
// rows. Bind by index so the lambda stays valid even if the vector is reallocated later.
|
||||
for (size_t c = 0; c < m_categories.size(); ++c)
|
||||
if (m_categories[c].section == Section::Material)
|
||||
on_master_toggle(c);
|
||||
if (m_categories[c].full_check != nullptr)
|
||||
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); });
|
||||
|
||||
// No filter is active at startup: every row matches until the user types.
|
||||
for (Row& row : m_rows)
|
||||
@@ -502,13 +512,13 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title,
|
||||
category.filament_color_chip = new wxStaticBitmap(category.page, wxID_ANY, *chip);
|
||||
header_sizer->Add(category.filament_color_chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
|
||||
}
|
||||
category.master_check = new wxCheckBox(category.page, wxID_ANY, title);
|
||||
category.master_check->SetFont(Label::Head_14);
|
||||
category.master_check->SetToolTip(_L("Export this material"));
|
||||
header_sizer->Add(category.master_check, 0, wxALIGN_CENTER_VERTICAL);
|
||||
category.header = new wxCheckBox(category.page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE);
|
||||
category.header->SetToolTip(_L("Select/deselect all settings in this material"));
|
||||
header_sizer->Add(category.header, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6));
|
||||
category.title_label = new wxStaticText(category.page, wxID_ANY, title);
|
||||
category.title_label->SetFont(Label::Head_14);
|
||||
header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
category.full_check = new wxCheckBox(category.page, wxID_ANY, _L("Full Publish"));
|
||||
category.full_check->SetFont(Label::Body_13);
|
||||
category.full_check->SetToolTip(_L("Embed the entire filament of this slot in the 3MF file"));
|
||||
header_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
|
||||
page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
|
||||
}
|
||||
|
||||
@@ -575,7 +585,8 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
|
||||
const wxString& value,
|
||||
const wxString& unit,
|
||||
size_t category_index,
|
||||
size_t subcategory_index)
|
||||
size_t subcategory_index,
|
||||
RowKind kind)
|
||||
{
|
||||
Category& category = m_categories[category_index];
|
||||
Row row;
|
||||
@@ -583,6 +594,7 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
|
||||
row.label = label;
|
||||
row.value = value;
|
||||
row.unit = unit;
|
||||
row.kind = kind;
|
||||
row.category = category.title;
|
||||
row.subcategory = category.subs[subcategory_index].title;
|
||||
row.section = category.section;
|
||||
@@ -595,82 +607,38 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
|
||||
Row& current = m_rows[row_index];
|
||||
current.check = new wxCheckBox(category.scroll, wxID_ANY, label);
|
||||
current.check->SetFont(Label::Body_13);
|
||||
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
|
||||
// The value is read-only text (incl. the Type row: the published type is the slot's
|
||||
// normalized type, the author cannot pick a different one here).
|
||||
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
|
||||
current.value_label->SetFont(Label::Body_13);
|
||||
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit);
|
||||
if (kind == RowKind::Color && !value.IsEmpty()) {
|
||||
if (wxBitmap* chip = get_extruder_color_icon(value.ToStdString(), "", FromDIP(12), FromDIP(12))) {
|
||||
current.color_chip = new wxStaticBitmap(category.scroll, wxID_ANY, *chip);
|
||||
row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
}
|
||||
}
|
||||
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
if (!unit.IsEmpty()) {
|
||||
current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit);
|
||||
current.unit_label->SetFont(Label::Body_13);
|
||||
current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
}
|
||||
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
|
||||
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
if (current.unit_label != nullptr)
|
||||
row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
|
||||
}
|
||||
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38));
|
||||
category.rows.push_back(row_index);
|
||||
category.subs[subcategory_index].rows.push_back(row_index);
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::on_category_toggle(size_t category_index)
|
||||
void PublishSettingsDialog::on_full_toggle(size_t category_index)
|
||||
{
|
||||
Category& cat = m_categories[category_index];
|
||||
// Defensive: a gated material header is disabled and cannot fire.
|
||||
if (cat.section == Section::Material && !cat.master)
|
||||
return;
|
||||
// A click on the header toggles between "all" and "none": if every child is
|
||||
// checked, uncheck all; otherwise check all.
|
||||
bool all_checked = true;
|
||||
cat.full = cat.full_check->GetValue();
|
||||
for (size_t r : cat.rows)
|
||||
if (!m_rows[r].check->GetValue()) {
|
||||
all_checked = false;
|
||||
break;
|
||||
}
|
||||
bool value = !all_checked;
|
||||
for (size_t r : cat.rows)
|
||||
m_rows[r].check->SetValue(value);
|
||||
update_all_headers();
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::on_master_toggle(size_t category_index)
|
||||
{
|
||||
Category& cat = m_categories[category_index];
|
||||
cat.master = cat.master_check->GetValue();
|
||||
for (size_t r : cat.rows)
|
||||
m_rows[r].check->Enable(cat.master);
|
||||
cat.header->Enable(cat.master);
|
||||
update_all_headers();
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::update_all_headers()
|
||||
{
|
||||
for (Category& category : m_categories)
|
||||
update_category_header(category);
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::update_category_header(Category& category)
|
||||
{
|
||||
if (category.header == nullptr)
|
||||
return;
|
||||
// A gated material section's tri-state must not reflect the preserved
|
||||
// (greyed-out) row values.
|
||||
if (category.section == Section::Material && !category.master) {
|
||||
category.header->Set3StateValue(wxCHK_UNCHECKED);
|
||||
return;
|
||||
}
|
||||
int checked = 0;
|
||||
for (size_t r : category.rows)
|
||||
if (m_rows[r].check->GetValue())
|
||||
++checked;
|
||||
|
||||
if (checked == 0)
|
||||
category.header->Set3StateValue(wxCHK_UNCHECKED);
|
||||
else if (checked == static_cast<int>(category.rows.size()))
|
||||
category.header->Set3StateValue(wxCHK_CHECKED);
|
||||
else
|
||||
category.header->Set3StateValue(wxCHK_UNDETERMINED);
|
||||
m_rows[r].check->Enable(!cat.full);
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
|
||||
@@ -845,7 +813,6 @@ void PublishSettingsDialog::select_all(bool value)
|
||||
for (Row& row : m_rows)
|
||||
if (row.check->IsEnabled())
|
||||
row.check->SetValue(value);
|
||||
update_all_headers();
|
||||
}
|
||||
|
||||
bool PublishSettingsDialog::row_is_visible(const Row& row) const
|
||||
@@ -876,9 +843,8 @@ void PublishSettingsDialog::select_visible(bool value)
|
||||
// re-enters apply_filter() - that is fine, the rows above were already
|
||||
// toggled and the trailing call below is idempotent.
|
||||
m_filter_ctrl->ChangeValue("");
|
||||
apply_filter(""); // resync visibility, headers and the All/None bar
|
||||
apply_filter(""); // resync visibility and the All/None bar
|
||||
}
|
||||
update_all_headers();
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::show_menu(wxMouseEvent& evt)
|
||||
@@ -942,24 +908,67 @@ std::vector<Slic3r::PublishedMaterialEntry> PublishSettingsDialog::GetPublishedM
|
||||
{
|
||||
std::vector<Slic3r::PublishedMaterialEntry> out;
|
||||
for (const Category& cat : m_categories) {
|
||||
// Only opted-in materials export their keys.
|
||||
if (cat.section != Section::Material || !cat.master)
|
||||
if (cat.section != Section::Material)
|
||||
continue;
|
||||
Slic3r::PublishedMaterialEntry entry;
|
||||
entry.filament_type = cat.filament_type;
|
||||
entry.filament_vendor = cat.filament_vendor;
|
||||
entry.filament_id = cat.filament_id;
|
||||
entry.slot = static_cast<int>(cat.filament_slot);
|
||||
for (size_t r : cat.rows)
|
||||
if (m_rows[r].check->GetValue())
|
||||
entry.keys.push_back(m_rows[r].key);
|
||||
// A section without any checked key carries no information for the writer.
|
||||
if (!entry.keys.empty())
|
||||
// "Full Publish": the entire filament preset of the slot is embedded; type and color
|
||||
// are implicitly published, and the per-key rows are disabled and their state is ignored.
|
||||
if (cat.full_check != nullptr && cat.full_check->GetValue()) {
|
||||
entry.full = true;
|
||||
entry.full_keys = full_keys_for_slot();
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = normalize_filament_type(cat.filament_type);
|
||||
for (size_t r : cat.rows) {
|
||||
const Row& row = m_rows[r];
|
||||
if (row.kind == RowKind::Color && !row.value.IsEmpty()) {
|
||||
entry.publish_color = true;
|
||||
entry.color = row.value.ToStdString();
|
||||
}
|
||||
}
|
||||
out.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
for (size_t r : cat.rows) {
|
||||
const Row& row = m_rows[r];
|
||||
if (!row.check->GetValue())
|
||||
continue;
|
||||
if (row.kind == RowKind::Color) {
|
||||
entry.publish_color = true;
|
||||
entry.color = row.value.ToStdString();
|
||||
} else if (row.kind == RowKind::Type) {
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = row.value.ToStdString();
|
||||
} else {
|
||||
entry.keys.push_back(row.key);
|
||||
}
|
||||
}
|
||||
// A material with only setting keys but none checked, or with nothing selected at all,
|
||||
// carries no information for the writer.
|
||||
if (!entry.keys.empty() || entry.publish_type || entry.publish_color)
|
||||
out.push_back(std::move(entry));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> PublishSettingsDialog::full_keys_for_slot() const
|
||||
{
|
||||
// The canonical filament preset keys, minus the structural keys the published overlay must
|
||||
// never touch (inherits, compatibility, *_settings_id, ...), plus filament_colour (not a
|
||||
// member of Preset::filament_options). The values travel in the exported config, masked to
|
||||
// this slot, and are applied on load onto the receiver's slot.
|
||||
const std::set<std::string>& denylist = publish_structural_keys();
|
||||
std::vector<std::string> keys;
|
||||
for (const std::string& key : Preset::filament_options())
|
||||
if (denylist.count(key) == 0)
|
||||
keys.emplace_back(key);
|
||||
keys.emplace_back("filament_colour");
|
||||
return keys;
|
||||
}
|
||||
|
||||
void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
{
|
||||
// Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint themselves.
|
||||
@@ -974,10 +983,10 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
cat.icon_bmp.msw_rescale();
|
||||
cat.icon->SetBitmap(cat.icon_bmp.bmp());
|
||||
}
|
||||
if (cat.header != nullptr)
|
||||
cat.header->Refresh();
|
||||
if (cat.master_check != nullptr)
|
||||
cat.master_check->Refresh();
|
||||
if (cat.full_check != nullptr)
|
||||
cat.full_check->Refresh();
|
||||
if (cat.title_label != nullptr)
|
||||
cat.title_label->Refresh();
|
||||
if (cat.filament_color_chip != nullptr) {
|
||||
std::string hex;
|
||||
const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
|
||||
@@ -994,6 +1003,14 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
for (SectionGroup& section : m_sections)
|
||||
section.tabs->Rescale();
|
||||
|
||||
// Refresh the per-row Color chips at the new DPI.
|
||||
for (Row& row : m_rows) {
|
||||
if (row.color_chip != nullptr && !row.value.IsEmpty()) {
|
||||
if (wxBitmap* chip = get_extruder_color_icon(row.value.ToStdString(), "", FromDIP(12), FromDIP(12)))
|
||||
row.color_chip->SetBitmap(*chip);
|
||||
}
|
||||
}
|
||||
|
||||
const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
|
||||
for (size_t category_index = 0; category_index < m_categories.size(); ++category_index) {
|
||||
const Category& category = m_categories[category_index];
|
||||
|
||||
@@ -59,6 +59,11 @@ private:
|
||||
// One selectable setting row: a checkbox (setting name) plus a value label
|
||||
// and a (optional) grey unit label. key is the full config key and may carry
|
||||
// a "#N" variant suffix (print/printer rows); material rows carry the base key.
|
||||
enum class RowKind {
|
||||
Setting, // a regular setting key
|
||||
Color, // material colour requirement (filament_colour)
|
||||
Type, // material type requirement (read-only text)
|
||||
};
|
||||
struct Row
|
||||
{
|
||||
std::string key;
|
||||
@@ -69,6 +74,7 @@ private:
|
||||
wxString unit;
|
||||
wxString section_title; // outer tab title, for filter matching
|
||||
Section section{Section::Print};
|
||||
RowKind kind{RowKind::Setting};
|
||||
size_t outer_index{0};
|
||||
size_t inner_index{0};
|
||||
size_t subcategory_index{0};
|
||||
@@ -77,6 +83,7 @@ private:
|
||||
wxCheckBox* check{nullptr};
|
||||
wxStaticText* value_label{nullptr};
|
||||
wxStaticText* unit_label{nullptr};
|
||||
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
|
||||
wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in its tab list sizer
|
||||
};
|
||||
|
||||
@@ -89,7 +96,7 @@ private:
|
||||
std::vector<size_t> rows;
|
||||
};
|
||||
|
||||
// An inner TabCtrl page with its select-all/material controls and content.
|
||||
// An inner TabCtrl page with its material controls and content.
|
||||
struct Category
|
||||
{
|
||||
wxString title;
|
||||
@@ -106,11 +113,11 @@ private:
|
||||
ScalableBitmap icon_bmp; // scalable bitmap for DPI changes
|
||||
wxStaticBitmap* icon{nullptr};
|
||||
wxStaticBitmap* filament_color_chip{nullptr};
|
||||
wxCheckBox* header{nullptr}; // material select-all tri-state; null for Printer/Process
|
||||
// Material opt-in: the master checkbox carries the material title and
|
||||
// gates whether this material's keys may be exported.
|
||||
bool master{false};
|
||||
wxCheckBox* master_check{nullptr};
|
||||
wxStaticText* title_label{nullptr}; // material title (static text, Full Publish carries the label elsewhere)
|
||||
// "Full Publish": serializing the entire filament preset of this slot. While checked,
|
||||
// the slot's rows (incl. Color/Type) are disabled.
|
||||
bool full{false};
|
||||
wxCheckBox* full_check{nullptr};
|
||||
// Material identity, only for Section::Material categories.
|
||||
std::string filament_type;
|
||||
std::string filament_vendor;
|
||||
@@ -118,7 +125,7 @@ private:
|
||||
// The author's 0-based filament slot this material section represents.
|
||||
size_t filament_slot{0};
|
||||
std::vector<Subcategory> subs;
|
||||
std::vector<size_t> rows; // flattened rows, for the tri-state math
|
||||
std::vector<size_t> rows; // flattened rows of this category
|
||||
};
|
||||
|
||||
// One outer TabCtrl page. Category entries are its inner tabs.
|
||||
@@ -140,25 +147,23 @@ private:
|
||||
void select_all(bool value);
|
||||
void select_visible(bool value);
|
||||
void show_menu(wxMouseEvent& evt);
|
||||
void update_category_header(Category& category);
|
||||
void set_row_bold(Row& row, bool bold);
|
||||
void on_category_toggle(size_t category_index);
|
||||
// Material opt-in toggled: enables/disables the material's rows + tri-state
|
||||
// and resyncs the header.
|
||||
void on_master_toggle(size_t category_index);
|
||||
// "Full Publish" toggled: disables/enables the material's rows.
|
||||
void on_full_toggle(size_t category_index);
|
||||
// Return/create the fixed outer page for a Section kind.
|
||||
size_t section_group_for(Section kind);
|
||||
size_t category_index_for(const wxString& title, Section section, const std::string& icon_name, size_t group,
|
||||
size_t source_index, const PublishMaterialIdentity& identity = PublishMaterialIdentity());
|
||||
size_t subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon);
|
||||
void add_row_ui(const std::string& key, const wxString& label, const wxString& value, const wxString& unit,
|
||||
size_t category_index, size_t subcategory_index);
|
||||
size_t category_index, size_t subcategory_index, RowKind kind = RowKind::Setting);
|
||||
// The non-structural filament keys of a slot's preset, for a "Full Publish" entry.
|
||||
std::vector<std::string> full_keys_for_slot() const;
|
||||
void save_scroll_position(Category& category);
|
||||
void show_outer_page(size_t section_index);
|
||||
void show_inner_page(size_t section_index, int inner_index);
|
||||
void on_outer_tab_changed(wxCommandEvent& event);
|
||||
void on_inner_tab_changed(size_t section_index, wxCommandEvent& event);
|
||||
void update_all_headers();
|
||||
bool row_is_visible(const Row& row) const;
|
||||
void apply_visibility();
|
||||
void bind_tab_events();
|
||||
|
||||
@@ -752,3 +752,98 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing v2: a "full publish" entry carries the whole slot's key list. Its vector
|
||||
// options keep only the author's slot value; the other slots are masked to their defaults so a
|
||||
// slot-1 full publish does not leak slot 0's data into the file.
|
||||
SCENARIO("Full-publish entries filter the whole slot and mask the other slots", "[3mf]") {
|
||||
GIVEN("a full print configuration with two filament slots") {
|
||||
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||
full_cfg.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
full_cfg.opt<ConfigOptionStrings>("filament_colour")->values = { "#111111", "#222222" };
|
||||
// filament_flow_ratio carries a non-empty option default (1.0) of the same type, so the
|
||||
// mask can restore it on the non-published slot.
|
||||
full_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio", true)->values = { 1.02, 0.98 };
|
||||
|
||||
PublishedMaterialEntry full_entry;
|
||||
full_entry.slot = 1;
|
||||
full_entry.full = true;
|
||||
full_entry.full_keys = { "filament_flow_ratio" };
|
||||
|
||||
WHEN("filtering with a full entry for slot 1") {
|
||||
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { full_entry });
|
||||
|
||||
THEN("the full key list is present with the author's slot value") {
|
||||
REQUIRE(filtered_cfg.option("filament_flow_ratio") != nullptr);
|
||||
REQUIRE(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[1] == 0.98);
|
||||
}
|
||||
THEN("the non-published slot is masked to its default") {
|
||||
REQUIRE(filtered_cfg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values[0] == 1.0);
|
||||
}
|
||||
THEN("the identity keys stay present") {
|
||||
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing v2: the extended per-entry fields (full dump list, published type and
|
||||
// colour) travel inside the published_material_keys metadata and round-trip unchanged.
|
||||
SCENARIO("Published 3MF round-trips the filament-publishing-v2 material metadata", "[3mf]") {
|
||||
GIVEN("a model carrying extended published material keys metadata") {
|
||||
Model model;
|
||||
std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl";
|
||||
REQUIRE(load_stl(src_file.c_str(), &model));
|
||||
model.add_default_instances();
|
||||
|
||||
const std::string material_keys_json =
|
||||
R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])";
|
||||
|
||||
model.model_info = std::make_shared<ModelInfo>();
|
||||
model.model_info->metadata_items["published_material_keys"] = material_keys_json;
|
||||
|
||||
ScopedTemporaryDir backup_dir("orca_pub_mat2");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
|
||||
WHEN("stored to and reloaded from a .3mf") {
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
||||
PlateDataPtrs dst_plates;
|
||||
std::vector<Preset*> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates,
|
||||
&project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
||||
THEN("the extended material metadata round-trips unchanged") {
|
||||
REQUIRE(loaded);
|
||||
REQUIRE(dst_model.model_info != nullptr);
|
||||
REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json);
|
||||
|
||||
// The value must parse back with every filament-publishing-v2 field intact.
|
||||
nlohmann::json entries = nlohmann::json::parse(material_keys_json);
|
||||
REQUIRE(entries.is_array());
|
||||
REQUIRE(entries.size() == 1);
|
||||
REQUIRE(entries[0]["full"].get<bool>() == true);
|
||||
REQUIRE(entries[0]["full_keys"].is_array());
|
||||
REQUIRE(entries[0]["full_keys"].size() == 2);
|
||||
REQUIRE(entries[0]["publish_type"].get<bool>() == true);
|
||||
REQUIRE(entries[0]["type"] == "PLA");
|
||||
REQUIRE(entries[0]["publish_color"].get<bool>() == false);
|
||||
}
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,6 +784,409 @@ TEST_CASE("Published 3MF applies material retraction keys onto the receiver's ma
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// Filament-publishing v2: a "full publish" slot serializes the entire filament of the slot. On
|
||||
// load the slot is matched positionally against the published (curated, vendor-agnostic) type:
|
||||
// a matching receiver type leaves the slot untouched, a mismatched type replaces it with the
|
||||
// first same-type visible preset (applying the author's full values on top), and a slot whose
|
||||
// type cannot be found in the receiver's library falls back to the author's values in-memory.
|
||||
TEST_CASE("Published 3MF full-published slots replace or ignore the receiver material by type", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
// Two author slots; filament_diameter drives the normalized per-slot vector sizes.
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PETG" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFT99" };
|
||||
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.9, 1.2 };
|
||||
return config;
|
||||
};
|
||||
// The full dump of slot 0, publishing the whole filament as type "ABS".
|
||||
auto make_full_abs_entry = [] {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.full = true;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "ABS";
|
||||
entry.full_keys = { "filament_retraction_length" };
|
||||
return entry;
|
||||
};
|
||||
|
||||
SECTION("type match leaves a full-published slot untouched") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
bundle.filament_presets = { "My PLA", "My PLA" };
|
||||
|
||||
PublishedMaterialEntry full = make_full_abs_entry();
|
||||
full.publish_type_value = "PLA"; // author requires PLA, receiver slot is PLA
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { full };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// The receiver keeps its own material and its own values: the full dump is ignored.
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.5 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
CHECK(pub.material_replacements.empty());
|
||||
}
|
||||
|
||||
SECTION("type mismatch replaces the slot with the first same-type preset and applies the full dump") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
Preset &abs = add_inmemory_preset(bundle.filaments, "My ABS");
|
||||
abs.config.opt_string("filament_type", 0u) = "ABS";
|
||||
abs.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.3 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_full_abs_entry() };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filament_presets[0] == "My ABS");
|
||||
// The author's slot-0 full values were applied onto the replacement.
|
||||
CHECK(bundle.filaments.find_preset("My ABS")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
REQUIRE(pub.material_replacements.size() == 1);
|
||||
}
|
||||
|
||||
SECTION("no same-type match creates a temporary project-embedded custom preset") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_full_abs_entry() };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// No ABS in the library: a temporary embedded preset is created and selected.
|
||||
CHECK(bundle.filament_presets[0] == "ABS (Published)");
|
||||
Preset *created = bundle.filaments.find_preset("ABS (Published)");
|
||||
REQUIRE(created != nullptr);
|
||||
CHECK(created->is_project_embedded);
|
||||
CHECK(created->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
// The original user preset remains untouched. Re-fetch by name: load_preset's deque
|
||||
// insertion relocated the presets, so the pre-load `pla` reference points at the
|
||||
// newly created "ABS (Published)" slot.
|
||||
CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.5 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
REQUIRE(pub.material_replacements.size() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Filament-publishing v2: a partially-published slot can carry a curated type and/or colour.
|
||||
// The colour is applied regardless of the type match; a type mismatch with no same-type
|
||||
// replacement keeps the receiver's material and reports the slot's keys as skipped. The
|
||||
// receiver's slot count grows only as far as the highest slot with published content.
|
||||
TEST_CASE("Published 3MF partial slots apply colour and gate keys by the published type", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PETG" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFT99" };
|
||||
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.9, 1.2 };
|
||||
return config;
|
||||
};
|
||||
|
||||
SECTION("matching type applies the keys and the colour") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "PLA";
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// Type matched: keys applied, colour applied.
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
CHECK(pub.material_replacements.empty());
|
||||
}
|
||||
|
||||
SECTION("type mismatch without a replacement keeps the material and skips the keys") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "ABS"; // no ABS in the receiver library
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// Colour still applies (type-independent); the material is kept and the keys skipped.
|
||||
CHECK(bundle.filament_presets[0] == "My PLA");
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.5 });
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(contains_key(pub.skipped_keys, "material:ABS (filament_retraction_length)"));
|
||||
CHECK(pub.material_replacements.empty());
|
||||
}
|
||||
|
||||
SECTION("receiver slot count grows to fit the highest published slot and assigns matching type preset") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG");
|
||||
petg.config.opt_string("filament_type", 0u) = "PETG";
|
||||
petg.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.8 };
|
||||
// The receiver has a single slot; the file carries two, only slot 1 is published.
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 1;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "PETG";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// The slot list was grown so author slot 1 has a material, automatically assigning the PETG preset.
|
||||
REQUIRE(bundle.filament_presets.size() == 2);
|
||||
CHECK(bundle.filament_presets[1] == "My PETG");
|
||||
}
|
||||
}
|
||||
|
||||
// The receiver's slot list grows only as far as the highest author slot that carries published
|
||||
// content: a 4-filament file whose author published nothing (or only a low slot) must not pull
|
||||
// filler materials into the receiver's setup, and the receiver never grows to the file's count.
|
||||
TEST_CASE("Published 3MF grows the receiver's slots only as far as the published slots", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
// Four author slots (a 4-filament model).
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2, 3, 4 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" };
|
||||
return config;
|
||||
};
|
||||
auto add_pla_preset = [](PresetBundle &bundle) {
|
||||
Preset &preset = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
preset.config.opt_string("filament_type", 0u) = "PLA";
|
||||
preset.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
return &preset;
|
||||
};
|
||||
auto make_color_entry = [](int slot) {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = slot;
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
return entry;
|
||||
};
|
||||
|
||||
// A file whose author published nothing for any slot: the receiver's setup is untouched.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true; // no material entries at all
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#123456" });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// Only slot 0 published: a single-slot receiver keeps its single slot; the file's other
|
||||
// three slots pull nothing in.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_color_entry(0) };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
CHECK(bundle.filaments.find_preset("My PLA")->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
}
|
||||
|
||||
// Slot 3 published: the receiver grows to 4 so the published slot exists; the filler is
|
||||
// the receiver's own first visible material.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
const std::string filler = bundle.filaments.first_visible().name;
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_color_entry(3) };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 4);
|
||||
CHECK(bundle.filament_presets[3] == filler);
|
||||
}
|
||||
|
||||
// Slots 0 and 2 published: the receiver grows to 3, never to the file's 4.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
add_pla_preset(bundle);
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_color_entry(0), make_color_entry(2) };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 3);
|
||||
}
|
||||
}
|
||||
|
||||
// The GUI displays the edited preset, a snapshot of the selected collection preset taken at
|
||||
// selection time. The published overlay modifies the collection presets in place, so the load
|
||||
// must re-select the first slot's filament (mirroring a normal project load) for the applied
|
||||
// colour/type/keys - and slot replacements - to surface in the GUI.
|
||||
TEST_CASE("Published 3MF refreshes the edited preset so the applied material values surface", "[Preset][Bundle][Published]")
|
||||
{
|
||||
auto make_file_config = [] {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75 };
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1 };
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard" };
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000" };
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA" };
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic" };
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99" };
|
||||
config.option<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.9 };
|
||||
return config;
|
||||
};
|
||||
auto make_entry = [] {
|
||||
PublishedMaterialEntry entry;
|
||||
entry.slot = 0;
|
||||
entry.publish_type = true;
|
||||
entry.publish_type_value = "PLA";
|
||||
entry.publish_color = true;
|
||||
entry.color = "#ABCDEF";
|
||||
entry.keys = { "filament_retraction_length" };
|
||||
return entry;
|
||||
};
|
||||
|
||||
SECTION("the edited preset carries the applied colour and keys") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
// Mirror the GUI: the displayed preset is the collection's edited preset.
|
||||
REQUIRE(bundle.filaments.select_preset_by_name("My PLA", false));
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_entry() };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
const Preset &edited = bundle.filaments.get_edited_preset();
|
||||
CHECK(edited.config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(edited.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
SECTION("a slot replacement is reflected in the edited preset") {
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
pla.config.opt<ConfigOptionStrings>("filament_colour", true)->values = { "#123456" };
|
||||
pla.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.5 };
|
||||
Preset &abs = add_inmemory_preset(bundle.filaments, "My ABS");
|
||||
abs.config.opt_string("filament_type", 0u) = "ABS";
|
||||
abs.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length", true)->values = { 0.3 };
|
||||
bundle.filament_presets = { "My PLA" };
|
||||
REQUIRE(bundle.filaments.select_preset_by_name("My PLA", false));
|
||||
|
||||
PublishedMaterialEntry entry = make_entry();
|
||||
entry.publish_type_value = "ABS"; // mismatch: replaced by the library's ABS
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config();
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets[0] == "My ABS");
|
||||
// The edited preset now displays the replacement with the author's values on top.
|
||||
const Preset &edited = bundle.filaments.get_edited_preset();
|
||||
CHECK(edited.name == "My ABS");
|
||||
CHECK(edited.config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(edited.config.opt<ConfigOptionFloatsNullable>("filament_retraction_length")->values == std::vector<double>{ 0.9 });
|
||||
}
|
||||
}
|
||||
|
||||
// Material-qualified keys whose receiver-side material match is missing or ambiguous must be
|
||||
// reported as skipped (material-qualified) and never applied; a single unqualified type
|
||||
// fallback still applies.
|
||||
|
||||
Reference in New Issue
Block a user