mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 02:57:39 +00:00
Publish 3MF: support mixed filaments and per-extruder slot selection
- Publish mixed-filament slots as whole units: serialize the filament_mixed_* definition into project_config on import, grow the receiver's parallel arrays in lockstep, and report unappliable definitions as skipped instead of dropping them silently - Per-extruder printer selection: one inner tab per extruder, rows keyed by full "#N" ids; single-extruder receivers collapse variants onto their slot (first applied, rest skipped), multi-extruder receivers override element-wise - New per-slot "Enable" toggle gating what gets published; enabling a mix auto-enables + Full Publishes its components - Mixed page previews: fixed-size ratio bar, ternary triangle (3 components) and Material Ratio vs Model Height graph (gradients), always visible regardless of Enable - Tab strip shows full swatch compositions with adjustable spacing; barycentric helpers shared via FilamentBitmapUtils
This commit is contained in:
@@ -5291,6 +5291,11 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
const std::set<std::string> printer_option_set(printer_options.begin(), printer_options.end());
|
const std::set<std::string> printer_option_set(printer_options.begin(), printer_options.end());
|
||||||
std::set<std::string> contract_excluded_keys;
|
std::set<std::string> contract_excluded_keys;
|
||||||
auto apply_published = [&](DynamicPrintConfig& target, const std::set<std::string>* allowlist) {
|
auto apply_published = [&](DynamicPrintConfig& target, const std::set<std::string>* allowlist) {
|
||||||
|
// Single-extruder receivers collapse a base key's per-extruder "#N" variants onto
|
||||||
|
// their single slot: only the first serialized variant of a base key is applied
|
||||||
|
// (the author's "left or right" whichever came first), the rest are reported as
|
||||||
|
// skipped. Per-target, so the process and printer passes each track their own bases.
|
||||||
|
std::set<std::string> collapsed_bases;
|
||||||
for (const std::string& key : published_config->published_keys) {
|
for (const std::string& key : published_config->published_keys) {
|
||||||
if (applied_keys.count(key) != 0)
|
if (applied_keys.count(key) != 0)
|
||||||
continue; // already applied
|
continue; // already applied
|
||||||
@@ -5309,7 +5314,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
if (src_opt == nullptr)
|
if (src_opt == nullptr)
|
||||||
continue; // key not present in the loaded config; record later
|
continue; // key not present in the loaded config; record later
|
||||||
if (src_opt->is_vector()) {
|
if (src_opt->is_vector()) {
|
||||||
const ConfigOption* dst_opt = target.option(base_key);
|
ConfigOption* dst_opt = target.option(base_key);
|
||||||
if (dst_opt == nullptr || !dst_opt->is_vector())
|
if (dst_opt == nullptr || !dst_opt->is_vector())
|
||||||
continue; // cannot apply; will be reported as skipped
|
continue; // cannot apply; will be reported as skipped
|
||||||
// Type mismatch: ConfigOptionVector::set() throws ConfigurationError on a
|
// Type mismatch: ConfigOptionVector::set() throws ConfigurationError on a
|
||||||
@@ -5321,7 +5326,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
if (dst_opt->type() != src_opt->type())
|
if (dst_opt->type() != src_opt->type())
|
||||||
continue;
|
continue;
|
||||||
// A '#N' variant key (e.g. per-extruder retraction_length#2) applies one
|
// A '#N' variant key (e.g. per-extruder retraction_length#2) applies one
|
||||||
// element, so the index only needs to be in range on both sides - the
|
// element, so the index only needs to be in range on the author's side - the
|
||||||
// receiver may have a different extruder count than the author. Out-of-range
|
// receiver may have a different extruder count than the author. Out-of-range
|
||||||
// indices are skipped (set_at would otherwise resize the receiver's vector).
|
// indices are skipped (set_at would otherwise resize the receiver's vector).
|
||||||
if (key.size() > base_key.size()) {
|
if (key.size() > base_key.size()) {
|
||||||
@@ -5341,16 +5346,33 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!valid || idx >= static_cast<const ConfigOptionVectorBase*>(src_opt)->size() ||
|
const size_t src_size = static_cast<const ConfigOptionVectorBase*>(src_opt)->size();
|
||||||
idx >= static_cast<const ConfigOptionVectorBase*>(dst_opt)->size())
|
if (!valid || idx >= src_size)
|
||||||
continue; // malformed or out-of-range variant: cannot apply; reported as skipped
|
continue; // malformed or out-of-range on the author's side: cannot apply; reported as skipped
|
||||||
|
const size_t dst_size = static_cast<const ConfigOptionVectorBase*>(dst_opt)->size();
|
||||||
|
if (dst_size == 1) {
|
||||||
|
// Single-extruder receiver: collapse the author's per-extruder slots
|
||||||
|
// onto the receiver's single slot. Only the first serialized variant
|
||||||
|
// of a base key is applied (the author's "left or right" whichever was
|
||||||
|
// published first); later variants of the same base are reported as
|
||||||
|
// skipped, mirroring the receiver's single extruder.
|
||||||
|
if (collapsed_bases.count(base_key) != 0)
|
||||||
|
continue;
|
||||||
|
collapsed_bases.insert(base_key);
|
||||||
|
static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, 0, idx);
|
||||||
|
} else {
|
||||||
|
if (idx >= dst_size)
|
||||||
|
continue; // out-of-range variant: cannot apply; reported as skipped
|
||||||
|
target.apply_only(config, {key}, true);
|
||||||
|
}
|
||||||
} else if (static_cast<const ConfigOptionVectorBase*>(src_opt)->size() !=
|
} else if (static_cast<const ConfigOptionVectorBase*>(src_opt)->size() !=
|
||||||
static_cast<const ConfigOptionVectorBase*>(dst_opt)->size()) {
|
static_cast<const ConfigOptionVectorBase*>(dst_opt)->size()) {
|
||||||
// Whole-vector base key: the receiver must have a matching vector size,
|
// Whole-vector base key: the receiver must have a matching vector size,
|
||||||
// otherwise applying would overwrite a different number of elements.
|
// otherwise applying would overwrite a different number of elements.
|
||||||
continue; // cannot apply; will be reported as skipped
|
continue; // cannot apply; will be reported as skipped
|
||||||
|
} else {
|
||||||
|
target.apply_only(config, {key}, true);
|
||||||
}
|
}
|
||||||
target.apply_only(config, {key}, true);
|
|
||||||
applied_keys.insert(key);
|
applied_keys.insert(key);
|
||||||
} else {
|
} else {
|
||||||
// A scalar key cannot carry a '#N' suffix; a hand-crafted file listing one
|
// A scalar key cannot carry a '#N' suffix; a hand-crafted file listing one
|
||||||
@@ -5688,6 +5710,30 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
proj_nozzle_map->values.resize(target_slots, 0);
|
proj_nozzle_map->values.resize(target_slots, 0);
|
||||||
if (proj_volume_map && proj_volume_map->values.size() < target_slots)
|
if (proj_volume_map && proj_volume_map->values.size() < target_slots)
|
||||||
proj_volume_map->values.resize(target_slots, static_cast<int>(NozzleVolumeType::nvtStandard));
|
proj_volume_map->values.resize(target_slots, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||||
|
// The mixed-color project arrays are parallel per-slot like filament_colour;
|
||||||
|
// grow them in lockstep so a published mix slot has room for its definition.
|
||||||
|
// Defaults mirror set_num_filaments (false / empty string).
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionBools>("filament_is_mixed"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, false);
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionStrings>("filament_mixed_components"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, std::string{});
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, std::string{});
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionBools>("filament_mixed_gradient"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, false);
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionStrings>("filament_mixed_gradient_range"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, std::string{});
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionStrings>("filament_mixed_gradient_curve"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, std::string{});
|
||||||
|
if (auto* opt = this->project_config.opt<ConfigOptionBools>("filament_mixed_gradient_per_part"))
|
||||||
|
if (opt->values.size() < target_slots)
|
||||||
|
opt->values.resize(target_slots, false);
|
||||||
if (this->ams_multi_color_filment.size() < target_slots)
|
if (this->ams_multi_color_filment.size() < target_slots)
|
||||||
this->ams_multi_color_filment.resize(target_slots);
|
this->ams_multi_color_filment.resize(target_slots);
|
||||||
for (size_t slot = old_colour_count; slot < target_slots; ++slot) {
|
for (size_t slot = old_colour_count; slot < target_slots; ++slot) {
|
||||||
@@ -5995,7 +6041,13 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
// Colour is slot-scoped and independent of the type gate; it is also synced
|
// Colour is slot-scoped and independent of the type gate; it is also synced
|
||||||
// into project_config for GUI rendering.
|
// into project_config for GUI rendering.
|
||||||
if (entry.publish_color && !entry.color.empty()) {
|
if (entry.publish_color && !entry.color.empty()) {
|
||||||
if (recv != nullptr) {
|
// A mixed-definition entry carries the mix's blended colour for the
|
||||||
|
// swatch only: never write it into the slot's (possibly shared) preset
|
||||||
|
// config, only into the project-level colour arrays.
|
||||||
|
const bool is_mixed_entry = std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) {
|
||||||
|
return publish_mixed_keys().count(publish_base_key(key)) != 0;
|
||||||
|
});
|
||||||
|
if (!is_mixed_entry && recv != nullptr) {
|
||||||
// Create the key when the target preset lacks it: the colour is a
|
// Create the key when the target preset lacks it: the colour is a
|
||||||
// requirement, not an override.
|
// requirement, not an override.
|
||||||
if (ConfigOptionStrings* colour = write_config.opt<ConfigOptionStrings>("filament_colour", true)) {
|
if (ConfigOptionStrings* colour = write_config.opt<ConfigOptionStrings>("filament_colour", true)) {
|
||||||
@@ -6016,8 +6068,36 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apply_slot && recv != nullptr)
|
if (apply_slot && recv != nullptr) {
|
||||||
apply_slot_keys(write_config, entry.keys, entry.slot, material_label);
|
// Mixed-color definition keys live in project_config (parallel per-slot
|
||||||
|
// arrays), not in a filament preset; route them there. Normal keys keep
|
||||||
|
// the per-slot preset path below.
|
||||||
|
const std::set<std::string>& mixed_keys = publish_mixed_keys();
|
||||||
|
std::vector<std::string> preset_keys;
|
||||||
|
for (const std::string& key : entry.keys) {
|
||||||
|
const std::string base_key = publish_base_key(key);
|
||||||
|
if (mixed_keys.count(base_key) != 0) {
|
||||||
|
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())) {
|
||||||
|
if (ConfigOption* dst_opt = this->project_config.option(base_key)) {
|
||||||
|
if (dst_opt->is_vector() && dst_opt->type() == src_opt->type() &&
|
||||||
|
slot < static_cast<const ConfigOptionVectorBase*>(dst_opt)->size()) {
|
||||||
|
static_cast<ConfigOptionVectorBase*>(dst_opt)->set_at(src_opt, slot, entry.slot);
|
||||||
|
material_applied = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The slot (or its arrays) could not be written: report rather
|
||||||
|
// than drop the mix silently.
|
||||||
|
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
preset_keys.emplace_back(key);
|
||||||
|
}
|
||||||
|
apply_slot_keys(write_config, preset_keys, entry.slot, material_label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,23 @@ const std::set<std::string>& publish_structural_keys()
|
|||||||
return structural_keys;
|
return structural_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::set<std::string>& publish_mixed_keys()
|
||||||
|
{
|
||||||
|
// Must match PresetBundle's s_project_options mixed-color group (PresetBundle.cpp): these
|
||||||
|
// are project-level parallel per-slot arrays, not filament-preset options, so the import
|
||||||
|
// material pass applies them into project_config instead of a filament preset config.
|
||||||
|
static const std::set<std::string> mixed_keys = {
|
||||||
|
"filament_is_mixed",
|
||||||
|
"filament_mixed_components",
|
||||||
|
"filament_mixed_sublayer_ratios",
|
||||||
|
"filament_mixed_gradient",
|
||||||
|
"filament_mixed_gradient_range",
|
||||||
|
"filament_mixed_gradient_curve",
|
||||||
|
"filament_mixed_gradient_per_part"
|
||||||
|
};
|
||||||
|
return mixed_keys;
|
||||||
|
}
|
||||||
|
|
||||||
// The printer tab's "Retraction" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order.
|
// The printer tab's "Retraction" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order.
|
||||||
// KEEP IN SYNC with that optgroup: the published-3MF printer allowlist is built from these
|
// KEEP IN SYNC with that optgroup: the published-3MF printer allowlist is built from these
|
||||||
// lists, so any key shown there must be publishable here (and vice versa).
|
// lists, so any key shown there must be publishable here (and vice versa).
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ std::string publish_base_key(const std::string &key);
|
|||||||
// filter_published_config because 3MF validation needs it - exported, never applied.
|
// filter_published_config because 3MF validation needs it - exported, never applied.
|
||||||
const std::set<std::string>& publish_structural_keys();
|
const std::set<std::string>& publish_structural_keys();
|
||||||
|
|
||||||
|
// The mixed-color filament project keys (parallel per-slot arrays, see PresetBundle's
|
||||||
|
// s_project_options): a mixed slot's full definition - which slots it blends, the sublayer
|
||||||
|
// ratios and the optional Z-gradient description. A published mixed slot always serializes
|
||||||
|
// these keys; on import they are applied into the receiver's project_config (not a filament
|
||||||
|
// preset), so the mix survives the round-trip.
|
||||||
|
const std::set<std::string>& publish_mixed_keys();
|
||||||
|
|
||||||
// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (key + tab icon id), kept
|
// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (key + tab icon id), kept
|
||||||
// together so the tab can later be migrated onto these lists.
|
// together so the tab can later be migrated onto these lists.
|
||||||
struct PublishablePrinterOption {
|
struct PublishablePrinterOption {
|
||||||
|
|||||||
@@ -11,6 +11,45 @@
|
|||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// Barycentric utilities for a ternary (triangle) ratio picker.
|
||||||
|
double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c)
|
||||||
|
{
|
||||||
|
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
||||||
|
{
|
||||||
|
double total = tri_signed_area2(v0, v1, v2);
|
||||||
|
if (std::abs(total) < 1e-9) return false;
|
||||||
|
double s0 = tri_signed_area2(p, v1, v2) / total;
|
||||||
|
double s1 = tri_signed_area2(v0, p, v2) / total;
|
||||||
|
double s2 = 1.0 - s0 - s1;
|
||||||
|
return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
|
||||||
|
double& w0, double& w1, double& w2)
|
||||||
|
{
|
||||||
|
double total = std::abs(tri_signed_area2(v0, v1, v2));
|
||||||
|
if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; }
|
||||||
|
w0 = std::abs(tri_signed_area2(p, v1, v2)) / total;
|
||||||
|
w1 = std::abs(tri_signed_area2(v0, p, v2)) / total;
|
||||||
|
w2 = 1.0 - w0 - w1;
|
||||||
|
w0 = std::clamp(w0, 0.0, 1.0);
|
||||||
|
w1 = std::clamp(w1, 0.0, 1.0);
|
||||||
|
w2 = std::clamp(w2, 0.0, 1.0);
|
||||||
|
double s = w0 + w1 + w2;
|
||||||
|
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
||||||
|
{
|
||||||
|
double w0, w1, w2;
|
||||||
|
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
|
||||||
|
return {w0 * v0.x + w1 * v1.x + w2 * v2.x,
|
||||||
|
w0 * v0.y + w1 * v1.y + w2 * v2.y};
|
||||||
|
}
|
||||||
|
|
||||||
void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from, const wxColour& to)
|
void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from, const wxColour& to)
|
||||||
{
|
{
|
||||||
if (rect.width <= 0 || rect.height <= 0) return;
|
if (rect.width <= 0 || rect.height <= 0) return;
|
||||||
@@ -73,7 +112,7 @@ std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
|||||||
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
|
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
|
||||||
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
|
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
|
||||||
// endpoints, otherwise the 0.10 -> 0.90 default.
|
// endpoints, otherwise the 0.10 -> 0.90 default.
|
||||||
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
|
Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
|
||||||
{
|
{
|
||||||
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
|
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
|
||||||
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
|
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
|
|||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// Barycentric utilities for a ternary (triangle) ratio picker, shared by the mixed-filament
|
||||||
|
// editor and the Publish dialog's read-only definition preview.
|
||||||
|
struct TriPoint { double x, y; };
|
||||||
|
|
||||||
|
double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c);
|
||||||
|
bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2);
|
||||||
|
void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
|
||||||
|
double& w0, double& w1, double& w2);
|
||||||
|
TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2);
|
||||||
|
|
||||||
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
|
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
|
||||||
// Use instead of wxDC::GradientFillLinear, whose CoreGraphics (CGShading) backend
|
// Use instead of wxDC::GradientFillLinear, whose CoreGraphics (CGShading) backend
|
||||||
// fails to render on some macOS builds; solid fills are unaffected.
|
// fails to render on some macOS builds; solid fills are unaffected.
|
||||||
@@ -51,6 +61,12 @@ std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
|
|||||||
// destination's height in pixels.
|
// destination's height in pixels.
|
||||||
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
|
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
|
||||||
|
|
||||||
|
// Resolve the curve a gradient slot is sampled with: the custom curve wins when it has at
|
||||||
|
// least two points, otherwise a straight line between gradient_range's endpoints, otherwise
|
||||||
|
// the 0.10 -> 0.90 default. Mirrors the slicer's ToolOrdering fallback so every preview
|
||||||
|
// agrees with what gets sliced. Always returns a two-point curve.
|
||||||
|
Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot);
|
||||||
|
|
||||||
// Fill rect with a ramp, ramp.front() along the bottom edge.
|
// Fill rect with a ramp, ramp.front() along the bottom edge.
|
||||||
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
|
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
|
||||||
|
|
||||||
|
|||||||
@@ -919,52 +919,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Triangle (ternary) ratio picker ----
|
// ---- Triangle (ternary) ratio picker ----
|
||||||
|
// The barycentric utilities (TriPoint, tri_contains, tri_barycentric, tri_clamp) live in
|
||||||
// Barycentric coordinate utilities
|
// FilamentBitmapUtils so the Publish dialog can mirror this picker read-only.
|
||||||
struct TriPoint { double x, y; };
|
|
||||||
|
|
||||||
static double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c)
|
|
||||||
{
|
|
||||||
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
|
||||||
{
|
|
||||||
double total = tri_signed_area2(v0, v1, v2);
|
|
||||||
if (std::abs(total) < 1e-9) return false;
|
|
||||||
double s0 = tri_signed_area2(p, v1, v2) / total;
|
|
||||||
double s1 = tri_signed_area2(v0, p, v2) / total;
|
|
||||||
double s2 = 1.0 - s0 - s1;
|
|
||||||
return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
|
|
||||||
double& w0, double& w1, double& w2)
|
|
||||||
{
|
|
||||||
double total = std::abs(tri_signed_area2(v0, v1, v2));
|
|
||||||
if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; }
|
|
||||||
w0 = std::abs(tri_signed_area2(p, v1, v2)) / total;
|
|
||||||
w1 = std::abs(tri_signed_area2(v0, p, v2)) / total;
|
|
||||||
w2 = 1.0 - w0 - w1;
|
|
||||||
w0 = std::clamp(w0, 0.0, 1.0);
|
|
||||||
w1 = std::clamp(w1, 0.0, 1.0);
|
|
||||||
w2 = std::clamp(w2, 0.0, 1.0);
|
|
||||||
double s = w0 + w1 + w2;
|
|
||||||
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
|
|
||||||
}
|
|
||||||
|
|
||||||
static TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
|
|
||||||
{
|
|
||||||
double w0, w1, w2;
|
|
||||||
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
|
|
||||||
w0 = std::clamp(w0, 0.0, 1.0);
|
|
||||||
w1 = std::clamp(w1, 0.0, 1.0);
|
|
||||||
w2 = std::clamp(w2, 0.0, 1.0);
|
|
||||||
double s = w0 + w1 + w2;
|
|
||||||
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
|
|
||||||
return {w0 * v0.x + w1 * v1.x + w2 * v2.x,
|
|
||||||
w0 * v0.y + w1 * v1.y + w2 * v2.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
wxBoxSizer* MixedFilamentDialog::create_triangle_picker()
|
wxBoxSizer* MixedFilamentDialog::create_triangle_picker()
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,10 @@
|
|||||||
#include "libslic3r/PublishSettings.hpp"
|
#include "libslic3r/PublishSettings.hpp"
|
||||||
|
|
||||||
#include <wx/wx.h>
|
#include <wx/wx.h>
|
||||||
|
#include <wx/colour.h>
|
||||||
#include <wx/scrolwin.h>
|
#include <wx/scrolwin.h>
|
||||||
#include <wx/menu.h>
|
#include <wx/menu.h>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -105,9 +107,18 @@ private:
|
|||||||
wxPoint scroll_pos{0, 0};
|
wxPoint scroll_pos{0, 0};
|
||||||
wxStaticBitmap* filament_color_chip{nullptr};
|
wxStaticBitmap* filament_color_chip{nullptr};
|
||||||
wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere)
|
wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere)
|
||||||
|
// "Enable": while unchecked nothing of this slot is exported and everything below the
|
||||||
|
// header row is hidden. For physical slots the Full Publish toggle sits on a second
|
||||||
|
// line (full_line_item) visible only when enabled; for mixed slots Enable alone implies
|
||||||
|
// publishing the mix definition, so no Full Publish widget exists at all.
|
||||||
|
wxCheckBox* enable_check{nullptr};
|
||||||
|
wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only)
|
||||||
// "Full Publish": while checked, the whole slot preset is serialized and its rows
|
// "Full Publish": while checked, the whole slot preset is serialized and its rows
|
||||||
// (incl. Color/Type) are disabled.
|
// (incl. Color/Type) are disabled.
|
||||||
wxCheckBox* full_check{nullptr};
|
wxCheckBox* full_check{nullptr};
|
||||||
|
// True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes
|
||||||
|
// the slot's gradient/ratio definition as a whole.
|
||||||
|
bool is_mixed{false};
|
||||||
// Material identity, only for Section::Material categories.
|
// Material identity, only for Section::Material categories.
|
||||||
std::string filament_type;
|
std::string filament_type;
|
||||||
std::string filament_vendor;
|
std::string filament_vendor;
|
||||||
@@ -118,6 +129,21 @@ private:
|
|||||||
std::vector<size_t> rows; // flattened rows of this category
|
std::vector<size_t> rows; // flattened rows of this category
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Frozen snapshot of a mixed filament slot's definition for the read-only visualization
|
||||||
|
// painted on the slot's page. Plain data only: the paint handler must never touch the
|
||||||
|
// config. For gradient slots the curve is pre-sampled (t, ratio) pairs, where ratio is the
|
||||||
|
// first component's share over model height; anchors carry the raw control points.
|
||||||
|
struct MixedVisualSpec
|
||||||
|
{
|
||||||
|
bool valid{false};
|
||||||
|
bool is_gradient{false};
|
||||||
|
std::vector<wxColour> component_colours; // colour per component, in config order
|
||||||
|
std::vector<double> ratios; // sublayer shares summing to ~1 (non-gradient)
|
||||||
|
std::vector<double> tri_weights; // 3-component mixes: barycentric shares
|
||||||
|
std::vector<std::pair<double, double>> gradient_samples;
|
||||||
|
std::vector<std::pair<double, double>> gradient_anchors;
|
||||||
|
};
|
||||||
|
|
||||||
// One outer TabCtrl page. Category entries are its inner tabs.
|
// One outer TabCtrl page. Category entries are its inner tabs.
|
||||||
struct SectionGroup
|
struct SectionGroup
|
||||||
{
|
{
|
||||||
@@ -127,13 +153,24 @@ private:
|
|||||||
ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change
|
ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change
|
||||||
wxPanel* page{nullptr};
|
wxPanel* page{nullptr};
|
||||||
TabCtrl* tabs{nullptr};
|
TabCtrl* tabs{nullptr};
|
||||||
|
// Second tab strip, below the main one, listing only the mixed-color filament slots.
|
||||||
|
// Present on the Material section only (null elsewhere).
|
||||||
|
TabCtrl* mixed_tabs{nullptr};
|
||||||
wxPanel* page_host{nullptr};
|
wxPanel* page_host{nullptr};
|
||||||
wxBoxSizer* page_host_sizer{nullptr};
|
wxBoxSizer* page_host_sizer{nullptr};
|
||||||
int selected_inner{-1};
|
int selected_inner{-1};
|
||||||
std::vector<size_t> categories; // indices into m_categories
|
// Selected mixed tab (index into mixed_categories), valid while a mixed slot page is shown.
|
||||||
|
int selected_mixed{-1};
|
||||||
|
std::vector<size_t> categories; // indices into m_categories (physical slots)
|
||||||
|
std::vector<size_t> mixed_categories; // indices into m_categories (mixed slots)
|
||||||
};
|
};
|
||||||
|
|
||||||
void build_option_model();
|
void build_option_model();
|
||||||
|
// Frozen snapshot of a mixed slot's definition for the page visualization, resolved from
|
||||||
|
// the full config once at dialog-build time. Gradient slots pre-sample exactly what the
|
||||||
|
// slicer will print: the custom curve wins over the gradient_range endpoints over the
|
||||||
|
// 0.10 -> 0.90 default (the resolution FilamentBitmapUtils::mixed_gradient_curve mirrors).
|
||||||
|
static MixedVisualSpec make_mixed_visual_spec(const Slic3r::DynamicPrintConfig& full, size_t slot);
|
||||||
void apply_filter(const wxString& filter_text);
|
void apply_filter(const wxString& filter_text);
|
||||||
// Menu-only pseudo filters: show only the checked ("Filter selected") or only the
|
// Menu-only pseudo filters: show only the checked ("Filter selected") or only the
|
||||||
// unchecked ("Filter non-selected") rows. The search box keeps the user's text.
|
// unchecked ("Filter non-selected") rows. The search box keeps the user's text.
|
||||||
@@ -147,13 +184,21 @@ private:
|
|||||||
void set_row_bold(Row& row, bool bold);
|
void set_row_bold(Row& row, bool bold);
|
||||||
// "Full Publish" toggled: disables/enables the material's rows.
|
// "Full Publish" toggled: disables/enables the material's rows.
|
||||||
void on_full_toggle(size_t category_index);
|
void on_full_toggle(size_t category_index);
|
||||||
|
// "Enable" toggled on a material slot: reveals/hides everything below the header and, for a
|
||||||
|
// mixed slot, auto-selects its component filaments' "Enable" + "Full Publish" toggles.
|
||||||
|
void on_enable_toggle(size_t category_index);
|
||||||
|
// Read-only visualization of a mixed slot's definition (a stacked ratio bar, or the
|
||||||
|
// Material Ratio vs Model Height graph for a gradient), inserted above the info hint
|
||||||
|
// inside the category's scroll area.
|
||||||
|
void add_mixed_visual(size_t category_index, const MixedVisualSpec& spec);
|
||||||
// Return/create the fixed outer page for a Section kind.
|
// Return/create the fixed outer page for a Section kind.
|
||||||
size_t section_group_for(Section kind);
|
size_t section_group_for(Section kind);
|
||||||
size_t category_index_for(const wxString& title,
|
size_t category_index_for(const wxString& title,
|
||||||
Section section,
|
Section section,
|
||||||
size_t group,
|
size_t group,
|
||||||
size_t source_index,
|
size_t source_index,
|
||||||
const PublishMaterialIdentity& identity = PublishMaterialIdentity());
|
const PublishMaterialIdentity& identity = PublishMaterialIdentity(),
|
||||||
|
bool is_mixed = false);
|
||||||
size_t subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon);
|
size_t subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon);
|
||||||
void add_row_ui(const std::string& key,
|
void add_row_ui(const std::string& key,
|
||||||
const wxString& label,
|
const wxString& label,
|
||||||
@@ -167,8 +212,10 @@ private:
|
|||||||
void save_scroll_position(Category& category);
|
void save_scroll_position(Category& category);
|
||||||
void show_outer_page(size_t section_index);
|
void show_outer_page(size_t section_index);
|
||||||
void show_inner_page(size_t section_index, int inner_index);
|
void show_inner_page(size_t section_index, int inner_index);
|
||||||
|
void show_mixed_page(size_t section_index, int mixed_index);
|
||||||
void on_outer_tab_changed(wxCommandEvent& event);
|
void on_outer_tab_changed(wxCommandEvent& event);
|
||||||
void on_inner_tab_changed(size_t section_index, wxCommandEvent& event);
|
void on_inner_tab_changed(size_t section_index, wxCommandEvent& event);
|
||||||
|
void on_mixed_tab_changed(size_t section_index, wxCommandEvent& event);
|
||||||
bool row_is_visible(const Row& row) const;
|
bool row_is_visible(const Row& row) const;
|
||||||
void apply_visibility();
|
void apply_visibility();
|
||||||
void bind_tab_events();
|
void bind_tab_events();
|
||||||
@@ -190,6 +237,7 @@ private:
|
|||||||
wxString m_info_nonsel;
|
wxString m_info_nonsel;
|
||||||
wxString m_info_allsel;
|
wxString m_info_allsel;
|
||||||
wxString m_info_empty;
|
wxString m_info_empty;
|
||||||
|
wxString m_info_mix; // body hint shown for a mixed slot (published as a whole)
|
||||||
|
|
||||||
ScalableBitmap m_search;
|
ScalableBitmap m_search;
|
||||||
ScalableBitmap m_menu;
|
ScalableBitmap m_menu;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#include <wx/dc.h>
|
#include <wx/dc.h>
|
||||||
|
|
||||||
wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent );
|
wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent);
|
||||||
wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent );
|
wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent);
|
||||||
|
|
||||||
BEGIN_EVENT_TABLE(TabCtrl, StaticBox)
|
BEGIN_EVENT_TABLE(TabCtrl, StaticBox)
|
||||||
|
|
||||||
@@ -22,11 +22,7 @@ END_EVENT_TABLE()
|
|||||||
#define TAB_BUTTON_PADDING_Y 2
|
#define TAB_BUTTON_PADDING_Y 2
|
||||||
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
|
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
|
||||||
|
|
||||||
TabCtrl::TabCtrl(wxWindow * parent,
|
TabCtrl::TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style)
|
||||||
wxWindowID id,
|
|
||||||
const wxPoint & pos,
|
|
||||||
const wxSize & size,
|
|
||||||
long style)
|
|
||||||
: StaticBox(parent, id, pos, size, style)
|
: StaticBox(parent, id, pos, size, style)
|
||||||
{
|
{
|
||||||
#if 0
|
#if 0
|
||||||
@@ -42,14 +38,11 @@ TabCtrl::TabCtrl(wxWindow * parent,
|
|||||||
hsizer->Add(sizer, 0, wxEXPAND | wxBOTTOM, border_width * 4);
|
hsizer->Add(sizer, 0, wxEXPAND | wxBOTTOM, border_width * 4);
|
||||||
SetSizer(hsizer);
|
SetSizer(hsizer);
|
||||||
Bind(wxEVT_COMMAND_BUTTON_CLICKED, &TabCtrl::buttonClicked, this);
|
Bind(wxEVT_COMMAND_BUTTON_CLICKED, &TabCtrl::buttonClicked, this);
|
||||||
//wxString reason;
|
// wxString reason;
|
||||||
//IsTransparentBackgroundSupported(&reason);
|
// IsTransparentBackgroundSupported(&reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
TabCtrl::~TabCtrl()
|
TabCtrl::~TabCtrl() { delete images; }
|
||||||
{
|
|
||||||
delete images;
|
|
||||||
}
|
|
||||||
|
|
||||||
int TabCtrl::GetSelection() const { return sel; }
|
int TabCtrl::GetSelection() const { return sel; }
|
||||||
|
|
||||||
@@ -75,14 +68,11 @@ void TabCtrl::SelectItem(int item)
|
|||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::Unselect()
|
void TabCtrl::Unselect() { SelectItem(-1); }
|
||||||
{
|
|
||||||
SelectItem(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabCtrl::Rescale()
|
void TabCtrl::Rescale()
|
||||||
{
|
{
|
||||||
for (auto & b : btns)
|
for (auto& b : btns)
|
||||||
b->Rescale();
|
b->Rescale();
|
||||||
relayout();
|
relayout();
|
||||||
}
|
}
|
||||||
@@ -96,23 +86,20 @@ bool TabCtrl::SetFont(wxFont const& font)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int TabCtrl::AppendItem(const wxString &item,
|
int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* clientData)
|
||||||
int image, int selImage,
|
|
||||||
void * clientData)
|
|
||||||
{
|
{
|
||||||
Button * btn = new Button();
|
Button* btn = new Button();
|
||||||
btn->Create(this, item, "", wxBORDER_NONE);
|
btn->Create(this, item, "", wxBORDER_NONE);
|
||||||
btn->SetFont(GetFont());
|
btn->SetFont(GetFont());
|
||||||
btn->SetTextColor(StateColor(
|
btn->SetTextColor(
|
||||||
std::make_pair(0x6B6B6C, (int) StateColor::NotChecked),
|
StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal)));
|
||||||
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal)));
|
|
||||||
btn->SetBackgroundColor(StateColor());
|
btn->SetBackgroundColor(StateColor());
|
||||||
btn->SetCornerRadius(0);
|
btn->SetCornerRadius(0);
|
||||||
btn->SetPaddingSize({TAB_BUTTON_PADDING});
|
btn->SetPaddingSize({TAB_BUTTON_PADDING});
|
||||||
btns.push_back(btn);
|
btns.push_back(btn);
|
||||||
if (btns.size() > 1)
|
if (btns.size() > 1)
|
||||||
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
||||||
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE * 2);
|
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space * 2);
|
||||||
sizer->AddStretchSpacer(1);
|
sizer->AddStretchSpacer(1);
|
||||||
relayout();
|
relayout();
|
||||||
return btns.size() - 1;
|
return btns.size() - 1;
|
||||||
@@ -144,7 +131,7 @@ bool TabCtrl::DeleteItem(int item)
|
|||||||
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
|
||||||
|
|
||||||
if (selection_changed) {
|
if (selection_changed) {
|
||||||
sel--; // `relayout()` uses `sel` so we need to update this before calling `relayout()`
|
sel--; // `relayout()` uses `sel` so we need to update this before calling `relayout()`
|
||||||
}
|
}
|
||||||
relayout();
|
relayout();
|
||||||
if (selection_changed) {
|
if (selection_changed) {
|
||||||
@@ -167,14 +154,12 @@ void TabCtrl::DeleteAllItems()
|
|||||||
|
|
||||||
unsigned int TabCtrl::GetCount() const { return btns.size(); }
|
unsigned int TabCtrl::GetCount() const { return btns.size(); }
|
||||||
|
|
||||||
wxString TabCtrl::GetItemText(unsigned int item) const
|
wxString TabCtrl::GetItemText(unsigned int item) const { return item < btns.size() ? btns[item]->GetLabel() : wxString{}; }
|
||||||
{
|
|
||||||
return item < btns.size() ? btns[item]->GetLabel() : wxString{};
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabCtrl::SetItemText(unsigned int item, wxString const &value)
|
void TabCtrl::SetItemText(unsigned int item, wxString const& value)
|
||||||
{
|
{
|
||||||
if (item >= btns.size()) return;
|
if (item >= btns.size())
|
||||||
|
return;
|
||||||
btns[item]->SetLabel(value);
|
btns[item]->SetLabel(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,61 +173,59 @@ void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap)
|
|||||||
|
|
||||||
bool TabCtrl::GetItemBold(unsigned int item) const
|
bool TabCtrl::GetItemBold(unsigned int item) const
|
||||||
{
|
{
|
||||||
if (item >= btns.size()) return false;
|
if (item >= btns.size())
|
||||||
|
return false;
|
||||||
return btns[item]->GetFont() == bold;
|
return btns[item]->GetFont() == bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::SetItemBold(unsigned int item, bool bold)
|
void TabCtrl::SetItemBold(unsigned int item, bool bold)
|
||||||
{
|
{
|
||||||
if (item >= btns.size()) return;
|
if (item >= btns.size())
|
||||||
|
return;
|
||||||
btns[item]->SetFont(bold ? this->bold : GetFont());
|
btns[item]->SetFont(bold ? this->bold : GetFont());
|
||||||
btns[item]->Rescale();
|
btns[item]->Rescale();
|
||||||
}
|
}
|
||||||
|
|
||||||
void* TabCtrl::GetItemData(unsigned int item) const
|
void* TabCtrl::GetItemData(unsigned int item) const
|
||||||
{
|
{
|
||||||
if (item >= btns.size()) return nullptr;
|
if (item >= btns.size())
|
||||||
|
return nullptr;
|
||||||
return btns[item]->GetClientData();
|
return btns[item]->GetClientData();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::SetItemData(unsigned int item, void* clientData)
|
void TabCtrl::SetItemData(unsigned int item, void* clientData)
|
||||||
{
|
{
|
||||||
if (item >= btns.size()) return;
|
if (item >= btns.size())
|
||||||
|
return;
|
||||||
btns[item]->SetClientData(clientData);
|
btns[item]->SetClientData(clientData);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::AssignImageList(wxImageList* imageList)
|
void TabCtrl::AssignImageList(wxImageList* imageList)
|
||||||
{
|
{
|
||||||
if (images == imageList) return;
|
if (images == imageList)
|
||||||
|
return;
|
||||||
delete images;
|
delete images;
|
||||||
images = imageList;
|
images = imageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::SetItemTextColour(unsigned int item, const StateColor &col)
|
void TabCtrl::SetItemTextColour(unsigned int item, const StateColor& col)
|
||||||
{
|
{
|
||||||
if (item >= btns.size()) return;
|
if (item >= btns.size())
|
||||||
|
return;
|
||||||
btns[item]->SetTextColor(col);
|
btns[item]->SetTextColor(col);
|
||||||
}
|
}
|
||||||
|
|
||||||
int TabCtrl::GetFirstVisibleItem() const
|
int TabCtrl::GetFirstVisibleItem() const { return btns.size() == 0 ? -1 : 0; }
|
||||||
{
|
|
||||||
return btns.size() == 0 ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int TabCtrl::GetNextVisible(int item) const
|
int TabCtrl::GetNextVisible(int item) const { return ++item < btns.size() ? item : -1; }
|
||||||
{
|
|
||||||
return ++item < btns.size() ? item : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TabCtrl::IsVisible(unsigned int item) const
|
bool TabCtrl::IsVisible(unsigned int item) const { return true; }
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
|
void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
|
||||||
{
|
{
|
||||||
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
|
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
|
||||||
if (sizeFlags & wxSIZE_USE_EXISTING) return;
|
if (sizeFlags & wxSIZE_USE_EXISTING)
|
||||||
|
return;
|
||||||
relayout();
|
relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +233,9 @@ void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
|
|||||||
|
|
||||||
WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
||||||
{
|
{
|
||||||
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTARROWS; }
|
if (nMsg == WM_GETDLGCODE) {
|
||||||
|
return DLGC_WANTARROWS;
|
||||||
|
}
|
||||||
return wxWindow::MSWWindowProc(nMsg, wParam, lParam);
|
return wxWindow::MSWWindowProc(nMsg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,15 +244,15 @@ WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
|
|||||||
void TabCtrl::relayout()
|
void TabCtrl::relayout()
|
||||||
{
|
{
|
||||||
int offset = 10;
|
int offset = 10;
|
||||||
int item = sel + 1;
|
int item = sel + 1;
|
||||||
int first = 0;
|
int first = 0;
|
||||||
for (int i = 0; i < item; ++i)
|
for (int i = 0; i < item; ++i)
|
||||||
offset += btns[i]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
offset += btns[i]->GetMinSize().x + item_space * 2;
|
||||||
if (item < btns.size())
|
if (item < btns.size())
|
||||||
offset += btns[item]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
offset += btns[item]->GetMinSize().x + item_space * 2;
|
||||||
int width = GetSize().x;
|
int width = GetSize().x;
|
||||||
for (int i = 0; i < btns.size(); ++i) {
|
for (int i = 0; i < btns.size(); ++i) {
|
||||||
auto size = btns[i]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
auto size = btns[i]->GetMinSize().x + item_space * 2;
|
||||||
if (i < sel && offset > width) {
|
if (i < sel && offset > width) {
|
||||||
sizer->Show(i * 2 + 1, false);
|
sizer->Show(i * 2 + 1, false);
|
||||||
sizer->Show(i * 2 + 2, false);
|
sizer->Show(i * 2 + 2, false);
|
||||||
@@ -288,23 +273,32 @@ void TabCtrl::relayout()
|
|||||||
sizer->GetItem(i * 2 + 2)->SetMinSize({0, 0});
|
sizer->GetItem(i * 2 + 2)->SetMinSize({0, 0});
|
||||||
}
|
}
|
||||||
if (item >= btns.size())
|
if (item >= btns.size())
|
||||||
-- item;
|
--item;
|
||||||
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
|
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
|
||||||
int b = GetSize().x - offset - 10 - (item + 1 - first) * TAB_BUTTON_SPACE * 8;
|
int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8;
|
||||||
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
|
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
|
||||||
Layout();
|
Layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
int TabCtrl::buttons_best_width() const
|
void TabCtrl::SetItemSpace(int space)
|
||||||
|
{
|
||||||
|
if (space < 0 || space == item_space)
|
||||||
|
return;
|
||||||
|
item_space = space;
|
||||||
|
relayout();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
int TabCtrl::GetFullSize() const
|
||||||
{
|
{
|
||||||
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing.
|
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing.
|
||||||
int width = 10;
|
int width = 10;
|
||||||
for (const Button *btn : btns)
|
for (const Button* btn : btns)
|
||||||
width += btn->GetMinSize().x + TAB_BUTTON_SPACE * 2;
|
width += btn->GetMinSize().x + item_space * 2;
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::buttonClicked(wxCommandEvent &event)
|
void TabCtrl::buttonClicked(wxCommandEvent& event)
|
||||||
{
|
{
|
||||||
SetFocus();
|
SetFocus();
|
||||||
auto btn = event.GetEventObject();
|
auto btn = event.GetEventObject();
|
||||||
@@ -312,7 +306,7 @@ void TabCtrl::buttonClicked(wxCommandEvent &event)
|
|||||||
SelectItem(iter == btns.end() ? -1 : iter - btns.begin());
|
SelectItem(iter == btns.end() ? -1 : iter - btns.begin());
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabCtrl::keyDown(wxKeyEvent &event)
|
void TabCtrl::keyDown(wxKeyEvent& event)
|
||||||
{
|
{
|
||||||
switch (event.GetKeyCode()) {
|
switch (event.GetKeyCode()) {
|
||||||
case WXK_UP:
|
case WXK_UP:
|
||||||
@@ -331,11 +325,13 @@ void TabCtrl::keyDown(wxKeyEvent &event)
|
|||||||
void TabCtrl::doRender(wxDC& dc)
|
void TabCtrl::doRender(wxDC& dc)
|
||||||
{
|
{
|
||||||
wxSize size = GetSize();
|
wxSize size = GetSize();
|
||||||
int states = state_handler.states();
|
int states = state_handler.states();
|
||||||
if (sel < 0) { return; }
|
if (sel < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto x1 = btns[sel]->GetPosition().x;
|
auto x1 = btns[sel]->GetPosition().x;
|
||||||
auto x2 = x1 + btns[sel]->GetSize().x;
|
auto x2 = x1 + btns[sel]->GetSize().x;
|
||||||
const int BS2 = (1 + border_width) / 2;
|
const int BS2 = (1 + border_width) / 2;
|
||||||
#if 0
|
#if 0
|
||||||
const int BS = border_width / 2;
|
const int BS = border_width / 2;
|
||||||
|
|||||||
@@ -3,33 +3,30 @@
|
|||||||
|
|
||||||
#include "Button.hpp"
|
#include "Button.hpp"
|
||||||
|
|
||||||
wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent );
|
wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent);
|
||||||
wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent );
|
wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent);
|
||||||
|
|
||||||
class TabCtrl : public StaticBox
|
class TabCtrl : public StaticBox
|
||||||
{
|
{
|
||||||
std::vector<Button*> btns;
|
std::vector<Button*> btns;
|
||||||
wxImageList* images = nullptr;
|
wxImageList* images = nullptr;
|
||||||
wxBoxSizer * sizer = nullptr;
|
wxBoxSizer* sizer = nullptr;
|
||||||
|
|
||||||
int sel = -1;
|
int sel = -1;
|
||||||
wxFont bold;
|
wxFont bold;
|
||||||
|
int item_space = 2; // space around each button, both sides (SetItemSpace)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TabCtrl(wxWindow * parent,
|
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
|
||||||
wxWindowID id,
|
|
||||||
const wxPoint & pos = wxDefaultPosition,
|
|
||||||
const wxSize & size = wxDefaultSize,
|
|
||||||
long style = 0);
|
|
||||||
|
|
||||||
~TabCtrl();
|
~TabCtrl();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual bool SetFont(wxFont const & font) override;
|
virtual bool SetFont(wxFont const& font) override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
int AppendItem(const wxString &item, int image = -1, int selImage = -1, void *clientData = nullptr);
|
int AppendItem(const wxString& item, int image = -1, int selImage = -1, void* clientData = nullptr);
|
||||||
int AppendItem(const wxString &item, const wxBitmap& bitmap, void *clientData = nullptr);
|
int AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData = nullptr);
|
||||||
|
|
||||||
bool DeleteItem(int item);
|
bool DeleteItem(int item);
|
||||||
|
|
||||||
@@ -37,7 +34,7 @@ public:
|
|||||||
|
|
||||||
unsigned int GetCount() const;
|
unsigned int GetCount() const;
|
||||||
|
|
||||||
int GetSelection() const;
|
int GetSelection() const;
|
||||||
|
|
||||||
void SelectItem(int item);
|
void SelectItem(int item);
|
||||||
|
|
||||||
@@ -46,16 +43,16 @@ public:
|
|||||||
virtual void Rescale();
|
virtual void Rescale();
|
||||||
|
|
||||||
wxString GetItemText(unsigned int item) const;
|
wxString GetItemText(unsigned int item) const;
|
||||||
void SetItemText(unsigned int item, wxString const &value);
|
void SetItemText(unsigned int item, wxString const& value);
|
||||||
void SetItemBitmap(unsigned int item, const wxBitmap& bitmap);
|
void SetItemBitmap(unsigned int item, const wxBitmap& bitmap);
|
||||||
|
|
||||||
bool GetItemBold(unsigned int item) const;
|
bool GetItemBold(unsigned int item) const;
|
||||||
void SetItemBold(unsigned int item, bool bold);
|
void SetItemBold(unsigned int item, bool bold);
|
||||||
|
|
||||||
void* GetItemData(unsigned int item) const;
|
void* GetItemData(unsigned int item) const;
|
||||||
void SetItemData(unsigned int item, void *clientData);
|
void SetItemData(unsigned int item, void* clientData);
|
||||||
|
|
||||||
void AssignImageList(wxImageList *imageList);
|
void AssignImageList(wxImageList* imageList);
|
||||||
|
|
||||||
void SetItemTextColour(unsigned int item, const StateColor& col);
|
void SetItemTextColour(unsigned int item, const StateColor& col);
|
||||||
|
|
||||||
@@ -64,8 +61,11 @@ public:
|
|||||||
int GetNextVisible(int item) const;
|
int GetNextVisible(int item) const;
|
||||||
bool IsVisible(unsigned int item) const;
|
bool IsVisible(unsigned int item) const;
|
||||||
|
|
||||||
// Width of the tab strip that keeps every button visible (used to size the Publish dialog).
|
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
|
||||||
int buttons_best_width() const;
|
// standard; call before appending items so every button picks it up.
|
||||||
|
void SetItemSpace(int space);
|
||||||
|
|
||||||
|
int GetFullSize() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override;
|
virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override;
|
||||||
@@ -76,10 +76,10 @@ private:
|
|||||||
|
|
||||||
void relayout();
|
void relayout();
|
||||||
|
|
||||||
void buttonClicked(wxCommandEvent & event);
|
void buttonClicked(wxCommandEvent& event);
|
||||||
void keyDown(wxKeyEvent &event);
|
void keyDown(wxKeyEvent& event);
|
||||||
|
|
||||||
void doRender(wxDC & dc) override;
|
void doRender(wxDC& dc) override;
|
||||||
|
|
||||||
// some useful events
|
// some useful events
|
||||||
bool sendTabCtrlEvent(bool changing = false);
|
bool sendTabCtrlEvent(bool changing = false);
|
||||||
|
|||||||
@@ -1024,3 +1024,55 @@ SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A published mixed filament serializes its whole definition (components, ratios, gradient)
|
||||||
|
// masked to the author's slot: the mix slot's values survive, the non-published slots reset to
|
||||||
|
// their defaults, so a partial publish never leaks another slot's mix data.
|
||||||
|
SCENARIO("Published mixed-filament keys are masked to the author's slot", "[3mf]") {
|
||||||
|
GIVEN("a full print configuration with three slots, one of them mixed") {
|
||||||
|
DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config();
|
||||||
|
full_cfg.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75 };
|
||||||
|
full_cfg.opt<ConfigOptionStrings>("filament_colour")->values = { "#111111", "#222222", "#333333" };
|
||||||
|
full_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values = { 0, 0, 1 };
|
||||||
|
full_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values = { "", "", "1,2" };
|
||||||
|
full_cfg.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" };
|
||||||
|
full_cfg.opt<ConfigOptionBools>("filament_mixed_gradient")->values = { 0, 0, 1 };
|
||||||
|
full_cfg.opt<ConfigOptionStrings>("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" };
|
||||||
|
full_cfg.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" };
|
||||||
|
full_cfg.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values = { 0, 0, 1 };
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix_entry;
|
||||||
|
mix_entry.slot = 2;
|
||||||
|
mix_entry.keys = {
|
||||||
|
"filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios",
|
||||||
|
"filament_mixed_gradient", "filament_mixed_gradient_range", "filament_mixed_gradient_curve",
|
||||||
|
"filament_mixed_gradient_per_part"
|
||||||
|
};
|
||||||
|
|
||||||
|
WHEN("filtering with a mixed entry for slot 2") {
|
||||||
|
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { mix_entry });
|
||||||
|
|
||||||
|
THEN("the author's mixed slot keeps its definition") {
|
||||||
|
REQUIRE(filtered_cfg.option("filament_is_mixed") != nullptr);
|
||||||
|
REQUIRE(filtered_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values == std::vector<unsigned char>{ 0, 0, 1 });
|
||||||
|
const auto& components = filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 3);
|
||||||
|
CHECK(components[2] == "1,2");
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2] == "0.6,0.4");
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values[2] == "0,0.1|1,0.9");
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_mixed_gradient")->values[2]);
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values[2]);
|
||||||
|
}
|
||||||
|
THEN("the non-published slots are masked to their defaults") {
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values[0] == "");
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionStrings>("filament_mixed_components")->values[1] == "");
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values[0] == 0);
|
||||||
|
CHECK(filtered_cfg.opt<ConfigOptionBools>("filament_is_mixed")->values[1] == 0);
|
||||||
|
}
|
||||||
|
THEN("the identity keys stay present") {
|
||||||
|
REQUIRE(filtered_cfg.option("filament_colour") != nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2545,3 +2545,252 @@ TEST_CASE("Published 3MF applies per-extruder printer keys across extruder-count
|
|||||||
CHECK(pub.skipped_keys.empty());
|
CHECK(pub.skipped_keys.empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A published mixed filament serializes its definition (components, ratios, gradient) into the
|
||||||
|
// receiver's project_config - the project-level parallel arrays, not a filament preset. The
|
||||||
|
// mix's own blended colour is carried as publish_color so the receiver renders the swatch.
|
||||||
|
TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's project config", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
auto make_file_config = [] {
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
// Three author slots: two physical PLA/PETG plus one virtual mixed slot (index 2)
|
||||||
|
// blending slots 1 and 2 at 60/40 with a gradient.
|
||||||
|
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75 };
|
||||||
|
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2, 3 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = {
|
||||||
|
"Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard"
|
||||||
|
};
|
||||||
|
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#0000FF", "#800080" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PETG", "PLA" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic", "Generic" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFT99", "GFL99" };
|
||||||
|
// The mixed slot's definition. These keys are project-level arrays in the full config;
|
||||||
|
// on export they are masked so only the published slot's entry survives.
|
||||||
|
config.opt<ConfigOptionBools>("filament_is_mixed")->values = { 0, 0, 1 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_components")->values = { "", "", "1,2" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" };
|
||||||
|
config.opt<ConfigOptionBools>("filament_mixed_gradient")->values = { 0, 0, 1 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" };
|
||||||
|
config.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values = { 0, 0, 1 };
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A receiver that already carries the mix slot at index 2 (e.g. a two-physical-plus-one-mix
|
||||||
|
// project with the same layout).
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||||
|
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG");
|
||||||
|
petg.config.opt_string("filament_type", 0u) = "PETG";
|
||||||
|
bundle.filament_presets = { "My PLA", "My PETG", "My PLA" };
|
||||||
|
|
||||||
|
// Grow the receiver's project arrays to 3 slots first, as set_num_filaments would.
|
||||||
|
bundle.set_num_filaments(3);
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix;
|
||||||
|
mix.filament_type = "PLA";
|
||||||
|
mix.filament_vendor = "Generic";
|
||||||
|
mix.filament_id = "GFL99";
|
||||||
|
mix.slot = 2;
|
||||||
|
mix.publish_color = true;
|
||||||
|
mix.color = "#800080";
|
||||||
|
mix.keys = { "filament_is_mixed", "filament_mixed_components",
|
||||||
|
"filament_mixed_sublayer_ratios", "filament_mixed_gradient",
|
||||||
|
"filament_mixed_gradient_range", "filament_mixed_gradient_curve",
|
||||||
|
"filament_mixed_gradient_per_part" };
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { mix };
|
||||||
|
DynamicPrintConfig config = make_file_config();
|
||||||
|
Preset::normalize(config);
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
// The definition landed in project_config's parallel arrays at the author slot.
|
||||||
|
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||||
|
REQUIRE(is_mixed.size() == 3);
|
||||||
|
CHECK(is_mixed[2]);
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 3);
|
||||||
|
CHECK(components[2] == "1,2");
|
||||||
|
const auto &ratios = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values;
|
||||||
|
REQUIRE(ratios.size() == 3);
|
||||||
|
CHECK(ratios[2] == "0.6,0.4");
|
||||||
|
const auto &gradient = bundle.project_config.opt<ConfigOptionBools>("filament_mixed_gradient")->values;
|
||||||
|
CHECK(gradient[2]);
|
||||||
|
const auto &range = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_gradient_range")->values;
|
||||||
|
CHECK(range[2] == "0.9,0.1");
|
||||||
|
const auto &curve = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values;
|
||||||
|
CHECK(curve[2] == "0,0.1|1,0.9");
|
||||||
|
const auto &per_part = bundle.project_config.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values;
|
||||||
|
CHECK(per_part[2]);
|
||||||
|
// The mix's blended colour crossed into project_config for the swatch.
|
||||||
|
const auto &colour = bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||||
|
REQUIRE(colour.size() == 3);
|
||||||
|
CHECK(colour[2] == "#800080");
|
||||||
|
// The other slots were not overwritten by the mask.
|
||||||
|
CHECK_FALSE(is_mixed[0]);
|
||||||
|
CHECK_FALSE(is_mixed[1]);
|
||||||
|
// Nothing skipped: every serialized mixed key was applied.
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A receiver with fewer slots: the slot is grown and seeded before the definition applies.
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||||
|
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
bundle.filament_presets = { "My PLA" };
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix;
|
||||||
|
mix.slot = 2;
|
||||||
|
mix.keys = { "filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios" };
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { mix };
|
||||||
|
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);
|
||||||
|
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||||
|
REQUIRE(is_mixed.size() == 3);
|
||||||
|
CHECK(is_mixed[2]);
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 3);
|
||||||
|
CHECK(components[2] == "1,2");
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A published mixed filament whose definition cannot be applied is reported as skipped instead
|
||||||
|
// of aborting the load: the entry lists a mixed key that the file's payload does not carry.
|
||||||
|
TEST_CASE("Published 3MF reports an unappliable mixed filament definition as skipped", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||||
|
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||||
|
bundle.filament_presets = { "My PLA" };
|
||||||
|
|
||||||
|
// Two author slots (so slot 1 is in range) but the payload omits the mixed arrays: the
|
||||||
|
// entry lists them, the file config does not.
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PLA" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic" };
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix;
|
||||||
|
mix.slot = 1;
|
||||||
|
mix.keys = { "filament_mixed_components", "filament_mixed_sublayer_ratios" };
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { mix };
|
||||||
|
Preset::normalize(config);
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
// The receiver grows to two slots; the missing payload keys are reported as skipped rather
|
||||||
|
// than dropped silently (material_label is empty for this entry).
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 2);
|
||||||
|
CHECK(contains_key(pub.skipped_keys, "material: (filament_mixed_components)"));
|
||||||
|
CHECK(contains_key(pub.skipped_keys, "material: (filament_mixed_sublayer_ratios)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single-extruder receiver collapses the author's per-extruder printer slots onto its single
|
||||||
|
// slot: the first serialized variant of a base key is applied, the remaining variants of that
|
||||||
|
// base key are reported as skipped.
|
||||||
|
TEST_CASE("Published 3MF collapses a multi-extruder publish onto a single-extruder receiver", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
auto make_file_config = [] {
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000" };
|
||||||
|
// Author has two extruders.
|
||||||
|
config.opt<ConfigOptionFloats>("retraction_length")->values = { 0.6, 0.9 };
|
||||||
|
config.opt<ConfigOptionFloats>("retraction_speed")->values = { 30.0, 40.0 };
|
||||||
|
Preset::normalize(config);
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Both extruders published: the first serialized variant (#0, left) lands on the receiver's
|
||||||
|
// single slot; the second variant (#1) is reported as skipped.
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values = { 0.8 };
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_speed")->values = { 25.0 };
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.published_keys = { "retraction_length#0", "retraction_length#1", "retraction_speed#0", "retraction_speed#1" };
|
||||||
|
DynamicPrintConfig config = make_file_config();
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
check_double_vector(bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values, { 0.6 });
|
||||||
|
check_double_vector(bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_speed")->values, { 30.0 });
|
||||||
|
CHECK(contains_key(pub.skipped_keys, "retraction_length#1"));
|
||||||
|
CHECK(contains_key(pub.skipped_keys, "retraction_speed#1"));
|
||||||
|
CHECK_FALSE(contains_key(pub.skipped_keys, "retraction_length#0"));
|
||||||
|
CHECK_FALSE(contains_key(pub.skipped_keys, "retraction_speed#0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the second extruder published: the single-extruder receiver still applies it (the
|
||||||
|
// author's "right" is the only serialized slot) and reports nothing skipped.
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values = { 0.8 };
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.published_keys = { "retraction_length#1" };
|
||||||
|
DynamicPrintConfig config = make_file_config();
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
check_double_vector(bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values, { 0.9 });
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A multi-extruder "similar setup" receiver overrides each published extruder slot element-wise
|
||||||
|
// (no collapsing): each '#N' variant applies to the matching receiver slot, out-of-range ones are
|
||||||
|
// reported as skipped.
|
||||||
|
TEST_CASE("Published 3MF overrides each extruder slot on a similar multi-extruder receiver", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
auto make_file_config = [] {
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
|
||||||
|
// Author has two extruders.
|
||||||
|
config.opt<ConfigOptionFloats>("retraction_length")->values = { 0.6, 0.9 };
|
||||||
|
Preset::normalize(config);
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Receiver with two extruders: both published slots override element-wise.
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values = { 0.8, 0.8 };
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.published_keys = { "retraction_length#0", "retraction_length#1" };
|
||||||
|
DynamicPrintConfig config = make_file_config();
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
check_double_vector(bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values, { 0.6, 0.9 });
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receiver with three extruders: slots 0 and 1 override, slot 2 keeps its own value.
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values = { 0.8, 0.8, 0.7 };
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.published_keys = { "retraction_length#0", "retraction_length#1" };
|
||||||
|
DynamicPrintConfig config = make_file_config();
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
check_double_vector(bundle.printers.get_edited_preset().config.opt<ConfigOptionFloats>("retraction_length")->values, { 0.6, 0.9, 0.7 });
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user